"""Named manifest snapshots — compliance baselines.

Snapshots are timestamped copies of a manifest stored in a local
directory.  They enable workflows like "create a Q1 baseline, then
compare it against Q2 to see what changed" -- using the existing
``diff_manifests()`` infrastructure.

Default location:
    ~/.local/share/certisigma-census/snapshots/
"""

from __future__ import annotations

import json
import logging
import os
import sqlite3
import time

from dataclasses import dataclass
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)


def _default_snapshot_dir() -> Path:
    return Path.home() / ".local" / "share" / "certisigma-census" / "snapshots"


def _resolve_snapshot_dir(override: str | Path | None = None) -> Path:
    if override:
        return Path(override).resolve()
    from .config import load_config
    cfg = load_config()
    configured = cfg.get("snapshot_dir", "")
    if configured:
        return Path(configured).resolve()
    return _default_snapshot_dir()


_VALID_NAME_CHARS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-_")


def _validate_name(name: str) -> None:
    """Ensure snapshot name is safe for filesystem use."""
    if not name:
        raise ValueError("Snapshot name cannot be empty")
    if len(name) > 128:
        raise ValueError(f"Snapshot name too long ({len(name)} > 128 chars)")
    if not all(c in _VALID_NAME_CHARS for c in name.lower()):
        raise ValueError(
            f"Invalid snapshot name '{name}': "
            "use only letters, digits, hyphens, and underscores"
        )


@dataclass
class SnapshotInfo:
    name: str
    path: Path
    created_at: str
    size_bytes: int
    manifest_root: str


def create_snapshot(
    name: str,
    manifest_path: Path,
    snapshot_dir: Path | str | None = None,
) -> SnapshotInfo:
    """Create a named snapshot by copying the manifest file."""
    _validate_name(name)
    sdir = _resolve_snapshot_dir(snapshot_dir)
    sdir.mkdir(parents=True, exist_ok=True)

    dest = sdir / f"{name}.db"
    if dest.exists():
        raise FileExistsError(f"Snapshot '{name}' already exists at {dest}")

    if not manifest_path.exists():
        raise FileNotFoundError(f"Manifest not found: {manifest_path}")

    src_conn = sqlite3.connect(str(manifest_path))
    try:
        dst_conn = sqlite3.connect(str(dest))
        try:
            src_conn.backup(dst_conn)
        finally:
            dst_conn.close()
    finally:
        src_conn.close()
    try:
        dest.chmod(0o600)
    except OSError:
        pass

    meta = {
        "name": name,
        "created_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
        "source_manifest": str(manifest_path),
    }
    meta_path = sdir / f"{name}.json"
    fd = os.open(str(meta_path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "w", encoding="utf-8") as f:
        f.write(json.dumps(meta, indent=2, default=str))

    from .manifest import Manifest
    with Manifest.load(dest) as m:
        root = m.root_dir

    return SnapshotInfo(
        name=name,
        path=dest,
        created_at=meta["created_at"],
        size_bytes=dest.stat().st_size,
        manifest_root=root,
    )


def list_snapshots(snapshot_dir: Path | str | None = None) -> list[SnapshotInfo]:
    """List all snapshots sorted by creation time (newest first)."""
    sdir = _resolve_snapshot_dir(snapshot_dir)
    if not sdir.exists():
        return []

    snapshots = []
    for meta_path in sorted(sdir.glob("*.json"), reverse=True):
        try:
            meta = json.loads(meta_path.read_text(encoding="utf-8"))
            db_path = meta_path.with_suffix(".db")
            if not db_path.exists():
                continue
            from .manifest import Manifest
            with Manifest.load(db_path) as m:
                root = m.root_dir
            snapshots.append(SnapshotInfo(
                name=meta["name"],
                path=db_path,
                created_at=meta.get("created_at", "unknown"),
                size_bytes=db_path.stat().st_size,
                manifest_root=root,
            ))
        except (json.JSONDecodeError, KeyError, TypeError, OSError, sqlite3.Error) as exc:
            logger.warning("Skipping corrupt snapshot metadata: %s (%s)", meta_path, exc)

    return snapshots


def get_snapshot_path(name: str, snapshot_dir: Path | str | None = None) -> Optional[Path]:
    """Get the path to a named snapshot's manifest, or None."""
    _validate_name(name)
    sdir = _resolve_snapshot_dir(snapshot_dir)
    db_path = sdir / f"{name}.db"
    return db_path if db_path.exists() else None


def delete_snapshot(name: str, snapshot_dir: Path | str | None = None) -> bool:
    """Delete a snapshot and its metadata. Returns True if deleted."""
    _validate_name(name)
    sdir = _resolve_snapshot_dir(snapshot_dir)
    db_path = sdir / f"{name}.db"
    meta_path = sdir / f"{name}.json"
    deleted = False
    for p in (db_path, meta_path):
        if p.exists():
            try:
                p.unlink()
                deleted = True
            except OSError as exc:
                logger.warning("Cannot delete snapshot file %s: %s", p, exc)
    return deleted
