from __future__ import annotations

import json
import os
import re
import shutil
from datetime import datetime
from pathlib import Path
from typing import Any


RUNTIME_BACKUP_DIR_NAMES = {"_acevo_backups"}


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


def _safe_slug(value: str, fallback: str = "item") -> str:
    text = str(value or "").strip().lower()
    text = text.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
    text = re.sub(r"[^a-z0-9_-]+", "_", text)
    text = re.sub(r"_+", "_", text).strip("_")
    return text or fallback


def _short_hash(value: str) -> str:
    import hashlib

    return hashlib.sha1(value.encode("utf-8", errors="ignore")).hexdigest()[:12]


def _win_path(path: Path) -> str:
    text = str(path.resolve())
    if os.name == "nt" and not text.startswith("\\\\?\\"):
        return "\\\\?\\" + text
    return text


def _make_writable_tree(path: Path) -> None:
    if not path.exists():
        return
    targets = [path]
    if path.is_dir():
        targets.extend(path.rglob("*"))
    for target in targets:
        try:
            os.chmod(_win_path(target), 0o700)
        except Exception:
            pass


def _load_my_skins(registry_path: Path) -> list[dict[str, Any]]:
    try:
        data = json.loads(registry_path.read_text(encoding="utf-8"))
    except Exception:
        return []
    raw = data.get("my_skins", []) if isinstance(data, dict) else data
    if not isinstance(raw, list):
        return []
    return [dict(item) for item in raw if isinstance(item, dict)]


def _skin_id_from_entry(entry: dict[str, Any]) -> str:
    skin_id = str(entry.get("skin_id") or "").strip()
    if skin_id:
        return skin_id
    key = str(entry.get("key") or "").strip()
    if ":" in key:
        return key.split(":", 1)[1].strip()
    return ""


def _car_id_from_entry(entry: dict[str, Any]) -> str:
    car_model = str(entry.get("car_model") or "").strip()
    if car_model:
        return car_model
    car_content_path = str(entry.get("car_content_path") or "").replace("/", "\\").strip("\\")
    if car_content_path:
        return car_content_path.split("\\")[-1]
    key = str(entry.get("key") or "").strip()
    if ":" in key:
        return key.split(":", 1)[0].strip()
    return ""


def _registered_local_skin_dirs(local_skins_root: Path, registry_path: Path) -> set[Path]:
    registered: set[Path] = set()
    root = local_skins_root.resolve()
    for entry in _load_my_skins(registry_path):
        raw_local = str(entry.get("local_root") or "").strip()
        if raw_local:
            local_path = Path(raw_local)
            try:
                local_path.resolve().relative_to(root)
                registered.add(local_path.resolve())
            except Exception:
                pass
        car_id = _car_id_from_entry(entry)
        skin_id = _skin_id_from_entry(entry)
        if car_id and skin_id:
            registered.add((local_skins_root / _safe_slug(car_id, "car") / _safe_slug(skin_id, "skin")).resolve())
    return registered


def _unique_destination(path: Path) -> Path:
    if not path.exists():
        return path
    index = 2
    while True:
        candidate = path.with_name(f"{path.name}_{index}")
        if not candidate.exists():
            return candidate
        index += 1


def _move_path(source: Path, backup_root: Path, label: str, base: Path, compact: bool = False) -> Path:
    relative = source.resolve().relative_to(base.resolve())
    if compact:
        target_name = f"{_safe_slug(source.parent.name, 'folder')}_{_safe_slug(source.name, 'item')}_{_short_hash(str(relative).lower())}"
        target = _unique_destination(backup_root / label / target_name)
    else:
        target = _unique_destination(backup_root / label / relative)
    target.parent.mkdir(parents=True, exist_ok=True)
    try:
        os.replace(_win_path(source), _win_path(target))
    except OSError:
        _make_writable_tree(source)
        shutil.move(_win_path(source), _win_path(target))
    return target


def _empty_parent_dirs(path: Path, stop_at: Path) -> list[str]:
    removed: list[str] = []
    current = path.parent
    stop = stop_at.resolve()
    while True:
        try:
            current.resolve().relative_to(stop)
        except Exception:
            break
        if current.resolve() == stop:
            break
        try:
            current.rmdir()
            removed.append(str(current))
        except OSError:
            break
        current = current.parent
    return removed


