from __future__ import annotations

import hashlib
import re
from pathlib import Path
from typing import Any, Callable


TextureConverter = Callable[[str | Path, str | Path, bool, bool], dict[str, Any]]
SkipReason = Callable[[Path], str]


THUMBNAIL_PNG_FILENAME = "thumbnail.png"


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


def _safe_id(value: Any, fallback: str = "item") -> str:
    raw = str(value or "").strip().replace("\\", "/").rstrip("/").split("/")[-1].lower()
    raw = re.sub(r"[^a-z0-9._-]+", "_", raw)
    raw = re.sub(r"_+", "_", raw).strip("._-")
    return raw or fallback


def _root_relative(root: Path, path: str | Path | None) -> str:
    if not path:
        return ""
    try:
        return str(Path(path).resolve().relative_to(root.resolve())).replace("/", "\\")
    except Exception:
        return _norm(path)


def _path_key(value: str | Path | None) -> str:
    return str(value or "").replace("/", "\\").strip().lower()


def _path_keys(value: str | Path | None) -> set[str]:
    raw = str(value or "").strip()
    if not raw:
        return set()
    key = _path_key(raw)
    keys = {key} if key else set()
    content_index = key.find("content\\cars\\")
    if content_index >= 0:
        keys.add(key[content_index:])
    return keys


def _content_rel_to_disk(root: Path, rel: str | Path | None) -> Path | None:
    text = _norm(rel)
    if not text.lower().startswith("content\\"):
        return None
    return root / Path(*[part for part in text.split("\\") if part])


def _template_cardesign_keys(root: Path, template: dict[str, Any]) -> set[str]:
    keys: set[str] = set()

    def add(value: str | Path | None) -> None:
        keys.update(_path_keys(value))

    cardesign_path = template.get("cardesignPath") or ""
    cardesign_rel = template.get("cardesign") or _root_relative(root, cardesign_path)
    add(cardesign_path)
    add(cardesign_rel)
    add(_root_relative(root, cardesign_path))
    for value in (cardesign_rel, _root_relative(root, cardesign_path)):
        disk = _content_rel_to_disk(root, value)
        if disk is not None:
            add(disk)
    return keys


def _skin_folder_from_cardesign(path: str | Path | None) -> Path | None:
    if not path:
        return None
    cardesign = Path(str(path))
    parent = cardesign.parent
    if not parent.name:
        return None
    if parent.name.lower() == "skins":
        return parent / cardesign.stem
    return parent


def _is_liverylab_owned_template(root: Path, template: dict[str, Any], excluded_keys: set[str]) -> bool:
    cardesign_path = template.get("cardesignPath") or ""
    cardesign_rel = template.get("cardesign") or _root_relative(root, cardesign_path)
    keys = _template_cardesign_keys(root, template)
    if keys & excluded_keys:
        return True
    skin_folder = _skin_folder_from_cardesign(cardesign_path)
    if skin_folder and (skin_folder / "liverylab_skin.json").is_file():
        return True
    text = " ".join(
        str(value or "")
        for value in (
            cardesign_path,
            cardesign_rel,
            template.get("design"),
            template.get("visualPreset"),
            template.get("mechanicalPreset"),
            template.get("carFinalState"),
        )
    ).replace("/", "\\").lower()
    return "\\liverylibrary\\" in text or "\\liveryprojects\\" in text


def _is_original_template(root: Path, template: dict[str, Any], included_keys: set[str]) -> bool:
    if not included_keys:
        return True
    return bool(_template_cardesign_keys(root, template) & included_keys)


def _template_id(template: dict[str, Any], index: int) -> str:
    cardesign = _norm(template.get("cardesign") or template.get("cardesignPath"))
    digest = hashlib.sha1(cardesign.lower().encode("utf-8", errors="ignore")).hexdigest()[:10]
    name = Path(cardesign.replace("\\", "/")).stem or f"template_{index}"
    return f"{_safe_id(name, 'template')}_{digest}"


