#!/usr/bin/env python3
"""Synchronize runtime secrets and execute a command without exporting their values."""

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 load_public_config(path: Path) -> dict[str, str]:
    values: dict[str, str] = {}
    try:
        lines = path.read_text(encoding="utf-8").splitlines()
    except OSError as exc:
        raise LoaderError(
            f"cannot read non-secret config: {exc.strerror or exc.__class__.__name__}"
        ) from None
    for number, raw in enumerate(lines, 1):
        line = raw.strip()
        if not line or line.startswith("#"):
            continue
        if "=" not in line:
            raise LoaderError(f"non-secret config has invalid syntax at line {number}")
        key, value = line.split("=", 1)
        key = key.strip()
        if not key or key in values:
            raise LoaderError(f"non-secret config has an invalid key at line {number}")
        values[key] = value.strip()
    return values


def loader_config_path(
    public: dict[str, str],
    explicit: Path | None,
    environ: dict[str, str],
) -> 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 is not None:
        return source, explicit
    override = environ.get(f"HAN_SECRETS_{source.upper()}_CONFIG")
    if override:
        return source, Path(override)
    environment = public.get("APP_ENV", "production")
    return source, Path(f"/etc/han/secrets/{environment}.{source}.json")


def prepare_environment(
    config_path: Path,
    source: str,
    environ: dict[str, str],
    *,
    synchronize: bool,
) -> dict[str, str]:
    document = load_json(config_path)
    if document.get("mode") != source:
        raise LoaderError("selected loader configuration mode does not match SECRETS_SOURCE")
    runtime = document.get("runtime_dir")
    if not isinstance(runtime, str) or not Path(runtime).is_absolute():
        raise LoaderError("loader configuration has an invalid runtime_dir")
    runtime_dir = Path(runtime)
    manifest = runtime_dir / "manifest"
    state_path = runtime_dir / "state.json"
    consumers: list[str] = []
    if synchronize or (source == "file" and not state_path.is_file()):
        consumers = run(config_path, environ=environ)
        state = {
            "version": 1,
            "source": source,
            "loader_config": str(config_path.resolve()),
        }
        descriptor, temporary = tempfile.mkstemp(
            prefix=".state.", suffix=".tmp", dir=runtime_dir
        )
        temporary_path = Path(temporary)
        try:
            os.chmod(temporary_path, 0o600)
            with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
                descriptor = -1
                json.dump(state, stream, separators=(",", ":"))
                stream.write("\n")
                stream.flush()
                os.fsync(stream.fileno())
            os.replace(temporary_path, state_path)
        except BaseException:
            if descriptor >= 0:
                os.close(descriptor)
            temporary_path.unlink(missing_ok=True)
            raise
    elif not state_path.is_file():
        raise LoaderError(
            "runtime secrets are not synchronized; restart han-secrets systemd unit"
        )
    else:
        state = load_json(state_path)
        if (
            state.get("version") != 1
            or state.get("source") != source
            or state.get("loader_config") != str(config_path.resolve())
        ):
            raise LoaderError(
                "runtime secret state does not match selected source/config; synchronize first"
            )
    if not manifest.is_file():
        raise LoaderError("runtime secret manifest was not materialized")
    manifest_entries: dict[str, str] = {}
    for line in manifest.read_text(encoding="utf-8").splitlines():
        key, value_path = line.split("=", 1)
        manifest_entries[key] = value_path
    specs = document.get("secrets")
    if not isinstance(specs, dict) or set(manifest_entries) != set(specs):
        raise LoaderError("runtime secret manifest does not match loader configuration")
    child = dict(environ)
    child["HAN_SECRETS_ACTIVE"] = "1"
    child["HAN_RUNTIME_SECRET_DIR"] = str(runtime_dir)
    child["HAN_RUNTIME_SECRET_MANIFEST"] = str(manifest)
    for key, value_path in manifest_entries.items():
        if not Path(value_path).is_file():
            raise LoaderError("runtime secret manifest references a missing file")
        child[f"{key}_FILE"] = value_path
    if synchronize or consumers:
        print(
            f"han-secrets: synchronized {len(consumers)} service scope(s) from {source}",
            file=sys.stderr,
        )
    return child


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    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(argv)
    if command and command[0] == "--":
        command.pop(0)
    if arguments.action == "run" and not command:
        parser.error("run requires a command after --")
    if arguments.action == "sync" and command:
        parser.error("sync does not accept a command")

    environment = dict(os.environ)
    try:
        public = load_public_config(arguments.config)
        source, loader_config = loader_config_path(
            public, arguments.loader_config, environment
        )
        child = prepare_environment(
            loader_config,
            source,
            environment,
            synchronize=arguments.action == "sync",
        )
    except (LoaderError, OSError, ValueError, json.JSONDecodeError) as exc:
        message = str(exc) if isinstance(exc, LoaderError) else exc.__class__.__name__
        print(f"han-secrets: {message}", 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())
