from __future__ import annotations

from collections.abc import Callable
from pathlib import Path
from typing import Any

from liverylab_core import paint_only_livery


MeshInspector = Callable[[str | Path, str | Path | None], dict[str, Any]]
DesignSlotExtractor = Callable[[bytes], dict[str, list[str]]]
MaterialInspector = Callable[[str | Path, str | Path | None], dict[str, Any]]
ProgressCallback = Callable[[dict[str, Any]], None]

IGNORED_CAR_DIRS = {"common_assets", "common_phsx"}


def _safe_ratio(part: int, total: int) -> float:
    return round(part / total, 3) if total else 0.0


def _status_label(status: str) -> str:
    return {
        "ready": "Ready",
        "paint_only": "Ready",
        "unsupported": "Unsupported",
        "unknown": "Unknown",
    }.get(status, status)


def _skin_uv_metrics(mesh: dict[str, Any]) -> dict[str, Any]:
    indices = mesh.get("indices") or []
    uvs = mesh.get("uvs") or []
    material_blocks = mesh.get("material_blocks") or []
    offset = 0
    skin_indices: list[int] = []
    skin_blocks: list[str] = []

    for block in material_blocks:
        count = int(block.get("index_count") or 0)
        name = str(block.get("name") or "")
        if "SKIN" in name.upper():
            skin_blocks.append(name)
            skin_indices.extend(indices[offset : offset + count])
        offset += count

    used_indices = [index for index in skin_indices if isinstance(index, int) and 0 <= index < len(uvs)]
    unique_points = [uvs[index] for index in sorted(set(used_indices))]
    if not unique_points:
        return {
            "skinBlocks": skin_blocks,
            "skinIndexPct": _safe_ratio(len(skin_indices), len(indices)),
            "skinUvBBoxArea": 0.0,
            "skinUvUnique": 0,
            "skinUvValidRatio": 0.0,
        }

    us = [float(point[0]) for point in unique_points]
    vs = [float(point[1]) for point in unique_points]
    bbox_area = (max(us) - min(us)) * (max(vs) - min(vs))
    rounded_unique = len({(round(float(point[0]), 4), round(float(point[1]), 4)) for point in unique_points})
    nonzero = sum(1 for point in unique_points if abs(float(point[0])) > 1e-6 or abs(float(point[1])) > 1e-6)
    return {
        "skinBlocks": skin_blocks,
        "skinIndexPct": _safe_ratio(len(skin_indices), len(indices)),
        "skinUvBBoxArea": round(bbox_area, 3),
        "skinUvUnique": rounded_unique,
        "skinUvValidRatio": _safe_ratio(nonzero, len(unique_points)),
    }


def _content_ref_to_disk(game_root: Path, ref: str) -> Path:
    normalized = str(ref or "").replace("\\", "/").lstrip("/")
    return game_root / normalized


def _is_relevant_skin_texture_name(path: Path) -> bool:
    name = path.name.lower()
    return path.suffix.lower() in {".texture", ".texturemips", ".dds"} and any(token in name for token in ("skin", "livery", "body"))


def _is_basecolor_skin_texture_ref(slot_name: str, ref: str) -> bool:
    slot = slot_name.lower()
    name = Path(ref.replace("\\", "/")).name.lower()
    if "basecolor" in slot:
        return True
    return any(token in name for token in ("skin_c", "skin_base", "livery_c", "body_c"))


def _is_basecolor_skin_texture_file(path: Path) -> bool:
    name = path.name.lower()
    return path.suffix.lower() in {".texture", ".texturemips", ".dds"} and any(
        token in name for token in ("skin_c", "skin_base", "livery_c", "body_c")
    )


