from __future__ import annotations

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


CONFIRM_DELETE_TOKEN = "DELETE_MIGRATED_ADVANCED_COPY"
KEEP_ADVANCED = "keep"
DELETE_ADVANCED = "delete"
QUEUE_ACTIVE_STATES = {"pending", "running", "blocked", "partial"}
QUEUE_SUCCESS_STATES = {"converted", "already_installed"}
BOOTSTRAP_FILENAMES = {"readme.txt", ".gitkeep"}
PRESERVED_LIBRARY_FILENAMES = {*BOOTSTRAP_FILENAMES, "my_skins.json"}
ADDITIONAL_ADVANCED_ROOTS = (
    Path("LiveryLibrary") / "Drivers",
    Path("LiveryLibrary") / "TyreBrands",
    Path("LiveryLibrary") / "GridPresets",
    Path("LiveryLibrary") / "Numberplates",
    Path("LiveryLibrary") / "LayerStudioLibrary",
    Path("LiveryProjects") / "Drivers",
)


def _now_iso() -> str:
    return datetime.now(timezone.utc).isoformat(timespec="seconds")


def _read_json(path: Path) -> dict[str, Any]:
    try:
        value = json.loads(path.read_text(encoding="utf-8"))
    except Exception:
        return {}
    return dict(value) if isinstance(value, dict) else {}


def write_manifest(path: str | Path, manifest: dict[str, Any]) -> Path:
    target = Path(path).expanduser()
    target.parent.mkdir(parents=True, exist_ok=True)
    temporary = target.with_name(f".{target.name}.{os.getpid()}.tmp")
    temporary.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
    os.replace(temporary, target)
    return target


def manifest_paths(target_root: str | Path) -> list[Path]:
    root = Path(target_root).expanduser() / "UserData" / "Migrations"
    if not root.is_dir():
        return []
    return sorted(root.glob("*/migration_manifest.json"), key=lambda path: path.stat().st_mtime, reverse=True)


def pending_manifest(target_root: str | Path) -> dict[str, Any]:
    for path in manifest_paths(target_root):
        manifest = _read_json(path)
        if str(manifest.get("migrationTarget") or "") != "official":
            continue
        queue = manifest.get("officialMigrationQueue") if isinstance(manifest.get("officialMigrationQueue"), dict) else {}
        cleanup = manifest.get("advancedCopyCleanup") if isinstance(manifest.get("advancedCopyCleanup"), dict) else {}
        queue_state = str(queue.get("state") or "")
        cleanup_state = str(cleanup.get("state") or "")
        if queue_state in QUEUE_ACTIVE_STATES or (queue_state == "completed" and cleanup_state == "pending"):
            return {"manifestPath": str(path), "manifest": manifest, "queue": queue, "cleanup": cleanup}
    return {}


def queue_summary(items: list[dict[str, Any]]) -> dict[str, Any]:
    counts = {
        "total": len(items),
        "pending": 0,
        "running": 0,
        "converted": 0,
        "alreadyInstalled": 0,
        "blocked": 0,
        "failed": 0,
        "warnings": 0,
    }
    for item in items:
        status = str(item.get("status") or "pending")
        if status == "already_installed":
            counts["alreadyInstalled"] += 1
        elif status in counts:
            counts[status] += 1
        else:
            counts["pending"] += 1
        if item.get("advancedOnlyFeatures"):
            counts["warnings"] += 1
    counts["successful"] = counts["converted"] + counts["alreadyInstalled"]
    counts["remaining"] = counts["pending"] + counts["running"] + counts["blocked"] + counts["failed"]
    return counts


def refresh_queue_state(manifest: dict[str, Any]) -> dict[str, Any]:
    queue = manifest.get("officialMigrationQueue") if isinstance(manifest.get("officialMigrationQueue"), dict) else {}
    items = [dict(item) for item in queue.get("items", []) if isinstance(item, dict)]
    summary = queue_summary(items)
    if summary["remaining"] == 0:
        state = "completed"
    elif summary["pending"] or summary["running"]:
        state = "partial"
    elif summary["blocked"]:
        state = "blocked"
    else:
        state = "partial"
    queue.update({"state": state, "items": items, "summary": summary, "updatedAt": _now_iso()})
    manifest["officialMigrationQueue"] = queue
    if state == "completed":
        cleanup = manifest.get("advancedCopyCleanup") if isinstance(manifest.get("advancedCopyCleanup"), dict) else {}
        cleanup.setdefault("state", "pending")
        cleanup["eligible"] = True
        manifest["advancedCopyCleanup"] = cleanup
    return manifest


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


def _folder_bytes(path: Path) -> int:
    if not path.exists():
        return 0
    if path.is_file():
        try:
            return path.stat().st_size
        except OSError:
            return 0
    total = 0
    for child in path.rglob("*"):
        if child.is_file():
            try:
                total += child.stat().st_size
            except OSError:
                pass
    return total


def _remove_path(path: Path) -> int:
    size = _folder_bytes(path)
    if path.is_dir():
        shutil.rmtree(path)
    elif path.exists():
        path.unlink()
    return size


def _prune_empty_parents(path: Path, stop: Path) -> None:
    current = path
    while current != stop and _path_is_within(current, stop):
        try:
            current.rmdir()
        except OSError:
            break
        current = current.parent


