from __future__ import annotations

import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from liverylab_core import online_league_payload


REGISTRY_FILENAME = "league_deployments.json"
REGISTRY_TYPE = "liverylabOnlineLeagueDeployments"
REGISTRY_FORMAT_VERSION = 1
OWNERSHIP_LEAGUE = "league_owned"
OWNERSHIP_REUSED = "reused_existing"
VALID_OWNERSHIP = {OWNERSHIP_LEAGUE, OWNERSHIP_REUSED}


def _now_iso() -> str:
    return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")


def _registry_path(library_root: str | Path) -> Path:
    return Path(library_root).expanduser() / REGISTRY_FILENAME


def _empty_registry() -> dict[str, Any]:
    return {
        "type": REGISTRY_TYPE,
        "formatVersion": REGISTRY_FORMAT_VERSION,
        "deployments": [],
    }


def _normalize_guid(value: Any) -> str:
    guid = str(value or "").strip().lower()
    if not online_league_payload.CAR_GUID_PATTERN.fullmatch(guid):
        raise ValueError(f"Invalid Online League car_guid: {value}")
    return guid


def source_descriptor_id(external_folder_name: Any, car_guid: Any) -> str:
    folder_name = online_league_payload.validate_external_folder_name(external_folder_name)
    return f"official_external_livery:{folder_name.lower()}:{_normalize_guid(car_guid)}"


def payload_fingerprint(payload_root: str | Path) -> dict[str, Any]:
    root = Path(payload_root).expanduser()
    inspection = online_league_payload.inspect_payload(root)
    errors = [str(item) for item in inspection.get("errors") or [] if str(item).strip()]
    if errors:
        raise ValueError("; ".join(errors[:8]))
    external_data = inspection.get("externalLivery") if isinstance(inspection.get("externalLivery"), dict) else {}
    referenced_files: list[dict[str, Any]] = []
    seen: set[str] = set()
    for row in inspection.get("referencedFiles") or []:
        relative = str(row.get("path") or "").strip().replace("\\", "/")
        key = relative.lower()
        if not relative or key in seen:
            continue
        seen.add(key)
        source = root / Path(*relative.split("/"))
        digest = hashlib.sha256(source.read_bytes()).hexdigest()
        referenced_files.append({"path": relative, "sha256": digest, "size": source.stat().st_size})
    referenced_files.sort(key=lambda item: str(item.get("path") or "").lower())
    canonical = json.dumps(
        {"externalLivery": external_data, "referencedFiles": referenced_files},
        sort_keys=True,
        ensure_ascii=False,
        separators=(",", ":"),
    ).encode("utf-8")
    return {
        "algorithm": "sha256",
        "value": hashlib.sha256(canonical).hexdigest(),
        "carGuid": _normalize_guid(inspection.get("carGuid")),
        "externalFolderName": online_league_payload.validate_external_folder_name(root.name),
        "referencedFileCount": len(referenced_files),
    }


def _normalize_reference(value: Any) -> dict[str, str] | None:
    if not isinstance(value, dict):
        return None
    installed_pack_id = str(value.get("installedPackId") or "").strip()
    entry_id = str(value.get("entryId") or "").strip()
    if not installed_pack_id or not entry_id:
        return None
    return {"installedPackId": installed_pack_id, "entryId": entry_id}


def _normalize_deployment(value: Any) -> dict[str, Any] | None:
    if not isinstance(value, dict):
        return None
    ownership = str(value.get("ownership") or "").strip().lower()
    if ownership not in VALID_OWNERSHIP:
        return None
    try:
        folder_name = online_league_payload.validate_external_folder_name(value.get("externalFolderName"))
        car_guid = _normalize_guid(value.get("carGuid"))
    except ValueError:
        return None
    fingerprint = str(value.get("payloadFingerprint") or "").strip().lower()
    if len(fingerprint) != 64 or any(char not in "0123456789abcdef" for char in fingerprint):
        return None
    references: list[dict[str, str]] = []
    reference_keys: set[tuple[str, str]] = set()
    for raw in value.get("references") or []:
        reference = _normalize_reference(raw)
        if reference is None:
            continue
        key = (reference["installedPackId"].lower(), reference["entryId"].lower())
        if key in reference_keys:
            continue
        reference_keys.add(key)
        references.append(reference)
    if not references:
        return None
    source_id = source_descriptor_id(folder_name, car_guid)
    return {
        "sourceDescriptorId": source_id,
        "externalFolderName": folder_name,
        "carGuid": car_guid,
        "payloadFingerprint": fingerprint,
        "ownership": ownership,
        "references": references,
        "createdAt": str(value.get("createdAt") or ""),
        "updatedAt": str(value.get("updatedAt") or ""),
    }


