from __future__ import annotations

import json
import re
import shutil
from pathlib import Path, PurePosixPath
from typing import Any

from liverylab_core import package_security, package_zip


EXTERNAL_LIVERY_JSON = "external_livery.json"
OFFICIAL_CONTENT_PARTS = ("content", "official_external_liveries")
OFFICIAL_PROJECT_TYPE = "official_external_livery"
SOURCE_VALIDATION_VERSION = 1
SOURCE_TYPE_OFFICIAL_PACKAGE = "official_package"
SOURCE_TYPE_EXTERNAL_FOLDER = "external_liveries_folder"
VALID_SOURCE_TYPES = {SOURCE_TYPE_OFFICIAL_PACKAGE, SOURCE_TYPE_EXTERNAL_FOLDER}
PROJECT_FOLDER_MARKERS = {"manifest.json", "liverylab_skin.json", "official_external_project.json", "league_pack.json", "layer-studio"}
CAR_GUID_PATTERN = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
BLOCKED_SUFFIXES = {".carfinalstate", ".carfinalstatewithconsumable"}
BLOCKED_DIRECTORY_NAMES = {"savedcar", "savedcars"}
MAX_EXTERNAL_FOLDER_NAME_LENGTH = 48
PREVIEW_NAMES = (
    "livery_thumbnail.png",
    "thumbnail.png",
    "preview.png",
    "snapshot.png",
)


def _read_json(path: Path) -> dict[str, Any]:
    try:
        data = json.loads(path.read_text(encoding="utf-8-sig"))
    except json.JSONDecodeError as exc:
        raise ValueError(f"{EXTERNAL_LIVERY_JSON} is not valid JSON: {exc}") from exc
    if not isinstance(data, dict):
        raise ValueError(f"{EXTERNAL_LIVERY_JSON} must contain a JSON object")
    return data


