from __future__ import annotations

from pathlib import Path
from typing import Any, Protocol

from liverylab_core.proto import patch_strings_recursive_exact

from . import driver_skin


class WriterLike(Protocol):
    def copy_file(self, source: str | Path, target: str | Path, label: str = "", backup: bool = True) -> Any: ...

    def write_bytes(self, target: str | Path, data: bytes, label: str = "", backup: bool = True) -> Any: ...


DEFAULT_RACING_VISUAL_PRESET = "content\\characters\\0_driver\\driver_male_racing.visualdriverpreset"
DEFAULT_RACING_DRIVER_PRESET = "content\\characters\\0_driver\\driver_male_racing.driverpreset"

DEFAULT_RACING_MATERIALS = (
    "content\\characters\\1_helmets\\helmet_1\\materials\\RT_HELMET_1_SKIN.material",
    "content\\characters\\1_helmets\\helmet_1\\materials\\RT_HELMET_1_Glass.material",
    "content\\characters\\2_suits\\suit_1\\RT_Suit_1.material",
    "content\\characters\\3_gloves\\gloves_1\\RT_gloves_1.material",
)

DEFAULT_RACING_MESHES = (
    "content\\characters\\1_helmets\\helmet_1\\meshes\\helmet_1.mesh",
    "content\\characters\\2_suits\\suit_1\\meshes\\male_suit_1.mesh",
    "content\\characters\\3_gloves\\gloves_1\\meshes\\male_gloves_1.mesh",
)

DEFAULT_RACING_CAR_HINTS = (
    "_gt2",
    "_gt3",
    "_gt4",
    "_gtc",
    "_lms",
    "_st_",
    "_cup",
    "_challenge",
    "_trofeo",
    "_racing",
    "_race",
    "_hypercar",
)

DRIVERFINALSTATE_TEMPLATE_CANDIDATES = (
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\f2004_def.driverfinalstate",
    "content\\cars\\ks_ferrari_sf_25\\skins\\driver\\canada_16.driverfinalstate",
)

TEMPLATE_REPLACEMENTS = {
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\helmet_2\\materials\\RT_HELMET_2_SKIN.material": DEFAULT_RACING_MATERIALS[0],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\helmet_2\\materials\\RT_HELMET_2_Glass.material": DEFAULT_RACING_MATERIALS[1],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\suit_2\\RT_Suit_2.material": DEFAULT_RACING_MATERIALS[2],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\gloves_2\\RT_Gloves_2.material": DEFAULT_RACING_MATERIALS[3],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\helmet_2\\meshes\\helmet_2.mesh": DEFAULT_RACING_MESHES[0],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\suit_2\\mesh\\male_suit_2.mesh": DEFAULT_RACING_MESHES[1],
    "content\\cars\\ks_ferrari_f2004\\skins\\driver\\gloves_2\\meshes\\male_gloves_2.mesh": DEFAULT_RACING_MESHES[2],
    "content\\characters\\1_helmets\\helmet_2\\materials\\RT_HELMET_2_SKIN.material": DEFAULT_RACING_MATERIALS[0],
    "content\\characters\\1_helmets\\helmet_2\\materials\\RT_HELMET_2_Glass.material": DEFAULT_RACING_MATERIALS[1],
    "content\\characters\\2_suits\\suit_2\\RT_Suit_2.material": DEFAULT_RACING_MATERIALS[2],
    "content\\characters\\3_gloves\\gloves_2\\RT_Gloves_2.material": DEFAULT_RACING_MATERIALS[3],
    "content\\characters\\1_helmets\\helmet_2\\meshes\\helmet_2.mesh": DEFAULT_RACING_MESHES[0],
    "content\\characters\\2_suits\\suit_2\\mesh\\male_suit_2.mesh": DEFAULT_RACING_MESHES[1],
    "content\\characters\\3_gloves\\gloves_2\\meshes\\male_gloves_2.mesh": DEFAULT_RACING_MESHES[2],
    "RT_HELMET_2_SKIN": "RT_HELMET_1_SKIN",
    "RT_HELMET_2_Glass": "RT_HELMET_1_Glass",
    "RT_SUIT_Omp": "RT_SUIT_Sparco",
    "RT_SUIT_2": "RT_SUIT_Sparco",
    "RT_Gloves_2": "RT_Gloves_1",
}


