from __future__ import annotations

import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Iterable

from liverylab_core import official_saved_car_state, official_variant_compatibility


RAW_DIRNAME = "Raw"
DERIVED_DIRNAME = "Derived"
MANIFEST_FILENAME = "official_car_resources_manifest.json"
DERIVED_MANIFEST_FILENAME = "preview_assets.json"
SAVED_CAR_SOURCES_DIRNAME = "SavedCarSources"
CONTENT_CARS_PREFIX = "content\\cars\\"
TARGET_EXTENSIONS = {
    ".actor",
    ".carfinalstate",
    ".carpart",
    ".compatiblepart",
    ".material",
    ".mechanicalcarpreset",
    ".mesh",
    ".texture",
    ".texturemips",
    ".visualcarpreset",
}
EXCLUDED_CAR_FOLDERS = {"common_assets", "dummycar"}
EXCLUDED_FILENAME_PREFIXES = ("int_",)
EXTERIOR_MESH_SUFFIX = "_exterior.mesh"
MESHES_DIRNAME = "meshes"
AO_TEXTURE_STEMS = {"ext_skin_ao"}
GENERATED_THUMBNAILS_DIRNAME = "generated\\thumbnails"
THUMBNAIL_PNG_FILENAME = "thumbnail.png"
PRESETS_DIRNAME = "presets"
PRESET_EXTENSIONS = {".carfinalstate", ".carpart", ".compatiblepart", ".mechanicalcarpreset", ".visualcarpreset"}
EXCLUDED_CAR_SUBTREES = {
    ("displays",),
    ("presets", "aftermarket"),
    ("textures", "damage_dirt"),
}


def _is_ext_skin_ao_stem(stem: str) -> bool:
    clean = str(stem or "").strip().lower()
    return clean in AO_TEXTURE_STEMS or (clean.startswith("ext_skin") and "_ao" in clean)


def _normalize_entry_name(name: str) -> str:
    return str(name or "").replace("/", "\\").lstrip("\\")


def raw_root(preview_assets_root: str | Path, *, create: bool = False) -> Path:
    root = Path(preview_assets_root) / RAW_DIRNAME
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def derived_root(preview_assets_root: str | Path, *, create: bool = False) -> Path:
    root = Path(preview_assets_root) / DERIVED_DIRNAME
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def raw_content_cars_root(preview_assets_root: str | Path) -> Path:
    return raw_root(preview_assets_root) / "content" / "cars"


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


def raw_car_root(preview_assets_root: str | Path, car_id: Any) -> Path:
    return raw_content_cars_root(preview_assets_root) / safe_car_id(car_id)


def derived_car_root(preview_assets_root: str | Path, car_id: Any, *, create: bool = False) -> Path:
    root = derived_root(preview_assets_root) / safe_car_id(car_id)
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def relocate_derived_asset_path(
    current_derived_root: str | Path,
    value: str | Path | None,
    *,
    stored_derived_root: str | Path | None = None,
) -> Path | None:
    """Resolve a generated asset after a portable installation folder was moved."""
    text = str(value or "").strip()
    if not text:
        return None
    current_root = Path(current_derived_root)
    candidate = Path(text)
    if not candidate.is_absolute():
        return current_root / candidate
    stored_text = str(stored_derived_root or "").strip()
    if stored_text:
        try:
            relative = candidate.relative_to(Path(stored_text))
        except (OSError, ValueError):
            pass
        else:
            relocated = current_root / relative
            if relocated.exists():
                return relocated
    if candidate.exists():
        return candidate
    return candidate


def resolve_derived_manifest_asset(
    current_derived_root: str | Path,
    manifest: dict[str, Any],
    key: str,
    fallback_name: str,
) -> Path:
    current_root = Path(current_derived_root)
    resolved = relocate_derived_asset_path(
        current_root,
        manifest.get(key),
        stored_derived_root=manifest.get("derivedRoot"),
    )
    if resolved is not None and resolved.exists():
        return resolved
    return current_root / fallback_name


def derived_manifest_path(preview_assets_root: str | Path, car_id: Any) -> Path:
    return derived_car_root(preview_assets_root, car_id) / DERIVED_MANIFEST_FILENAME


def manifest_path(preview_assets_root: str | Path) -> Path:
    return Path(preview_assets_root) / MANIFEST_FILENAME


