"""SQLite backup and restore helpers for coordinator operations."""

from __future__ import annotations

import sqlite3
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path


@dataclass(frozen=True)
class BackupResult:
    source_path: Path
    output_path: Path
    size_bytes: int


@dataclass(frozen=True)
class RestoreResult:
    backup_path: Path
    target_path: Path
    snapshot_path: Path | None
    size_bytes: int


def timestamp_slug(now: datetime | None = None) -> str:
    current = now or datetime.now()
    return current.strftime("%Y%m%d-%H%M%S")


def default_backup_output_path(database_path: str | Path, *, backup_dir: str | Path | None = None) -> Path:
    database = Path(database_path).expanduser()
    target_dir = Path(backup_dir).expanduser() if backup_dir else database.parent / "backups"
    return target_dir / f"{database.stem}-{timestamp_slug()}.sqlite3"


def default_restore_snapshot_path(database_path: str | Path, *, snapshot_dir: str | Path | None = None) -> Path:
    database = Path(database_path).expanduser()
    target_dir = Path(snapshot_dir).expanduser() if snapshot_dir else database.parent / "restore-snapshots"
    return target_dir / f"{database.stem}-pre-restore-{timestamp_slug()}.sqlite3"


def backup_sqlite_database(source_path: str | Path, output_path: str | Path) -> BackupResult:
    source = Path(source_path).expanduser()
    output = Path(output_path).expanduser()

    if not source.exists():
        raise FileNotFoundError(f"Database does not exist: {source}")

    output.parent.mkdir(parents=True, exist_ok=True)
    temp_output = output.with_name(f"{output.name}.tmp")
    if temp_output.exists():
        temp_output.unlink()

    try:
        with sqlite3.connect(source) as source_db:
            try:
                source_db.execute("PRAGMA wal_checkpoint(PASSIVE)")
            except sqlite3.DatabaseError:
                pass
            with sqlite3.connect(temp_output) as backup_db:
                source_db.backup(backup_db)
                backup_db.commit()

        temp_output.replace(output)
    except Exception:
        temp_output.unlink(missing_ok=True)
        raise
    return BackupResult(source_path=source, output_path=output, size_bytes=output.stat().st_size)


def restore_sqlite_database(
    backup_path: str | Path,
    target_path: str | Path,
    *,
    snapshot_path: str | Path | None = None,
) -> RestoreResult:
    backup = Path(backup_path).expanduser()
    target = Path(target_path).expanduser()
    snapshot = Path(snapshot_path).expanduser() if snapshot_path else None

    if not backup.exists():
        raise FileNotFoundError(f"Backup does not exist: {backup}")

    target.parent.mkdir(parents=True, exist_ok=True)
    snapshot_created = snapshot if snapshot and target.exists() else None

    if snapshot_created:
        backup_sqlite_database(target, snapshot_created)

    temp_restore = target.with_name(f"{target.name}.restore-tmp")
    # Clean up any leftover temp files and their sidecars
    for path in (temp_restore, Path(f"{temp_restore}-wal"), Path(f"{temp_restore}-shm")):
        if path.exists():
            path.unlink()

    try:
        with sqlite3.connect(backup) as backup_db:
            with sqlite3.connect(temp_restore) as restore_db:
                backup_db.backup(restore_db)
                # Don't set WAL mode here — init_db() does that on startup.

        temp_restore.replace(target)
    except Exception:
        # Clean up partial temp files on failure
        for path in (temp_restore, Path(f"{temp_restore}-wal"), Path(f"{temp_restore}-shm")):
            if path.exists():
                path.unlink()
        raise

    # Clean up any sidecars from temp restore and backup
    for base in (target, temp_restore, backup):
        for suffix in ("-wal", "-shm"):
            sidecar = Path(f"{base}{suffix}")
            if sidecar.exists():
                sidecar.unlink()

    return RestoreResult(
        backup_path=backup,
        target_path=target,
        snapshot_path=snapshot_created,
        size_bytes=target.stat().st_size,
    )