def _display_name_from_car_id(car_id: str) -> str:
    clean = str(car_id or "").strip()
    without_prefix = clean[3:] if clean.startswith("ks_") else clean
    parts = [part for part in without_prefix.split("_") if part]
    return " ".join(part.upper() if len(part) <= 3 else part[:1].upper() + part[1:] for part in parts) or clean or "Unknown car"


def _generated_thumbnail_source(root: Path, car_id: str, mechanical_rel: str, visual_rel: str) -> Path | None:
    if not car_id or not mechanical_rel or not visual_rel:
        return None
    mechanical_stem = Path(_norm(mechanical_rel).replace("\\", "/")).stem
    visual_stem = Path(_norm(visual_rel).replace("\\", "/")).stem
    if not mechanical_stem or not visual_stem:
        return None
    name = f"{car_id}-{mechanical_stem}-{visual_stem}.texture"
    return root / "content" / "cars" / car_id / "generated" / "thumbnails" / name


def _first_usable_thumbnail_source(root: Path, car_id: str, skip_reason: SkipReason | None) -> Path | None:
    folder = root / "content" / "cars" / car_id / "generated" / "thumbnails"
    if not folder.exists() or not folder.is_dir():
        return None
    candidates = [path for path in sorted(folder.glob("*.texture"), key=lambda item: item.name.lower()) if path.with_suffix(".texturemips").exists()]
    if not candidates:
        return None
    if skip_reason:
        usable = []
        for path in candidates:
            try:
                if not skip_reason(path):
                    usable.append(path)
            except Exception:
                continue
        if usable:
            return usable[0]
    return candidates[0]


def _convert_thumbnail(
    *,
    source_texture: Path | None,
    target_png: Path,
    convert_texture_to_png: TextureConverter,
    skip_reason: SkipReason | None,
) -> dict[str, Any]:
    result = {
        "sourceTexturePath": str(source_texture) if source_texture else "",
        "sourceTexturemipsPath": str(source_texture.with_suffix(".texturemips")) if source_texture else "",
        "thumbnailPath": str(target_png),
        "thumbnailExists": target_png.exists(),
        "status": "missing_source",
        "reason": "Generated thumbnail source not found",
        "converted": False,
    }
    if not source_texture:
        return result
    source_mips = source_texture.with_suffix(".texturemips")
    if not source_texture.exists():
        return result
    if not source_mips.exists():
        result["status"] = "missing_mips"
        result["reason"] = "Generated thumbnail .texturemips missing"
        return result
    if skip_reason:
        reason = skip_reason(source_texture)
        if reason:
            result["status"] = "unsupported_source"
            result["reason"] = reason
            return result
    try:
        target_png.parent.mkdir(parents=True, exist_ok=True)
        needs_convert = not target_png.exists() or source_mips.stat().st_mtime > target_png.stat().st_mtime
        if needs_convert:
            converted = convert_texture_to_png(source_texture, target_png, overwrite=True, create_dds=False)
            result["converted"] = not bool(converted.get("skipped"))
            result["conversion"] = converted
        result["thumbnailExists"] = target_png.exists()
        result["status"] = "generated" if result["converted"] else "ready"
        result["reason"] = ""
    except Exception as exc:
        result["status"] = "conversion_failed"
        result["reason"] = str(exc)
    return result