def load_registry(library_root: str | Path) -> dict[str, Any]:
    path = _registry_path(library_root)
    if not path.is_file():
        return {**_empty_registry(), "path": str(path), "warnings": []}
    warnings: list[str] = []
    try:
        raw = json.loads(path.read_text(encoding="utf-8-sig"))
    except Exception as exc:
        return {**_empty_registry(), "path": str(path), "warnings": [f"League deployment registry could not be read: {exc}"]}
    if not isinstance(raw, dict) or raw.get("type") != REGISTRY_TYPE or int(raw.get("formatVersion") or 0) != REGISTRY_FORMAT_VERSION:
        return {**_empty_registry(), "path": str(path), "warnings": ["League deployment registry has an unsupported format."]}
    deployments: list[dict[str, Any]] = []
    seen: set[str] = set()
    for value in raw.get("deployments") or []:
        deployment = _normalize_deployment(value)
        if deployment is None:
            warnings.append("An invalid League deployment registry entry was ignored.")
            continue
        source_id = deployment["sourceDescriptorId"]
        if source_id in seen:
            warnings.append(f"Duplicate League deployment registry entry was ignored: {source_id}")
            continue
        seen.add(source_id)
        deployments.append(deployment)
    return {
        "type": REGISTRY_TYPE,
        "formatVersion": REGISTRY_FORMAT_VERSION,
        "deployments": deployments,
        "path": str(path),
        "warnings": warnings,
    }


def deployment_for_source(library_root: str | Path, source_id: Any) -> dict[str, Any] | None:
    clean_source_id = str(source_id or "").strip().lower()
    if not clean_source_id:
        return None
    registry = load_registry(library_root)
    deployment = next(
        (item for item in registry.get("deployments") or [] if item.get("sourceDescriptorId") == clean_source_id),
        None,
    )
    return dict(deployment) if isinstance(deployment, dict) else None


def pack_reference_status(
    library_root: str | Path,
    installed_pack_id: Any,
    entries: list[dict[str, Any]],
) -> dict[str, Any]:
    pack_id = str(installed_pack_id or "").strip()
    registry = load_registry(library_root)
    by_reference: dict[str, dict[str, Any]] = {}
    for deployment in registry.get("deployments") or []:
        for reference in deployment.get("references") or []:
            if str(reference.get("installedPackId") or "").strip().lower() != pack_id.lower():
                continue
            entry_id = str(reference.get("entryId") or "").strip().lower()
            if entry_id:
                by_reference[entry_id] = deployment
    rows: list[dict[str, Any]] = []
    for entry in entries:
        entry_id = str(entry.get("id") or "").strip()
        deployment = by_reference.get(entry_id.lower())
        rows.append({
            "entryId": entry_id,
            "active": deployment is not None,
            "ownership": str(deployment.get("ownership") or "") if deployment else "",
            "sourceDescriptorId": str(deployment.get("sourceDescriptorId") or "") if deployment else "",
        })
    active_count = sum(1 for row in rows if row["active"])
    league_owned_count = sum(1 for row in rows if row["ownership"] == OWNERSHIP_LEAGUE)
    reused_count = sum(1 for row in rows if row["ownership"] == OWNERSHIP_REUSED)
    total = len(rows)
    return {
        "state": "active" if total and active_count == total else ("partial" if active_count else "local_only"),
        "entryCount": total,
        "activeCount": active_count,
        "leagueOwnedCount": league_owned_count,
        "reusedExistingCount": reused_count,
        "entries": rows,
        "warnings": list(registry.get("warnings") or []),
    }