def cleanup_advanced_copy(
    manifest_path: str | Path,
    decision: str,
    *,
    load_skins: Callable[[], list[dict[str, Any]]],
    save_skins: Callable[[list[dict[str, Any]]], Any],
    confirm_token: str = "",
) -> dict[str, Any]:
    path = Path(manifest_path).expanduser().resolve()
    manifest = _read_json(path)
    queue = manifest.get("officialMigrationQueue") if isinstance(manifest.get("officialMigrationQueue"), dict) else {}
    clean_decision = str(decision or "").strip().lower()
    cleanup = manifest.get("advancedCopyCleanup") if isinstance(manifest.get("advancedCopyCleanup"), dict) else {}
    if clean_decision == KEEP_ADVANCED:
        queue_completed = str(queue.get("state") or "") == "completed"
        cleanup.update({
            "state": "kept",
            "decision": KEEP_ADVANCED,
            "claimedAt": cleanup.get("claimedAt") or _now_iso(),
            "eligible": queue_completed,
        })
        if queue_completed:
            cleanup["completedAt"] = cleanup.get("completedAt") or _now_iso()
        manifest["advancedCopyCleanup"] = cleanup
        write_manifest(path, manifest)
        return {"status": "kept", "removedBytes": 0, "removedPaths": [], "manifestPath": str(path)}
    if str(queue.get("state") or "") != "completed":
        raise RuntimeError("The Official migration must complete successfully before Advanced copy cleanup.")
    if clean_decision != DELETE_ADVANCED:
        raise ValueError("Advanced copy cleanup decision must be keep or delete.")
    if str(confirm_token or "") != CONFIRM_DELETE_TOKEN:
        raise PermissionError("Deleting the copied Advanced migration data requires confirmation.")

    target_root = Path(str(manifest.get("target", {}).get("root") or "")).resolve()
    from liverylab_core import legacy_workspace_migration

    expected_copy_fingerprint = str(manifest.get("advancedCopyFingerprint") or "")
    current_copy_fingerprint = legacy_workspace_migration.advanced_copy_fingerprint(target_root)
    if expected_copy_fingerprint and current_copy_fingerprint != expected_copy_fingerprint:
        raise RuntimeError(
            "The copied Advanced workspace changed after migration. It was kept so newly created or edited Advanced data is not deleted."
        )
    library_root = target_root / "LiveryLibrary"
    projects_root = target_root / "LiveryProjects"
    skins = load_skins()
    kept: list[dict[str, Any]] = []
    removed_entries: list[str] = []
    candidate_paths: list[Path] = []
    migration_id = str(manifest.get("migrationId") or "")
    for entry in skins:
        marker = entry.get("legacy_workspace_migration") if isinstance(entry.get("legacy_workspace_migration"), dict) else {}
        if str(marker.get("migrationId") or "") != migration_id:
            kept.append(entry)
            continue
        removed_entries.append(str(entry.get("key") or ""))
        for key, root in (("local_root", library_root), ("project_root", projects_root), ("project_folder", projects_root)):
            raw = str(entry.get(key) or "").strip()
            if not raw:
                continue
            candidate = Path(raw).expanduser()
            if _path_is_within(candidate, root):
                candidate_paths.append(candidate)

    removed_paths: list[str] = []
    removed_bytes = 0
    unique_candidates = sorted({path.resolve() for path in candidate_paths}, key=lambda item: len(item.parts), reverse=True)
    for candidate in unique_candidates:
        if not candidate.exists():
            continue
        removed_bytes += _remove_path(candidate)
        removed_paths.append(str(candidate))
        stop = library_root if _path_is_within(candidate, library_root) else projects_root
        _prune_empty_parents(candidate.parent, stop)

    for relative in ADDITIONAL_ADVANCED_ROOTS:
        candidate = target_root / relative
        if candidate.exists():
            removed_bytes += _remove_path(candidate)
            removed_paths.append(str(candidate))

    if projects_root.is_dir():
        for child in list(projects_root.iterdir()):
            if child.name.lower() in BOOTSTRAP_FILENAMES:
                continue
            removed_bytes += _remove_path(child)
            removed_paths.append(str(child))

    if library_root.is_dir():
        for child in list(library_root.iterdir()):
            if child.name.lower() in PRESERVED_LIBRARY_FILENAMES:
                continue
            removed_bytes += _remove_path(child)
            removed_paths.append(str(child))

    export_root = target_root / "LiveryPackageExport"
    if export_root.is_dir():
        for child in list(export_root.iterdir()):
            if child.name.lower() in BOOTSTRAP_FILENAMES:
                continue
            removed_bytes += _remove_path(child)
            removed_paths.append(str(child))

    save_skins(kept)
    cleanup.update(
        {
            "state": "deleted",
            "decision": DELETE_ADVANCED,
            "completedAt": _now_iso(),
            "eligible": True,
            "removedEntries": removed_entries,
            "removedPaths": removed_paths,
            "removedBytes": removed_bytes,
            "sourceInstallationPreserved": True,
        }
    )
    manifest["advancedCopyCleanup"] = cleanup
    write_manifest(path, manifest)
    return {
        "status": "deleted",
        "removedBytes": removed_bytes,
        "removedPaths": removed_paths,
        "removedEntries": removed_entries,
        "manifestPath": str(path),
        "sourceInstallationPreserved": True,
    }
