from __future__ import annotations

import os
import struct
from collections import Counter, defaultdict
from pathlib import Path
from typing import Any

from liverylab_core.proto import is_printable_utf8, parse_fields, read_varint


OFFICIAL_LIVERY_TEXTURE_STEMS = {
    "ext_skin_c",
    "ext_skin_d",
    "ext_skin_m",
    "ext_skin_r",
    "ext_skin_cc",
    "ext_skin_cr",
    "ext_banner_c",
}

EXT_SKIN_MATERIAL_TEXTURE_SLOTS = {
    "Base_BaseColorMap": "ext_skin_c",
    "SpatMap": "ext_skin_d",
    "Base_MetalnessMap": "ext_skin_m",
    "Base_RoughnessMap": "ext_skin_r",
    "Base_ClearCoatMap": "ext_skin_cc",
    "Base_ClearCoatRoughnessMap": "ext_skin_cr",
}

EXT_BANNER_MATERIAL_TEXTURE_SLOTS = {
    "Base_BaseColorMap": "ext_banner_c",
}


def _norm_slashes(value: str) -> str:
    return str(value or "").replace("/", "\\")


def read_packed_varints(raw: bytes) -> list[int]:
    values: list[int] = []
    pos = 0
    while pos < len(raw):
        value, pos = read_varint(raw, pos)
        values.append(int(value))
    return values


def parse_texture_descriptor(texture_path: str | Path) -> dict[str, Any]:
    path = Path(texture_path)
    fields = parse_fields(path.read_bytes())
    scalars: dict[int, int] = {}
    len_fields: dict[int, list[bytes]] = {}
    for field in fields:
        if field.wire == 0:
            scalars[field.number] = int(field.value)
        elif field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
            len_fields.setdefault(field.number, []).append(bytes(field.value))

    width = scalars.get(1)
    height = scalars.get(2)
    mips = scalars.get(3)
    if not width or not height or not mips:
        raise RuntimeError("Could not read width/height/mip count from .texture")

    tile_layout_raw = len_fields.get(12, [None])[0]
    tile_w = 256
    tile_h = 256
    mip_tile_offsets: list[int] = []
    mip_tile_counts: list[int] = []
    if tile_layout_raw:
        layout_fields = parse_fields(tile_layout_raw)
        layout_scalars: dict[int, int] = {}
        layout_len: dict[int, bytes] = {}
        for field in layout_fields:
            if field.wire == 0:
                layout_scalars[field.number] = int(field.value)
            elif field.wire == 2 and isinstance(field.value, (bytes, bytearray)):
                layout_len[field.number] = bytes(field.value)
        tile_w = int(layout_scalars.get(1, tile_w) or tile_w)
        tile_h = int(layout_scalars.get(2, tile_h) or tile_h)
        mip_tile_offsets = read_packed_varints(layout_len[4]) if 4 in layout_len else []
        mip_tile_counts = read_packed_varints(layout_len[5]) if 5 in layout_len else []

    return {
        "width": int(width),
        "height": int(height),
        "mips": int(mips),
        "ace_format_code": scalars.get(4),
        "tile_w": tile_w,
        "tile_h": tile_h,
        "mip_tile_offsets": mip_tile_offsets,
        "mip_tile_counts": mip_tile_counts,
    }


def _texture_rel_path(path: Path, root: Path | None) -> str:
    if root:
        try:
            return _norm_slashes(str(path.relative_to(root)))
        except ValueError:
            pass
    return _norm_slashes(str(path))


def _candidate_row(path: Path, root: Path | None, *, source: str = "filename_stem") -> dict[str, Any]:
    desc = parse_texture_descriptor(path)
    return {
        "path": str(path),
        "relativePath": _texture_rel_path(path, root),
        "source": source,
        "stem": path.stem.lower(),
        "width": int(desc["width"]),
        "height": int(desc["height"]),
        "mips": int(desc["mips"]),
        "aceFormatCode": desc.get("ace_format_code"),
        "tileWidth": int(desc.get("tile_w") or 0),
        "tileHeight": int(desc.get("tile_h") or 0),
    }


def _fixed32_to_float(raw: bytes) -> float:
    return struct.unpack("<f", raw)[0]


