#!/usr/bin/env python3
"""taler-monitord — probe domain stacks on an interval; UI reads JSON only."""
from __future__ import annotations

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

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import stack_status_html as H  # noqa: E402
import stack_status_lib as L  # noqa: E402

# Match lib.sh: load ~/.config/taler-monitoring/env (or TALER_MONITORING_ENV), then
# refresh module flags that were read at import time.
_ENV_LOADED = L.load_monitoring_env()
if _ENV_LOADED:
    L.refresh_runtime_flags_from_env()


def _pid_path() -> Path:
    return L.monitord_state_dir() / "taler-monitord.pid"


def _write_pid() -> None:
    _pid_path().write_text(str(os.getpid()) + "\n", encoding="utf-8")


def _clear_pid() -> None:
    p = _pid_path()
    try:
        p.unlink()
    except OSError:
        pass


def _publish_one(local: Path, ssh_host: str, remote: str) -> str | None:
    remote_dir = str(Path(remote).parent)
    mk = subprocess.run(
        [
            "ssh",
            "-o",
            "BatchMode=yes",
            "-o",
            "ConnectTimeout=8",
            ssh_host,
            f"mkdir -p -- {remote_dir!s}",
        ],
        capture_output=True,
        timeout=15,
        check=False,
    )
    if mk.returncode != 0:
        err = (mk.stderr or b"").decode("utf-8", "replace").strip()[:200]
        print(f"publish mkdir fail ({remote}): {err}", file=sys.stderr, flush=True)
        return None
    r = subprocess.run(
        [
            "scp",
            "-o",
            "BatchMode=yes",
            "-o",
            "ConnectTimeout=8",
            str(local),
            f"{ssh_host}:{remote}",
        ],
        capture_output=True,
        timeout=30,
        check=False,
    )
    if r.returncode != 0:
        err = (r.stderr or b"").decode("utf-8", "replace").strip()[:200]
        print(f"publish fail ({remote}): {err}", file=sys.stderr, flush=True)
        return None
    if "/var/www/monitoring-sites/" in remote:
        subprocess.run(
            [
                "ssh",
                "-o",
                "BatchMode=yes",
                "-o",
                "ConnectTimeout=8",
                ssh_host,
                (
                    f"chmod 755 -- {remote_dir!s} && chmod 644 -- {remote!s} && "
                    f"setfacl -m u:caddy:rx,m::r-x -- {remote_dir!s} && "
                    f"setfacl -m u:caddy:r,m::r-- -- {remote!s} || true"
                ),
            ],
            capture_output=True,
            timeout=15,
            check=False,
        )
    else:
        subprocess.run(
            [
                "ssh",
                "-o",
                "BatchMode=yes",
                "-o",
                "ConnectTimeout=8",
                ssh_host,
                f"chmod 755 -- {remote_dir!s} && chmod 644 -- {remote!s} || true",
            ],
            capture_output=True,
            timeout=15,
            check=False,
        )
    return f"{ssh_host}:{remote}"


def publish_html(doc: dict) -> str | None:
    """Render HTML and scp to Cyon (hacktivism) paths.

    Env:
      TALER_MONITORD_PUBLISH=0 to disable
      TALER_MONITORD_PUBLISH_SSH=hacktivism
      TALER_MONITORD_PUBLISH_PATH=… single path (overrides multi default)
      TALER_MONITORD_PUBLISH_PATHS=comma-separated paths
    Default paths: public_html/stacks + public_html/monitoring.hacktivism.ch
    """
    if os.environ.get("TALER_MONITORD_PUBLISH", "1") in ("0", "false", "no", ""):
        return None
    ssh_host = os.environ.get("TALER_MONITORD_PUBLISH_SSH", "hacktivism").strip() or "hacktivism"
    single = (os.environ.get("TALER_MONITORD_PUBLISH_PATH") or "").strip()
    multi = (os.environ.get("TALER_MONITORD_PUBLISH_PATHS") or "").strip()
    if single:
        remotes = [single]
    elif multi:
        remotes = [p.strip() for p in multi.split(",") if p.strip()]
    else:
        remotes = [
            "/home/hacktivi/public_html/stacks/index.html",
            "/home/hacktivi/public_html/monitoring.hacktivism.ch/index.html",
        ]
    html_text = H.render_html(doc)
    with tempfile.NamedTemporaryFile("w", encoding="utf-8", suffix=".html", delete=False) as f:
        f.write(html_text)
        local = Path(f.name)
    published: list[str] = []
    try:
        for remote in remotes:
            got = _publish_one(local, ssh_host, remote)
            if got:
                published.append(got)
        return "; ".join(published) if published else None
    except (OSError, subprocess.TimeoutExpired) as e:
        print(f"publish error: {e}", file=sys.stderr, flush=True)
        return None
    finally:
        try:
            local.unlink()
        except OSError:
            pass


def run_once(*, domains: Path, out_paths: list[Path] | None, public_only: bool) -> Path:
    stacks = L.parse_domains_conf(domains)
    prev = L.read_status(out_paths[0] if out_paths else None)
    if not prev and out_paths:
        for p in out_paths:
            prev = L.read_status(p)
            if prev:
                break
    doc = L.build_stack_status(stacks, prev_doc=prev, public_only=public_only)
    paths = out_paths or L.status_json_paths()
    path = L.write_stack_status(doc, paths)
    # always keep a local HTML copy too
    local_html = L.monitord_state_dir() / "index.html"
    local_html.write_text(H.render_html(doc), encoding="utf-8")
    published = publish_html(doc)
    if published:
        print(f"published {published}", flush=True)
    return path


def main() -> int:
    ap = argparse.ArgumentParser(description="Collect Taler stack status for the detached UI")
    ap.add_argument("--interval", type=float, default=float(os.environ.get("TALER_MONITORD_INTERVAL", "20")))
    ap.add_argument("--once", action="store_true", help="Single probe cycle then exit")
    ap.add_argument("--domains-conf", type=Path, default=None)
    ap.add_argument("--out", type=Path, action="append", default=None, help="Output JSON path (repeatable)")
    # Default on: include GOA / hacktivism.ch (taler.hacktivism.ch merchant). Opt out: --no-include-local
    _incl = os.environ.get("TALER_MONITORD_INCLUDE_LOCAL", "1") not in ("0", "false", "no", "")
    ap.add_argument(
        "--include-local",
        action=argparse.BooleanOptionalAction,
        default=_incl,
        help="Probe local=1 stacks (hacktivism GOA). Default on; --no-include-local to skip",
    )
    args = ap.parse_args()
    if _ENV_LOADED:
        print(f"env {_ENV_LOADED}", flush=True)
    domains = args.domains_conf or L.default_domains_conf()
    if not domains.is_file():
        print(f"domains.conf not found: {domains}", file=sys.stderr)
        return 2
    public_only = not bool(args.include_local)
    out_paths = list(args.out) if args.out else None

    if args.once:
        path = run_once(domains=domains, out_paths=out_paths, public_only=public_only)
        print(path)
        return 0

    _write_pid()
    try:
        while True:
            try:
                path = run_once(domains=domains, out_paths=out_paths, public_only=public_only)
                print(f"ok {path}", flush=True)
            except Exception as e:  # keep loop alive
                print(f"error {type(e).__name__}: {e}", file=sys.stderr, flush=True)
            time.sleep(max(1.0, float(args.interval)))
    finally:
        _clear_pid()


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