from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Any, Iterable

from liverylab_core import skin_policy


SUPPORTED_SOURCE_VERSIONS = {"1.0.2", "1.0.3"}
REGISTRY_REL_PATH = Path("LiveryLibrary") / "my_skins.json"
MIGRATION_ROOT_NAMES = ("UserData", "LiveryLibrary", "LiveryProjects", "LiveryPackageExport")
EXCLUDED_SCAN_DIR_NAMES = {
    "__pycache__",
    ".cache",
    "cache",
    "code cache",
    "gpucache",
    "webview",
    "_tmp",
    "tmp",
    "temp",
}
EXTERNAL_EDITOR_SUFFIXES = {
    ".afdesign",
    ".afphoto",
    ".ai",
    ".kra",
    ".ora",
    ".psb",
    ".psd",
    ".svg",
    ".xcf",
}
ADVANCED_ONLY_ENTRY_FIELDS = {
    "tyreBrand": ("tyre_brand", "tyreBrand"),
    "numberplateVariant": ("numberplate_variant", "numberplateVariant"),
    "numberplateBake": ("numberplate_bake", "numberplateBake"),
    "roadPlate": ("road_plate", "roadPlate"),
    "driverProfile": ("driver_profile", "driverProfile"),
    "sourceSkin": ("source_skin", "sourceSkin"),
}
FEATURE_ROOTS = {
    "driverProfiles": Path("LiveryLibrary") / "Drivers",
    "driverProjects": Path("LiveryProjects") / "Drivers",
    "tyreBrands": Path("LiveryLibrary") / "TyreBrands",
    "customGrids": Path("LiveryLibrary") / "GridPresets",
    "numberplates": Path("LiveryLibrary") / "Numberplates",
    "layerStudioLibrary": Path("LiveryLibrary") / "LayerStudioLibrary",
}


def _read_text(path: Path, limit: int = 1024 * 1024) -> str:
    if not path.is_file():
        return ""
    if path.stat().st_size > limit:
        raise ValueError(f"File is unexpectedly large: {path}")
    return path.read_text(encoding="utf-8-sig", errors="strict")


def _read_json(path: Path, *, limit: int = 32 * 1024 * 1024) -> Any:
    return json.loads(_read_text(path, limit=limit))


def _version_from_root(root: Path) -> tuple[str, str]:
    for candidate in (root / "VERSION", root / "_internal" / "VERSION"):
        try:
            value = _read_text(candidate, limit=128).strip()
        except (OSError, UnicodeError, ValueError):
            continue
        if value:
            return value, str(candidate)
    return "", ""


def _candidate_roots(selected: Path) -> list[tuple[Path, str]]:
    rows: list[tuple[Path, str]] = []

    def add(path: Path, kind: str) -> None:
        try:
            resolved = path.expanduser().resolve()
        except OSError:
            resolved = path.expanduser().absolute()
        if any(existing == resolved for existing, _kind in rows):
            return
        rows.append((resolved, kind))

    add(selected, "portable")
    if selected.name.lower() == "_internal":
        add(selected.parent, "portable")
    add(selected / "release", "source_release")
    return rows


def _root_score(root: Path) -> int:
    if not root.is_dir():
        return -1
    version, _path = _version_from_root(root)
    score = 5 if version else 0
    score += 4 if (root / "LiveryLab.exe").is_file() else 0
    score += 3 if (root / "_internal").is_dir() else 0
    score += sum(2 for name in MIGRATION_ROOT_NAMES if (root / name).is_dir())
    score += 4 if (root / REGISTRY_REL_PATH).is_file() else 0
    return score


def resolve_legacy_root(selected_path: str | Path) -> dict[str, Any]:
    raw = str(selected_path or "").strip()
    if not raw:
        raise ValueError("Choose the old LiveryLab folder first.")
    selected = Path(raw).expanduser()
    if not selected.exists():
        raise FileNotFoundError(f"Selected folder does not exist: {selected}")
    if not selected.is_dir():
        raise NotADirectoryError(f"Select the LiveryLab folder, not a file: {selected}")

    candidates = [
        {"path": path, "kind": kind, "score": _root_score(path)}
        for path, kind in _candidate_roots(selected)
    ]
    candidates.sort(key=lambda row: (-int(row["score"]), len(Path(row["path"]).parts)))
    best = candidates[0]
    if int(best["score"]) < 5:
        raise ValueError(
            "The selected folder is not a recognizable LiveryLab installation. "
            "Select the portable folder that contains VERSION and the LiveryLab data folders."
        )
    return {
        "selectedPath": str(selected.resolve()),
        "root": str(best["path"]),
        "sourceKind": best["kind"],
        "score": int(best["score"]),
    }


def _coerce_registry(data: Any) -> list[dict[str, Any]]:
    if isinstance(data, dict):
        data = data.get("my_skins", [])
    if not isinstance(data, list):
        raise ValueError("Legacy My Liveries registry must contain a my_skins list.")
    return [dict(row) for row in data if isinstance(row, dict)]


