from __future__ import annotations

import json
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any

from liverylab_core import default_racing_driver, driver_skin


DRIVERS_DIRNAME = "Drivers"
DRIVER_MANIFEST_FILENAME = "manifest.json"
DRIVER_PROFILE_TYPE = "liverylab_driver_profile"
DRIVER_PROFILE_VERSION = 1
SUPPORTED_DRIVER_CARS = {"ks_ferrari_sf_25", "ks_ferrari_f2004"}
DRIVER_SUPPORT_MESSAGE = "Driver profiles are currently supported for Ferrari SF25 and Ferrari F2004 only."


def _json_ready(value: Any) -> Any:
    if isinstance(value, Path):
        return str(value)
    if isinstance(value, dict):
        return {str(key): _json_ready(item) for key, item in value.items()}
    if isinstance(value, (list, tuple, set)):
        return [_json_ready(item) for item in value]
    return value


def sanitize_slug(value: str, fallback: str = "driver") -> str:
    text = str(value or "").strip().lower()
    text = text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
    text = re.sub(r"[^a-z0-9_-]+", "_", text)
    text = re.sub(r"_+", "_", text).strip("_")
    return text or fallback


def is_driver_car_supported(car_model: str | None = None, car_content_path: str | None = None) -> bool:
    car_key = sanitize_slug(car_model or "", "")
    if not car_key and car_content_path:
        parts = driver_skin.split_content_path(str(car_content_path or ""))
        car_key = sanitize_slug(parts[-1] if parts else "", "")
    return car_key in SUPPORTED_DRIVER_CARS


def require_driver_car_supported(car_model: str | None = None, car_content_path: str | None = None) -> None:
    if not is_driver_car_supported(car_model, car_content_path):
        car_label = str(car_model or car_content_path or "").strip() or "this car"
        raise ValueError(f"{DRIVER_SUPPORT_MESSAGE} Current car: {car_label}.")


def driver_support_info(car_model: str | None = None, car_content_path: str | None = None) -> dict[str, Any]:
    return {
        "supported": is_driver_car_supported(car_model, car_content_path),
        "supportedCars": sorted(SUPPORTED_DRIVER_CARS),
        "message": DRIVER_SUPPORT_MESSAGE,
    }


def drivers_root(library_root: str | Path, create: bool = True) -> Path:
    root = Path(library_root).expanduser().resolve() / DRIVERS_DIRNAME
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def _manifest_paths(library_root: str | Path) -> list[Path]:
    root = drivers_root(library_root, create=False)
    if not root.exists():
        return []
    return sorted(
        (
            path
            for path in root.glob(f"*/*/{DRIVER_MANIFEST_FILENAME}")
            if not any(part.lower() in {"_trash", "trash"} for part in path.parts)
        ),
        key=lambda path: str(path).lower(),
    )


def _read_manifest(path: Path) -> dict[str, Any] | None:
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return None
    if not isinstance(data, dict):
        return None
    data["manifestPath"] = str(path)
    data["assetRoot"] = str(path.parent)
    return data


def _write_manifest(profile_root: Path, manifest: dict[str, Any]) -> Path:
    profile_root.mkdir(parents=True, exist_ok=True)
    manifest_path = profile_root / DRIVER_MANIFEST_FILENAME
    manifest_path.write_text(json.dumps(_json_ready(manifest), indent=2, ensure_ascii=False), encoding="utf-8")
    return manifest_path


def list_driver_profiles(library_root: str | Path, car_model: str | None = None) -> dict[str, Any]:
    requested_car = sanitize_slug(car_model or "", "") if car_model else ""
    items: list[dict[str, Any]] = []
    for manifest_path in _manifest_paths(library_root):
        manifest = _read_manifest(manifest_path)
        if not manifest:
            continue
        if requested_car and sanitize_slug(str(manifest.get("carModel") or "")) != requested_car:
            continue
        items.append(manifest)
    items.sort(key=lambda item: (str(item.get("carModel") or "").lower(), str(item.get("name") or item.get("id") or "").lower()))
    return {
        "libraryRoot": str(Path(library_root).expanduser().resolve()),
        "driversRoot": str(drivers_root(library_root, create=False)),
        "count": len(items),
        "items": _json_ready(items),
    }