def _resolve(root: str | Path, rel: str) -> Path:
    return Path(root) / Path(*driver_skin.split_content_path(rel))


def is_default_racing_driver_car(car_content_path: str | None) -> bool:
    car_id = Path(str(car_content_path or "").replace("\\", "/")).name.lower()
    return any(hint in car_id for hint in DEFAULT_RACING_CAR_HINTS)


def _target_driver_ref(car_content_path: str, skin_id: str, source_ref: str) -> str:
    category = driver_skin.classify_driver_ref(source_ref)
    name = Path(source_ref.replace("\\", "/")).name
    if category == "helmetGlass":
        folder = "helmet_1\\materials"
    elif category == "helmet":
        folder = "helmet_1\\materials" if name.lower().endswith(".material") else "helmet_1"
    elif category == "suit":
        folder = "suit_1"
    elif category == "gloves":
        folder = "gloves_1"
    else:
        folder = "materials"
    return driver_skin.normalize_game_rel_path(f"{car_content_path}\\skins\\{skin_id}\\driver\\{folder}\\{name}")


def _target_texture_ref(car_content_path: str, skin_id: str, source_ref: str, material_ref: str, slot_name: str) -> str:
    category = driver_skin.classify_driver_ref(source_ref, slot_name)
    name = Path(source_ref.replace("\\", "/")).name
    if category == "helmetGlass":
        folder = "helmet_1\\textures"
    elif category == "helmet":
        folder = "helmet_1\\textures"
    elif category == "suit":
        folder = "suit_1\\textures"
    elif category == "gloves":
        folder = "gloves_1\\textures"
    else:
        folder = f"{driver_skin.classify_driver_ref(material_ref)}\\textures"
    return driver_skin.normalize_game_rel_path(f"{car_content_path}\\skins\\{skin_id}\\driver\\{folder}\\{name}")


def _target_driverfinalstate_ref(car_content_path: str, skin_id: str) -> str:
    return driver_skin.normalize_game_rel_path(f"{car_content_path}\\skins\\{skin_id}\\driver\\default_racing.driverfinalstate")


def _copy_file(source: Path, target: Path, writer: WriterLike | None, label: str, overwrite: bool) -> None:
    if target.exists() and not overwrite:
        try:
            if source.read_bytes() == target.read_bytes():
                return
        except OSError:
            pass
        raise FileExistsError(f"Target file exists: {target}")
    if writer is not None:
        writer.copy_file(source, target, label=label, backup=True)
        return
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_bytes(source.read_bytes())


def _write_bytes(target: Path, data: bytes, writer: WriterLike | None, label: str, overwrite: bool) -> None:
    if target.exists() and not overwrite and target.read_bytes() != data:
        raise FileExistsError(f"Target file exists: {target}")
    if writer is not None:
        writer.write_bytes(target, data, label=label, backup=True)
        return
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_bytes(data)


def _normalized_replacements(replacements: dict[str, str]) -> dict[str, str]:
    result: dict[str, str] = {}
    for old, new in replacements.items():
        if not old or not new:
            continue
        old_norm = driver_skin._normalize_exact_text(driver_skin.normalize_game_rel_path(old))
        new_norm = driver_skin.normalize_game_rel_path(new)
        if old_norm and new_norm and old_norm.lower() != new_norm.lower():
            result[old_norm] = new_norm
    return result


def _find_driverfinalstate_template(root: Path) -> tuple[str, Path] | None:
    for rel in DRIVERFINALSTATE_TEMPLATE_CANDIDATES:
        path = _resolve(root, rel)
        if path.exists():
            return driver_skin.normalize_game_rel_path(rel), path
    for path in root.glob("content/cars/ks_*/skins/driver/*.driverfinalstate"):
        if path.exists():
            return driver_skin.normalize_game_rel_path(str(path.relative_to(root))), path
    return None


