#!/usr/bin/env python3
"""vexor_synthetic - Naemon plugin that runs a SyntheticCheck flow.

Usage:
    vexor_synthetic --id <check_id>

Resolves the check definition + credential straight from the Vexor DB,
runs the flow, and emits a single Nagios-compatible status line with
per-step perfdata so the UI can graph latency per step over time.
"""
from __future__ import annotations

import argparse
import json
import os
import sys
from typing import Optional

# Allow running from /opt/vexor/plugins while imports come from /opt/vexor/api
sys.path.insert(0, "/opt/vexor/api")

# Load DB env (host/user/pass) before importing app.database
ENV_FILE = "/etc/vexor/db.env"
if os.path.exists(ENV_FILE):
    with open(ENV_FILE) as f:
        for line in f:
            line = line.strip()
            if not line or line.startswith("#") or "=" not in line:
                continue
            k, v = line.split("=", 1)
            os.environ.setdefault(k, v.strip().strip('"').strip("'"))


def _exit(code: int, line: str) -> None:
    print(line)
    sys.exit(code)


def _load_check_sync(check_id: int) -> tuple[dict, Optional[dict]]:
    """Load check + credential row using a sync DB connection (the venv's
    pymysql is fine for read-only checks; we avoid the asyncio overhead)."""
    import pymysql

    host = os.environ.get("DB_HOST") or os.environ.get("VEXOR_DB_HOST") or "127.0.0.1"
    user = os.environ.get("DB_USER") or os.environ.get("VEXOR_DB_USER") or "vexor"
    password = os.environ.get("DB_PASSWORD") or os.environ.get("VEXOR_DB_PASSWORD") or ""
    db = os.environ.get("DB_NAME") or os.environ.get("VEXOR_DB_NAME") or "vexor"
    port = int(os.environ.get("DB_PORT") or os.environ.get("VEXOR_DB_PORT") or 3306)

    conn = pymysql.connect(host=host, user=user, password=password,
                           database=db, port=port, cursorclass=pymysql.cursors.DictCursor)
    try:
        with conn.cursor() as cur:
            cur.execute(
                "SELECT id, name, flow_json, credential_id, timeout_seconds, enabled "
                "FROM synthetic_checks WHERE id=%s", (check_id,))
            check = cur.fetchone()
            if not check:
                _exit(3, f"UNKNOWN - synthetic check id={check_id} not found")
            cred = None
            if check["credential_id"]:
                cur.execute(
                    "SELECT id, name, kind, username, password_enc, extra_json "
                    "FROM host_credentials WHERE id=%s", (check["credential_id"],))
                cred = cur.fetchone()
    finally:
        conn.close()
    return check, cred


def _build_secret_ctx(cred: Optional[dict]) -> dict:
    """Decrypt credential into the template context. Mapping:

    - http_basic: {username, password}
    - http_token: {token}
    - http_oauth: {client_id, client_secret, token_url}
    """
    if not cred:
        return {}
    try:
        from app.services.crypto import decrypt
    except Exception as e:
        _exit(3, f"UNKNOWN - failed to import crypto: {e}")
    secret: dict = {}
    kind = cred.get("kind") or ""
    pw_enc = cred.get("password_enc")
    pw = decrypt(pw_enc) if pw_enc else None
    extra: dict = {}
    if cred.get("extra_json"):
        try:
            extra = json.loads(cred["extra_json"])
        except Exception:
            extra = {}
    if kind == "http_basic":
        secret["username"] = cred.get("username") or ""
        secret["password"] = pw or ""
    elif kind == "http_token":
        secret["token"] = pw or ""
    elif kind == "http_oauth":
        secret["client_id"] = cred.get("username") or ""
        secret["client_secret"] = pw or ""
        secret["token_url"] = extra.get("token_url", "")
        secret["scope"] = extra.get("scope", "")
        secret["audience"] = extra.get("audience", "")
    else:
        # Generic mapping: anything we can decrypt
        if cred.get("username"):
            secret["username"] = cred["username"]
        if pw:
            secret["password"] = pw
    return secret


def main() -> None:
    p = argparse.ArgumentParser()
    p.add_argument("--id", type=int, required=True)
    args = p.parse_args()

    check, cred = _load_check_sync(args.id)

    if not check.get("enabled"):
        _exit(3, f"UNKNOWN - synthetic check {check['name']} is disabled")

    try:
        steps = json.loads(check["flow_json"] or "[]")
    except json.JSONDecodeError as e:
        _exit(3, f"UNKNOWN - invalid flow_json: {e}")
    if not isinstance(steps, list) or not steps:
        _exit(3, f"UNKNOWN - flow for '{check['name']}' has no steps")

    secret_ctx = _build_secret_ctx(cred)

    try:
        from app.services.synthetic_engine import run_flow
    except Exception as e:
        _exit(3, f"UNKNOWN - cannot import flow engine: {e}")

    timeout = int(check.get("timeout_seconds") or 30)
    result = run_flow(steps, secret=secret_ctx, timeout_seconds=timeout)

    # Build Nagios output: short summary + perfdata
    ok_steps = sum(1 for s in result.steps if s.ok)
    total = len(result.steps)
    perf_parts: list[str] = []
    perf_parts.append(f"total_ms={result.duration_ms}ms;;;0")
    for s in result.steps:
        # Replace anything weird in step name
        safe = "".join(c if c.isalnum() else "_" for c in s.name)[:48]
        if not safe:
            safe = f"step{s.index+1}"
        perf_parts.append(f"step_{s.index+1}_{safe}={s.duration_ms}ms;;;0")
    perf = " ".join(perf_parts)

    if result.ok:
        line = f"OK - {check['name']}: {total}/{total} steps in {result.duration_ms}ms | {perf}"
        print(line)
        sys.exit(0)

    # Find first failing step for the headline
    first_fail = next((s for s in result.steps if not s.ok), None)
    if first_fail:
        detail = first_fail.error or ""
        if not detail and first_fail.assertions:
            failed = [a for a in first_fail.assertions if not a.get("ok")]
            if failed:
                a = failed[0]
                detail = f"assertion failed: {a['expr']} (actual={a.get('actual','')})"
        line = (f"CRITICAL - {check['name']}: step {first_fail.index+1} "
                f"'{first_fail.name}' failed ({detail or 'unknown'}); "
                f"{ok_steps}/{total} steps passed | {perf}")
    else:
        line = f"CRITICAL - {check['name']}: {ok_steps}/{total} steps passed | {perf}"

    print(line)
    sys.exit(2)


if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception as e:  # pragma: no cover
        print(f"UNKNOWN - vexor_synthetic crashed: {type(e).__name__}: {e}")
        sys.exit(3)