def _skin_local_texture_evidence(material_ref: str, game_root: Path, inspect_material: MaterialInspector) -> dict[str, Any]:
    material_disk = _content_ref_to_disk(game_root, material_ref)
    material_parent = material_disk.parent
    material_parent_ref = str(Path(str(material_ref).replace("\\", "/")).parent).replace("\\", "/").lower()
    texture_refs: list[str] = []
    basecolor_texture_refs: list[str] = []
    texture_files: list[str] = []
    basecolor_texture_files: list[str] = []

    if material_disk.exists():
        try:
            material = inspect_material(material_disk, root=game_root)
        except Exception:
            material = {}
        for slot in material.get("texture_slots", []):
            slot_name = str(slot.get("name") or "")
            value = str(slot.get("value") or "").replace("\\", "/")
            if not value:
                continue
            value_lower = value.lower()
            if value_lower.startswith(material_parent_ref + "/") and Path(value_lower).suffix in {".texture", ".texturemips", ".dds"}:
                texture_refs.append(value)
                if _is_basecolor_skin_texture_ref(slot_name, value):
                    basecolor_texture_refs.append(value)

    if material_parent.exists():
        texture_files = [str(path) for path in material_parent.iterdir() if path.is_file() and _is_relevant_skin_texture_name(path)]
        basecolor_texture_files = [str(path) for path in material_parent.iterdir() if path.is_file() and _is_basecolor_skin_texture_file(path)]

    return {
        "material": material_ref,
        "textureRefs": texture_refs,
        "basecolorTextureRefs": basecolor_texture_refs,
        "textureFiles": texture_files,
        "basecolorTextureFiles": basecolor_texture_files,
        "hasEvidence": bool(texture_refs or texture_files),
        "hasBasecolorEvidence": bool(basecolor_texture_refs or basecolor_texture_files),
    }


def _is_generated_liverylab_upgrade_material_ref(material_ref: str) -> bool:
    return Path(str(material_ref or "").replace("\\", "/")).name == paint_only_livery.MATERIAL_FILENAME


def _design_template_metrics(
    skins_dir: Path,
    game_root: Path,
    extract_design_slot_materials: DesignSlotExtractor,
    inspect_material: MaterialInspector,
) -> dict[str, Any]:
    designs = sorted(skins_dir.rglob("*.design")) if skins_dir.exists() else []
    ext_skin_refs: list[str] = []
    skin_local_evidence: list[dict[str, Any]] = []
    skin_local_refs = 0
    skin_local_texture_refs = 0
    skin_local_texture_files = 0
    skin_local_basecolor_refs = 0
    skin_local_basecolor_files = 0
    car_level_refs = 0
    missing_ext_skin = 0
    ignored_generated_liverylab_refs = 0

    for design in designs:
        try:
            slots = extract_design_slot_materials(design.read_bytes())
            refs = slots.get("EXT_SKIN") or []
        except Exception:
            refs = []
        if not refs:
            missing_ext_skin += 1
        for ref in refs:
            normalized = str(ref).replace("/", "\\")
            if _is_generated_liverylab_upgrade_material_ref(normalized):
                ignored_generated_liverylab_refs += 1
                continue
            ext_skin_refs.append(normalized)
            if "\\skins\\" in normalized.lower():
                skin_local_refs += 1
                evidence = _skin_local_texture_evidence(normalized, game_root, inspect_material)
                skin_local_evidence.append(evidence)
                skin_local_texture_refs += len(evidence["textureRefs"])
                skin_local_texture_files += len(evidence["textureFiles"])
                skin_local_basecolor_refs += len(evidence["basecolorTextureRefs"])
                skin_local_basecolor_files += len(evidence["basecolorTextureFiles"])
            else:
                car_level_refs += 1

    return {
        "designCount": len(designs),
        "extSkinRefCount": len(ext_skin_refs),
        "skinLocalRefCount": skin_local_refs,
        "skinLocalReadyRefCount": sum(1 for item in skin_local_evidence if item.get("hasBasecolorEvidence")),
        "skinLocalTextureRefCount": skin_local_texture_refs,
        "skinLocalTextureFileCount": skin_local_texture_files,
        "skinLocalBasecolorRefCount": skin_local_basecolor_refs,
        "skinLocalBasecolorFileCount": skin_local_basecolor_files,
        "carLevelRefCount": car_level_refs,
        "missingExtSkinCount": missing_ext_skin,
        "ignoredGeneratedLiveryLabRefCount": ignored_generated_liverylab_refs,
        "sampleExtSkinRefs": ext_skin_refs[:3],
        "sampleSkinLocalTextureRefs": [ref for item in skin_local_evidence for ref in item.get("textureRefs", [])][:3],
        "sampleSkinLocalTextureFiles": [Path(path).name for item in skin_local_evidence for path in item.get("textureFiles", [])][:3],
        "sampleSkinLocalBasecolorRefs": [ref for item in skin_local_evidence for ref in item.get("basecolorTextureRefs", [])][:3],
        "sampleSkinLocalBasecolorFiles": [Path(path).name for item in skin_local_evidence for path in item.get("basecolorTextureFiles", [])][:3],
    }


