from __future__ import annotations

from pathlib import Path
from typing import Any

from liverylab_core import texture_descriptors
from liverylab_core.proto import is_printable_utf8, parse_fields


BLANK_START_POINT_ID = "blank"
ORIGINAL_GAME_LIVERY_ID = "original_game_livery"

_TEXTURE_LABELS = {
    "ext_skin_c": "Base color",
    "ext_skin_d": "Spat map",
    "ext_skin_m": "Metalness",
    "ext_skin_r": "Roughness",
    "ext_skin_cc": "Clear coat",
    "ext_skin_cr": "Clear coat roughness",
}


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


def _rel(path: Path, root: Path | None) -> str:
    if root:
        try:
            return _norm(path.resolve().relative_to(root.resolve()))
        except Exception:
            pass
    return _norm(path)


def _root_path(root: str | Path | None) -> Path | None:
    if not root:
        return None
    try:
        return Path(root).expanduser().resolve()
    except Exception:
        return Path(str(root))


def _car_root(root: Path | None, car_id: str) -> Path | None:
    clean_car_id = str(car_id or "").strip()
    if not root or not clean_car_id:
        return None
    return root / "content" / "cars" / clean_car_id


def _resolve_root_path(root: Path | None, value: Any) -> Path | None:
    text = _norm(value)
    if not text:
        return None
    path = Path(text)
    if not path.is_absolute() and root:
        path = root / path
    return path


def _extract_design_slot_materials(design_data: bytes) -> dict[str, list[str]]:
    slots: dict[str, list[str]] = {}
    for field in parse_fields(design_data):
        if field.number != 3 or field.wire != 2:
            continue
        try:
            slot_fields = parse_fields(bytes(field.value))
        except Exception:
            continue
        slot_id = None
        slot_material = None
        for slot_field in slot_fields:
            if slot_field.wire != 2:
                continue
            text = is_printable_utf8(slot_field.value)
            if slot_field.number == 1:
                slot_id = text
            elif slot_field.number == 3:
                slot_material = text
        if slot_id and slot_material:
                slots.setdefault(slot_id, []).append(slot_material)
    return slots


def _extract_cardesign_design_path(cardesign_data: bytes) -> str:
    for field in parse_fields(cardesign_data):
        if field.number == 1 and field.wire == 2:
            text = is_printable_utf8(field.value)
            if text:
                return text
    return ""


def _input_material_candidates(root: Path | None, car_id: str, template: dict[str, Any] | None) -> list[Path]:
    candidates: list[Path] = []
    data = template if isinstance(template, dict) else {}

    def add(value: Any) -> None:
        path = _resolve_root_path(root, value)
        if path is None:
            return
        if path.suffix.lower() == ".material":
            candidates.append(path)

    for key in ("materialPath", "material", "sourceMaterialPath", "sourceMaterial"):
        add(data.get(key))

    design_path = _resolve_root_path(root, data.get("design") or data.get("designPath"))
    if not design_path:
        cardesign_path = _resolve_root_path(root, data.get("cardesign") or data.get("cardesignPath"))
        if cardesign_path and cardesign_path.exists() and cardesign_path.is_file():
            try:
                design_ref = _extract_cardesign_design_path(cardesign_path.read_bytes())
            except Exception:
                design_ref = ""
            design_path = _resolve_root_path(root, design_ref) if design_ref else None
    if design_path and design_path.exists() and design_path.is_file():
        try:
            slot_materials = _extract_design_slot_materials(design_path.read_bytes())
        except Exception:
            slot_materials = {}
        for slot_id, material_refs in slot_materials.items():
            if not str(slot_id or "").strip().upper().startswith("EXT_SKIN"):
                continue
            for material_ref in material_refs:
                add(material_ref)

    unique: list[Path] = []
    seen: set[str] = set()
    for item in candidates:
        key = str(item).lower()
        if key in seen:
            continue
        seen.add(key)
        unique.append(item)
    return unique


def _texture_row(
    *,
    root: Path | None,
    car_path: Path,
    material_path: Path,
    slot_id: str,
    slot_name: str,
    texture_ref: str,
) -> dict[str, Any]:
    resolved = texture_descriptors._resolve_material_texture_ref(car_path, root, texture_ref)
    texture_path = resolved if resolved is not None else None
    mips_path = texture_path.with_suffix(".texturemips") if texture_path else None
    return {
        "slotId": slot_id,
        "label": _TEXTURE_LABELS.get(slot_id, slot_id),
        "materialSlot": slot_name,
        "materialPath": _rel(material_path, root),
        "materialValue": _norm(texture_ref),
        "sourceTexturePath": _rel(texture_path, root) if texture_path else "",
        "sourceTextureExists": bool(texture_path and texture_path.exists()),
        "sourceTexturemipsPath": _rel(mips_path, root) if mips_path else "",
        "sourceTexturemipsExists": bool(mips_path and mips_path.exists()),
        "targetPng": f"{slot_id}.png",
        "ready": bool(texture_path and texture_path.exists() and mips_path and mips_path.exists()),
    }


