from __future__ import annotations

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

from liverylab_core import saved_games_paths


GRID_CACHE_BACKUPS_DIRNAME = "GridCacheBackups"
DELETE_FILES = (
    "last_grid.customgrid",
    "GameModeType_INSTANT_RACE.gamemodesave",
)
BACKUP_ONLY_FILES = (
    "gamemode.lastgamemode",
)
ACE_ROOT_DELETE_PATTERNS = (
    "leaderboard_results_*.championshipresult_session.json",
)
MANIFEST_NAME = "manifest.json"


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


def _timestamp() -> str:
    return datetime.now().strftime("%Y%m%d_%H%M%S")


def game_modes_root() -> Path:
    return ace_root() / "GameModes"


def ace_root() -> Path:
    return saved_games_paths.ace_root()


def backup_root(library_root: str | Path) -> Path:
    return Path(library_root) / GRID_CACHE_BACKUPS_DIRNAME


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


def _file_info(path: Path, action: str) -> dict[str, Any]:
    exists = path.exists() and path.is_file()
    return {
        "name": path.name,
        "path": str(path),
        "action": action,
        "exists": exists,
        "size": path.stat().st_size if exists else 0,
        "modifiedAt": datetime.fromtimestamp(path.stat().st_mtime, timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") if exists else "",
    }


def _latest_backup(root: Path) -> dict[str, Any] | None:
    if not root.exists():
        return None
    backups = [path for path in root.iterdir() if path.is_dir() and (path / MANIFEST_NAME).exists()]
    if not backups:
        return None
    latest = max(backups, key=lambda item: item.name)
    try:
        manifest = json.loads((latest / MANIFEST_NAME).read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        manifest = {}
    return {
        "path": str(latest),
        "createdAt": str(manifest.get("createdAt") or ""),
        "files": manifest.get("files") if isinstance(manifest.get("files"), list) else [],
    }


def cache_status(library_root: str | Path) -> dict[str, Any]:
    root = ace_root()
    modes_root = game_modes_root()
    backups = backup_root(library_root)
    files = [
        _file_info(modes_root / name, "delete") for name in DELETE_FILES
    ] + [
        _file_info(modes_root / name, "backup-only") for name in BACKUP_ONLY_FILES
    ] + [
        _file_info(path, "delete") for pattern in ACE_ROOT_DELETE_PATTERNS for path in sorted(root.glob(pattern))
    ]
    return {
        "aceRoot": str(root),
        "aceRootExists": root.exists(),
        "gameModesRoot": str(modes_root),
        "gameModesRootExists": modes_root.exists(),
        "backupRoot": str(backups),
        "files": files,
        "hasResettableFiles": any(item["exists"] and item["action"] == "delete" for item in files),
        "latestBackup": _latest_backup(backups),
    }


def reset_cache(library_root: str | Path) -> dict[str, Any]:
    root = ace_root()
    modes_root = game_modes_root()
    if not root.exists() or not root.is_dir():
        raise FileNotFoundError(f"ACE Saved Games folder not found: {root}")
    if not modes_root.exists() or not modes_root.is_dir():
        raise FileNotFoundError(f"ACE GameModes folder not found: {modes_root}")

    target_rows: list[tuple[Path, str]] = [(modes_root / name, "delete") for name in DELETE_FILES]
    target_rows.extend((modes_root / name, "backup-only") for name in BACKUP_ONLY_FILES)
    target_rows.extend((path, "delete") for pattern in ACE_ROOT_DELETE_PATTERNS for path in sorted(root.glob(pattern)))
    for target, _action in target_rows:
        if not (_is_child(target, modes_root) or _is_child(target, root)):
            raise ValueError(f"Refusing to touch path outside ACE Saved Games: {target}")

    backups = backup_root(library_root)
    backup_dir = backups / _timestamp()
    backup_dir.mkdir(parents=True, exist_ok=True)

    manifest_files: list[dict[str, Any]] = []
    deleted: list[str] = []
    for target, action in target_rows:
        entry = _file_info(target, action)
        if entry["exists"]:
            relative = target.relative_to(root)
            backup_file = backup_dir / relative
            backup_file.parent.mkdir(parents=True, exist_ok=True)
            shutil.copy2(target, backup_file)
            entry["backupPath"] = str(backup_file)
        manifest_files.append(entry)

    for target, action in target_rows:
        if action != "delete":
            continue
        if target.exists() and target.is_file():
            target.unlink()
            deleted.append(str(target))

    manifest = {
        "schema": 1,
        "createdAt": _now_iso(),
        "aceRoot": str(root),
        "gameModesRoot": str(modes_root),
        "files": manifest_files,
        "deleted": deleted,
    }
    (backup_dir / MANIFEST_NAME).write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")

    return {
        "backup": str(backup_dir),
        "deleted": deleted,
        "status": cache_status(library_root),
    }


def restore_latest_backup(library_root: str | Path) -> dict[str, Any]:
    root = ace_root()
    modes_root = game_modes_root()
    if not root.exists() or not root.is_dir():
        raise FileNotFoundError(f"ACE Saved Games folder not found: {root}")
    if not modes_root.exists() or not modes_root.is_dir():
        raise FileNotFoundError(f"ACE GameModes folder not found: {modes_root}")
    latest = _latest_backup(backup_root(library_root))
    if not latest:
        raise FileNotFoundError("No ACE grid cache backup found")
    backup_dir = Path(str(latest["path"]))
    restored: list[str] = []
    for source in backup_dir.rglob("*"):
        if source.name == MANIFEST_NAME or not source.is_file():
            continue
        relative = source.relative_to(backup_dir)
        if relative.parts and relative.parts[0] == "GameModes":
            target = root / relative
        elif source.name in (*DELETE_FILES, *BACKUP_ONLY_FILES):
            target = modes_root / source.name
        else:
            target = root / relative
        if not source.exists() or not source.is_file():
            continue
        if not (_is_child(target, modes_root) or _is_child(target, root)):
            raise ValueError(f"Refusing to restore path outside ACE Saved Games: {target}")
        target.parent.mkdir(parents=True, exist_ok=True)
        shutil.copy2(source, target)
        restored.append(str(target))
    return {
        "backup": str(backup_dir),
        "restored": restored,
        "status": cache_status(library_root),
    }
