#!/usr/bin/env python3
"""Synchronize VM2 runtime secrets, then execute a command with paths only."""

from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
import tempfile
from pathlib import Path

from secrets_loader import LoaderError, load_json, run


def public_config(path: Path) -> dict[str, str]:
    result: dict[str, str] = {}
    for number, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            raise LoaderError(f"invalid non-secret config at line {number}")
        key, value = line.split("=", 1)
        if not key or key in result:
            raise LoaderError(f"invalid/duplicate non-secret key at line {number}")
        result[key] = value
    return result


def selected_config(public: dict[str, str], explicit: Path | None) -> tuple[str, Path]:
    source = public.get("SECRETS_SOURCE")
    if source not in {"selectel", "file"}:
        raise LoaderError("SECRETS_SOURCE must explicitly be selectel or file")
    if explicit:
        return source, explicit
    environment = public.get("APP_ENV", "production")
    return source, Path(f"/etc/han/secrets/vm2-{environment}.{source}.json")


def prepare(config_path: Path, source: str, synchronize: bool) -> dict[str, str]:
    document = load_json(config_path)
    if document.get("mode") != source:
        raise LoaderError("loader mode does not match SECRETS_SOURCE")
    runtime = Path(str(document.get("runtime_dir", "")))
    state_path = runtime / "state.json"
    if synchronize:
        run(config_path)
        descriptor, temporary = tempfile.mkstemp(prefix=".state.", dir=runtime)
        with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
            json.dump(
                {"version": 1, "source": source, "loader_config": str(config_path.resolve())},
                stream,
                separators=(",", ":"),
            )
            stream.write("\n")
            stream.flush()
            os.fsync(stream.fileno())
        os.chmod(temporary, 0o600)
        os.replace(temporary, state_path)
    if not state_path.is_file():
        raise LoaderError("runtime secrets are not synchronized")
    state = load_json(state_path)
    if state != {
        "version": 1,
        "source": source,
        "loader_config": str(config_path.resolve()),
    }:
        raise LoaderError("runtime secret state does not match selected configuration")
    manifest = runtime / "manifest"
    entries: dict[str, str] = {}
    for line in manifest.read_text(encoding="utf-8").splitlines():
        key, separator, value = line.partition("=")
        if not separator or key in entries or not Path(value).is_file():
            raise LoaderError("runtime secret manifest is invalid")
        entries[key] = value
    if set(entries) != set(document.get("secrets", {})):
        raise LoaderError("runtime secret manifest does not match configuration")
    child = dict(os.environ)
    child["HAN_SECRETS_ACTIVE"] = "1"
    child["HAN_RUNTIME_SECRET_DIR"] = str(runtime)
    child["HAN_RUNTIME_SECRET_MANIFEST"] = str(manifest)
    for key, value in entries.items():
        child[f"{key}_FILE"] = value
    return child


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("action", choices=("sync", "run"))
    parser.add_argument("--config", type=Path, default=Path(".env"))
    parser.add_argument("--loader-config", type=Path)
    arguments, command = parser.parse_known_args()
    if command and command[0] == "--":
        command.pop(0)
    if arguments.action == "run" and not command:
        parser.error("run requires a command after --")
    try:
        source, config = selected_config(public_config(arguments.config), arguments.loader_config)
        child = prepare(config, source, arguments.action == "sync")
    except (LoaderError, OSError, ValueError, json.JSONDecodeError) as exc:
        print(f"han-secrets-vm2: {exc}", file=sys.stderr)
        return 1
    if arguments.action == "sync":
        return 0
    if os.name == "nt":
        return subprocess.call(command, env=child)
    os.execvpe(command[0], command, child)
    return 127


if __name__ == "__main__":
    raise SystemExit(main())
