from __future__ import annotations

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

from liverylab_core import official_saved_car_state


TEMPLATE_META = "template_meta.json"
DEFAULT_TEMPLATE_NAME = "base.carfinalstatewithconsumable"
SOURCE_NONE = "none"
SOURCE_USER_SAVED_CAR = "user_saved_car"
SOURCE_IMPORTED_PACKAGE = "imported_package_saved_car"
SOURCE_VENDOR_TEMPLATE = "vendor_template"
SOURCE_LIVERYLAB_CLONED_TEMPLATE = "liverylab_cloned_template"
SOURCE_UNKNOWN = "unknown"
SOURCE_TYPES = {
    SOURCE_NONE,
    SOURCE_USER_SAVED_CAR,
    SOURCE_IMPORTED_PACKAGE,
    SOURCE_VENDOR_TEMPLATE,
    SOURCE_LIVERYLAB_CLONED_TEMPLATE,
    SOURCE_UNKNOWN,
}


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


def car_template_root(templates_root: str | Path, car_model: Any, *, create: bool = False) -> Path:
    root = Path(templates_root).expanduser() / _safe_segment(car_model)
    if create:
        root.mkdir(parents=True, exist_ok=True)
    return root


def default_template_path(templates_root: str | Path, car_model: Any) -> Path:
    return car_template_root(templates_root, car_model) / DEFAULT_TEMPLATE_NAME


def _read_json(path: Path) -> dict[str, Any]:
    if not path.exists() or not path.is_file():
        return {}
    try:
        data = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return data if isinstance(data, dict) else {}


def _sha256_file(path: Path) -> str:
    hasher = hashlib.sha256()
    with path.open("rb") as stream:
        for chunk in iter(lambda: stream.read(1024 * 1024), b""):
            hasher.update(chunk)
    return hasher.hexdigest()


def _modified_iso(path: Path) -> str:
    try:
        return datetime.fromtimestamp(path.stat().st_mtime).isoformat(timespec="seconds")
    except Exception:
        return ""


def _source_type(value: Any) -> str:
    text = str(value or "").strip().lower()
    return text if text in SOURCE_TYPES else SOURCE_UNKNOWN


def _template_meta_for_path(path: Path) -> dict[str, Any]:
    folder_meta = _read_json(path.parent / TEMPLATE_META)
    file_meta = _read_json(path.with_suffix(path.suffix + ".json"))
    merged = dict(folder_meta)
    merged.update(file_meta)
    return merged