def get_driver_profile(library_root: str | Path, profile_id: str, car_model: str | None = None) -> dict[str, Any] | None:
    profile_key = sanitize_slug(profile_id, "")
    if not profile_key:
        return None
    for manifest in list_driver_profiles(library_root, car_model).get("items", []):
        if sanitize_slug(str(manifest.get("id") or ""), "") == profile_key:
            return manifest
    return None


def _unique_profile_id(car_root: Path, requested: str) -> str:
    base = sanitize_slug(requested, "driver")
    candidate = base
    for index in range(2, 1000):
        if not (car_root / candidate / DRIVER_MANIFEST_FILENAME).exists():
            return candidate
        candidate = f"{base}_{index}"
    return f"{base}_{datetime.now().strftime('%H%M%S')}"


def _path_inside(path: Path, root: Path) -> bool:
    try:
        path.resolve().relative_to(root.resolve())
        return True
    except Exception:
        return False


def _rel_from_root(root: Path, path: Path) -> str:
    return str(path.resolve().relative_to(root.resolve())).replace("/", "\\")


def _source_roots(entry: dict[str, Any], local_root: Path) -> list[Path]:
    roots: list[Path] = [local_root]
    game_root_text = str(entry.get("game_root") or "").strip()
    if game_root_text:
        game_root = Path(game_root_text).expanduser().resolve()
        if game_root.exists() and all(game_root != root for root in roots):
            roots.append(game_root)
    return roots


def _resolve_ref_in_roots(ref: str, roots: list[Path]) -> tuple[str, Path] | None:
    rel = driver_skin.normalize_game_rel_path(ref)
    if not rel:
        return None
    parts = Path(*driver_skin.split_content_path(rel))
    for root in roots:
        candidate = root / parts
        if candidate.exists() and candidate.is_file():
            return rel, candidate.resolve()
    return rel, (roots[0] / parts).resolve() if roots else None


def _copy_file(source: Path, target: Path) -> None:
    target.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(source, target)


def _asset_files(asset_root: Path) -> list[tuple[str, Path]]:
    if not asset_root.exists():
        return []
    files: list[tuple[str, Path]] = []
    for path in sorted((item for item in asset_root.rglob("*") if item.is_file()), key=lambda item: str(item).lower()):
        rel = driver_skin.normalize_game_rel_path(str(path.relative_to(asset_root)))
        if rel:
            files.append((rel, path.resolve()))
    return files


def _collect_driver_profile_files(
    local_root: Path,
    skin_folder: Path,
    visual_path: Path,
    design_path: Path | None,
    roots: list[Path],
    driver_ref_override: str | None = None,
) -> list[tuple[str, Path]]:
    files: dict[str, tuple[str, Path]] = {}

    def add(path: Path | None) -> None:
        if path is None or not path.exists() or not path.is_file():
            return
        source_root = next((root for root in roots if _path_inside(path, root)), None)
        if not source_root:
            return
        rel = _rel_from_root(source_root, path)
        files[driver_skin.normalize_game_rel_path(rel).lower()] = (driver_skin.normalize_game_rel_path(rel), path.resolve())

    def add_ref(ref: str) -> Path | None:
        resolved = _resolve_ref_in_roots(ref, roots)
        if not resolved:
            return None
        rel, path = resolved
        if path and path.exists():
            files[rel.lower()] = (rel, path.resolve())
            return path
        return None

    def add_material_tree(material_ref: str) -> None:
        material_path = add_ref(material_ref)
        if not material_path:
            return
        try:
            material = driver_skin.inspect_driver_material_data(material_path.read_bytes())
        except Exception:
            material = {}
        for slot in material.get("textureSlots", []):
            texture_ref = driver_skin.normalize_game_rel_path(str(slot.get("value") or ""))
            if not texture_ref:
                continue
            texture_path = add_ref(texture_ref)
            if texture_path:
                add_ref(str(Path(texture_ref).with_suffix(".texturemips")))

    add(visual_path)
    add(design_path)
    driver_ref = driver_skin.normalize_game_rel_path(str(driver_ref_override or ""))
    if not driver_ref and visual_path.exists():
        try:
            driver_ref = driver_skin.extract_visual_driverfinalstate_ref(visual_path.read_bytes())
        except Exception:
            driver_ref = ""
    if driver_ref:
        driver_path = add_ref(driver_ref)
        if driver_path:
            try:
                driver_data = driver_path.read_bytes()
                driver_material_refs = driver_skin.extract_driverfinalstate_material_refs(driver_data)
                for material_ref in driver_skin.extract_driverfinalstate_material_refs(driver_data):
                    add_material_tree(material_ref)
                for mesh_ref in driver_skin.extract_driverfinalstate_mesh_refs(driver_data):
                    if not driver_skin.source_driver_mesh_should_localize(mesh_ref, driver_material_refs):
                        continue
                    mesh_path = add_ref(mesh_ref)
                    if not mesh_path or not mesh_path.exists():
                        continue
                    for material_ref in driver_skin.extract_mesh_material_refs(mesh_path.read_bytes()):
                        material_category = driver_skin.classify_driver_ref(material_ref)
                        if material_category not in driver_skin.DRIVER_VISUAL_MATERIAL_CATEGORIES:
                            continue
                        if not driver_skin.source_driver_material_for_mesh_material(mesh_ref, material_ref, driver_material_refs):
                            add_material_tree(material_ref)
            except Exception:
                pass
    if design_path and design_path.exists():
        try:
            slots = driver_skin.extract_design_driver_slots(design_path.read_bytes())
        except Exception:
            slots = {}
        for material_refs in slots.values():
            for material_ref in material_refs:
                add_material_tree(material_ref)
    return sorted(files.values(), key=lambda item: item[0].lower())