def _write_json(path: Path, data: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def _safe_relative_path(value: Any, label: str) -> Path:
    text = str(value or "").strip().replace("\\", "/")
    pure = PurePosixPath(text)
    if not text or pure.is_absolute() or ".." in pure.parts or ":" in text:
        raise ValueError(f"Invalid {label}: {value}")
    return Path(*pure.parts)


def validate_external_folder_name(value: Any) -> str:
    name = str(value or "").strip()
    if (
        not name
        or len(name) > MAX_EXTERNAL_FOLDER_NAME_LENGTH
        or name in {".", ".."}
        or any(char in name for char in '<>:"/\\|?*')
        or Path(name).name != name
    ):
        raise ValueError(f"Invalid ExternalLiveries folder name: {value}")
    return name


def _manifest_payload_root(source_root: Path) -> Path | None:
    manifest_path = source_root / "manifest.json"
    if not manifest_path.is_file():
        return None
    try:
        manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
    except Exception:
        return None
    if not isinstance(manifest, dict):
        return None
    official = manifest.get("officialExternal") if isinstance(manifest.get("officialExternal"), dict) else {}
    content_root = str(official.get("contentRoot") or "").strip()
    if not content_root:
        return None
    relative = _safe_relative_path(content_root, "Official package content root")
    candidate = source_root / relative
    return candidate if (candidate / EXTERNAL_LIVERY_JSON).is_file() else None


def validate_official_source(source_root: str | Path, source_kind: str) -> dict[str, Any]:
    root = Path(source_root).expanduser()
    kind = str(source_kind or "").strip().lower()
    if kind == "zip":
        manifest_path = root / "manifest.json"
        if not manifest_path.is_file():
            raise ValueError(
                "Official League packs only accept Official livery ZIPs. The selected ZIP has no LiveryLab manifest."
            )
        try:
            manifest = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
        except Exception as exc:
            raise ValueError(f"Official livery package manifest is invalid: {exc}") from exc
        if not isinstance(manifest, dict):
            raise ValueError("Official livery package manifest must contain a JSON object.")
        project_type = str(manifest.get("projectType") or "").strip().lower()
        skin = manifest.get("skin") if isinstance(manifest.get("skin"), dict) else {}
        skin_project_type = str(skin.get("projectType") or "").strip().lower()
        official = manifest.get("officialExternal") if isinstance(manifest.get("officialExternal"), dict) else {}
        content_root = str(official.get("contentRoot") or "").strip()
        if project_type != OFFICIAL_PROJECT_TYPE or (skin_project_type and skin_project_type != OFFICIAL_PROJECT_TYPE) or not content_root:
            raise ValueError(
                "Advanced livery packages cannot be used in Official League packs. Export this livery through the Official workflow first."
            )
        payload_root = _manifest_payload_root(root)
        if payload_root is None:
            raise ValueError("Official livery package does not contain a valid officialExternal.contentRoot payload.")
        return {
            "formatVersion": SOURCE_VALIDATION_VERSION,
            "verified": True,
            "sourceType": SOURCE_TYPE_OFFICIAL_PACKAGE,
            "projectType": OFFICIAL_PROJECT_TYPE,
            "contentRoot": content_root.replace("\\", "/"),
        }
    if kind == "folder":
        if not (root / EXTERNAL_LIVERY_JSON).is_file():
            raise ValueError(
                "Official League folder imports require one direct ExternalLiveries folder with external_livery.json in its root."
            )
        markers = sorted(name for name in PROJECT_FOLDER_MARKERS if (root / name).exists())
        if markers:
            raise ValueError(
                "Project folders cannot be added directly to Official League packs. Select the deployed ExternalLiveries folder instead."
            )
        return {
            "formatVersion": SOURCE_VALIDATION_VERSION,
            "verified": True,
            "sourceType": SOURCE_TYPE_EXTERNAL_FOLDER,
            "projectType": OFFICIAL_PROJECT_TYPE,
            "contentRoot": ".",
        }
    raise ValueError("Online League source must be an Official livery ZIP or one direct ExternalLiveries folder.")


def is_verified_official_source(value: Any) -> bool:
    if not isinstance(value, dict):
        return False
    try:
        format_version = int(value.get("formatVersion") or 0)
    except (TypeError, ValueError):
        return False
    return (
        format_version == SOURCE_VALIDATION_VERSION
        and value.get("verified") is True
        and str(value.get("sourceType") or "").strip() in VALID_SOURCE_TYPES
        and str(value.get("projectType") or "").strip().lower() == OFFICIAL_PROJECT_TYPE
    )


def find_payload_root(source_root: str | Path) -> Path:
    root = Path(source_root).expanduser()
    if not root.exists() or not root.is_dir():
        raise FileNotFoundError(f"Online League livery source folder not found: {root}")
    manifest_candidate = _manifest_payload_root(root)
    if manifest_candidate is not None:
        return manifest_candidate
    direct = root / EXTERNAL_LIVERY_JSON
    if direct.is_file():
        return root
    candidates = sorted(
        (path.parent for path in root.rglob(EXTERNAL_LIVERY_JSON) if path.is_file()),
        key=lambda path: path.as_posix().lower(),
    )
    preferred = []
    for candidate in candidates:
        parts = tuple(part.lower() for part in candidate.relative_to(root).parts)
        if len(parts) >= 3 and parts[:2] == OFFICIAL_CONTENT_PARTS:
            preferred.append(candidate)
    selected = preferred or candidates
    if not selected:
        raise ValueError(f"No {EXTERNAL_LIVERY_JSON} was found in the selected livery source.")
    if len(selected) != 1:
        preview = ", ".join(str(path.relative_to(root)) for path in selected[:5])
        raise ValueError(f"The selected source contains multiple ExternalLiveries payloads: {preview}")
    return selected[0]


def _texture_references(value: Any, field_path: str = "") -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    if isinstance(value, dict):
        for key, child in value.items():
            key_text = str(key)
            next_path = f"{field_path}.{key_text}" if field_path else key_text
            if key_text.endswith("_texture_path") and isinstance(child, str) and child.strip():
                rows.append({"field": next_path, "path": child.strip()})
            rows.extend(_texture_references(child, next_path))
    elif isinstance(value, list):
        for index, child in enumerate(value):
            next_path = f"{field_path}[{index}]" if field_path else f"[{index}]"
            rows.extend(_texture_references(child, next_path))
    return rows


def _blocked_file(relative: Path) -> bool:
    return relative.suffix.lower() in BLOCKED_SUFFIXES or any(part.lower() in BLOCKED_DIRECTORY_NAMES for part in relative.parts)


def inspect_payload(payload_root: str | Path) -> dict[str, Any]:
    root = Path(payload_root).expanduser()
    external_json = root / EXTERNAL_LIVERY_JSON
    errors: list[str] = []
    warnings: list[str] = []
    excluded: list[str] = []
    if not external_json.is_file():
        return {
            "payloadRoot": str(root),
            "folderName": root.name,
            "carGuid": "",
            "externalLiveryJson": "",
            "referencedFiles": [],
            "missingReferences": [],
            "excludedFiles": [],
            "errors": [f"{EXTERNAL_LIVERY_JSON} is missing."],
            "warnings": [],
            "ready": False,
        }
    data = _read_json(external_json)
    car_guid = str(data.get("car_guid") or "").strip()
    if not CAR_GUID_PATTERN.fullmatch(car_guid):
        errors.append("external_livery.json does not contain a valid car_guid.")
    references: list[dict[str, str]] = []
    missing: list[dict[str, str]] = []
    seen: set[str] = set()
    for row in _texture_references(data):
        try:
            relative = _safe_relative_path(row.get("path"), f"texture reference {row.get('field')}")
        except ValueError as exc:
            errors.append(str(exc))
            continue
        key = relative.as_posix().lower()
        if key in seen:
            continue
        seen.add(key)
        if _blocked_file(relative):
            errors.append(f"Blocked ExternalLiveries reference: {relative.as_posix()}")
            continue
        source = root / relative
        item = {"field": str(row.get("field") or ""), "path": relative.as_posix()}
        if not source.is_file():
            missing.append(item)
            errors.append(f"Referenced ExternalLiveries file is missing: {relative.as_posix()}")
        else:
            references.append(item)
    referenced_names = {row["path"].lower() for row in references}
    for path in sorted((item for item in root.rglob("*") if item.is_file()), key=lambda item: item.as_posix().lower()):
        relative = path.relative_to(root)
        name = relative.as_posix()
        if name.lower() == EXTERNAL_LIVERY_JSON or name.lower() in referenced_names:
            continue
        excluded.append(name)
    if external_json.read_bytes().startswith(b"\xef\xbb\xbf"):
        warnings.append("UTF-8 BOM will be removed from external_livery.json.")
    return {
        "payloadRoot": str(root),
        "folderName": root.name,
        "carGuid": car_guid.lower(),
        "externalLiveryJson": str(external_json),
        "externalLivery": data,
        "referencedFiles": references,
        "missingReferences": missing,
        "excludedFiles": excluded,
        "errors": errors,
        "warnings": warnings,
        "ready": not errors,
    }


def find_preview(source_root: str | Path, payload_root: str | Path) -> Path | None:
    root = Path(source_root).expanduser()
    payload = Path(payload_root).expanduser()
    candidates: list[tuple[int, Path]] = []
    wanted = {name.lower(): index for index, name in enumerate(PREVIEW_NAMES)}
    for path in root.rglob("*"):
        if not path.is_file() or path.suffix.lower() not in {".png", ".jpg", ".jpeg"}:
            continue
        try:
            path.relative_to(payload)
            inside_payload = True
        except ValueError:
            inside_payload = False
        name = path.name.lower()
        if not ("thumbnail" in name or "preview" in name or "snapshot" in name):
            continue
        score = wanted.get(name, 50)
        if "thumbnail" in name or "preview" in name or "snapshot" in name:
            score = min(score, 20)
        if inside_payload:
            score += 100
        candidates.append((score, path))
    return sorted(candidates, key=lambda row: (row[0], row[1].as_posix().lower()))[0][1] if candidates else None


def sanitize_payload(source_root: str | Path, target_root: str | Path) -> dict[str, Any]:
    source_container = Path(source_root).expanduser()
    payload_root = find_payload_root(source_container)
    inspection = inspect_payload(payload_root)
    if inspection.get("errors"):
        raise ValueError("; ".join(str(item) for item in inspection["errors"][:8]))
    external_folder_name = validate_external_folder_name(payload_root.name)
    target = Path(target_root).expanduser()
    if target.exists():
        shutil.rmtree(target)
    target.mkdir(parents=True, exist_ok=True)
    external_data = inspection.get("externalLivery") if isinstance(inspection.get("externalLivery"), dict) else {}
    if (payload_root / EXTERNAL_LIVERY_JSON).is_symlink():
        raise ValueError(f"Unsupported symlink in ExternalLiveries payload: {EXTERNAL_LIVERY_JSON}")
    _write_json(target / EXTERNAL_LIVERY_JSON, external_data)
    copied = [EXTERNAL_LIVERY_JSON]
    for row in inspection.get("referencedFiles") or []:
        relative = _safe_relative_path(row.get("path"), "ExternalLiveries texture path")
        source = payload_root / relative
        if source.is_symlink():
            raise ValueError(f"Unsupported symlink in ExternalLiveries payload: {relative.as_posix()}")
        suffix = source.suffix.lower()
        if suffix in package_security.DANGEROUS_EXTENSIONS or suffix in package_security.NESTED_ARCHIVE_EXTENSIONS:
            raise ValueError(f"Blocked ExternalLiveries file type: {source.name}")
        package_zip.copy_file(source, target / relative)
        copied.append(relative.as_posix())
    preview = find_preview(source_container, payload_root)
    sanitized = inspect_payload(target)
    sanitized.update({
        "folderName": external_folder_name,
        "sourcePayloadRoot": str(payload_root),
        "copiedFiles": copied,
        "copiedCount": len(copied),
        "previewSource": str(preview) if preview else "",
        "excludedFiles": list(inspection.get("excludedFiles") or []),
    })
    return sanitized


def assert_pack_payload_safe(payload_root: str | Path) -> dict[str, Any]:
    root = Path(payload_root).expanduser()
    for path in root.rglob("*") if root.exists() else []:
        if path.is_symlink():
            raise ValueError(f"Unsupported symlink in Online League payload: {path.relative_to(root).as_posix()}")
        if not path.is_file():
            continue
        relative = path.relative_to(root)
        if _blocked_file(relative):
            raise ValueError(f"Online League packs cannot contain SavedCars or car final states: {relative.as_posix()}")
        suffix = path.suffix.lower()
        if suffix in package_security.DANGEROUS_EXTENSIONS or suffix in package_security.NESTED_ARCHIVE_EXTENSIONS:
            raise ValueError(f"Blocked file in Online League ExternalLiveries payload: {relative.as_posix()}")
    inspection = inspect_payload(root)
    if inspection.get("errors"):
        raise ValueError("; ".join(str(item) for item in inspection["errors"][:8]))
    allowed = {EXTERNAL_LIVERY_JSON, *(str(row.get("path") or "") for row in inspection.get("referencedFiles") or [])}
    unexpected = [str(item) for item in inspection.get("excludedFiles") or [] if str(item) not in allowed]
    if unexpected:
        raise ValueError(f"Unexpected file in Online League ExternalLiveries payload: {unexpected[0]}")
    return inspection