def _iter_files(root: Path, errors: list[str] | None = None) -> Iterable[Path]:
    if not root.is_dir():
        return
    stack = [root]
    while stack:
        current = stack.pop()
        try:
            with os.scandir(current) as entries:
                for entry in entries:
                    path = Path(entry.path)
                    try:
                        if entry.is_dir(follow_symlinks=False):
                            if entry.name.strip().lower() in EXCLUDED_SCAN_DIR_NAMES:
                                continue
                            stack.append(path)
                        elif entry.is_file(follow_symlinks=False):
                            yield path
                    except OSError as exc:
                        if errors is not None and len(errors) < 20:
                            errors.append(f"{path}: {exc}")
        except OSError as exc:
            if errors is not None and len(errors) < 20:
                errors.append(f"{current}: {exc}")


def _folder_usage(path: Path) -> dict[str, Any]:
    errors: list[str] = []
    file_count = 0
    total_bytes = 0
    for file_path in _iter_files(path, errors) or []:
        file_count += 1
        try:
            total_bytes += file_path.stat().st_size
        except OSError as exc:
            if len(errors) < 20:
                errors.append(f"{file_path}: {exc}")
    return {
        "path": str(path),
        "exists": path.is_dir(),
        "fileCount": file_count,
        "bytes": total_bytes,
        "errors": errors,
    }


def _path_suffix_from_named_root(path: Path, root_name: str) -> Path | None:
    parts = list(path.parts)
    matches = [index for index, part in enumerate(parts) if part.lower() == root_name.lower()]
    if not matches:
        return None
    return Path(*parts[matches[-1] + 1 :])


def _portable_path(root: Path, value: Any, root_name: str) -> Path | None:
    text = str(value or "").strip()
    if not text:
        return None
    candidate = Path(text).expanduser()
    if candidate.exists():
        resolved_candidate = candidate.resolve()
        try:
            resolved_candidate.relative_to((root / root_name).resolve())
            return resolved_candidate
        except ValueError:
            pass
    suffix = _path_suffix_from_named_root(candidate, root_name)
    if suffix is None:
        return None
    rebased = root / root_name / suffix
    return rebased.resolve() if rebased.exists() else None


def _entry_local_root(root: Path, entry: dict[str, Any]) -> Path:
    portable = _portable_path(root, entry.get("local_root"), "LiveryLibrary")
    if portable is not None:
        return portable
    car_model = str(entry.get("car_model") or "car").strip() or "car"
    skin_id = str(entry.get("skin_id") or entry.get("display_name") or "skin").strip() or "skin"
    return root / "LiveryLibrary" / "LocalSkins" / car_model / skin_id


def _entry_project_root(root: Path, entry: dict[str, Any]) -> Path:
    for key in ("project_root", "projectRoot", "project_folder", "projectFolder"):
        portable = _portable_path(root, entry.get(key), "LiveryProjects")
        if portable is not None:
            return portable
    car_model = str(entry.get("car_model") or "car").strip() or "car"
    skin_id = str(entry.get("skin_id") or entry.get("display_name") or "skin").strip() or "skin"
    return root / "LiveryProjects" / car_model / skin_id


def _entry_summary(root: Path, entry: dict[str, Any]) -> dict[str, Any]:
    policy = skin_policy.policy_for_skin(entry)
    local_root = _entry_local_root(root, entry)
    project_root = _entry_project_root(root, entry)
    editable = policy.get("editable") is True
    local_exists = local_root.is_dir()
    project_exists = project_root.is_dir()
    complete = local_exists and (project_exists if editable else True)
    advanced_only_features = [
        label
        for label, keys in ADVANCED_ONLY_ENTRY_FIELDS.items()
        if any(bool(entry.get(key)) for key in keys)
    ]
    return {
        "key": str(entry.get("key") or ""),
        "carModel": str(entry.get("car_model") or ""),
        "skinId": str(entry.get("skin_id") or ""),
        "displayName": str(entry.get("display_name") or entry.get("skin_id") or ""),
        "origin": str(policy.get("origin") or "unknown"),
        "projectState": str(policy.get("projectState") or "none"),
        "editable": editable,
        "readOnly": policy.get("readOnly") is True,
        "active": entry.get("active") is not False,
        "complete": complete,
        "localRoot": str(local_root),
        "localRootExists": local_exists,
        "projectRoot": str(project_root),
        "projectRootExists": project_exists,
        "advancedOnlyFeatures": advanced_only_features,
        "officialConversionStatus": "incomplete" if not complete else ("ready_with_feature_loss" if advanced_only_features else "ready"),
    }


def _feature_inventory(root: Path) -> dict[str, Any]:
    features: dict[str, Any] = {}
    for key, rel_path in FEATURE_ROOTS.items():
        usage = _folder_usage(root / rel_path)
        features[key] = {
            **usage,
            "present": usage["fileCount"] > 0,
        }

    project_root = root / "LiveryProjects"
    layer_projects: set[str] = set()
    external_editor_files = 0
    for path in _iter_files(project_root) or []:
        if path.name.lower() == "layers.json" and "layer-studio" in {part.lower() for part in path.parts}:
            layer_projects.add(str(path.parent))
        if path.suffix.lower() in EXTERNAL_EDITOR_SUFFIXES:
            external_editor_files += 1
    features["layerStudioProjects"] = {"count": len(layer_projects), "present": bool(layer_projects)}
    features["externalEditorFiles"] = {"count": external_editor_files, "present": external_editor_files > 0}
    return features