def _driver_signature(source_skin: dict[str, Any], design_slots: list[str]) -> dict[str, Any]:
    driver_meta = source_skin.get("driver_skin") if isinstance(source_skin.get("driver_skin"), dict) else {}
    return {
        "driverfinalstate": str(driver_meta.get("driverfinalstate") or ""),
        "materials": int(driver_meta.get("materials") or 0),
        "designSlots": sorted({str(slot) for slot in design_slots if str(slot)}),
    }


def _is_default_racing_driver_profile_source(driver_meta: dict[str, Any], car_content_path: str) -> bool:
    source = str(driver_meta.get("source") or "").strip().lower()
    return source == "default_racing_driver" and default_racing_driver.is_default_racing_driver_car(car_content_path)


def _find_game_root(roots: list[Path]) -> Path | None:
    for root in roots:
        if (root / "content" / "characters").exists() and (root / "content" / "cars").exists():
            return root
    return None


def _create_clean_default_racing_profile_files(
    game_root: Path,
    asset_root: Path,
    car_content_path: str,
    skin_id: str,
    visual_rel: str,
    visual_path: Path,
    design_rel: str,
    design_path: Path | None,
) -> list[tuple[str, Path]]:
    target_visual = asset_root / Path(*driver_skin.split_content_path(visual_rel))
    _copy_file(visual_path, target_visual)
    if design_rel and design_path and design_path.exists():
        _copy_file(design_path, asset_root / Path(*driver_skin.split_content_path(design_rel)))
    default_racing_driver.localize_default_racing_driver_assets(
        game_root,
        asset_root,
        car_content_path,
        skin_id,
        target_visual,
        overwrite=True,
    )
    return sorted(_asset_files(asset_root), key=lambda item: item[0].lower())