def _paint_only_upgrade_metrics(car_dir: Path, game_root: Path, design_metrics: dict[str, Any]) -> dict[str, Any]:
    template_material = game_root / Path(*paint_only_livery.split_content_path(paint_only_livery.TEMPLATE_MATERIAL_REL))
    template_exists = template_material.exists()
    car_level_refs = [
        str(ref)
        for ref in design_metrics.get("sampleExtSkinRefs", [])
        if paint_only_livery.is_car_level_ext_skin_material(f"content\\cars\\{car_dir.name}", str(ref))
    ]
    has_car_level_ext_skin = bool(car_level_refs) or int(design_metrics.get("carLevelRefCount") or 0) > 0
    skin_local_missing_basecolor = (
        int(design_metrics.get("skinLocalRefCount") or 0) > 0
        and int(design_metrics.get("skinLocalReadyRefCount") or 0) == 0
        and (
            int(design_metrics.get("skinLocalTextureRefCount") or 0) > 0
            or int(design_metrics.get("skinLocalTextureFileCount") or 0) > 0
        )
    )
    upgrade_mode = ""
    if has_car_level_ext_skin:
        upgrade_mode = "car_level_ext_skin"
    elif skin_local_missing_basecolor:
        upgrade_mode = "skin_local_missing_basecolor"
    return {
        "liveryUpgradeAvailable": bool((has_car_level_ext_skin or skin_local_missing_basecolor) and template_exists),
        "liveryUpgradeMode": upgrade_mode if template_exists else "",
        "liveryUpgradeTemplate": paint_only_livery.TEMPLATE_MATERIAL_REL if template_exists else "",
    }