def build_catalog(
    *,
    root: str | Path,
    analysis: dict[str, Any],
    output_root: str | Path,
    convert_texture_to_png: TextureConverter,
    skip_reason: SkipReason | None = None,
    exclude_cardesign_paths: set[str | Path] | list[str | Path] | tuple[str | Path, ...] | None = None,
    include_cardesign_paths: set[str | Path] | list[str | Path] | tuple[str | Path, ...] | None = None,
) -> dict[str, Any]:
    game_root = Path(root).expanduser().resolve()
    thumb_root = Path(output_root)
    excluded_keys = {key for value in (exclude_cardesign_paths or []) for key in _path_keys(value)}
    included_keys = {key for value in (include_cardesign_paths or []) for key in _path_keys(value)}
    cars: list[dict[str, Any]] = []
    converted_count = 0
    thumbnail_count = 0
    warning_count = 0
    source_template_count = 0
    skipped_liverylab_count = 0
    skipped_non_original_count = 0

    for car_index, car in enumerate(analysis.get("cars") or [], start=1):
        if not isinstance(car, dict):
            continue
        car_id = str(car.get("carId") or "").strip()
        if not car_id:
            continue
        templates: list[dict[str, Any]] = []
        for template_index, template in enumerate(car.get("skinTemplates") or [], start=1):
            if not isinstance(template, dict):
                continue
            source_template_count += 1
            if _is_liverylab_owned_template(game_root, template, excluded_keys):
                skipped_liverylab_count += 1
                continue
            if not _is_original_template(game_root, template, included_keys):
                skipped_non_original_count += 1
                continue
            template_id = _template_id(template, template_index)
            mechanical = _norm(template.get("mechanicalPreset"))
            visual = _norm(template.get("visualPreset"))
            source = _generated_thumbnail_source(game_root, car_id, mechanical, visual)
            if not source or not source.exists():
                source = _first_usable_thumbnail_source(game_root, car_id, skip_reason)
            target_png = thumb_root / _safe_id(car_id, f"car_{car_index}") / f"{template_id}.png"
            thumb = _convert_thumbnail(
                source_texture=source,
                target_png=target_png,
                convert_texture_to_png=convert_texture_to_png,
                skip_reason=skip_reason,
            )
            if thumb.get("converted"):
                converted_count += 1
            if thumb.get("thumbnailExists"):
                thumbnail_count += 1
            if thumb.get("status") not in {"ready", "generated"}:
                warning_count += 1
            templates.append(
                {
                    "id": template_id,
                    "label": Path(str(template.get("cardesign") or template.get("cardesignPath") or f"template_{template_index}")).stem,
                    "status": template.get("status") or "unknown",
                    "ready": str(template.get("status") or "").lower() == "ready",
                    "message": template.get("message") or "",
                    "reasons": template.get("reasons") or [],
                    "missing": template.get("missing") or [],
                    "cardesign": template.get("cardesign") or _root_relative(game_root, template.get("cardesignPath")),
                    "cardesignPath": template.get("cardesignPath") or "",
                    "design": template.get("design") or "",
                    "visualPreset": visual,
                    "mechanicalPreset": mechanical,
                    "carFinalState": template.get("carFinalState") or "",
                    "actor": template.get("actor") or "",
                    **thumb,
                }
            )
        ready_templates = [template for template in templates if template.get("ready")]
        selected = next((template for template in ready_templates if template.get("thumbnailExists")), None) or (ready_templates[0] if ready_templates else None)
        metrics = car.get("metrics") if isinstance(car.get("metrics"), dict) else {}
        cars.append(
            {
                "carId": car_id,
                "displayName": _display_name_from_car_id(car_id),
                "status": car.get("status") or "unknown",
                "ready": bool(ready_templates),
                "templateCount": len(templates),
                "readyTemplateCount": len(ready_templates),
                "unsupportedTemplateCount": max(0, len(templates) - len(ready_templates)),
                "carType": car.get("carType") or "",
                "thumbnailPath": selected.get("thumbnailPath") if selected else "",
                "thumbnailExists": bool(selected and selected.get("thumbnailExists")),
                "selectedTemplateId": selected.get("id") if selected else "",
                "templates": templates,
                "metrics": metrics,
            }
        )

    ready_cars = [car for car in cars if car.get("ready")]
    return {
        "schemaVersion": 1,
        "workflow": "advanced_extracted_content",
        "thumbnailRoot": str(thumb_root),
        "creatorCars": cars,
        "readyCarCount": len(ready_cars),
        "carCount": len(cars),
        "thumbnailCount": thumbnail_count,
        "convertedThumbnailCount": converted_count,
        "warningCount": warning_count,
        "sourceTemplateCount": source_template_count,
        "skippedLiveryLabTemplateCount": skipped_liverylab_count,
        "skippedNonOriginalTemplateCount": skipped_non_original_count,
        "originalTemplateWhitelistCount": len(included_keys),
    }