def infer_driver_metadata_from_skin_entry(skin_entry: dict[str, Any]) -> dict[str, Any]:
    entry = skin_entry if isinstance(skin_entry, dict) else {}
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    local_root_text = str(entry.get("local_root") or "").strip()
    if not local_root_text:
        return {}
    local_root = Path(local_root_text).expanduser().resolve()
    if not local_root.exists():
        return {}

    visual_rel = driver_skin.normalize_game_rel_path(str(files.get("visual_preset") or ""))
    if not visual_rel:
        return {}
    visual_path = local_root / Path(*driver_skin.split_content_path(visual_rel))
    if not visual_path.exists():
        return {}
    try:
        driver_ref = driver_skin.extract_visual_driverfinalstate_ref(visual_path.read_bytes())
    except Exception:
        return {}
    if not driver_ref:
        return {}

    roots = _source_roots(entry, local_root)
    resolved_driver = _resolve_ref_in_roots(driver_ref, roots)
    driver_path = resolved_driver[1] if resolved_driver else local_root / Path(*driver_skin.split_content_path(driver_ref))
    material_refs: list[str] = []
    if driver_path.exists():
        try:
            material_refs = driver_skin.extract_driverfinalstate_material_refs(driver_path.read_bytes())
        except Exception:
            material_refs = []

    design_slots: list[str] = []
    design_rel = driver_skin.normalize_game_rel_path(str(files.get("design") or ""))
    design_path = local_root / Path(*driver_skin.split_content_path(design_rel)) if design_rel else None
    if design_path and design_path.exists():
        try:
            design_slots = sorted(driver_skin.extract_design_driver_slots(design_path.read_bytes()))
        except Exception:
            design_slots = []

    return {
        "driverfinalstate": driver_ref,
        "local_driverfinalstate": driver_ref if driver_path.exists() else "",
        "materials": len(material_refs),
        "designSlots": design_slots,
        "source": "local_visual_preset",
        "complete": bool(driver_path.exists()),
    }


def has_usable_driver_metadata(driver_meta: dict[str, Any] | None) -> bool:
    meta = driver_meta if isinstance(driver_meta, dict) else {}
    driver_ref = str(
        meta.get("local_driverfinalstate")
        or meta.get("localDriverFinalState")
        or meta.get("driverfinalstate")
        or meta.get("driverFinalState")
        or ""
    ).strip()
    try:
        material_count = int(meta.get("materials") or meta.get("materialCount") or 0)
    except (TypeError, ValueError):
        material_count = 0
    return bool(driver_ref and material_count > 0)