def _move_runtime_backup_dirs(root: Path, backup_root: Path, label: str) -> tuple[int, int, list[dict[str, str]]]:
    if not root.exists():
        return 0, 0, []
    moved_dirs = 0
    moved_files = 0
    moved: list[dict[str, str]] = []
    backup_dirs = [
        path
        for path in sorted(root.rglob("*"), key=lambda item: len(item.parts), reverse=True)
        if path.is_dir() and path.name.lower() in RUNTIME_BACKUP_DIR_NAMES
    ]
    for backup_dir in backup_dirs:
        try:
            file_count = sum(1 for item in backup_dir.rglob("*") if item.is_file())
            destination = _move_path(backup_dir, backup_root, label, root, compact=True)
            moved_dirs += 1
            moved_files += file_count
            moved.append({"from": str(backup_dir), "to": str(destination), "files": str(file_count)})
            _empty_parent_dirs(backup_dir, root)
        except Exception as exc:
            moved.append({"from": str(backup_dir), "error": str(exc)})
    return moved_dirs, moved_files, moved


def _move_orphan_local_skins(local_skins_root: Path, registry_path: Path, backup_root: Path) -> tuple[int, list[dict[str, str]]]:
    if not local_skins_root.exists():
        return 0, []
    registered = _registered_local_skin_dirs(local_skins_root, registry_path)
    moved_count = 0
    moved: list[dict[str, str]] = []
    for car_dir in sorted([item for item in local_skins_root.iterdir() if item.is_dir()], key=lambda item: item.name.lower()):
        if car_dir.name.lower() in RUNTIME_BACKUP_DIR_NAMES:
            continue
        for skin_dir in sorted([item for item in car_dir.iterdir() if item.is_dir()], key=lambda item: item.name.lower()):
            if skin_dir.name.lower() in RUNTIME_BACKUP_DIR_NAMES:
                continue
            try:
                resolved = skin_dir.resolve()
            except Exception:
                resolved = skin_dir
            if resolved in registered:
                continue
            try:
                destination = _move_path(skin_dir, backup_root, "orphan_local_skins", local_skins_root)
                moved_count += 1
                moved.append({"from": str(skin_dir), "to": str(destination)})
            except Exception as exc:
                moved.append({"from": str(skin_dir), "error": str(exc)})
        _empty_parent_dirs(car_dir / "_placeholder", local_skins_root)
    return moved_count, moved


def cleanup_app_storage(
    *,
    local_skins_root: Path,
    livery_library_root: Path,
    livery_projects_root: Path,
    user_data_root: Path,
    registry_path: Path,
) -> dict[str, Any]:
    backup_root = user_data_root / "cleanup_backups" / _now_stamp()
    report: dict[str, Any] = {
        "backupFolder": str(backup_root),
        "movedRuntimeBackupDirs": 0,
        "movedRuntimeBackupFiles": 0,
        "movedOrphanSkinDirs": 0,
        "errorCount": 0,
        "moved": [],
    }

    for root, label in (
        (livery_library_root, "livery_library_runtime_backups"),
        (livery_projects_root, "livery_projects_runtime_backups"),
    ):
        moved_dirs, moved_files, moved = _move_runtime_backup_dirs(root, backup_root, label)
        report["movedRuntimeBackupDirs"] += moved_dirs
        report["movedRuntimeBackupFiles"] += moved_files
        report["errorCount"] += sum(1 for row in moved if row.get("error"))
        report["moved"].extend(moved)

    moved_orphans, moved = _move_orphan_local_skins(local_skins_root, registry_path, backup_root)
    report["movedOrphanSkinDirs"] += moved_orphans
    report["errorCount"] += sum(1 for row in moved if row.get("error"))
    report["moved"].extend(moved)

    if report["movedRuntimeBackupDirs"] or report["movedOrphanSkinDirs"] or report["errorCount"]:
        backup_root.mkdir(parents=True, exist_ok=True)
        (backup_root / "cleanup_manifest.json").write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
    return report
