from __future__ import annotations

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

from liverylab_core import advanced_content_setup, content_package_discovery


CONTENT_PACKAGE_NAME = "content.kspkg"
DISABLED_CONTENT_PACKAGE_NAME = "content.kspkg.disabled-by-liverylab"
LEGACY_DISABLED_CONTENT_PACKAGE_PATTERNS = ("content.kspkg.liverylab-disabled*",)
MANIFEST_FILENAME = "content_package_manifest.json"
MANIFEST_HISTORY_DIRNAME = "history"


def _resolve_root(root: str | Path) -> Path:
    candidate = Path(root).expanduser()
    try:
        return candidate.resolve()
    except Exception:
        return candidate.absolute()


def _path_is_within(path: Path, parent: Path) -> bool:
    try:
        path.resolve().relative_to(parent.resolve())
        return True
    except Exception:
        return False


def _manifest_path(manifest_dir: str | Path | None) -> Path | None:
    if not manifest_dir:
        return None
    return _resolve_root(manifest_dir) / MANIFEST_FILENAME


def _read_manifest(manifest_dir: str | Path | None) -> dict[str, Any]:
    path = _manifest_path(manifest_dir)
    if not path or 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 _file_record(path: Path) -> dict[str, Any]:
    try:
        stat = path.stat()
        return {
            "path": str(path),
            "exists": True,
            "size": int(stat.st_size),
            "mtimeNs": int(stat.st_mtime_ns),
            "modifiedAt": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
        }
    except Exception:
        return {"path": str(path), "exists": False, "size": 0, "mtimeNs": 0, "modifiedAt": ""}


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


def _manifest_disabled_package(root_path: Path, manifest_dir: str | Path | None) -> Path | None:
    data = _read_manifest(manifest_dir)
    if not data or data.get("restoredAt"):
        return None
    if str(data.get("action") or "") != "disabled_content_package":
        return None
    try:
        manifest_root = _resolve_root(str(data.get("root") or ""))
    except Exception:
        return None
    if manifest_root != root_path:
        return None
    disabled_path = Path(str(data.get("disabledPath") or ""))
    if not disabled_path.exists() or not disabled_path.is_file():
        return None
    if not _path_is_within(disabled_path, root_path):
        return None
    if not disabled_path.name.lower().startswith(CONTENT_PACKAGE_NAME.lower()):
        return None
    return disabled_path


def _disabled_packages(root_path: Path, manifest_dir: str | Path | None = None) -> list[Path]:
    candidates: list[Path] = []
    seen: set[str] = set()

    def add(path: Path | None) -> None:
        if not path or not path.exists() or not path.is_file():
            return
        if content_package_discovery.is_known_non_content_package(path):
            return
        key = str(path.resolve()).lower()
        if key in seen:
            return
        seen.add(key)
        candidates.append(path)

    add(_manifest_disabled_package(root_path, manifest_dir))
    for path in sorted(root_path.glob(f"{DISABLED_CONTENT_PACKAGE_NAME}*"), key=lambda item: item.name.lower()):
        add(path)
    for pattern in LEGACY_DISABLED_CONTENT_PACKAGE_PATTERNS:
        for path in sorted(root_path.glob(pattern), key=lambda item: item.name.lower()):
            add(path)
    for row in content_package_discovery.discover_renamed_content_packages(
        root_path,
        active_name=CONTENT_PACKAGE_NAME,
        exclude_paths=candidates,
    ):
        add(Path(str(row.get("path") or "")))
    return candidates


def content_package_status(root: str | Path, manifest_dir: str | Path | None = None) -> dict[str, Any]:
    root_text = str(root or "").strip()
    if not root_text:
        return {
            "root": "",
            "activePath": "",
            "activeExists": False,
            "disabledExists": False,
            "disabledPath": "",
            "disabledPaths": [],
            "disabledCount": 0,
            "disabledAmbiguous": False,
            "contentPackageAvailable": False,
            "contentPackagePath": "",
            "mixedPackageState": False,
            "activePackage": {},
            "disabledPackages": [],
            "contentExists": False,
            "contentCarsExists": False,
            "manifestPath": str(_manifest_path(manifest_dir) or ""),
            "manifest": {},
            "state": "missing",
        }
    root_path = _resolve_root(root_text)
    active_path = root_path / CONTENT_PACKAGE_NAME
    disabled_paths = _disabled_packages(root_path, manifest_dir) if root_path.exists() else []
    manifest = _read_manifest(manifest_dir)
    active_exists = active_path.is_file()
    disabled_exists = bool(disabled_paths)
    disabled_ambiguous = len(disabled_paths) > 1
    if active_exists and disabled_exists:
        state = "mixed"
    elif active_exists:
        state = "active"
    elif disabled_ambiguous:
        state = "ambiguous"
    elif disabled_exists:
        state = "disabled"
    else:
        state = "missing"
    return {
        "root": str(root_path),
        "activePath": str(active_path),
        "activeExists": active_exists,
        "disabledExists": disabled_exists,
        "disabledPath": str(disabled_paths[0]) if disabled_paths else "",
        "disabledPaths": [str(path) for path in disabled_paths],
        "disabledCount": len(disabled_paths),
        "disabledAmbiguous": disabled_ambiguous,
        "contentPackageAvailable": bool(active_exists or len(disabled_paths) == 1),
        "contentPackagePath": str(active_path if active_exists else (disabled_paths[0] if len(disabled_paths) == 1 else "")),
        "mixedPackageState": bool(active_exists and disabled_exists),
        "activePackage": _file_record(active_path),
        "disabledPackages": [_file_record(path) for path in disabled_paths],
        "contentExists": (root_path / "content").exists(),
        "contentCarsExists": (root_path / "content" / "cars").exists(),
        "manifestPath": str(_manifest_path(manifest_dir) or ""),
        "manifest": manifest,
        "state": state,
    }


