from __future__ import annotations

import hashlib
import shutil
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path

from jarvis_finance.config.paths import is_relative_to


@dataclass(frozen=True)
class BackupResult:
    backup_file: Path
    checksum_file: Path
    sha256: str
    size_bytes: int


@dataclass(frozen=True)
class VerifyBackupResult:
    backup_file: Path
    checksum_file: Path
    sha256: str
    checksum_ok: bool
    exists: bool


@dataclass(frozen=True)
class RestoreResult:
    restored_to: Path
    source_file: Path
    verified: bool


def sha256_file(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def write_checksum_file(backup_file: Path, sha256: str) -> Path:
    checksum_file = backup_file.with_suffix(backup_file.suffix + ".sha256")
    checksum_file.write_text(f"{sha256}  {backup_file.name}\n", encoding="utf-8")
    return checksum_file


def read_expected_checksum(checksum_file: Path) -> str | None:
    if not checksum_file.exists():
        return None
    text = checksum_file.read_text(encoding="utf-8", errors="ignore").strip()
    if not text:
        return None
    return text.split()[0]


def ensure_outside_repo(path: Path, repo_root: Path) -> None:
    if is_relative_to(path.resolve(), repo_root.resolve()):
        raise ValueError(f"Backup/restore path must be outside the Git repository: {path}")


def backup_runtime_db(*, db_path: Path, backups_dir: Path, repo_root: Path, timestamp: str | None = None) -> BackupResult:
    db_path = db_path.expanduser().resolve()
    backups_dir = backups_dir.expanduser().resolve()
    ensure_outside_repo(backups_dir, repo_root)
    ensure_outside_repo(db_path, repo_root)
    if not db_path.exists():
        raise FileNotFoundError(f"Runtime DB not found: {db_path}")
    backups_dir.mkdir(parents=True, exist_ok=True)
    stamp = timestamp or datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
    backup_file = backups_dir / f"finance-runtime-db-{stamp}.sqlite3"
    shutil.copy2(db_path, backup_file)
    checksum = sha256_file(backup_file)
    checksum_file = write_checksum_file(backup_file, checksum)
    return BackupResult(
        backup_file=backup_file,
        checksum_file=checksum_file,
        sha256=checksum,
        size_bytes=backup_file.stat().st_size,
    )


def verify_backup(*, backup_file: Path, repo_root: Path | None = None) -> VerifyBackupResult:
    backup_file = backup_file.expanduser().resolve()
    if repo_root is not None:
        ensure_outside_repo(backup_file, repo_root)
    exists = backup_file.exists() and backup_file.is_file()
    if not exists:
        return VerifyBackupResult(
            backup_file=backup_file,
            checksum_file=backup_file.with_suffix(backup_file.suffix + ".sha256"),
            sha256="",
            checksum_ok=False,
            exists=False,
        )
    actual = sha256_file(backup_file)
    checksum_file = backup_file.with_suffix(backup_file.suffix + ".sha256")
    expected = read_expected_checksum(checksum_file)
    return VerifyBackupResult(
        backup_file=backup_file,
        checksum_file=checksum_file,
        sha256=actual,
        checksum_ok=bool(expected and expected == actual),
        exists=True,
    )


def restore_runtime_db(*, backup_file: Path, db_path: Path, repo_root: Path, yes: bool = False) -> RestoreResult:
    if not yes:
        raise PermissionError("Restore requires explicit confirmation via --yes")
    backup_file = backup_file.expanduser().resolve()
    db_path = db_path.expanduser().resolve()
    ensure_outside_repo(backup_file, repo_root)
    ensure_outside_repo(db_path, repo_root)
    verification = verify_backup(backup_file=backup_file, repo_root=repo_root)
    if not verification.exists:
        raise FileNotFoundError(f"Backup file not found: {backup_file}")
    if not verification.checksum_ok:
        raise ValueError(f"Backup checksum verification failed: {backup_file}")
    db_path.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(backup_file, db_path)
    return RestoreResult(restored_to=db_path, source_file=backup_file, verified=True)
