from __future__ import annotations

import os
import shutil
from dataclasses import asdict, dataclass
from datetime import datetime
from pathlib import Path
from typing import Any

from liverylab_core.game_write_guard import require_game_write_ready

APP_STORAGE_DIR_NAMES = {
    "liverylibrary",
    "liveryprojects",
    "liverypackageexport",
    "officialliverylibrary",
    "officialliveryprojects",
    "officialliverypackageexport",
    "userdata",
}


def is_app_storage_path(path: str | Path) -> bool:
    return any(part.lower() in APP_STORAGE_DIR_NAMES for part in Path(path).parts)


@dataclass
class WritePlanEntry:
    action: str
    target: str
    source: str = ""
    label: str = ""
    backup: bool = True


@dataclass
class WriteResult:
    action: str
    target: str
    source: str = ""
    label: str = ""
    status: str = "written"
    backup: str = ""
    bytes_written: int = 0

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)


class GameFileWriter:
    def __init__(
        self,
        root: str | Path,
        operation: str,
        required_paths: list[str | Path] | None = None,
        backup_root: str | Path | None = None,
        require_content_cars: bool = True,
        backups_enabled: bool = True,
    ) -> None:
        self.root = Path(root)
        self.operation = operation
        self.backup_root = Path(backup_root) if backup_root else None
        self.backups_enabled = bool(backups_enabled)
        self.preflight = require_game_write_ready(root, operation, required_paths or [], require_content_cars=require_content_cars)
        self.plan: list[WritePlanEntry] = []
        self.results: list[WriteResult] = []

    def backup_file(self, path: Path) -> Path | None:
        if not self.backups_enabled:
            return None
        if not path.exists() or not path.is_file():
            return None
        if is_app_storage_path(path):
            return None
        if self.backup_root:
            try:
                rel = path.resolve().relative_to(self.root.resolve())
            except Exception:
                rel_parts = [part for part in path.parts if part not in {path.anchor, os.sep}]
                rel = Path("_external", *rel_parts)
            backup_dir = self.backup_root / rel.parent
        else:
            backup_dir = path.parent / "_acevo_backups"
        backup_dir.mkdir(parents=True, exist_ok=True)
        stamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
        backup = backup_dir / f"{path.name}.{stamp}.bak"
        shutil.copy2(path, backup)
        return backup

    def add_plan(self, action: str, target: str | Path, source: str | Path | None = None, label: str = "", backup: bool = True) -> WritePlanEntry:
        entry = WritePlanEntry(
            action=action,
            target=str(target),
            source=str(source or ""),
            label=label,
            backup=backup,
        )
        self.plan.append(entry)
        return entry

    def write_bytes(self, target: str | Path, data: bytes, label: str = "", backup: bool = True) -> WriteResult:
        target_path = Path(target)
        self.add_plan("write_bytes", target_path, label=label, backup=backup)
        target_path.parent.mkdir(parents=True, exist_ok=True)
        backup_path = self.backup_file(target_path) if backup and target_path.exists() else None
        target_path.write_bytes(data)
        result = WriteResult(
            action="write_bytes",
            target=str(target_path),
            label=label,
            backup=str(backup_path) if backup_path else "",
            bytes_written=len(data),
        )
        self.results.append(result)
        return result

    def copy_file(self, source: str | Path, target: str | Path, label: str = "", backup: bool = True) -> WriteResult:
        source_path = Path(source)
        target_path = Path(target)
        self.add_plan("copy_file", target_path, source_path, label=label, backup=backup)
        if not source_path.exists() or not source_path.is_file():
            raise FileNotFoundError(f"Source file not found: {source_path}")
        try:
            if target_path.exists() and source_path.resolve() == target_path.resolve():
                result = WriteResult(
                    action="copy_file",
                    target=str(target_path),
                    source=str(source_path),
                    label=label,
                    status="skipped",
                    bytes_written=target_path.stat().st_size if target_path.exists() else 0,
                )
                self.results.append(result)
                return result
        except OSError:
            pass
        target_path.parent.mkdir(parents=True, exist_ok=True)
        backup_path = self.backup_file(target_path) if backup and target_path.exists() else None
        shutil.copy2(source_path, target_path)
        result = WriteResult(
            action="copy_file",
            target=str(target_path),
            source=str(source_path),
            label=label,
            backup=str(backup_path) if backup_path else "",
            bytes_written=target_path.stat().st_size if target_path.exists() else 0,
        )
        self.results.append(result)
        return result

    def move_file(self, source: str | Path, target: str | Path, label: str = "", backup: bool = True) -> WriteResult:
        source_path = Path(source)
        target_path = Path(target)
        self.add_plan("move_file", target_path, source_path, label=label, backup=backup)
        if not source_path.exists() or not source_path.is_file():
            raise FileNotFoundError(f"Source file not found: {source_path}")
        target_path.parent.mkdir(parents=True, exist_ok=True)
        backup_path = self.backup_file(target_path) if backup and target_path.exists() else None
        shutil.move(str(source_path), str(target_path))
        result = WriteResult(
            action="move_file",
            target=str(target_path),
            source=str(source_path),
            label=label,
            backup=str(backup_path) if backup_path else "",
            bytes_written=target_path.stat().st_size if target_path.exists() else 0,
        )
        self.results.append(result)
        return result

    def move_path(self, source: str | Path, target: str | Path, label: str = "", backup: bool = False) -> WriteResult:
        source_path = Path(source)
        target_path = Path(target)
        self.add_plan("move_path", target_path, source_path, label=label, backup=backup)
        if not source_path.exists():
            raise FileNotFoundError(f"Source path not found: {source_path}")
        target_path.parent.mkdir(parents=True, exist_ok=True)
        backup_path = self.backup_file(target_path) if backup and target_path.exists() and target_path.is_file() else None
        shutil.move(str(source_path), str(target_path))
        result = WriteResult(
            action="move_path",
            target=str(target_path),
            source=str(source_path),
            label=label,
            backup=str(backup_path) if backup_path else "",
            bytes_written=target_path.stat().st_size if target_path.exists() and target_path.is_file() else 0,
        )
        self.results.append(result)
        return result

    def delete_file(self, target: str | Path, label: str = "", backup: bool = True) -> WriteResult:
        target_path = Path(target)
        self.add_plan("delete_file", target_path, label=label, backup=backup)
        backup_path = self.backup_file(target_path) if backup and target_path.exists() and target_path.is_file() else None
        if target_path.exists() and target_path.is_file():
            target_path.unlink()
        result = WriteResult(
            action="delete_file",
            target=str(target_path),
            label=label,
            status="deleted",
            backup=str(backup_path) if backup_path else "",
        )
        self.results.append(result)
        return result

    def to_dict(self) -> dict[str, Any]:
        return {
            "operation": self.operation,
            "root": str(self.root),
            "backupsEnabled": self.backups_enabled,
            "preflight": self.preflight,
            "plan": [asdict(row) for row in self.plan],
            "results": [row.to_dict() for row in self.results],
        }