def _inspect_material(
    *,
    root: Path | None,
    car_path: Path,
    material_path: Path,
) -> tuple[list[dict[str, Any]], list[str]]:
    warnings: list[str] = []
    rows: list[dict[str, Any]] = []
    if not material_path.exists():
        warnings.append(f"{_rel(material_path, root)} not found")
        return rows, warnings
    try:
        material_info = texture_descriptors._inspect_material_texture_slots(material_path)
        mapping = texture_descriptors._material_slot_mapping(material_path, material_info, root)
        if not mapping:
            return rows, warnings
        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 texture_descriptors._is_default_material_texture_ref(texture_ref):
                continue
            slot_id = mapping.get(slot_name)
            if not slot_id or not slot_id.startswith("ext_skin"):
                continue
            rows.append(_texture_row(
                root=root,
                car_path=car_path,
                material_path=material_path,
                slot_id=slot_id,
                slot_name=slot_name,
                texture_ref=texture_ref,
            ))
    except Exception as exc:
        warnings.append(f"{_rel(material_path, root)}: {exc}")
    return rows, warnings


def blank_start_point() -> dict[str, Any]:
    return {
        "id": BLANK_START_POINT_ID,
        "mode": "blank",
        "label": "Blank livery",
        "description": "Starts from a neutral generated livery texture.",
        "available": True,
        "enabled": True,
        "source": "generated",
    }


def inspect_original_game_livery(
    *,
    root: str | Path | None,
    car_id: str,
    workflow: str = "advanced",
    template: dict[str, Any] | None = None,
) -> dict[str, Any]:
    workflow_key = str(workflow or "").strip().lower() or "advanced"
    game_root = _root_path(root)
    car_path = _car_root(game_root, car_id)
    result: dict[str, Any] = {
        "id": ORIGINAL_GAME_LIVERY_ID,
        "mode": ORIGINAL_GAME_LIVERY_ID,
        "label": "Original AC EVO livery",
        "description": "Uses the original loose-content livery textures as the creator start point.",
        "workflow": workflow_key,
        "available": False,
        "enabled": False,
        "source": "game_loose_content",
        "carId": str(car_id or "").strip(),
        "root": str(game_root or ""),
        "carRoot": str(car_path or ""),
        "materialPath": "",
        "textureCount": 0,
        "readyTextureCount": 0,
        "primaryTexturePath": "",
        "textures": [],
        "warnings": [],
    }

    if workflow_key != "advanced":
        result["warnings"].append("Original game livery start point is only available for the Advanced workflow.")
        return result
    if not game_root:
        result["warnings"].append("Game root is not configured.")
        return result
    if not car_path or not car_path.exists():
        result["warnings"].append("Loose content car folder was not found.")
        return result

    material_candidates = _input_material_candidates(game_root, car_id, template)
    if not material_candidates:
        result["warnings"].append("No explicit original EXT_SKIN material was provided for this start point.")
        return result

    all_warnings: list[str] = []
    best_rows: list[dict[str, Any]] = []
    best_material = Path()
    for material_path in material_candidates:
        rows, warnings = _inspect_material(root=game_root, car_path=car_path, material_path=material_path)
        all_warnings.extend(warnings)
        ready_count = sum(1 for row in rows if row.get("ready"))
        best_ready_count = sum(1 for row in best_rows if row.get("ready"))
        has_primary = any(row.get("slotId") == "ext_skin_c" and row.get("ready") for row in rows)
        best_has_primary = any(row.get("slotId") == "ext_skin_c" and row.get("ready") for row in best_rows)
        if has_primary and not best_has_primary or ready_count > best_ready_count or (rows and not best_rows):
            best_rows = rows
            best_material = material_path

    primary = next((row for row in best_rows if row.get("slotId") == "ext_skin_c"), None)
    splat_fallback = next((row for row in best_rows if row.get("slotId") == "ext_skin_d" and row.get("ready")), None)
    ready_start_point = bool((primary and primary.get("ready")) or splat_fallback)
    ready_count = sum(1 for row in best_rows if row.get("ready"))
    result.update({
        "available": ready_start_point,
        "enabled": ready_start_point,
        "materialPath": _rel(best_material, game_root) if str(best_material) else "",
        "textureCount": len(best_rows),
        "readyTextureCount": ready_count,
        "primaryTexturePath": str(primary.get("sourceTexturePath") or "") if primary else "",
        "fallbackTexturePath": str(splat_fallback.get("sourceTexturePath") or "") if splat_fallback else "",
        "startPointTextureMode": "base_color" if primary and primary.get("ready") else "splat_map_only" if splat_fallback else "",
        "textures": best_rows,
        "warnings": all_warnings,
    })
    if not best_rows and not result["warnings"]:
        result["warnings"].append("No ext_skin material texture slots were found for this car.")
    elif primary and not primary.get("ready") and not splat_fallback:
        result["warnings"].append("Original base color texture or .texturemips is missing.")
    return result


def list_start_points(
    *,
    root: str | Path | None,
    car_id: str,
    workflow: str = "",
    template: dict[str, Any] | None = None,
) -> dict[str, Any]:
    workflow_key = str(workflow or "").strip().lower() or "advanced"
    points = [blank_start_point()]
    original = inspect_original_game_livery(
        root=root,
        car_id=car_id,
        workflow=workflow_key,
        template=template,
    ) if workflow_key == "advanced" else None
    if original:
        points.append(original)
    return {
        "schemaVersion": 1,
        "workflow": workflow_key,
        "carId": str(car_id or "").strip(),
        "startPoints": points,
        "counts": {
            "total": len(points),
            "available": sum(1 for item in points if item.get("available")),
        },
        "warnings": list(original.get("warnings", [])) if isinstance(original, dict) else [],
    }