def _decode_material_value(raw: bytes) -> object:
    if raw == b"":
        return None
    try:
        fields = parse_fields(raw)
    except Exception:
        text = is_printable_utf8(raw)
        return text if text is not None else {"raw_hex": raw.hex(" ")}
    if len(fields) == 1 and fields[0].number == 1 and fields[0].wire == 5:
        return _fixed32_to_float(fields[0].value)
    if len(fields) == 1 and fields[0].number == 2 and fields[0].wire == 2:
        text = is_printable_utf8(fields[0].value)
        if text is not None:
            return text
    if len(fields) == 1 and fields[0].wire == 2:
        nested_raw = fields[0].value
        if nested_raw == b"":
            return []
        try:
            nested = parse_fields(nested_raw)
            vals = []
            for nested_field in nested:
                if nested_field.wire == 5 and isinstance(nested_field.value, (bytes, bytearray)) and len(nested_field.value) == 4:
                    vals.append(_fixed32_to_float(nested_field.value))
            if vals:
                return vals
            if len(nested) == 1 and nested[0].wire == 2:
                text = is_printable_utf8(nested[0].value)
                if text is not None:
                    return text
        except Exception:
            pass
    return [
        {
            "field": field.number,
            "wire": field.wire,
            "value": is_printable_utf8(field.value)
            if isinstance(field.value, bytes) and is_printable_utf8(field.value)
            else (field.value.hex(" ") if isinstance(field.value, bytes) else field.value),
        }
        for field in fields
    ]


def _material_parse_entry(raw: bytes) -> tuple[str | None, object]:
    fields = parse_fields(raw)
    name = None
    value_raw = None
    value_varint = None
    for field in fields:
        if field.number == 1 and field.wire == 2 and isinstance(field.value, bytes):
            name = is_printable_utf8(field.value)
        elif field.number == 2:
            if field.wire == 2 and isinstance(field.value, bytes):
                value_raw = field.value
            elif field.wire == 0:
                value_varint = int(field.value)
    if value_raw is not None:
        return name, _decode_material_value(value_raw)
    if value_varint is not None:
        return name, value_varint
    return name, None


def _inspect_material_texture_slots(material_path: Path) -> dict[str, Any]:
    fields = parse_fields(material_path.read_bytes())
    result: dict[str, Any] = {"materialPath": "", "textureSlots": []}
    for field in fields:
        if field.number == 20 and field.wire == 2 and isinstance(field.value, bytes):
            result["materialPath"] = is_printable_utf8(field.value) or ""
        elif field.number == 5 and field.wire == 2 and isinstance(field.value, bytes):
            name, value = _material_parse_entry(field.value)
            result["textureSlots"].append({"name": name, "value": value})
    return result


def _resolve_material_texture_ref(car_root: Path, root: Path | None, ref: str) -> Path | None:
    clean = ref.replace("\\", os.sep).replace("/", os.sep)
    candidates: list[Path] = []
    clean_path = Path(clean)
    if root:
        candidates.append(root / clean_path)
        parts = clean.split(os.sep)
        if parts and parts[0].lower() == "content":
            candidates.append(root / Path(*parts[1:]))
            candidates.append(root.parent / clean_path)
            if len(parts) >= 3 and parts[0].lower() == "content" and parts[1].lower() == "cars":
                candidates.append(root / Path(*parts[2:]))
                candidates.append(car_root.parent / Path(*parts[2:]))
    candidates.append(car_root / clean_path)
    candidates.append(car_root / clean_path.name)
    candidates.append(car_root / "textures" / clean_path.name)
    candidates.append(car_root / "skins" / clean_path.name)

    seen: set[str] = set()
    for candidate in candidates:
        key = str(candidate).lower()
        if key in seen:
            continue
        seen.add(key)
        if candidate.exists() and candidate.is_file():
            return candidate

    basename = clean_path.name.lower()
    try:
        for path in car_root.rglob(basename):
            if path.is_file() and path.name.lower() == basename:
                return path
    except Exception:
        pass
    return None


def _is_default_material_texture_ref(ref: str) -> bool:
    clean = _norm_slashes(ref).lower()
    return clean.startswith("editor\\textures\\default_material\\")


def _material_slot_mapping(material_path: Path, material_info: dict[str, Any], root: Path | None) -> dict[str, str]:
    identity = " ".join(
        str(value or "").lower()
        for value in (
            material_path.name,
            _texture_rel_path(material_path, root),
            material_info.get("materialPath"),
        )
    )
    if "banner" in identity:
        return EXT_BANNER_MATERIAL_TEXTURE_SLOTS
    if "ext_skin" in identity or "ext-skin" in identity:
        return EXT_SKIN_MATERIAL_TEXTURE_SLOTS
    return {}