def is_target_entry_name(name: str) -> bool:
    normalized = _normalize_entry_name(name)
    lowered = normalized.lower()
    if not lowered.startswith(CONTENT_CARS_PREFIX) or lowered.endswith("\\"):
        return False
    parts = lowered.split("\\")
    if len(parts) < 4 or parts[0] != "content" or parts[1] != "cars":
        return False
    car_folder = parts[2]
    if not car_folder or car_folder in EXCLUDED_CAR_FOLDERS:
        return False
    for excluded in EXCLUDED_CAR_SUBTREES:
        end = 3 + len(excluded)
        if len(parts) >= end and tuple(parts[3:end]) == excluded:
            return False
    file_name = parts[-1]
    if file_name.startswith(EXCLUDED_FILENAME_PREFIXES):
        return False
    suffix = Path(lowered).suffix
    if suffix in {".actor", ".material", ".texture"}:
        return True
    if suffix in PRESET_EXTENSIONS:
        return len(parts) >= 5 and parts[3] == PRESETS_DIRNAME
    in_generated_thumbnails = len(parts) >= 5 and "\\".join(parts[3:-1]) == GENERATED_THUMBNAILS_DIRNAME
    if suffix == ".texturemips":
        if in_generated_thumbnails:
            return True
        stem = Path(file_name).stem.lower()
        return _is_ext_skin_ao_stem(stem)
    if suffix != ".mesh":
        return False
    if len(parts) < 5 or parts[3] != MESHES_DIRNAME:
        return False
    if len(parts) == 5:
        return parts[-1].endswith(EXTERIOR_MESH_SUFFIX)
    if parts[4] in {"rim", "rims"}:
        return False
    return True


def filter_entries(entries: Iterable[Any]) -> list[Any]:
    return [entry for entry in entries if not getattr(entry, "is_directory", False) and is_target_entry_name(getattr(entry, "name", ""))]


def _file_count(root: Path) -> int:
    if not root.exists():
        return 0
    return sum(1 for path in root.rglob("*") if path.is_file())


def list_raw_car_ids(preview_assets_root: str | Path) -> list[str]:
    root = raw_content_cars_root(preview_assets_root)
    if not root.exists():
        return []
    values: list[str] = []
    for path in sorted(root.iterdir(), key=lambda item: item.name.lower()):
        if not path.is_dir():
            continue
        car_id = safe_car_id(path.name, "")
        if not car_id or car_id in EXCLUDED_CAR_FOLDERS:
            continue
        values.append(car_id)
    return values


def build_manifest(
    *,
    pack_path: str | Path,
    preview_assets_root: str | Path,
    raw_preview_assets_root: str | Path | None = None,
    matched_entries: Iterable[Any],
    extraction_result: dict[str, Any],
) -> dict[str, Any]:
    pack = Path(pack_path)
    raw_context = Path(raw_preview_assets_root) if raw_preview_assets_root is not None else Path(preview_assets_root)
    entries = list(matched_entries)
    car_id_set: set[str] = set()
    for entry in entries:
        parts = _normalize_entry_name(getattr(entry, "name", "")).split("\\")
        if len(parts) >= 3:
            car_id = safe_car_id(parts[2], "")
            if car_id:
                car_id_set.add(car_id)
    car_ids = sorted(car_id_set)
    return {
        "schemaVersion": 1,
        "workflow": "official_external_livery",
        "createdAt": datetime.now(timezone.utc).isoformat(),
        "sourcePackage": str(pack),
        "sourcePackageName": pack.name,
        "sourcePackageSize": pack.stat().st_size if pack.exists() else None,
        "sourcePackageMtime": pack.stat().st_mtime if pack.exists() else None,
        "entryCount": len(entries),
        "carCount": len([car_id for car_id in car_ids if car_id]),
        "carIds": [car_id for car_id in car_ids if car_id],
        "fileCount": int(extraction_result.get("file_count", 0) or 0),
        "directoryCount": int(extraction_result.get("directory_count", 0) or 0),
        "encryptedCount": int(extraction_result.get("encrypted_count", 0) or 0),
        "extensions": sorted(TARGET_EXTENSIONS),
        "prefix": CONTENT_CARS_PREFIX,
        "excludedCarFolders": sorted(EXCLUDED_CAR_FOLDERS),
        "excludedCarSubtrees": ["\\".join(row) for row in sorted(EXCLUDED_CAR_SUBTREES)],
        "excludedFilenamePrefixes": list(EXCLUDED_FILENAME_PREFIXES),
        "meshRule": f"{CONTENT_CARS_PREFIX}<car_id>\\{MESHES_DIRNAME}\\*{EXTERIOR_MESH_SUFFIX}",
        "variantMeshRule": f"{CONTENT_CARS_PREFIX}<car_id>\\{MESHES_DIRNAME}\\<variant_parts>\\*.mesh except rim/rims",
        "presetRule": f"{CONTENT_CARS_PREFIX}<car_id>\\{PRESETS_DIRNAME}\\*.{{visualcarpreset,mechanicalcarpreset,carfinalstate,compatiblepart,carpart}}",
        "textureRule": f"{CONTENT_CARS_PREFIX}<car_id>\\**\\*.texture",
        "textureMipsRule": f"{CONTENT_CARS_PREFIX}<car_id>\\**\\ext_skin*_ao.texturemips",
        "thumbnailRule": f"{CONTENT_CARS_PREFIX}<car_id>\\{GENERATED_THUMBNAILS_DIRNAME}\\*.texture[mips]",
        "includedTextureMips": ["ext_skin*_ao.texturemips"],
        "includedGeneratedThumbnails": True,
        "excludedTextureMips": "except ambient occlusion preview maps and generated car thumbnails",
        "previewAssetsRoot": str(Path(preview_assets_root)),
        "rawPreviewAssetsRoot": str(raw_context),
        "rawRoot": str(raw_root(raw_context)),
        "rawContentCarsRoot": str(raw_content_cars_root(raw_context)),
        "derivedRoot": str(derived_root(preview_assets_root)),
    }