def localize_default_racing_driver_assets(
    game_root: str | Path,
    target_root: str | Path,
    car_content_path: str,
    skin_id: str,
    target_visual_path: str | Path,
    overwrite: bool = False,
    writer: WriterLike | None = None,
) -> dict[str, Any]:
    root = Path(game_root)
    output_root = Path(target_root)
    car_ref = driver_skin.normalize_game_rel_path(car_content_path).rstrip("\\")
    warnings: list[str] = []
    copied: list[str] = []
    patched: list[str] = []

    missing = [
        rel
        for rel in (*DEFAULT_RACING_MATERIALS, *DEFAULT_RACING_MESHES, DEFAULT_RACING_VISUAL_PRESET, DEFAULT_RACING_DRIVER_PRESET)
        if not _resolve(root, rel).exists()
    ]
    if missing:
        return {"plan": {}, "copied": copied, "patched": patched, "copiedCount": 0, "patchedCount": 0, "warnings": [f"Default racing driver source missing: {missing[0]}"]}

    template = _find_driverfinalstate_template(root)
    if not template:
        return {"plan": {}, "copied": copied, "patched": patched, "copiedCount": 0, "patchedCount": 0, "warnings": ["No driver final state template was found."]}
    source_driverfinalstate_ref, source_driverfinalstate_path = template
    target_driverfinalstate_ref = _target_driverfinalstate_ref(car_ref, skin_id)
    target_driverfinalstate_path = _resolve(output_root, target_driverfinalstate_ref)

    material_refs: list[driver_skin.DriverMaterialRef] = []
    replacements: dict[str, str] = dict(TEMPLATE_REPLACEMENTS)

    for material_ref in DEFAULT_RACING_MATERIALS:
        source_material_path = _resolve(root, material_ref)
        target_material_ref = _target_driver_ref(car_ref, skin_id, material_ref)
        target_material_path = _resolve(output_root, target_material_ref)
        material_data = source_material_path.read_bytes()
        material_info = driver_skin.inspect_driver_material_data(material_data)
        texture_slots: list[driver_skin.DriverTextureSlot] = []
        replacements[material_ref] = target_material_ref

        for slot in material_info.get("textureSlots") or []:
            slot_name = str(slot.get("name") or "")
            texture_ref = driver_skin.normalize_game_rel_path(str(slot.get("value") or ""))
            if not texture_ref:
                continue
            source_texture_path = _resolve(root, texture_ref)
            if not source_texture_path.exists():
                warnings.append(f"Default racing texture was not found: {texture_ref}")
                continue
            target_texture_ref = _target_texture_ref(car_ref, skin_id, texture_ref, material_ref, slot_name)
            target_texture_path = _resolve(output_root, target_texture_ref)
            _copy_file(source_texture_path, target_texture_path, writer, "default racing driver texture", overwrite=overwrite)
            copied.append(str(target_texture_path))
            replacements[texture_ref] = target_texture_ref

            source_mips_path = source_texture_path.with_suffix(".texturemips")
            target_mips_ref = driver_skin.normalize_game_rel_path(str(Path(target_texture_ref).with_suffix(".texturemips")))
            target_mips_path = _resolve(output_root, target_mips_ref)
            mips_exists = source_mips_path.exists()
            if mips_exists:
                _copy_file(source_mips_path, target_mips_path, writer, "default racing driver texture mips", overwrite=overwrite)
                copied.append(str(target_mips_path))

            texture_slots.append(
                driver_skin.DriverTextureSlot(
                    material_ref=material_ref,
                    slot_name=slot_name,
                    texture_ref=texture_ref,
                    target_ref=target_texture_ref,
                    category=driver_skin.classify_driver_ref(texture_ref, slot_name),
                    source_path=str(source_texture_path),
                    target_path=str(target_texture_path),
                    mips_source_path=str(source_mips_path),
                    mips_target_path=str(target_mips_path),
                    exists=True,
                    mips_exists=mips_exists,
                )
            )

        material_replacements = _normalized_replacements(replacements)
        target_material_data = patch_strings_recursive_exact(material_data, material_replacements)
        neutral_parameters = driver_skin.png_only_neutral_parameters_for_category(driver_skin.classify_driver_ref(material_ref))
        if neutral_parameters:
            target_material_data = driver_skin.set_material_parameters_data(
                target_material_data,
                neutral_parameters,
            )
        _write_bytes(target_material_path, target_material_data, writer, "default racing driver material", overwrite=overwrite)
        copied.append(str(target_material_path))
        if target_material_data != material_data:
            patched.append(str(target_material_path))

        material_refs.append(
            driver_skin.DriverMaterialRef(
                source_ref=material_ref,
                target_ref=target_material_ref,
                category=driver_skin.classify_driver_ref(material_ref),
                source_path=str(source_material_path),
                target_path=str(target_material_path),
                exists=True,
                texture_slots=texture_slots,
                suit_layer_parameters=dict(material_info.get("suitLayerParameters") or {}),
            )
        )

    driverfinalstate_data = source_driverfinalstate_path.read_bytes()
    target_material_by_category = {material.category: material.target_ref for material in material_refs}
    default_mesh_by_category = {
        driver_skin.classify_driver_ref(mesh_ref): mesh_ref
        for mesh_ref in DEFAULT_RACING_MESHES
    }
    for material_ref in driver_skin.extract_driverfinalstate_material_refs(driverfinalstate_data):
        target_material_ref = target_material_by_category.get(driver_skin.classify_driver_ref(material_ref))
        if target_material_ref:
            replacements[material_ref] = target_material_ref
    for mesh_ref in driver_skin.extract_driverfinalstate_mesh_refs(driverfinalstate_data):
        default_mesh_ref = default_mesh_by_category.get(driver_skin.classify_driver_ref(mesh_ref))
        if default_mesh_ref:
            replacements[mesh_ref] = default_mesh_ref

    final_replacements = _normalized_replacements(replacements)
    local_driverfinalstate_data = patch_strings_recursive_exact(driverfinalstate_data, final_replacements)
    _write_bytes(target_driverfinalstate_path, local_driverfinalstate_data, writer, "default racing driver final state", overwrite=overwrite)
    copied.append(str(target_driverfinalstate_path))
    if local_driverfinalstate_data != driverfinalstate_data:
        patched.append(str(target_driverfinalstate_path))

    visual_path = Path(target_visual_path)
    visual_data = visual_path.read_bytes()
    local_visual_data = driver_skin.patch_visual_driverfinalstate_ref(visual_data, target_driverfinalstate_ref)
    if local_visual_data != visual_data:
        _write_bytes(visual_path, local_visual_data, writer, "visual default racing driver final state", overwrite=True)
        patched.append(str(visual_path))

    plan = driver_skin.DriverSkinPlan(
        car_content_path=car_ref,
        skin_id=skin_id,
        source_visual_ref="",
        target_visual_ref=driver_skin.normalize_game_rel_path(str(visual_path.relative_to(output_root))) if driver_skin._is_under_root(visual_path, output_root) else "",
        source_driverfinalstate_ref=source_driverfinalstate_ref,
        target_driverfinalstate_ref=target_driverfinalstate_ref,
        source_driverfinalstate_path=str(source_driverfinalstate_path),
        target_driverfinalstate_path=str(target_driverfinalstate_path),
        driver_materials=material_refs,
        warnings=warnings,
    )
    plan_dict = plan.to_dict()
    plan_dict["source"] = "default_racing_driver"
    plan_dict["source_visual_preset"] = DEFAULT_RACING_VISUAL_PRESET
    plan_dict["source_driver_preset"] = DEFAULT_RACING_DRIVER_PRESET
    plan_dict["source_meshes"] = list(DEFAULT_RACING_MESHES)
    return {
        "plan": plan_dict,
        "copied": copied,
        "patched": patched,
        "copiedCount": len(copied),
        "patchedCount": len(patched),
        "warnings": warnings,
    }
