"""
TCF 2.2 TC String Encoder — generates IAB Transparency & Consent strings.

Implements the TC String v2.2 format per IAB specification.
The TC String is a base64url-encoded bitfield containing:
- CMP metadata (ID, version, screen, consent language)
- Purpose consents (12 standard IAB purposes)
- Vendor consents (per IAB vendor ID)
- Legitimate interest signals

Reference: https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework

NOTE: This is a simplified encoder for CMP integration. For full GVL
(Global Vendor List) support, integrate with the IAB GVL API.
"""

import base64
import math
from datetime import datetime, timezone
from typing import Any


# IAB TCF 2.2 Standard Purposes
IAB_PURPOSES = {
    1: {"name": "Store and/or access information on a device", "name_de": "Informationen auf Geraet speichern/abrufen"},
    2: {"name": "Select basic ads", "name_de": "Einfache Anzeigen auswaehlen"},
    3: {"name": "Create a personalised ads profile", "name_de": "Personalisiertes Anzeigenprofil erstellen"},
    4: {"name": "Select personalised ads", "name_de": "Personalisierte Anzeigen auswaehlen"},
    5: {"name": "Create a personalised content profile", "name_de": "Personalisiertes Inhaltsprofil erstellen"},
    6: {"name": "Select personalised content", "name_de": "Personalisierte Inhalte auswaehlen"},
    7: {"name": "Measure ad performance", "name_de": "Anzeigen-Leistung messen"},
    8: {"name": "Measure content performance", "name_de": "Inhalte-Leistung messen"},
    9: {"name": "Apply market research to generate audience insights", "name_de": "Marktforschung fuer Zielgruppen"},
    10: {"name": "Develop and improve products", "name_de": "Produkte entwickeln und verbessern"},
    11: {"name": "Use limited data to select content", "name_de": "Eingeschraenkte Daten fuer Inhalte nutzen"},
    12: {"name": "Use limited data to select ads", "name_de": "Eingeschraenkte Daten fuer Anzeigen nutzen"},
}

# IAB Special Features
IAB_SPECIAL_FEATURES = {
    1: {"name": "Use precise geolocation data", "name_de": "Praezise Standortdaten verwenden"},
    2: {"name": "Actively scan device characteristics for identification", "name_de": "Geraetemerkmale aktiv scannen"},
}

# Category-to-Purpose mapping (how our banner categories map to IAB purposes)
CATEGORY_PURPOSE_MAP = {
    "necessary": [],  # No consent needed
    "functional": [1, 11],  # Device access + limited data for content
    "statistics": [1, 7, 8, 9, 10],  # Device access + measurement + research
    "marketing": [1, 2, 3, 4, 5, 6, 7, 12],  # Most purposes
}


def _int_to_bits(value: int, length: int) -> str:
    """Convert integer to fixed-length bit string."""
    return bin(value)[2:].zfill(length)


def _datetime_to_deciseconds(dt: datetime) -> int:
    """Convert datetime to deciseconds since epoch (IAB format)."""
    epoch = datetime(2000, 1, 1, tzinfo=timezone.utc)
    return int((dt - epoch).total_seconds() * 10)


def _bits_to_base64url(bits: str) -> str:
    """Convert bit string to base64url encoding (TC String format)."""
    # Pad to multiple of 8
    padding = (8 - len(bits) % 8) % 8
    bits += "0" * padding
    # Convert to bytes
    byte_array = bytearray()
    for i in range(0, len(bits), 8):
        byte_array.append(int(bits[i:i+8], 2))
    # Base64url encode (no padding)
    return base64.urlsafe_b64encode(bytes(byte_array)).rstrip(b"=").decode("ascii")