def write_manifest(
    preview_assets_root: str | Path,
    *,
    pack_path: str | Path,
    raw_preview_assets_root: str | Path | None = None,
    matched_entries: Iterable[Any],
    extraction_result: dict[str, Any],
) -> dict[str, Any]:
    root = Path(preview_assets_root)
    root.mkdir(parents=True, exist_ok=True)
    data = build_manifest(
        pack_path=pack_path,
        preview_assets_root=root,
        raw_preview_assets_root=raw_preview_assets_root,
        matched_entries=matched_entries,
        extraction_result=extraction_result,
    )
    manifest = manifest_path(root)
    manifest.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
    return data


def _derived_manifest_status(path: Path) -> dict[str, Any]:
    car_id = safe_car_id(path.parent.name, "")
    data: dict[str, Any] = {}
    error = ""
    if path.exists():
        try:
            data = json.loads(path.read_text(encoding="utf-8"))
        except Exception as exc:
            error = str(exc)
    glb_path = resolve_derived_manifest_asset(path.parent, data, "glbPath", "exterior.glb")
    uv_path = resolve_derived_manifest_asset(path.parent, data, "uvTemplatePath", "EXT_SKIN_UV.png")
    ao_path = resolve_derived_manifest_asset(path.parent, data, "aoTexturePath", "ext_skin_ao.png")
    thumbnail_path = resolve_derived_manifest_asset(path.parent, data, "thumbnailPath", THUMBNAIL_PNG_FILENAME)
    raw_variants = data.get("vehicleVariants") if isinstance(data.get("vehicleVariants"), list) else []
    compatibility_metadata_ready = bool(raw_variants) and all(
        isinstance(variant, dict) and "visualPresetExternalCustomizableKnown" in variant
        for variant in raw_variants
    )
    ready = bool(
        path.exists()
        and not error
        and data.get("ready") is True
        and glb_path.exists()
        and uv_path.exists()
        and compatibility_metadata_ready
    )
    variants: list[dict[str, Any]] = []
    preview_assets_root = path.parents[2]
    for raw_variant in raw_variants:
        if not isinstance(raw_variant, dict):
            continue
        variant, visual_status = official_variant_compatibility.enrich_vehicle_variant(
            preview_assets_root,
            raw_variant,
        )
        source_relative = str(variant.get("savedCarSource") or "").strip()
        source_parts = [part for part in source_relative.replace("\\", "/").split("/") if part]
        source_path = path.parent.joinpath(*source_parts) if source_parts else None
        if source_path is not None and source_path.exists() and source_path.is_file():
            inspection = official_saved_car_state.inspect_saved_car_file(source_path)
            compatibility = official_variant_compatibility.compatibility(inspection, variant)
        else:
            compatibility = {
                "compatible": False,
                "reason": "The prepared SavedCar source for this variant is missing.",
                "materialRefs": [],
                "materialStems": [],
                "neutralMaterialStems": [],
                "visualPresetExternalCustomizableKnown": bool(
                    variant.get("visualPresetExternalCustomizableKnown")
                ),
                "visualPresetExternalCustomizable": bool(variant.get("visualPresetExternalCustomizable")),
                "visualPresetStatus": visual_status,
            }
        variant["officialExternalCompatible"] = compatibility["compatible"]
        variant["officialExternalCompatibilityReason"] = compatibility["reason"]
        variant["officialExternalMaterialRefs"] = compatibility["materialRefs"]
        variant["officialExternalMaterialStems"] = compatibility["materialStems"]
        variant["officialExternalNeutralMaterialStems"] = compatibility["neutralMaterialStems"]
        variant["officialExternalCompatibilitySource"] = str(compatibility.get("compatibilitySource") or "")
        variant["officialExternalVisualPresetStatus"] = compatibility.get("visualPresetStatus") or visual_status
        variants.append(variant)
    compatible_variants = [variant for variant in variants if variant.get("officialExternalCompatible") is True]
    return {
        "carId": car_id,
        "ready": ready,
        "manifestPath": str(path),
        "manifestExists": path.exists(),
        "manifestError": error,
        "glbPath": str(glb_path),
        "glbExists": glb_path.exists(),
        "uvTemplatePath": str(uv_path),
        "uvTemplateExists": uv_path.exists(),
        "aoTexturePath": str(ao_path),
        "aoTextureExists": ao_path.exists(),
        "thumbnailPath": str(thumbnail_path),
        "thumbnailExists": thumbnail_path.exists(),
        "displayName": str(data.get("displayName") or ""),
        "carType": _classify_creator_car_type(car_id, data),
        "vehicleVariants": variants,
        "vehicleVariantCount": len(variants),
        "officialExternalCompatibleVariantCount": len(compatible_variants),
        "officialExternalBlockedVariantCount": len(variants) - len(compatible_variants),
        "officialExternalCompatibilityMetadataReady": compatibility_metadata_ready,
        "defaultVehicleVariantId": str(data.get("defaultVehicleVariantId") or ""),
    }