def _write_registry(library_root: str | Path, deployments: list[dict[str, Any]]) -> Path:
    path = _registry_path(library_root)
    path.parent.mkdir(parents=True, exist_ok=True)
    payload = {
        "type": REGISTRY_TYPE,
        "formatVersion": REGISTRY_FORMAT_VERSION,
        "updatedAt": _now_iso(),
        "deployments": deployments,
    }
    temp = path.with_suffix(path.suffix + ".tmp")
    temp.write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding="utf-8")
    temp.replace(path)
    return path


def register_reference(
    library_root: str | Path,
    *,
    installed_pack_id: Any,
    entry_id: Any,
    external_folder_name: Any,
    car_guid: Any,
    payload_fingerprint_value: Any,
    ownership: Any,
) -> dict[str, Any]:
    clean_ownership = str(ownership or "").strip().lower()
    if clean_ownership not in VALID_OWNERSHIP:
        raise ValueError(f"Invalid Online League deployment ownership: {ownership}")
    reference = _normalize_reference({"installedPackId": installed_pack_id, "entryId": entry_id})
    if reference is None:
        raise ValueError("Online League deployment pack and entry ids are required.")
    folder_name = online_league_payload.validate_external_folder_name(external_folder_name)
    guid = _normalize_guid(car_guid)
    source_id = source_descriptor_id(folder_name, guid)
    fingerprint = str(payload_fingerprint_value or "").strip().lower()
    if len(fingerprint) != 64 or any(char not in "0123456789abcdef" for char in fingerprint):
        raise ValueError("Invalid Online League payload fingerprint.")
    registry = load_registry(library_root)
    deployments = [dict(item) for item in registry.get("deployments") or []]
    current = next((item for item in deployments if item.get("sourceDescriptorId") == source_id), None)
    now = _now_iso()
    if current is None:
        current = {
            "sourceDescriptorId": source_id,
            "externalFolderName": folder_name,
            "carGuid": guid,
            "payloadFingerprint": fingerprint,
            "ownership": clean_ownership,
            "references": [],
            "createdAt": now,
            "updatedAt": now,
        }
        deployments.append(current)
    else:
        if current.get("payloadFingerprint") != fingerprint:
            raise ValueError("Online League deployment conflicts with a different payload at the same ExternalLiveries destination.")
        current_ownership = str(current.get("ownership") or "")
        if current_ownership != clean_ownership and current_ownership != OWNERSHIP_LEAGUE:
            raise ValueError("An existing normal livery cannot be converted to League ownership automatically.")
        current["updatedAt"] = now
    references = [dict(item) for item in current.get("references") or [] if isinstance(item, dict)]
    reference_key = (reference["installedPackId"].lower(), reference["entryId"].lower())
    reference_added = not any((str(item.get("installedPackId") or "").lower(), str(item.get("entryId") or "").lower()) == reference_key for item in references)
    if reference_added:
        references.append(reference)
        current["references"] = references
        current["updatedAt"] = now
    path = _write_registry(library_root, deployments)
    return {"deployment": dict(current), "referenceAdded": reference_added, "path": str(path), "warnings": list(registry.get("warnings") or [])}