def build_texture_size_hints(car_root: str | Path, *, root: str | Path | None = None) -> dict[str, Any]:
    car_path = Path(car_root)
    rel_root = Path(root) if root is not None else car_path
    slots: dict[str, Any] = {}
    warnings: list[str] = []
    grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
    texture_files = sorted(car_path.rglob("*.texture"), key=lambda row: str(row).lower()) if car_path.exists() else []

    for texture_path in texture_files:
        stem = texture_path.stem.lower()
        if stem not in OFFICIAL_LIVERY_TEXTURE_STEMS:
            continue
        try:
            grouped[stem].append(_candidate_row(texture_path, rel_root))
        except Exception as exc:
            warnings.append(f"{_texture_rel_path(texture_path, rel_root)}: {exc}")

    material_files = sorted(car_path.rglob("*.material"), key=lambda row: str(row).lower()) if car_path.exists() else []
    for material_path in material_files:
        try:
            material_info = _inspect_material_texture_slots(material_path)
            slot_mapping = _material_slot_mapping(material_path, material_info, rel_root)
            if not slot_mapping:
                continue
            for slot in material_info.get("textureSlots", []):
                slot_name = str(slot.get("name") or "")
                texture_ref = slot.get("value")
                if not isinstance(texture_ref, str) or ".texture" not in texture_ref.lower():
                    continue
                if _is_default_material_texture_ref(texture_ref):
                    continue
                slot_id = slot_mapping.get(slot_name)
                if not slot_id:
                    continue
                resolved = _resolve_material_texture_ref(car_path, rel_root, texture_ref)
                if not resolved:
                    warnings.append(f"{_texture_rel_path(material_path, rel_root)} {slot_name}: unresolved texture ref {texture_ref}")
                    continue
                row = _candidate_row(resolved, rel_root, source="material_slot")
                row["materialPath"] = _texture_rel_path(material_path, rel_root)
                row["materialSlot"] = slot_name
                row["materialValue"] = _norm_slashes(texture_ref)
                grouped[slot_id].append(row)
        except Exception as exc:
            warnings.append(f"{_texture_rel_path(material_path, rel_root)}: {exc}")

    for slot_id in sorted(OFFICIAL_LIVERY_TEXTURE_STEMS):
        candidates = grouped.get(slot_id, [])
        selection_candidates = [row for row in candidates if row.get("source") == "material_slot"] or candidates
        size_counts: Counter[tuple[int, int]] = Counter((int(row["width"]), int(row["height"])) for row in selection_candidates)
        ranked = sorted(
            size_counts.items(),
            key=lambda item: (-item[1], -(item[0][0] * item[0][1]), -item[0][0], -item[0][1]),
        )
        ranked_sizes = [
            {"width": width, "height": height, "count": count}
            for (width, height), count in ranked
        ]
        selected = ranked_sizes[0] if ranked_sizes else {}
        top_count = int(selected.get("count") or 0)
        second_count = int(ranked_sizes[1].get("count") or 0) if len(ranked_sizes) > 1 else 0
        confidence = "missing"
        if len(ranked_sizes) == 1:
            confidence = "high"
        elif top_count > second_count:
            confidence = "medium"
        elif ranked_sizes:
            confidence = "low"
        slots[slot_id] = {
            "slotId": slot_id,
            "ready": bool(selected),
            "ambiguous": len(ranked_sizes) > 1,
            "confidence": confidence,
            "selectionRule": "material_slot_most_common_then_largest" if selection_candidates is not candidates else "most_common_then_largest",
            "width": selected.get("width"),
            "height": selected.get("height"),
            "candidateCount": len(candidates),
            "materialCandidateCount": len([row for row in candidates if row.get("source") == "material_slot"]),
            "sizes": ranked_sizes,
            "candidates": candidates[:25],
        }

    ready_count = sum(1 for row in slots.values() if row.get("ready"))
    ambiguous_count = sum(1 for row in slots.values() if row.get("ambiguous"))
    return {
        "schemaVersion": 1,
        "source": "raw_texture_descriptors",
        "carRoot": str(car_path),
        "root": str(rel_root),
        "slotCount": len(slots),
        "readySlotCount": ready_count,
        "ambiguousSlotCount": ambiguous_count,
        "textureDescriptorCount": sum(len(rows) for rows in grouped.values()),
        "materialDescriptorCount": sum(1 for rows in grouped.values() for row in rows if row.get("source") == "material_slot"),
        "slots": slots,
        "warnings": warnings,
    }
