from __future__ import annotations

import hashlib
from pathlib import Path
from typing import Any

from liverylab_core import (
    official_external_liveries,
    official_external_project_assets,
    official_saved_car_state,
    official_variant_compatibility,
)


THUMBNAIL_FILENAME = "livery_thumbnail.png"


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


def _details(entry: dict[str, Any]) -> dict[str, Any]:
    value = entry.get("official_external")
    if not isinstance(value, dict):
        value = entry.get("officialExternal")
    return value if isinstance(value, dict) else {}


def _variant(entry: dict[str, Any], details: dict[str, Any]) -> dict[str, Any]:
    metadata = details.get("metadata") if isinstance(details.get("metadata"), dict) else {}
    value = details.get("vehicleVariant") or metadata.get("vehicleVariant") or entry.get("vehicle_variant") or entry.get("vehicleVariant")
    return dict(value) if isinstance(value, dict) else {}


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


def _candidate_paths(
    root: Path,
    entry: dict[str, Any],
    details: dict[str, Any],
    *,
    allow_linked_saved_car: bool,
) -> tuple[list[Path], list[Path]]:
    generated = details.get("generatedSavedCar") if isinstance(details.get("generatedSavedCar"), dict) else {}
    files = entry.get("files") if isinstance(entry.get("files"), dict) else {}
    local_values = [generated.get("relativePath"), files.get("saved_car")]
    local: list[Path] = []
    external: list[Path] = []
    seen: set[str] = set()

    def add(path: Path, target: list[Path]) -> None:
        try:
            key = str(path.resolve(strict=False)).casefold()
        except Exception:
            key = str(path).casefold()
        if key in seen:
            return
        seen.add(key)
        target.append(path)

    for value in local_values:
        text = str(value or "").strip()
        if not text:
            continue
        path = Path(text).expanduser()
        if not path.is_absolute():
            path = root / Path(*[part for part in text.replace("\\", "/").split("/") if part])
        try:
            path.resolve(strict=False).relative_to(root.resolve())
        except Exception:
            continue
        add(path, local)

    for path in sorted(root.rglob(f"*{official_saved_car_state.SAVED_CAR_SUFFIX}")):
        if path.is_file():
            add(path, local)

    if allow_linked_saved_car:
        linked = details.get("linkedSavedCar") if isinstance(details.get("linkedSavedCar"), dict) else {}
        linked_values = [
            linked.get("path"),
            generated.get("path"),
            *(details.get("savedCarPaths") if isinstance(details.get("savedCarPaths"), list) else []),
        ]
        for value in linked_values:
            text = str(value or "").strip()
            if text:
                add(Path(text).expanduser(), external)
    return local, external


