#!/usr/bin/env python3
"""Generate a digest-locked K3s upgrade/rollback plan without applying it."""
from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
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 (  # noqa: E402
    Phase1Error,
    absolute_path_without_symlink_resolution,
    read_json,
    sha256_file,
    utc_now,
)

VERSION_RE = re.compile(r"\bk3s version (v[^\s]+)")
SHA_RE = re.compile(r"^[0-9a-f]{64}$")


def candidate_version(binary: Path) -> str:
    if binary.is_symlink() or not binary.is_file():
        raise Phase1Error(f"candidate K3s binary is missing or unsafe: {binary}")
    completed = subprocess.run(
        [str(binary), "--version"],
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        text=True,
        timeout=30,
        check=False,
    )
    if completed.returncode != 0:
        raise Phase1Error(f"candidate K3s binary failed --version: {completed.stderr[-2000:]}")
    match = VERSION_RE.search(completed.stdout)
    if not match:
        raise Phase1Error("cannot parse version from candidate K3s binary")
    return match.group(1)


def build_plan(
    version_lock_path: Path,
    binary: Path,
    expected_version: str,
    expected_sha256: str,
) -> dict[str, Any]:
    lock = read_json(version_lock_path)
    if not isinstance(lock, dict) or lock.get("schema_version") != 2:
        raise Phase1Error(
            "unsupported current version-lock schema; Phase 1 requires separate launcher/runtime evidence"
        )
    if lock.get("managed_by") != "overdeck-k3s-phase1":
        raise Phase1Error("current version lock is not managed by Overdeck Phase 1")
    current_version = lock.get("version")
    launcher = lock.get("launcher") or {}
    current_sha = launcher.get("sha256")
    if not isinstance(current_version, str) or not isinstance(current_sha, str) or not SHA_RE.fullmatch(current_sha):
        raise Phase1Error("current version lock has no valid launcher SHA-256")
    if not SHA_RE.fullmatch(expected_sha256):
        raise Phase1Error("--expected-sha256 must be 64 lowercase hexadecimal characters")
    actual_sha = sha256_file(binary)
    if actual_sha != expected_sha256:
        raise Phase1Error(f"candidate digest mismatch: expected {expected_sha256}, got {actual_sha}")
    actual_version = candidate_version(binary)
    if actual_version != expected_version:
        raise Phase1Error(f"candidate version mismatch: expected {expected_version}, got {actual_version}")
    if actual_version == current_version and actual_sha == current_sha:
        raise Phase1Error("candidate is identical to the current canonical K3s binary")
    return {
        "schema_version": 1,
        "status": "planned",
        "generated_utc": utc_now(),
        "mutation_performed": False,
        "apply_supported_by_phase1": False,
        "current": {"version": current_version, "launcher_sha256": current_sha},
        "candidate": {
            "version": actual_version,
            "launcher_sha256": actual_sha,
            "launcher_path": str(binary.resolve()),
        },
        "required_execution_order": [
            "create-and-offline-verify-a-fresh-datastore-plus-server-token-backup",
            "prove-candidate-on-one-agent-before-the-server",
            "capture-current-binary-config-systemd-and-readiness-receipt",
            "stage-candidate-by-digest-without-overwriting-current-binary",
            "perform-explicit-maintenance-window-server-switch",
            "verify-service-api-nodes-storage-and-workloads",
            "update-version-lock-only-after-all-gates-pass",
        ],
        "rollback_contract": {
            "binary": "restore exact current binary SHA-256 from the transaction backup",
            "config": "restore exact pre-upgrade config and systemd files atomically",
            "datastore": "use the paired datastore and original server token when schema rollback requires restore",
            "automatic_version_adoption": False,
        },
        "next_action": "A later migration phase must wrap this plan in tested live transaction code; Phase 1 refuses --apply.",
    }


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--version-lock", type=Path, required=True)
    parser.add_argument("--candidate-binary", type=Path, required=True)
    parser.add_argument("--expected-version", required=True)
    parser.add_argument("--expected-sha256", required=True)
    parser.add_argument("--output", type=Path)
    return parser


def main(argv: Sequence[str] | None = None) -> int:
    args = build_parser().parse_args(argv)
    plan = build_plan(
        absolute_path_without_symlink_resolution(args.version_lock),
        absolute_path_without_symlink_resolution(args.candidate_binary),
        args.expected_version,
        args.expected_sha256,
    )
    payload = json.dumps(plan, 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)