def _classify_creator_car_type(car_id: str, manifest: dict[str, Any]) -> str:
    clean_car_id = safe_car_id(car_id, "")
    known_race_car_ids = {
        "ks_audi_r8_lms_gt3_evo_2",
        "ks_audi_r8_lms_gt4_evo",
        "ks_bmw_m2_cs_racing",
        "ks_bmw_m4_gt3",
        "ks_caterham_academy",
        "ks_ferrari_296_gt3",
        "ks_ferrari_488_challenge_evo",
        "ks_ferrari_f2004",
        "ks_ferrari_f40_lm",
        "ks_ferrari_sf_25",
        "ks_ford_mustang_gt3",
        "ks_lamborghini_huracan_st_evo2",
        "ks_maserati_mc20_gt2",
        "ks_mazda_mx5_nd_cup",
        "ks_mercedes_amg_gt2",
        "ks_porsche_718_cayman_gt4_cs_mr",
        "ks_porsche_935",
        "ks_porsche_991ii_gt2_rs_clubsport_evo",
        "ks_porsche_992_gt3_cup",
        "ks_porsche_992_gt3_r_rennsport",
    }
    known_road_car_ids = {
        "ks_lamborghini_huracan_sto",
        "ks_lotus_emira",
        "ks_lotus_exige_v6_cup",
        "ks_porsche_718_cayman_gt4_rs",
        "ks_porsche_992_gt3_rs",
    }
    if clean_car_id in known_race_car_ids:
        return "race"
    if clean_car_id in known_road_car_ids:
        return "road"
    values: list[str] = [car_id]
    for key in ("displayName", "sourceMeshRelPath", "sourceMeshPath", "thumbnailSourceTexturePath"):
        values.append(str(manifest.get(key) or ""))
    variants = manifest.get("vehicleVariants") if isinstance(manifest.get("vehicleVariants"), list) else []
    for variant in variants[:12]:
        if not isinstance(variant, dict):
            continue
        for key in (
            "label",
            "displayLabel",
            "mechanicalPreset",
            "mechanicalPresetName",
            "mechanicalPresetShortName",
            "visualPreset",
            "visualPresetName",
            "visualPresetPresentation",
            "compatibleRim",
        ):
            values.append(str(variant.get(key) or ""))
    haystack = " ".join(values).lower()
    race_terms = (
        " gt2",
        " gt3",
        " gt4",
        " gte",
        " lmp",
        " lmh",
        " challenge",
        " competizione",
        " dtm",
        " formula",
        " f1",
        " indy",
        " lms",
        " touring",
        " trackday",
        " race",
        " racing",
        "_gt2",
        "_gt3",
        "_gt4",
        "_gte",
        "_lms",
    )
    if any(term in haystack for term in race_terms):
        return "race"
    return "road"