class TCFEncoderService:
    """Generates TC Strings per IAB TCF 2.2 specification."""

    def __init__(
        self,
        cmp_id: int = 1,
        cmp_version: int = 1,
        consent_screen: int = 1,
        consent_language: str = "DE",
    ):
        self.cmp_id = cmp_id
        self.cmp_version = cmp_version
        self.consent_screen = consent_screen
        self.consent_language = consent_language

    def encode(
        self,
        purpose_consents: dict[int, bool],
        vendor_consents: dict[int, bool],
        purpose_li: dict[int, bool] | None = None,
        special_features: dict[int, bool] | None = None,
    ) -> str:
        """Generate a TC String from consent decisions.

        Args:
            purpose_consents: {purpose_id: True/False} for purposes 1-12
            vendor_consents: {vendor_id: True/False} for IAB vendor IDs
            purpose_li: Legitimate interest signals per purpose
            special_features: Special feature opt-ins
        Returns:
            Base64url-encoded TC String
        """
        now = datetime.now(timezone.utc)
        created = _datetime_to_deciseconds(now)
        updated = created

        bits = ""
        # Core TC String v2 fields
        bits += _int_to_bits(2, 6)                    # Version (6 bits) = 2
        bits += _int_to_bits(created, 36)              # Created (36 bits)
        bits += _int_to_bits(updated, 36)              # LastUpdated (36 bits)
        bits += _int_to_bits(self.cmp_id, 12)          # CmpId (12 bits)
        bits += _int_to_bits(self.cmp_version, 12)     # CmpVersion (12 bits)
        bits += _int_to_bits(self.consent_screen, 6)   # ConsentScreen (6 bits)

        # ConsentLanguage (12 bits = 2 × 6-bit letters)
        lang = self.consent_language.upper()[:2]
        bits += _int_to_bits(ord(lang[0]) - ord("A"), 6)
        bits += _int_to_bits(ord(lang[1]) - ord("A"), 6)

        # VendorListVersion (12 bits) — use 0 if not fetching GVL
        bits += _int_to_bits(0, 12)
        # TcfPolicyVersion (6 bits) = 4 for TCF 2.2
        bits += _int_to_bits(4, 6)
        # IsServiceSpecific (1 bit) = 1
        bits += "1"
        # UseNonStandardTexts (1 bit) = 0
        bits += "0"

        # SpecialFeatureOptIns (12 bits)
        sf = special_features or {}
        for i in range(1, 13):
            bits += "1" if sf.get(i, False) else "0"

        # PurposesConsent (24 bits)
        for i in range(1, 25):
            bits += "1" if purpose_consents.get(i, False) else "0"

        # PurposesLITransparency (24 bits)
        li = purpose_li or {}
        for i in range(1, 25):
            bits += "1" if li.get(i, False) else "0"

        # Purpose one treatment (1 bit) = 0, PublisherCC (12 bits) = DE
        bits += "0"
        bits += _int_to_bits(ord("D") - ord("A"), 6)
        bits += _int_to_bits(ord("E") - ord("A"), 6)

        # Vendor consents — Range encoding
        max_vendor = max(vendor_consents.keys()) if vendor_consents else 0
        bits += _int_to_bits(max_vendor, 16)  # MaxVendorId
        # Use bitfield encoding (simpler than range)
        bits += "0"  # IsRangeEncoding = 0 (bitfield)
        for i in range(1, max_vendor + 1):
            bits += "1" if vendor_consents.get(i, False) else "0"

        # Vendor legitimate interests (same pattern)
        bits += _int_to_bits(max_vendor, 16)
        bits += "0"
        for i in range(1, max_vendor + 1):
            bits += "1" if vendor_consents.get(i, False) else "0"  # Simplified: same as consent

        return _bits_to_base64url(bits)

    def encode_from_categories(
        self,
        categories: list[str],
        vendor_consents: dict[int, bool] | None = None,
    ) -> str:
        """Generate TC String from banner category selections.

        Maps our banner categories (necessary, statistics, marketing, functional)
        to IAB purposes and generates the TC String.
        """
        purpose_consents: dict[int, bool] = {}
        for cat in categories:
            for purpose_id in CATEGORY_PURPOSE_MAP.get(cat, []):
                purpose_consents[purpose_id] = True

        return self.encode(
            purpose_consents=purpose_consents,
            vendor_consents=vendor_consents or {},
        )

    @staticmethod
    def get_purposes() -> list[dict[str, Any]]:
        """Return all 12 IAB purposes with translations."""
        return [
            {"id": pid, "name": info["name"], "name_de": info["name_de"]}
            for pid, info in IAB_PURPOSES.items()
        ]

    @staticmethod
    def get_special_features() -> list[dict[str, Any]]:
        return [
            {"id": fid, "name": info["name"], "name_de": info["name_de"]}
            for fid, info in IAB_SPECIAL_FEATURES.items()
        ]

    @staticmethod
    def get_category_purpose_map() -> dict[str, list[int]]:
        return CATEGORY_PURPOSE_MAP