def release_reference(library_root: str | Path, *, installed_pack_id: Any, entry_id: Any) -> dict[str, Any]:
    reference = _normalize_reference({"installedPackId": installed_pack_id, "entryId": entry_id})
    if reference is None:
        raise ValueError("Online League deployment pack and entry ids are required.")
    registry = load_registry(library_root)
    deployments: list[dict[str, Any]] = []
    released: dict[str, Any] | None = None
    key = (reference["installedPackId"].lower(), reference["entryId"].lower())
    for raw in registry.get("deployments") or []:
        deployment = dict(raw)
        references = [
            dict(item)
            for item in deployment.get("references") or []
            if isinstance(item, dict)
            and (str(item.get("installedPackId") or "").lower(), str(item.get("entryId") or "").lower()) != key
        ]
        if len(references) != len(deployment.get("references") or []):
            released = dict(deployment)
            released["remainingReferences"] = len(references)
            deployment["references"] = references
            deployment["updatedAt"] = _now_iso()
        if references:
            deployments.append(deployment)
    path = _write_registry(library_root, deployments)
    return {
        "released": released is not None,
        "deployment": released or {},
        "removeLeagueOwnedFiles": bool(released and released.get("ownership") == OWNERSHIP_LEAGUE and not released.get("remainingReferences")),
        "path": str(path),
        "warnings": list(registry.get("warnings") or []),
    }


def classify_existing_target(
    target_root: str | Path,
    *,
    expected_fingerprint: Any,
    library_root: str | Path,
) -> dict[str, Any]:
    target = Path(target_root).expanduser()
    expected = str(expected_fingerprint or "").strip().lower()
    if not target.exists():
        return {"status": "missing", "ownership": OWNERSHIP_LEAGUE, "ready": True, "fingerprint": ""}
    if not target.is_dir():
        return {"status": "conflict", "ownership": "", "ready": False, "fingerprint": "", "reason": "ExternalLiveries target is not a folder."}
    try:
        fingerprint = payload_fingerprint(target)
        source_id = source_descriptor_id(target.name, fingerprint.get("carGuid"))
    except Exception as exc:
        return {"status": "conflict", "ownership": "", "ready": False, "fingerprint": "", "reason": str(exc)}
    actual = str(fingerprint.get("value") or "")
    if actual != expected:
        return {"status": "conflict", "ownership": "", "ready": False, "fingerprint": actual, "sourceDescriptorId": source_id, "reason": "ExternalLiveries target contains a different payload."}
    registry = load_registry(library_root)
    existing = next((item for item in registry.get("deployments") or [] if item.get("sourceDescriptorId") == source_id and item.get("payloadFingerprint") == actual), None)
    ownership = OWNERSHIP_LEAGUE if existing and existing.get("ownership") == OWNERSHIP_LEAGUE else OWNERSHIP_REUSED
    return {"status": "identical", "ownership": ownership, "ready": True, "fingerprint": actual, "sourceDescriptorId": source_id}


def match_league_owned_descriptors(library_root: str | Path, descriptors: list[dict[str, Any]]) -> dict[str, Any]:
    registry = load_registry(library_root)
    league_owned = {
        str(item.get("sourceDescriptorId") or "").strip().lower(): item
        for item in registry.get("deployments") or []
        if item.get("ownership") == OWNERSHIP_LEAGUE and item.get("references")
    }
    matches: list[dict[str, Any]] = []
    warnings = list(registry.get("warnings") or [])
    for descriptor in descriptors:
        source_id = str(descriptor.get("id") or "").strip().lower()
        deployment = league_owned.get(source_id)
        if deployment is None:
            continue
        folder = Path(str(descriptor.get("externalLiveryFolder") or "")).expanduser()
        try:
            fingerprint = payload_fingerprint(folder)
        except Exception as exc:
            warnings.append(f"League-owned livery could not be verified and was not filtered: {source_id}: {exc}")
            continue
        if str(fingerprint.get("value") or "") != str(deployment.get("payloadFingerprint") or ""):
            warnings.append(f"League-owned livery payload changed and was not filtered: {source_id}")
            continue
        matches.append({
            "sourceDescriptorId": source_id,
            "externalFolderName": deployment.get("externalFolderName") or "",
            "carGuid": deployment.get("carGuid") or "",
            "payloadFingerprint": deployment.get("payloadFingerprint") or "",
            "referenceCount": len(deployment.get("references") or []),
        })
    return {
        "sourceIds": sorted({item["sourceDescriptorId"] for item in matches}),
        "matches": matches,
        "matchCount": len(matches),
        "registryPath": registry.get("path") or str(_registry_path(library_root)),
        "warnings": warnings,
    }
