"""
INF parser service for Windows driver INF files.

Extracts driver names (DriverDesc), resolves %TOKEN% references,
auto-detects encoding (ANSI/UTF-8/UTF-16), handles multi-model INFs,
detects unused files, architecture, and presence of .cat files.

Source: Microsoft WDK — General Syntax Rules for INF Files
        https://learn.microsoft.com/en-us/windows-hardware/drivers/install/general-syntax-rules-for-inf-files
"""
from __future__ import annotations

import configparser
import re
from dataclasses import dataclass, field


@dataclass
class ParsedInf:
    """Result of parsing a Windows INF file."""

    driver_names: list[str]  # resolved DriverDesc values, deduplicated and sorted
    inf_filename: str  # which .inf file was parsed (basename from ZIP)
    architecture: str | None  # 'x64', 'x86', 'arm64', or None if ambiguous/unknown
    has_cat_file: bool  # True if a .cat file exists in the ZIP member list
    unused_files: list[str]  # ZIP members not referenced anywhere in the INF text


def _detect_encoding(raw: bytes) -> str:
    """Detect INF file encoding by sniffing BOM bytes.

    INF files from real vendors arrive as:
    - ANSI / Windows-1252 (cp1252)  — most legacy drivers
    - UTF-8 with BOM               — modern drivers
    - UTF-16 LE with BOM           — HP/Canon x64 signed drivers (most common UTF-16)
    - UTF-16 BE with BOM           — rare

    Source: Microsoft WDK — general-syntax-rules-for-inf-files
    """
    if raw[:2] in (b"\xff\xfe", b"\xfe\xff"):
        return "utf-16"  # UTF-16 LE or BE with BOM
    if raw[:3] == b"\xef\xbb\xbf":
        return "utf-8-sig"  # UTF-8 with BOM
    return "cp1252"  # ANSI / Windows-1252 safe fallback


def _neutralize_bare_lines(inf_text: str) -> str:
    """Rewrite INF lines that lack ``=`` so configparser can ingest the file.

    Real vendor INFs contain sections like ``[SourceDisksFiles]`` or copy-list
    sections whose entries are bare filenames (no key/value). configparser is
    strict and aborts on those. We only care about ``key = value`` lines for
    DriverDesc extraction, so it's safe to convert each bare payload line into
    a synthetic ``__bare_N = <original>`` entry.
    """
    out: list[str] = []
    counter = 0
    for line in inf_text.splitlines():
        stripped = line.strip()
        if (
            not stripped
            or stripped.startswith(";")
            or stripped.startswith("#")
            or stripped.startswith("[")
            or "=" in stripped
        ):
            out.append(line)
            continue
        counter += 1
        out.append(f"__bare_{counter} = {stripped}")
    return "\n".join(out)


def _resolve_tokens(value: str, strings: dict[str, str]) -> str:
    """Expand %TOKEN% placeholders using the [Strings] section lookup dict.

    Keys in ``strings`` must already be lowercased (configparser lowercases
    option names by default).

    Source: Microsoft WDK — general-syntax-rules-for-inf-files — strkey% syntax
    """

    def replacer(match: re.Match) -> str:
        key = match.group(1).lower()
        return strings.get(key, match.group(0))

    return re.sub(r"%([^%]+)%", replacer, value)


def parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedInf:
    """Parse a Windows INF file and extract driver metadata.

    Args:
        inf_text:     Decoded text content of the .inf file.
        inf_filename: Name of the .inf file (used as-is in the result).
        zip_names:    List of all member paths from the containing ZIP archive.
                      Used for unused-file detection and .cat presence check.

    Returns:
        ParsedInf dataclass with driver_names, architecture, has_cat_file,
        unused_files, and inf_filename.

    Notes:
        - Uses RawConfigParser (NOT ConfigParser) to avoid %(interpolation)s
          interference with %TOKEN% INF syntax.
        - strict=False is required because real INF files frequently have
          duplicate option keys within a section (multiple hardware IDs).
        - Architecture detection: if exactly one arch hint found -> return it;
          multiple arch hints -> None (ambiguous / multi-arch INF).
        - driver_names are sorted for deterministic dropdown order.
    """
    parser = configparser.RawConfigParser(
        comment_prefixes=(";", "#"),
        strict=False,  # real INFs have duplicate keys
        delimiters=("=",),
    )
    # Preserve original option-key casing so DriverDesc literals keep their case.
    # configparser lowercases keys by default, which would mangle "Acme SuperPrint 9000"
    # into "acme superprint 9000". We disable that behaviour here and manually lowercase
    # only when building the [Strings] lookup dict.
    parser.optionxform = str  # type: ignore[assignment]
    parser.read_string(_neutralize_bare_lines(inf_text))

    # Build strings lookup with LOWERCASED keys (case-insensitive token resolution).
    # INF token references are case-insensitive per WDK spec.
    strings: dict[str, str] = {}
    if parser.has_section("Strings"):
        for key, val in parser.items("Strings"):
            # INF string values are typically surrounded by double-quotes; strip them.
            strings[key.lower()] = val.strip('"')

    # Collect Models section base names from [Manufacturer]
    # Format per WDK: mfg-id = models-section-name[,target-OS-version[,target-OS-version...]]
    models_section_names: list[str] = []
    if parser.has_section("Manufacturer"):
        for _mfg_key, mfg_val in parser.items("Manufacturer"):
            # Resolve any %TOKEN% in the manufacturer value (rare, but safe)
            resolved_val = _resolve_tokens(mfg_val, strings)
            parts = [p.strip() for p in resolved_val.split(",")]
            if parts:
                models_section_names.append(parts[0])

    # For each referenced Models section base name, find all matching sections
    # (undecorated, .NTamd64, .NTarm64, .NTx86, etc.) and extract DriverDesc entries.
    driver_names: set[str] = set()
    arch_hints: set[str] = set()

    all_sections_lower = {s.lower(): s for s in parser.sections()}

    for base_name in models_section_names:
        base_lower = base_name.lower()
        for section_lower, section in all_sections_lower.items():
            # Match: exact base name (undecorated) OR base name + .NT<arch> decoration
            if section_lower == base_lower:
                # Undecorated section — architecture hint: x86
                arch_hints.add("x86")
                suffix = ""
            elif section_lower.startswith(base_lower + ".nt"):
                suffix = section_lower[len(base_lower):]  # e.g. ".ntamd64"
                if "amd64" in suffix:
                    arch_hints.add("x64")
                elif "arm64" in suffix:
                    arch_hints.add("arm64")
                elif "x86" in suffix:
                    arch_hints.add("x86")
                else:
                    # Generic .NT decoration (no specific arch) — treat as x86
                    arch_hints.add("x86")
            else:
                continue

            # Each option key in a Models section is a device-description (DriverDesc)
            for key, _val in parser.items(section):
                if key.startswith("__bare_"):
                    continue
                resolved = _resolve_tokens(key, strings)
                # Skip empty, purely numeric, or clearly non-driver-name entries
                if resolved and not resolved.isdigit():
                    driver_names.add(resolved)

    # Architecture: unambiguous only when exactly one arch hint found
    architecture: str | None = arch_hints.pop() if len(arch_hints) == 1 else None

    # .cat file detection
    has_cat_file = any(name.lower().endswith(".cat") for name in zip_names)

    # Unused files: ZIP members whose basename does not appear anywhere in INF text
    inf_lower = inf_text.lower()
    unused_files: list[str] = []
    for member in zip_names:
        # Normalise path separators, then take basename
        basename = member.replace("\\", "/").rsplit("/", 1)[-1]
        if basename.lower() not in inf_lower:
            unused_files.append(member)

    return ParsedInf(
        driver_names=sorted(driver_names),
        inf_filename=inf_filename,
        architecture=architecture,
        has_cat_file=has_cat_file,
        unused_files=unused_files,
    )
