"""C#-compatible wafer license file parsing.

License files are AES-128-CBC encrypted `.bin` blobs named
``X00000-00.bin`` where ``X`` is the wafer family code, ``00000`` the
5-digit serial number, and ``00`` the 2-digit license level. The blob is
``ciphertext || iv`` (IV is the trailing 16 bytes). The decrypted text is
a fixed-offset record::

    [0:6]   serial       e.g. "A00001"
    [7:15]  mfg date     8 hex chars
    [16:18] level        2 decimal digits ("00" basic, "01"+ adds replay)
    [19:27] license date 8 hex chars

The key is hardcoded to match the C# application exactly
"""

import logging
import re
from dataclasses import dataclass
from pathlib import Path

from pygui.backend.crypto.crypto_helper import CryptoHelper

log = logging.getLogger(__name__)

LICENSE_KEY = b"ThisIsA16ByteKey"
LICENSE_FILENAME_RE = re.compile(r"^[A-Z]\d{5}-\d{2}\.bin$")

_MIN_TEXT_LEN = 27


# ===== License Record =====
@dataclass(frozen=True)
class License:
    """One parsed license file (all fields as stored in the file)."""

    serial: str        # "A00001"
    level: str         # "00".."03"
    mfg_date_hex: str  # 8 hex chars
    lic_date_hex: str  # 8 hex chars

    @property
    def level_int(self) -> int:
        """Level as int; -1 if unparseable."""
        try:
            return int(self.level, 10)
        except ValueError:
            return -1

    @property
    def mfg_date_decimal(self) -> str:
        """Mfg date shown as decimal (C# grid does Convert.ToInt64(hex, 16))."""
        return _hex_to_decimal(self.mfg_date_hex)

    @property
    def lic_date_decimal(self) -> str:
        """License date shown as decimal, same conversion as the C# grid."""
        return _hex_to_decimal(self.lic_date_hex)


def _hex_to_decimal(hex_str: str) -> str:
    try:
        return str(int(hex_str, 16))
    except ValueError:
        return ""


# ===== Blob Parsing =====
def split_data_iv(blob: bytes) -> tuple[bytes, bytes]:
    """Split a license blob into (ciphertext, iv) — IV is the last 16 bytes."""
    if len(blob) < 32:
        raise ValueError("License blob too short")
    return blob[:-16], blob[-16:]


def parse_license_blob(blob: bytes) -> License | None:
    """Decrypt and parse one license blob. None if malformed."""
    try:
        data, iv = split_data_iv(blob)
        message = CryptoHelper.decrypt_aes(data, LICENSE_KEY, iv)
        text = message.decode("utf-8")
    except (ValueError, UnicodeDecodeError) as exc:
        log.warning("Undecryptable license blob: %s", exc)
        return None

    if len(text) < _MIN_TEXT_LEN:
        log.warning("License text too short (%d chars)", len(text))
        return None

    return License(
        serial=text[0:6],
        mfg_date_hex=text[7:15],
        level=text[16:18],
        lic_date_hex=text[19:27],
    )


# ===== Directory Scan =====
def read_license_files(licenses_dir: str | Path) -> list[License]:
    """Parse every valid license .bin in the directory.

    A file counts only if its name matches ``X00000-00.bin`` AND equals
    ``{serial}-{level}.bin`` from its own decrypted payload — same
    tamper check as the C# app.
    """
    directory = Path(licenses_dir)
    if not directory.is_dir():
        return []

    licenses: list[License] = []
    for path in sorted(directory.glob("*.bin")):
        if not LICENSE_FILENAME_RE.match(path.name):
            continue
        try:
            blob = path.read_bytes()
        except OSError as exc:
            log.warning("Cannot read license file %s: %s", path, exc)
            continue
        lic = parse_license_blob(blob)
        if lic is None:
            continue
        if path.name != f"{lic.serial}-{lic.level}.bin":
            log.warning("License filename mismatch: %s", path.name)
            continue
        licenses.append(lic)
    return licenses