def derived_car_statuses(preview_assets_root: str | Path) -> list[dict[str, Any]]:
    root = derived_root(preview_assets_root)
    if not root.exists():
        return []
    rows: list[dict[str, Any]] = []
    for folder in sorted(root.iterdir(), key=lambda item: item.name.lower()):
        if not folder.is_dir():
            continue
        rows.append(_derived_manifest_status(folder / DERIVED_MANIFEST_FILENAME))
    return rows


def inspect(
    preview_assets_root: str | Path,
    *,
    pack_path: str | Path | None = None,
    raw_preview_assets_root: str | Path | None = None,
) -> dict[str, Any]:
    root = Path(preview_assets_root)
    derived = derived_root(root)
    manifest = manifest_path(root)
    manifest_data: dict[str, Any] = {}
    manifest_error = ""
    if manifest.exists():
        try:
            manifest_data = json.loads(manifest.read_text(encoding="utf-8"))
        except Exception as exc:
            manifest_error = str(exc)
    manifest_raw_context = str(manifest_data.get("rawPreviewAssetsRoot") or "").strip()
    if raw_preview_assets_root is not None:
        raw_context = Path(raw_preview_assets_root)
    elif manifest_raw_context:
        raw_context = Path(manifest_raw_context)
    else:
        raw_context = root
    raw = raw_root(raw_context)
    raw_cars = raw_content_cars_root(raw_context)
    file_count = _file_count(raw_cars)
    car_ids = list_raw_car_ids(raw_context)
    derived_statuses = derived_car_statuses(root)
    derived_ready_ids = [row["carId"] for row in derived_statuses if row.get("ready")]
    preparation = manifest_data.get("derivedPreparation") if isinstance(manifest_data.get("derivedPreparation"), dict) else {}
    expected_car_count = int(preparation.get("carCount") or manifest_data.get("carCount") or 0)
    failed_count = int(preparation.get("failedCount") or 0)
    derived_ready = bool(
        manifest.exists()
        and not manifest_error
        and failed_count == 0
        and (
            (expected_car_count > 0 and len(derived_ready_ids) >= expected_car_count)
            or (expected_car_count == 0 and bool(preparation.get("ready") is True) and len(derived_ready_ids) > 0)
            or (expected_car_count == 0 and len(derived_ready_ids) > 0)
        )
    )
    raw_ready = bool(manifest.exists() and raw_cars.exists() and file_count > 0 and not manifest_error)
    ready = derived_ready
    pack = Path(pack_path) if pack_path else None
    return {
        "ready": ready,
        "rawReady": raw_ready,
        "derivedReady": derived_ready,
        "previewAssetsRoot": str(root),
        "rawPreviewAssetsRoot": str(raw_context),
        "rawRoot": str(raw),
        "rawContentCarsRoot": str(raw_cars),
        "derivedRoot": str(derived),
        "manifestPath": str(manifest),
        "manifestExists": manifest.exists(),
        "manifestError": manifest_error,
        "rawContentCarsExists": raw_cars.exists(),
        "rawFileCount": file_count,
        "rawCarCount": len(car_ids),
        "rawCarIds": car_ids[:500],
        "derivedCarCount": len(derived_statuses),
        "derivedReadyCarCount": len(derived_ready_ids),
        "derivedCarIds": [row["carId"] for row in derived_statuses[:500]],
        "derivedReadyCarIds": derived_ready_ids[:500],
        "derivedStatuses": derived_statuses[:500],
        "creatorCars": derived_statuses[:500],
        "sourcePackage": str(pack) if pack else "",
        "sourcePackageExists": pack.exists() if pack else False,
        "manifest": manifest_data,
    }