def content_package_source_path(
    root: str | Path,
    manifest_dir: str | Path | None = None,
    *,
    require_active: bool = False,
) -> Path | None:
    status = content_package_status(root, manifest_dir)
    active_path = Path(str(status.get("activePath") or ""))
    if status.get("activeExists") and active_path.is_file():
        return active_path
    if require_active:
        return None
    disabled_paths = [Path(str(value)) for value in status.get("disabledPaths", []) if str(value).strip()]
    if len(disabled_paths) > 1:
        names = ", ".join(path.name for path in disabled_paths)
        raise RuntimeError(
            "Multiple valid renamed AC EVO content packages were found. "
            f"Keep only one package and try again: {names}"
        )
    if len(disabled_paths) == 1 and disabled_paths[0].is_file():
        return disabled_paths[0]
    return None


def next_disabled_content_package_path(root: str | Path) -> Path:
    root_path = _resolve_root(root)
    base = root_path / DISABLED_CONTENT_PACKAGE_NAME
    if not base.exists():
        return base
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    stamped = root_path / f"{DISABLED_CONTENT_PACKAGE_NAME}-{stamp}"
    if not stamped.exists():
        return stamped
    for index in range(2, 1000):
        candidate = root_path / f"{DISABLED_CONTENT_PACKAGE_NAME}-{stamp}-{index}"
        if not candidate.exists():
            return candidate
    raise FileExistsError("Could not find a free disabled content package name")


def disable_content_package(root: str | Path, manifest_dir: str | Path | None = None) -> dict[str, Any]:
    root_path = _resolve_root(root)
    active_path = root_path / CONTENT_PACKAGE_NAME
    if not active_path.exists():
        result = content_package_status(root_path, manifest_dir)
        result["renamed"] = False
        return result
    setup = advanced_content_setup.status(
        manifest_dir or root_path,
        root_path,
        content_package_status(root_path, manifest_dir),
    )
    if setup.get("ready") is not True:
        raise RuntimeError(
            "Advanced content setup is not current: "
            + str(setup.get("reason") or "Run Extract all in Advanced Setup first.")
        )
    target_path = next_disabled_content_package_path(root_path)
    source_record = _file_record(active_path)
    active_path.rename(target_path)
    manifest_path = _write_manifest(
        manifest_dir,
        {
            "action": "disabled_content_package",
            "root": str(root_path),
            "originalPath": str(active_path),
            "disabledPath": str(target_path),
            "source": source_record,
            "disabled": _file_record(target_path),
            "disabledAt": datetime.now().isoformat(timespec="seconds"),
            "restoredAt": "",
            "restoredTo": "",
        },
    )
    result = content_package_status(root_path, manifest_dir)
    result.update(
        {
            "renamed": True,
            "renamedFrom": str(active_path),
            "renamedTo": str(target_path),
            "manifestPath": str(manifest_path or result.get("manifestPath") or ""),
        }
    )
    return result


def _mark_manifest_restored(manifest_dir: str | Path | None, restored_from: Path, restored_to: Path) -> Path | None:
    data = _read_manifest(manifest_dir)
    if not data:
        return None
    data["restoredAt"] = datetime.now().isoformat(timespec="seconds")
    data["restoredFrom"] = str(restored_from)
    data["restoredTo"] = str(restored_to)
    data["restored"] = _file_record(restored_to)
    path = _write_manifest(manifest_dir, data)
    if path and path.exists():
        history_dir = path.parent / MANIFEST_HISTORY_DIRNAME
        history_dir.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
        history_path = history_dir / f"{path.stem}-{stamp}{path.suffix}"
        try:
            history_path.write_text(path.read_text(encoding="utf-8"), encoding="utf-8")
        except Exception:
            pass
    return path


def restore_content_package(root: str | Path, manifest_dir: str | Path | None = None) -> dict[str, Any]:
    root_path = _resolve_root(root)
    active_path = root_path / CONTENT_PACKAGE_NAME
    disabled_paths = _disabled_packages(root_path, manifest_dir) if root_path.exists() else []
    if active_path.exists():
        result = content_package_status(root_path, manifest_dir)
        result["renamed"] = False
        result["renamedFrom"] = ""
        result["renamedTo"] = ""
        return result
    if not disabled_paths:
        result = content_package_status(root_path, manifest_dir)
        result["renamed"] = False
        result["renamedFrom"] = ""
        result["renamedTo"] = ""
        return result
    if len(disabled_paths) > 1:
        names = ", ".join(path.name for path in disabled_paths)
        raise RuntimeError(
            "Multiple valid renamed AC EVO content packages were found. "
            f"Keep only the package that should be restored and try again: {names}"
        )
    source_path = disabled_paths[0]
    source_path.rename(active_path)
    manifest_path = _mark_manifest_restored(manifest_dir, source_path, active_path)
    result = content_package_status(root_path, manifest_dir)
    result.update(
        {
            "renamed": True,
            "renamedFrom": str(source_path),
            "renamedTo": str(active_path),
            "manifestPath": str(manifest_path or result.get("manifestPath") or ""),
        }
    )
    return result