def inspect_template_file(path: str | Path, *, expected_car_model: str = "") -> dict[str, Any]:
    item = Path(path).expanduser()
    meta = _template_meta_for_path(item)
    exists = item.exists() and item.is_file()
    inspection = official_saved_car_state.inspect_saved_car_file(item) if exists else {}
    expected_car = _safe_segment(expected_car_model or meta.get("carModel") or item.parent.name)
    inspected_car = _safe_segment(inspection.get("carModel") or "")
    filename_guid = official_saved_car_state.normalize_guid(inspection.get("filenameGuid"))
    car_guid = official_saved_car_state.normalize_guid(inspection.get("carGuid"))
    warnings = list(inspection.get("warnings") or [])
    errors = list(inspection.get("errors") or [])

    if not exists:
        errors.append(f"SavedCar template file is missing: {item}")
    if exists and item.suffix.lower() != official_saved_car_state.SAVED_CAR_SUFFIX:
        errors.append(f"SavedCar template must use {official_saved_car_state.SAVED_CAR_SUFFIX}")
    if exists and not inspection.get("valid"):
        errors.append("SavedCar template is not a valid car final state wrapper.")
    if exists and not inspection.get("wrapped"):
        warnings.append("SavedCar template is a raw carfinalstate, not a carfinalstatewithconsumable wrapper.")
    if filename_guid and car_guid and filename_guid != car_guid:
        errors.append("SavedCar template filename GUID does not match the inner GUID.")
    if expected_car and inspected_car and inspected_car != expected_car:
        errors.append(f"SavedCar template car model is {inspected_car}, expected {expected_car}.")

    ready_for_linking = bool(exists and inspection.get("valid") and inspection.get("wrapped") and car_guid and not errors)
    clone_candidate = ready_for_linking
    return {
        "path": str(item),
        "name": item.name,
        "folder": str(item.parent),
        "exists": exists,
        "size": item.stat().st_size if exists else 0,
        "modifiedAt": _modified_iso(item),
        "sha256": _sha256_file(item) if exists else "",
        "sourceType": _source_type(meta.get("sourceType") or meta.get("source")),
        "sourceLabel": str(meta.get("sourceLabel") or meta.get("label") or "").strip(),
        "cloneAllowed": bool(meta.get("cloneAllowed", False)),
        "cloneCandidate": clone_candidate,
        "readyForLinking": ready_for_linking,
        "expectedCarModel": expected_car,
        "carModel": inspection.get("carModel") or "",
        "carGuid": car_guid,
        "filenameGuid": filename_guid,
        "savedCarStateValid": bool(inspection.get("valid")),
        "savedCarStateWrapped": bool(inspection.get("wrapped")),
        "savedCarStateFieldPath": str(inspection.get("fieldPath") or ""),
        "carData": inspection.get("carData") or "",
        "actorPath": inspection.get("actorPath") or "",
        "mechanicalPreset": inspection.get("mechanicalPreset") or "",
        "visualPreset": inspection.get("visualPreset") or "",
        "compatibleRim": inspection.get("compatibleRim") or "",
        "rimDesign": inspection.get("rimDesign") or "",
        "rimGuid": inspection.get("rimGuid") or "",
        "materialRefs": list(inspection.get("materialRefs") or []),
        "compatibleParts": list(inspection.get("compatibleParts") or []),
        "contentRefs": list(inspection.get("contentRefs") or []),
        "guids": list(inspection.get("guids") or []),
        "metadata": meta,
        "warnings": warnings,
        "errors": errors,
    }


def _candidate_files(root: Path, car_model: str = "") -> list[Path]:
    if not root.exists() or not root.is_dir():
        return []
    search_root = car_template_root(root, car_model) if car_model else root
    if not search_root.exists() or not search_root.is_dir():
        return []
    return sorted(
        (path for path in search_root.rglob(f"*{official_saved_car_state.SAVED_CAR_SUFFIX}") if path.is_file()),
        key=lambda item: str(item).lower(),
    )


def scan_templates(templates_root: str | Path, *, car_model: str = "") -> dict[str, Any]:
    root = Path(templates_root).expanduser()
    clean_car = _safe_segment(car_model, "") if car_model else ""
    items = [inspect_template_file(path, expected_car_model=clean_car or path.parent.name) for path in _candidate_files(root, clean_car)]
    ready_items = [item for item in items if item.get("readyForLinking")]
    clone_candidates = [item for item in ready_items if item.get("cloneCandidate")]
    return {
        "templatesRoot": str(root),
        "templatesRootExists": root.exists() and root.is_dir(),
        "carModel": clean_car,
        "items": items,
        "itemCount": len(items),
        "readyItems": ready_items,
        "readyCount": len(ready_items),
        "cloneCandidates": clone_candidates,
        "cloneCandidateCount": len(clone_candidates),
        "status": "ready" if ready_items else ("empty" if root.exists() else "missing"),
    }


def template_status_for_entry(entry: dict[str, Any] | None, templates_root: str | Path | None) -> dict[str, Any]:
    if not templates_root:
        return {}
    data = entry if isinstance(entry, dict) else {}
    car_model = str(data.get("car_model") or Path(str(data.get("car_content_path") or "")).name or "").strip()
    if not car_model:
        return {
            "templatesRoot": str(Path(templates_root).expanduser()),
            "status": "unknown_car",
            "items": [],
            "itemCount": 0,
            "readyCount": 0,
            "cloneCandidateCount": 0,
        }
    return scan_templates(templates_root, car_model=car_model)