def create_driver_profile_from_skin(
    library_root: str | Path,
    skin_entry: dict[str, Any],
    profile_name: str | None = None,
    profile_id: str | None = None,
    overwrite: bool = False,
) -> dict[str, Any]:
    entry = skin_entry if isinstance(skin_entry, dict) else {}
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    source_skin = entry.get("source_skin") if isinstance(entry.get("source_skin"), dict) else {}
    driver_meta = source_skin.get("driver_skin") if isinstance(source_skin.get("driver_skin"), dict) else {}

    local_root_text = str(entry.get("local_root") or "").strip()
    if not local_root_text:
        raise ValueError("Skin has no local library root.")
    local_root = Path(local_root_text).expanduser().resolve()
    if not local_root.exists():
        raise FileNotFoundError(f"Local skin root not found: {local_root}")

    visual_rel = driver_skin.normalize_game_rel_path(str(files.get("visual_preset") or ""))
    design_rel = driver_skin.normalize_game_rel_path(str(files.get("design") or ""))
    skin_folder_rel = driver_skin.normalize_game_rel_path(str(files.get("skin_folder") or ""))
    visual_path = local_root / Path(*driver_skin.split_content_path(visual_rel))
    design_path = local_root / Path(*driver_skin.split_content_path(design_rel)) if design_rel else None
    skin_folder = local_root / Path(*driver_skin.split_content_path(skin_folder_rel)) if skin_folder_rel else local_root
    if not visual_path.exists():
        raise FileNotFoundError(f"Local visual preset not found: {visual_rel}")
    if not driver_meta:
        driver_meta = infer_driver_metadata_from_skin_entry(entry)
    if not has_usable_driver_metadata(driver_meta):
        raise ValueError("Skin has no usable driver metadata. Create it from a driver-capable visual preset first.")
    source_skin = {**source_skin, "driver_skin": driver_meta}

    car_model = sanitize_slug(str(entry.get("car_model") or driver_skin.split_content_path(str(entry.get("car_content_path") or ""))[-1] or "car"), "car")
    require_driver_car_supported(car_model, entry.get("car_content_path"))
    name = str(profile_name or entry.get("display_name") or entry.get("skin_id") or "Driver").strip()
    car_root = drivers_root(library_root) / car_model
    clean_id = sanitize_slug(profile_id or name, "driver")
    if not overwrite:
        clean_id = _unique_profile_id(car_root, clean_id)
    profile_root = car_root / clean_id
    if profile_root.exists() and overwrite:
        shutil.rmtree(profile_root)
    elif profile_root.exists():
        raise FileExistsError(f"Driver profile already exists: {clean_id}")

    asset_root = profile_root / "assets"
    copied: list[str] = []
    roots = _source_roots(entry, local_root)
    car_content_path = driver_skin.norm_slashes(str(entry.get("car_content_path") or ""))
    source_driver_ref = driver_skin.normalize_game_rel_path(
        str(
            driver_meta.get("driverfinalstate")
            or driver_meta.get("driverFinalState")
            or driver_meta.get("local_driverfinalstate")
            or driver_meta.get("localDriverFinalState")
            or ""
        )
    )
    local_driver_ref = driver_skin.normalize_game_rel_path(
        str(driver_meta.get("local_driverfinalstate") or driver_meta.get("localDriverFinalState") or "")
    )
    if _is_default_racing_driver_profile_source(driver_meta, car_content_path):
        game_root = _find_game_root(roots)
        if not game_root:
            raise FileNotFoundError("Game root for clean default racing driver assets was not found.")
        source_files = _create_clean_default_racing_profile_files(
            game_root,
            asset_root,
            car_content_path,
            str(entry.get("skin_id") or ""),
            visual_rel,
            visual_path,
            design_rel,
            design_path,
        )
        copied = [rel for rel, _source in source_files]
    else:
        source_files = _collect_driver_profile_files(local_root, skin_folder, visual_path, design_path, roots, source_driver_ref)
        for rel, source in source_files:
            target = asset_root / Path(*driver_skin.split_content_path(rel))
            _copy_file(source, target)
            copied.append(rel)
        target_visual = asset_root / Path(*driver_skin.split_content_path(visual_rel))
        if source_driver_ref and target_visual.exists():
            raw_visual = target_visual.read_bytes()
            new_visual = driver_skin.patch_visual_driverfinalstate_ref(raw_visual, source_driver_ref)
            if new_visual != raw_visual:
                target_visual.write_bytes(new_visual)

    design_slots: list[str] = []
    if design_path and design_path.exists():
        try:
            design_slots = sorted(driver_skin.extract_design_driver_slots(design_path.read_bytes()))
        except Exception:
            design_slots = []
    now = datetime.now().isoformat(timespec="seconds")
    manifest = {
        "formatVersion": DRIVER_PROFILE_VERSION,
        "type": DRIVER_PROFILE_TYPE,
        "id": clean_id,
        "name": name,
        "carModel": car_model,
        "carContentPath": car_content_path,
        "sourceSkinKey": str(entry.get("key") or ""),
        "sourceSkinId": str(entry.get("skin_id") or ""),
        "sourceDisplayName": str(entry.get("display_name") or ""),
        "sourceVisualPreset": visual_rel,
        "localDriverFinalState": "" if source_driver_ref and source_driver_ref.lower() != local_driver_ref.lower() else local_driver_ref,
        "driverFinalState": source_driver_ref,
        "designSlots": design_slots,
        "signature": _driver_signature(source_skin, design_slots),
        "files": copied,
        "createdAt": now,
        "updatedAt": now,
    }
    manifest_path = _write_manifest(profile_root, manifest)
    return {
        "item": _json_ready({**manifest, "manifestPath": str(manifest_path), "assetRoot": str(profile_root)}),
        "copiedCount": len(copied),
        "copiedFiles": copied,
        "assetRoot": str(profile_root),
    }


def delete_driver_profile(library_root: str | Path, profile_id: str, car_model: str | None = None) -> dict[str, Any]:
    profile = get_driver_profile(library_root, profile_id, car_model)
    if not profile:
        raise FileNotFoundError(f"Driver profile not found: {profile_id}")
    asset_root = Path(str(profile.get("assetRoot") or ""))
    if not asset_root.exists():
        return {"deleted": False, "profileId": profile_id, "skipped": "asset root already missing"}
    trash_root = drivers_root(library_root) / "_trash"
    trash_root.mkdir(parents=True, exist_ok=True)
    stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    target = trash_root / f"{asset_root.name}_{stamp}"
    shutil.move(str(asset_root), str(target))
    return {"deleted": True, "profileId": profile_id, "trashDir": str(target), "item": profile}
