#!/usr/bin/env python3
"""Verify and optionally materialize an isolated Overdeck K3s restore tree.

The input is the decrypted Phase 1 TAR.  The command never prints token or
credential contents.  ``--restore-root`` must name an empty, non-live directory;
it reconstructs the exact filesystem layout needed by the documented restore
runbook without stopping or modifying a running K3s server.
"""
from __future__ import annotations

import argparse
import json
import os
import shutil
import sqlite3
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Any, Sequence

LIB_DIR = Path(__file__).resolve().parent / "lib"
sys.path.insert(0, str(LIB_DIR))
from phase1_common import Phase1Error, safe_extract_tar, sha256_file, utc_now, verify_manifest  # noqa: E402
from phase1_common import absolute_path_without_symlink_resolution  # noqa: E402


def verify_sqlite(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
    del manifest
    state_db = root / "payload/datastore/db/state.db"
    if not state_db.is_file():
        raise Phase1Error("SQLite backup is missing payload/datastore/db/state.db")
    uri = f"file:{state_db}?mode=ro"
    connection = sqlite3.connect(uri, uri=True, timeout=30)
    try:
        quick = connection.execute("PRAGMA quick_check").fetchone()
        integrity = connection.execute("PRAGMA integrity_check").fetchone()
    finally:
        connection.close()
    quick_value = quick[0] if quick else "missing-result"
    integrity_value = integrity[0] if integrity else "missing-result"
    if quick_value != "ok" or integrity_value != "ok":
        raise Phase1Error(
            f"SQLite restore verification failed: quick_check={quick_value!r} integrity_check={integrity_value!r}"
        )
    return {
        "type": "sqlite",
        "state_db": "payload/datastore/db/state.db",
        "size": state_db.stat().st_size,
        "sha256": sha256_file(state_db),
        "quick_check": quick_value,
        "integrity_check": integrity_value,
    }


def verify_etcd(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
    snapshot_root = root / "payload/datastore/snapshot"
    snapshots = [path for path in snapshot_root.rglob("*") if path.is_file() and not path.is_symlink()]
    if len(snapshots) != 1:
        raise Phase1Error(f"expected exactly one etcd snapshot; found {len(snapshots)}")
    snapshot = snapshots[0]
    result: dict[str, Any] = {
        "type": "etcd",
        "snapshot": snapshot.relative_to(root).as_posix(),
        "size": snapshot.stat().st_size,
        "sha256": sha256_file(snapshot),
        "container_check": "opaque-nonempty",
    }
    if zipfile.is_zipfile(snapshot):
        with zipfile.ZipFile(snapshot) as archive:
            failed_member = archive.testzip()
            if failed_member is not None:
                raise Phase1Error(f"compressed etcd snapshot failed ZIP validation at {failed_member}")
        result["container_check"] = "zip-ok"
    elif snapshot.stat().st_size <= 0:
        raise Phase1Error("etcd snapshot is empty")
    expected_sha = manifest.get("datastore", {}).get("snapshot_sha256")
    if expected_sha is not None and expected_sha != result["sha256"]:
        raise Phase1Error("etcd snapshot SHA-256 does not match datastore metadata")
    expected_size = manifest.get("datastore", {}).get("snapshot_size")
    if expected_size is not None and expected_size != result["size"]:
        raise Phase1Error("etcd snapshot size does not match datastore metadata")
    return result


def ensure_empty_restore_root(destination: Path) -> Path:
    if destination.is_symlink():
        raise Phase1Error(f"restore root must not be a symlink: {destination}")
    resolved = destination.resolve()
    if resolved == Path("/"):
        raise Phase1Error("refusing to materialize a restore into the live filesystem root")
    if destination.exists():
        if not destination.is_dir():
            raise Phase1Error(f"restore root is not a directory: {destination}")
        if any(destination.iterdir()):
            raise Phase1Error(f"restore root must be empty: {destination}")
    else:
        destination.mkdir(parents=True, mode=0o700)
    os.chmod(destination, 0o700)
    return resolved


def copy_regular_tree(source: Path, destination: Path) -> int:
    if not source.is_dir() or source.is_symlink():
        raise Phase1Error(f"restore source directory is missing or unsafe: {source}")
    copied = 0
    for root, dirs, files in os.walk(source, followlinks=False):
        root_path = Path(root)
        dirs[:] = [name for name in dirs if not (root_path / name).is_symlink()]
        relative = root_path.relative_to(source)
        target_root = destination / relative
        target_root.mkdir(parents=True, exist_ok=True)
        os.chmod(target_root, root_path.stat().st_mode & 0o777)
        for name in files:
            incoming = root_path / name
            if incoming.is_symlink() or not incoming.is_file():
                raise Phase1Error(f"restore payload contains an unsafe file: {incoming}")
            target = target_root / name
            if target.exists() or target.is_symlink():
                raise Phase1Error(f"restore target collision: {target}")
            shutil.copy2(incoming, target, follow_symlinks=False)
            copied += 1
    return copied


def copy_regular_file(source: Path, destination: Path, *, mode: int | None = None) -> None:
    if source.is_symlink() or not source.is_file():
        raise Phase1Error(f"restore source file is missing or unsafe: {source}")
    destination.parent.mkdir(parents=True, exist_ok=True)
    if destination.exists() or destination.is_symlink():
        raise Phase1Error(f"restore target collision: {destination}")
    shutil.copy2(source, destination, follow_symlinks=False)
    if mode is not None:
        os.chmod(destination, mode)


def safe_absolute_restore_target(root: Path, absolute_path: str, *, label: str) -> Path:
    candidate = Path(absolute_path)
    if not candidate.is_absolute() or ".." in candidate.parts:
        raise Phase1Error(f"backup manifest has an invalid {label} path: {absolute_path!r}")
    target = root / candidate.relative_to("/")
    resolved = target.resolve()
    if resolved != root and root not in resolved.parents:
        raise Phase1Error(f"backup manifest {label} path escapes restore root: {absolute_path!r}")
    return target


def verify_restored_sqlite(target_root: Path) -> dict[str, Any]:
    state_db = target_root / "var/lib/rancher/k3s/server/db/state.db"
    connection = sqlite3.connect(f"file:{state_db}?mode=ro", uri=True, timeout=30)
    try:
        verdict = connection.execute("PRAGMA integrity_check").fetchone()
    finally:
        connection.close()
    value = verdict[0] if verdict else "missing-result"
    if value != "ok":
        raise Phase1Error(f"materialized SQLite restore failed integrity_check: {value}")
    return {"state_db": str(state_db.relative_to(target_root)), "integrity_check": value}


def materialize_restore(
    extracted_root: Path,
    manifest: dict[str, Any],
    destination: Path,
    datastore: dict[str, Any],
) -> dict[str, Any]:
    target_root = ensure_empty_restore_root(destination)
    copied = 0

    config_source = extracted_root / "payload/config/etc-rancher-k3s"
    config_target = target_root / "etc/rancher/k3s"
    copied += copy_regular_tree(config_source, config_target)

    token_source = extracted_root / str(manifest["server_token_path"])
    token_target = target_root / "var/lib/rancher/k3s/server/token"
    copy_regular_file(token_source, token_target, mode=0o600)
    copied += 1
    agent_target = target_root / "var/lib/rancher/k3s/server/agent-token"
    agent_descriptor = manifest.get("agent_token") if manifest.get("schema_version") == 2 else None
    if isinstance(agent_descriptor, dict):
        classification = agent_descriptor.get("classification")
        if classification == "regular":
            agent_source = extracted_root / "payload/server/agent-token"
            copy_regular_file(agent_source, agent_target, mode=0o600)
            copied += 1
        elif classification == "symlink-to-server-token":
            agent_target.parent.mkdir(parents=True, exist_ok=True)
            if agent_target.exists() or agent_target.is_symlink():
                raise Phase1Error(f"restore target collision: {agent_target}")
            os.symlink("token", agent_target)
            copied += 1
        elif classification != "absent":
            raise Phase1Error("unsupported agent-token restoration descriptor")
    else:
        agent_source = extracted_root / "payload/server/agent-token"
        if agent_source.is_file() and not agent_source.is_symlink():
            copy_regular_file(agent_source, agent_target, mode=0o600)
            copied += 1

    if datastore["type"] == "sqlite":
        copied += copy_regular_tree(
            extracted_root / "payload/datastore/db",
            target_root / "var/lib/rancher/k3s/server/db",
        )
        datastore_restore = verify_restored_sqlite(target_root)
        activation = {
            "method": "filesystem-replacement-while-k3s-stopped",
            "requires_console": True,
            "live_command_executed": False,
        }
    else:
        snapshot_source = extracted_root / datastore["snapshot"]
        snapshot_target = (
            target_root
            / "var/lib/rancher/k3s/server/db/snapshots/phase1-restore"
            / snapshot_source.name
        )
        copy_regular_file(snapshot_source, snapshot_target, mode=0o600)
        copied += 1
        datastore_restore = {
            "snapshot": str(snapshot_target.relative_to(target_root)),
            "size": snapshot_target.stat().st_size,
            "sha256": sha256_file(snapshot_target),
        }
        activation = {
            "method": "k3s-cluster-reset-restore-path",
            "restore_path": f"/var/lib/rancher/k3s/server/db/snapshots/phase1-restore/{snapshot_source.name}",
            "requires_console": True,
            "live_command_executed": False,
        }

    systemd_manifest = manifest.get("systemd") or {}
    systemd_source = extracted_root / "payload/systemd"
    known = [
        (systemd_source / "k3s.service", systemd_manifest.get("service_path") or "/etc/systemd/system/k3s.service"),
        (
            systemd_source / "k3s.service.env",
            systemd_manifest.get("environment_path") or "/etc/systemd/system/k3s.service.env",
        ),
    ]
    for source, absolute_target in known:
        if source.is_file():
            copy_regular_file(
                source,
                safe_absolute_restore_target(target_root, str(absolute_target), label="systemd"),
            )
            copied += 1
    fragment_dir = systemd_source / "fragment"
    unit_fragment = systemd_manifest.get("unit_fragment")
    fragments = [path for path in fragment_dir.glob("*") if path.is_file()] if fragment_dir.is_dir() else []
    if fragments and isinstance(unit_fragment, str) and unit_fragment:
        if len(fragments) != 1:
            raise Phase1Error("backup contains multiple ambiguous systemd unit fragments")
        target = safe_absolute_restore_target(target_root, unit_fragment, label="unit fragment")
        if not target.exists():
            copy_regular_file(fragments[0], target)
            copied += 1

    plan = {
        "schema_version": 1,
        "created_utc": utc_now(),
        "source_transaction": manifest.get("transaction_id"),
        "server": manifest.get("server"),
        "k3s": manifest.get("k3s"),
        "datastore": datastore_restore,
        "activation": activation,
        "server_token": {
            "path": "var/lib/rancher/k3s/server/token",
            "present": token_target.is_file(),
            "size": token_target.stat().st_size,
            "value_exposed": False,
        },
        "agent_token": {
            "classification": (agent_descriptor or {}).get("classification", "legacy-regular-or-absent"),
            "path": "var/lib/rancher/k3s/server/agent-token",
            "present": agent_target.exists() or agent_target.is_symlink(),
            "is_symlink": agent_target.is_symlink(),
            "link_target": os.readlink(agent_target) if agent_target.is_symlink() else None,
            "value_exposed": False,
        },
        "live_root_modified": False,
        "ownership_applied": False,
        "copied_file_count": copied,
    }
    plan_path = target_root / "OVERDECK_RESTORE_PLAN.json"
    plan_path.write_text(json.dumps(plan, indent=2, sort_keys=True) + "\n")
    os.chmod(plan_path, 0o600)
    return {
        "status": "materialized",
        "target_root": str(target_root),
        "copied_file_count": copied,
        "datastore": datastore_restore,
        "server_token_present": True,
        "server_token_size": token_target.stat().st_size,
        "server_token_value_exposed": False,
        "agent_token": {
            "classification": (agent_descriptor or {}).get("classification", "legacy-regular-or-absent"),
            "present": agent_target.exists() or agent_target.is_symlink(),
            "is_symlink": agent_target.is_symlink(),
            "link_target": os.readlink(agent_target) if agent_target.is_symlink() else None,
            "value_exposed": False,
        },
        "activation": activation,
        "live_root_modified": False,
        "plan": str(plan_path.relative_to(target_root)),
    }


def validate_canonical_files(root: Path, manifest: dict[str, Any]) -> dict[str, Any]:
    contract = root / "payload/config/etc-rancher-k3s/overdeck/control-plane.json"
    version_lock = root / "payload/config/etc-rancher-k3s/overdeck/version-lock.json"
    managed_config = root / "payload/config/etc-rancher-k3s/config.yaml.d/90-overdeck-control-plane.yaml"
    paths = [contract, version_lock, managed_config]
    present = [path.is_file() and not path.is_symlink() for path in paths]
    purpose = manifest.get("purpose", "legacy")
    if not any(present):
        if purpose != "prechange":
            raise Phase1Error("post-change/scheduled backup is missing canonical control-plane files")
        return {"present": False, "purpose": purpose, "reason": "prechange-baseline"}
    if not all(present):
        missing = [path.relative_to(root).as_posix() for path, exists in zip(paths, present) if not exists]
        raise Phase1Error(f"backup has an incomplete canonical control-plane file set: {missing}")
    try:
        contract_doc = json.loads(contract.read_text())
        lock_doc = json.loads(version_lock.read_text())
    except json.JSONDecodeError as exc:
        raise Phase1Error(f"canonical control-plane JSON is invalid: {exc}") from exc
    if contract_doc.get("managed_by") != "overdeck-k3s-phase1":
        raise Phase1Error("canonical control-plane contract has an unexpected manager")
    if contract_doc.get("schema_version") not in {1, 2}:
        raise Phase1Error("canonical control-plane contract has an unsupported schema")
    if lock_doc.get("managed_by") != "overdeck-k3s-phase1":
        raise Phase1Error("canonical version lock has an unexpected manager")
    if lock_doc.get("schema_version") not in {1, 2}:
        raise Phase1Error("canonical version lock has an unsupported schema")
    manifest_k3s = manifest.get("k3s") or {}
    if lock_doc.get("version") != manifest_k3s.get("version"):
        raise Phase1Error("version-lock version does not match backup manifest")
    if lock_doc.get("schema_version") == 2:
        lock_launcher = lock_doc.get("launcher") or {}
        manifest_launcher = manifest_k3s.get("launcher") or {}
        if lock_launcher.get("sha256") != manifest_launcher.get("sha256"):
            raise Phase1Error("version-lock launcher SHA-256 does not match backup manifest")
    else:
        if lock_doc.get("binary_sha256") != manifest_k3s.get("binary_sha256"):
            raise Phase1Error("legacy version-lock binary SHA-256 does not match backup manifest")
    contract_server = contract_doc.get("server") or {}
    manifest_server = manifest.get("server") or {}
    for key in ["machine_id", "tailscale_ipv4"]:
        if contract_server.get(key) != manifest_server.get(key):
            raise Phase1Error(f"control-plane contract {key} does not match backup manifest")
    managed_text = managed_config.read_text(errors="replace")
    if 'write-kubeconfig-mode: "0600"' not in managed_text or "tls-san+:" not in managed_text:
        raise Phase1Error("canonical managed K3s config is missing required Phase 1 settings")
    if managed_config.stat().st_mode & 0o077:
        raise Phase1Error("canonical managed K3s config permissions are too broad")
    return {
        "present": True,
        "purpose": purpose,
        "managed_config": managed_config.relative_to(root).as_posix(),
        "contract": contract_doc,
        "version_lock": lock_doc,
    }


def verify_archive(archive: Path, *, restore_root: Path | None = None) -> dict[str, Any]:
    if not archive.is_file() or archive.is_symlink():
        raise Phase1Error(f"backup archive does not exist or is unsafe: {archive}")
    with tempfile.TemporaryDirectory(prefix="overdeck-k3s-restore-proof-") as tmp:
        root = Path(tmp)
        safe_extract_tar(archive, root)
        manifest_path = root / "manifest.json"
        if not manifest_path.is_file():
            raise Phase1Error("backup archive is missing manifest.json")
        manifest = verify_manifest(root, manifest_path)
        datastore_type = manifest["datastore"]["type"]
        datastore = verify_sqlite(root, manifest) if datastore_type == "sqlite" else verify_etcd(root, manifest)
        canonical = validate_canonical_files(root, manifest)
        token = root / manifest["server_token_path"]
        if token.stat().st_size < 16:
            raise Phase1Error("server token file is unexpectedly small")
        if token.stat().st_mode & 0o077:
            raise Phase1Error("server token permissions are too broad in the backup")
        materialized = (
            materialize_restore(root, manifest, restore_root, datastore) if restore_root is not None else None
        )
        return {
            "schema_version": 2,
            "status": "verified",
            "verified_utc": utc_now(),
            "archive": str(archive),
            "archive_sha256": sha256_file(archive),
            "file_count": len(manifest["files"]),
            "server": manifest.get("server"),
            "k3s": manifest.get("k3s"),
            "datastore": datastore,
            "server_token": {
                "present": True,
                "size": token.stat().st_size,
                "value_exposed": False,
            },
            "canonical_config": canonical,
            "isolated_restore": materialized,
        }


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--archive", type=Path, required=True, help="Decrypted Phase 1 tar archive")
    parser.add_argument(
        "--restore-root",
        type=Path,
        help="Materialize an isolated restore tree into this empty non-live directory",
    )
    parser.add_argument("--output", type=Path, help="Write verification JSON to this path")
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    result = verify_archive(
        absolute_path_without_symlink_resolution(args.archive),
        restore_root=(
            absolute_path_without_symlink_resolution(args.restore_root)
            if args.restore_root
            else None
        ),
    )
    payload = json.dumps(result, indent=2, sort_keys=True) + "\n"
    if args.output:
        args.output.parent.mkdir(parents=True, exist_ok=True)
        args.output.write_text(payload)
    sys.stdout.write(payload)
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except Phase1Error as exc:
        json.dump({"schema_version": 1, "status": "error", "error": str(exc)}, sys.stdout, indent=2, sort_keys=True)
        sys.stdout.write("\n")
        raise SystemExit(2)