def validate_payload(
    payload_root: str | Path,
    entry: dict[str, Any],
    *,
    allow_linked_saved_car: bool = False,
) -> dict[str, Any]:
    root = Path(payload_root).expanduser()
    details = _details(entry)
    errors: list[str] = []
    warnings: list[str] = []
    if not root.is_dir():
        return {
            "ready": False,
            "payloadRoot": str(root),
            "errors": [f"Official package payload folder is missing: {root}"],
            "warnings": [],
            "copyPlan": [],
            "savedCar": {},
            "assetStatus": {},
        }

    material_batch = str(details.get("materialBatch") or details.get("linkedSavedCarMaterialBatch") or "").strip()
    asset_status = official_external_project_assets.inspect_project(root, material_batch=material_batch)
    external_json = root / official_external_liveries.EXTERNAL_LIVERY_JSON
    if not external_json.is_file():
        errors.append("Official package payload is missing external_livery.json.")
    elif official_external_liveries.external_livery_json_has_bom(external_json):
        errors.append("Official package external_livery.json has an unsupported UTF-8 BOM.")
    else:
        try:
            external_data, _external_warnings = official_external_liveries._read_external_livery_json(external_json)
        except Exception as exc:
            errors.append(f"Official package external_livery.json is invalid: {exc}")
        else:
            if not official_saved_car_state.normalize_guid(external_data.get("car_guid")):
                errors.append("Official package external_livery.json has no valid car_guid.")

    copy_plan: list[dict[str, Any]] = []
    if external_json.is_file():
        copy_plan.append({
            "kind": "manifest",
            "name": external_json.name,
            "source": str(external_json),
            "slot": "",
            "label": external_json.name,
        })
    thumbnail = root / THUMBNAIL_FILENAME
    if thumbnail.is_file():
        try:
            from PIL import Image

            with Image.open(thumbnail) as image:
                image.verify()
                image_format = str(image.format or "").upper()
            if image_format != "PNG":
                raise ValueError(f"expected PNG data, found {image_format or 'unknown format'}")
        except Exception as exc:
            warnings.append(f"Official package thumbnail was ignored because it is invalid: {exc}")
        else:
            copy_plan.append({
                "kind": "thumbnail_png",
                "name": thumbnail.name,
                "source": str(thumbnail),
                "slot": "",
                "label": "Livery thumbnail",
            })
    for slot in asset_status.get("slots") or []:
        if not isinstance(slot, dict) or not slot.get("referenced"):
            continue
        relative = str(slot.get("relativePath") or slot.get("filename") or "").strip()
        source = Path(str(slot.get("path") or root / relative)).expanduser()
        if not source.is_file():
            errors.append(f"Official package referenced texture is missing: {relative or source.name}")
            continue
        copy_plan.append({
            "kind": "texture_png",
            "name": relative or source.name,
            "source": str(source),
            "slot": str(slot.get("id") or ""),
            "label": str(slot.get("label") or relative or source.name),
        })

    local_candidates, linked_candidates = _candidate_paths(
        root,
        entry,
        details,
        allow_linked_saved_car=allow_linked_saved_car,
    )
    existing_local = [path for path in local_candidates if path.is_file()]
    candidates = existing_local or [path for path in linked_candidates if path.is_file()]
    if len(existing_local) > 1:
        errors.append("Official package payload contains more than one SavedCar.")
    if not candidates:
        errors.append("Official package payload has no embedded SavedCar.")

    expected_guid = official_saved_car_state.normalize_guid(
        entry.get("car_guid")
        or entry.get("carGuid")
        or details.get("carGuid")
        or details.get("linkedSavedCarGuid")
        or asset_status.get("carGuid")
    )
    expected_car = str(entry.get("car_model") or entry.get("carModel") or "").strip().lower()
    variant = _variant(entry, details)
    selected: dict[str, Any] = {}
    valid_rows: list[dict[str, Any]] = []
    candidate_errors: list[str] = []
    for path in candidates:
        inspection = official_saved_car_state.inspect_saved_car_file(path)
        row_errors = [str(item) for item in inspection.get("errors") or [] if str(item).strip()]
        actual_guid = official_saved_car_state.official_livery_guid(inspection)
        actual_car = str(inspection.get("carModel") or "").strip().lower()
        if not inspection.get("valid") or not inspection.get("wrapped"):
            row_errors.append("SavedCar is not a valid wrapped car state.")
        if expected_guid and not official_saved_car_state.matches_official_livery_guid(inspection, expected_guid):
            row_errors.append("SavedCar GUID does not match the Official livery.")
        if expected_car and actual_car != expected_car:
            row_errors.append("SavedCar car model does not match the Official livery.")
        compatibility = official_variant_compatibility.compatibility(
            inspection,
            variant,
            preview_assets_root=None,
            require_visual_flag=False,
        )
        expected_visual = _normalized_ref(variant.get("visualPreset"))
        actual_visual = _normalized_ref(inspection.get("visualPreset"))
        if expected_visual and actual_visual and expected_visual != actual_visual:
            row_errors.append("SavedCar visual preset does not match the Official livery metadata.")
        if not compatibility.get("compatible"):
            row_errors.append(str(compatibility.get("reason") or "SavedCar is not compatible with Official External Liveries."))
        if not row_errors:
            valid_rows.append({"path": path, "inspection": inspection, "compatibility": compatibility})
        else:
            candidate_errors.extend(f"{path.name}: {message}" for message in dict.fromkeys(row_errors))
    if len(valid_rows) != 1:
        if candidates:
            errors.append("Official package payload does not contain exactly one compatible SavedCar.")
            errors.extend(candidate_errors)
    else:
        row = valid_rows[0]
        path = row["path"]
        inspection = row["inspection"]
        actual_guid = official_saved_car_state.official_livery_guid(inspection)
        generated = details.get("generatedSavedCar") if isinstance(details.get("generatedSavedCar"), dict) else {}
        actual_hash = _sha256(path)
        expected_hash = str(generated.get("sha256") or "").strip().lower()
        if expected_hash and actual_hash != expected_hash:
            errors.append("Official package SavedCar checksum does not match its metadata.")
        selected = {
            "path": str(path),
            "filename": path.name,
            "sha256": actual_hash,
            "size": path.stat().st_size,
            "carGuid": actual_guid,
            "instanceCarGuid": inspection.get("carGuid") or "",
            "liveryLinkerGuid": inspection.get("liveryLinkerGuid") or "",
            "carModel": inspection.get("carModel") or "",
            "visualPreset": inspection.get("visualPreset") or "",
            "compatibility": row["compatibility"],
            "embedded": bool(existing_local),
        }

    if asset_status.get("carGuid") and expected_guid and official_saved_car_state.normalize_guid(asset_status.get("carGuid")) != expected_guid:
        errors.append("external_livery.json car_guid does not match the Official package metadata.")
    return {
        "ready": not errors,
        "payloadRoot": str(root),
        "errors": errors,
        "warnings": warnings,
        "copyPlan": copy_plan,
        "copyPlanCount": len(copy_plan),
        "savedCar": selected,
        "assetStatus": asset_status,
    }