def _load_settings(root: Path, warnings: list[str]) -> dict[str, Any]:
    path = root / "UserData" / "settings.json"
    if not path.is_file():
        return {"path": str(path), "exists": False, "defaultRootFolder": "", "gameRootExists": False}
    try:
        data = _read_json(path)
    except Exception as exc:
        warnings.append(f"Legacy settings could not be read: {exc}")
        data = {}
    if not isinstance(data, dict):
        data = {}
    game_root_text = str(data.get("default_root_folder") or "").strip()
    return {
        "path": str(path),
        "exists": True,
        "defaultRootFolder": game_root_text,
        "gameRootExists": bool(game_root_text and Path(game_root_text).expanduser().is_dir()),
    }


def inspect_legacy_install(
    selected_path: str | Path,
    *,
    target_root: str | Path | None = None,
) -> dict[str, Any]:
    resolved = resolve_legacy_root(selected_path)
    root = Path(resolved["root"])
    version, version_path = _version_from_root(root)
    blockers: list[str] = []
    warnings: list[str] = []

    if version not in SUPPORTED_SOURCE_VERSIONS:
        detected = version or "unknown"
        blockers.append(
            f"LiveryLab {detected} is not supported by this migration. "
            f"This migration currently supports {', '.join(sorted(SUPPORTED_SOURCE_VERSIONS))}."
        )

    if target_root is not None:
        try:
            target = Path(target_root).expanduser().resolve()
            if target == root.resolve():
                blockers.append("The selected source is the current LiveryLab installation.")
        except OSError:
            pass

    registry_path = root / REGISTRY_REL_PATH
    registry_items: list[dict[str, Any]] = []
    registry_error = ""
    if registry_path.is_file():
        try:
            registry_items = _coerce_registry(_read_json(registry_path))
        except Exception as exc:
            registry_error = str(exc)
            blockers.append(f"Legacy My Liveries registry could not be read: {exc}")
    else:
        warnings.append("No My Liveries registry was found. Other Advanced workspace data can still be inspected.")

    entries = [_entry_summary(root, entry) for entry in registry_items]
    editable_count = sum(1 for entry in entries if entry["editable"])
    read_only_count = sum(1 for entry in entries if entry["readOnly"])
    unknown_count = sum(1 for entry in entries if entry["origin"] == skin_policy.ORIGIN_UNKNOWN)
    incomplete_count = sum(1 for entry in entries if not entry["complete"])
    if unknown_count:
        warnings.append(f"{unknown_count} livery entries have unknown ownership metadata and need review before migration.")
    if incomplete_count:
        warnings.append(f"{incomplete_count} livery entries are missing local library or editable project data.")

    storage: dict[str, Any] = {}
    total_files = 0
    total_bytes = 0
    scan_errors: list[str] = []
    for name in MIGRATION_ROOT_NAMES:
        usage = _folder_usage(root / name)
        storage[name] = usage
        total_files += int(usage["fileCount"])
        total_bytes += int(usage["bytes"])
        scan_errors.extend(str(error) for error in usage.get("errors", []))
    if scan_errors:
        warnings.append(f"{len(scan_errors)} files or folders could not be inspected completely.")

    recognized_data = any(int(row.get("fileCount") or 0) for row in storage.values())
    if not recognized_data:
        blockers.append("No migratable LiveryLab data was found in the selected installation.")

    settings = _load_settings(root, warnings)
    features = _feature_inventory(root)
    status = "ready" if not blockers else "blocked"
    return {
        "status": status,
        "ready": status == "ready",
        "readOnlyInspection": True,
        "supportedSourceVersions": sorted(SUPPORTED_SOURCE_VERSIONS),
        "source": {
            **resolved,
            "version": version,
            "versionPath": version_path,
            "registryPath": str(registry_path),
            "registryExists": registry_path.is_file(),
            "registryError": registry_error,
        },
        "settings": settings,
        "liveries": {
            "total": len(entries),
            "editable": editable_count,
            "readOnly": read_only_count,
            "active": sum(1 for entry in entries if entry["active"]),
            "inactive": sum(1 for entry in entries if not entry["active"]),
            "unknownPolicy": unknown_count,
            "incomplete": incomplete_count,
            "items": entries,
        },
        "storage": {
            "totalFiles": total_files,
            "totalBytes": total_bytes,
            "roots": storage,
            "scanErrors": scan_errors[:20],
        },
        "features": features,
        "warnings": warnings,
        "blockers": blockers,
        "notes": [
            "This inspection does not copy, delete, rename or modify files.",
            "Advanced and Official liveries will remain separate versions when Official conversion is added.",
            "Read-only liveries will remain read-only in both workflows.",
        ],
    }