def analyze_car(
    car_dir: Path,
    game_root: Path,
    inspect_mesh_readable: MeshInspector,
    extract_design_slot_materials: DesignSlotExtractor,
    inspect_material: MaterialInspector,
) -> dict[str, Any]:
    result: dict[str, Any] = {
        "carId": car_dir.name,
        "status": "unknown",
        "statusLabel": "Unknown",
        "reasons": [],
        "missing": [],
        "mesh": None,
        "metrics": {},
    }

    meshes_dir = car_dir / "meshes"
    exterior_meshes = sorted(meshes_dir.glob("*exterior*.mesh")) if meshes_dir.exists() else []
    design_metrics = _design_template_metrics(car_dir / "skins", game_root, extract_design_slot_materials, inspect_material)
    result["metrics"].update(design_metrics)
    upgrade_metrics = _paint_only_upgrade_metrics(car_dir, game_root, design_metrics)
    result["metrics"].update(upgrade_metrics)

    if not exterior_meshes:
        result["status"] = "unsupported"
        result["statusLabel"] = _status_label("unsupported")
        result["missing"].append("Exterior mesh")
        result["reasons"].append("No exterior mesh was found.")
        return result

    mesh_path = exterior_meshes[0]
    try:
        mesh = inspect_mesh_readable(mesh_path, root=game_root)
    except Exception as exc:
        result["status"] = "unknown"
        result["statusLabel"] = _status_label("unknown")
        result["missing"].append("Readable exterior mesh")
        result["reasons"].append(f"Exterior mesh could not be inspected: {exc}")
        return result

    uv_metrics = _skin_uv_metrics(mesh)
    result["mesh"] = {
        "path": str(mesh_path),
        "name": mesh_path.name,
        "vertexCount": int(mesh.get("vertex_count") or 0),
        "uvCount": int(mesh.get("uv_count") or 0),
        "triangleCount": int(mesh.get("triangle_count") or 0),
        "materialCount": len(mesh.get("material_blocks") or []),
    }
    result["metrics"].update(uv_metrics)

    if not uv_metrics["skinBlocks"]:
        result["status"] = "unsupported"
        result["missing"].append("EXT_SKIN material block")
        result["reasons"].append("Exterior mesh has no EXT_SKIN material block.")
    elif uv_metrics["skinUvValidRatio"] < 0.5 or uv_metrics["skinUvBBoxArea"] < 0.01 or uv_metrics["skinUvUnique"] < 100:
        result["status"] = "unsupported"
        result["missing"].append("Usable EXT_SKIN UV area")
        result["reasons"].append("EXT_SKIN UV coverage is too small or mostly empty.")
    elif design_metrics["skinLocalReadyRefCount"] > 0:
        result["status"] = "ready"
        result["reasons"].append("Skin-local EXT_SKIN template includes local BaseColor texture evidence.")
        result["reasons"].append("EXT_SKIN material has mapped UV area.")
    elif upgrade_metrics["liveryUpgradeAvailable"]:
        result["status"] = "ready"
        result["reasons"].append("EXT_SKIN UVs are present and LiveryLab can generate a skin-local EXT_SKIN_C material.")
        if upgrade_metrics.get("liveryUpgradeMode") == "skin_local_missing_basecolor":
            result["reasons"].append("Skin-local EXT_SKIN material can be upgraded with a generated BaseColor texture.")
        else:
            result["reasons"].append("Generated livery upgrade template is available.")
    elif design_metrics["carLevelRefCount"] > 0 or design_metrics["skinLocalRefCount"] > 0:
        result["status"] = "unsupported"
        result["missing"].append("Generated skin-local EXT_SKIN_C/BaseColor setup")
        result["reasons"].append("EXT_SKIN UVs are present, but no native or generated skin-local BaseColor workflow is available.")
    else:
        result["status"] = "unsupported"
        result["missing"].append("EXT_SKIN design reference")
        result["reasons"].append("No usable EXT_SKIN reference was found in stock designs.")

    result["statusLabel"] = _status_label(str(result["status"]))
    return result


def analyze_cars(
    game_root: str | Path,
    inspect_mesh_readable: MeshInspector,
    extract_design_slot_materials: DesignSlotExtractor,
    inspect_material: MaterialInspector,
    progress: ProgressCallback | None = None,
) -> dict[str, Any]:
    root = Path(game_root).expanduser().resolve()
    cars_dir = root / "content" / "cars"
    if not cars_dir.is_dir():
        raise FileNotFoundError(f"Cars folder not found: {cars_dir}")

    car_dirs = [
        car_dir
        for car_dir in sorted(cars_dir.iterdir(), key=lambda path: path.name.lower())
        if car_dir.is_dir() and car_dir.name not in IGNORED_CAR_DIRS
    ]
    cars: list[dict[str, Any]] = []
    counts: dict[str, int] = {}
    total = len(car_dirs)

    if progress:
        progress({"phase": "start", "current": "", "index": 0, "total": total, "counts": counts})

    for index, car_dir in enumerate(car_dirs, start=1):
        if progress:
            progress({"phase": "checking", "current": car_dir.name, "index": index, "total": total, "counts": counts})
        car = analyze_car(car_dir, root, inspect_mesh_readable, extract_design_slot_materials, inspect_material)
        cars.append(car)
        status = str(car.get("status") or "unknown")
        counts[status] = counts.get(status, 0) + 1
        if progress:
            progress(
                {
                    "phase": "checked",
                    "current": car_dir.name,
                    "index": index,
                    "total": total,
                    "counts": counts,
                    "status": status,
                    "statusLabel": car.get("statusLabel"),
                }
            )

    if progress:
        progress({"phase": "done", "current": "", "index": total, "total": total, "counts": counts})

    return {
        "root": str(root),
        "carsDir": str(cars_dir),
        "carCount": len(cars),
        "counts": counts,
        "cars": cars,
    }
