#!/usr/bin/python3
"""ctg-alert-mail: deliver one host alert to THIS host's configured destination.

Every host-side alert sender in ctg-infrastructure (runner-host-cleanup,
pg-backup-monitor, deploy-watcher artifact rejections, the Rustinion canary)
calls this one tool. Where the alert goes is decided per host, in
/etc/ctg-alerts/route.conf, never by the sender (owner decisions 2026-10-17):

  * RedWeek hosts (rw-man-01..05)  -> email to ron.farrer@redweek.com (Resend)
  * personal / Errant hosts        -> the self-hosted ntfy topic
                                      https://ntfy.canutethegreat.com/ctg, no email
  * never the owner's Gmail: canutethegreat@gmail.com is refused outright.

There is no built-in default destination: without a route.conf (or the
CTG_ALERT_* overrides below) the send fails with exit 1, so a forgotten config
shows up in the sender's log instead of mail landing somewhere unexpected.

route.conf (key = value, '#' comments):
    channel  = email | ntfy
    email_to = someone@example.com        (channel = email; comma-separate several)
    ntfy_url = https://ntfy.example/topic (channel = ntfy)

Usage:
    echo "body text" | ctg-alert-mail --subject "rw-man-02 disk"
    ctg-alert-mail --subject S --body "text" [--priority default|high|...] [--tags warning]
    ctg-alert-mail --dry-run --subject S < body   # print destination + alert, send nothing

Prints the Resend email id (email) or ntfy message id on success; exits 1 with
the reason on stderr on any failure, so callers keep their alert state
"unsent" and retry on their next run.

Environment overrides (tests, one-off use):
    CTG_ALERT_CONF      route file           (default /etc/ctg-alerts/route.conf)
    CTG_ALERT_CHANNEL   email | ntfy         (overrides route.conf)
    CTG_ALERT_TO        email recipient(s)   (overrides route.conf)
    CTG_ALERT_NTFY_URL  ntfy topic URL       (overrides route.conf)
    CTG_ALERT_FROM      sender               (default "CTG Alerts <alerts@errant.solutions>")
    CTG_ALERT_KEY_FILE  Resend API key file  (default /etc/ctg-alerts/resend.key;
                        a send-only key restricted to errant.solutions)
    CTG_ALERT_API       Resend endpoint      (default https://api.resend.com/emails)

Source: CanuteTheGreat/ctg-infrastructure alerting/ (install with alerting/install.sh).
"""

import argparse
import json
import os
import socket
import sys
import urllib.error
import urllib.request

DEFAULT_CONF = "/etc/ctg-alerts/route.conf"
DEFAULT_FROM = "CTG Alerts <alerts@errant.solutions>"
DEFAULT_KEY_FILE = "/etc/ctg-alerts/resend.key"
DEFAULT_API = "https://api.resend.com/emails"
# Owner, 2026-10-17: "I don't want a bunch of alerts on my Gmail."
REFUSED_RECIPIENTS = {"canutethegreat@gmail.com"}
NTFY_PRIORITIES = {"min", "low", "default", "high", "urgent", "max", "1", "2", "3", "4", "5"}


class ConfigError(Exception):
    pass


def load_route() -> dict:
    """The effective destination: route.conf overlaid with CTG_ALERT_* env."""
    env = os.environ.get
    path = env("CTG_ALERT_CONF", DEFAULT_CONF)
    route: dict = {}
    try:
        with open(path) as fh:
            for n, raw in enumerate(fh, 1):
                line = raw.split("#", 1)[0].strip()
                if not line:
                    continue
                if "=" not in line:
                    raise ConfigError(f"{path}:{n}: expected key = value")
                k, v = (s.strip() for s in line.split("=", 1))
                route[k] = v
    except FileNotFoundError:
        pass
    for key, var in (("channel", "CTG_ALERT_CHANNEL"), ("email_to", "CTG_ALERT_TO"), ("ntfy_url", "CTG_ALERT_NTFY_URL")):
        if env(var):
            route[key] = env(var)
    channel = route.get("channel", "")
    if channel == "email":
        to = [a.strip() for a in route.get("email_to", "").split(",") if a.strip()]
        if not to:
            raise ConfigError(f"channel = email but no email_to in {path}")
        refused = [a for a in to if a.lower() in REFUSED_RECIPIENTS]
        if refused:
            raise ConfigError(f"refusing to alert {', '.join(refused)} (owner: no alerts to his Gmail)")
        return {"channel": "email", "to": to}
    if channel == "ntfy":
        url = route.get("ntfy_url", "")
        if not url.startswith(("https://", "http://")):
            raise ConfigError(f"channel = ntfy but no http(s) ntfy_url in {path}")
        return {"channel": "ntfy", "url": url}
    raise ConfigError(f"no alert destination configured: set channel = email|ntfy in {path}")


def _post(req: urllib.request.Request, timeout: float, what: str) -> dict:
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read() or b"{}")
    except urllib.error.HTTPError as err:
        detail = err.read()[:300].decode(errors="replace")
        raise RuntimeError(f"{what} HTTP {err.code}: {detail}") from None


def send_email(to: list, subject: str, body: str, timeout: float) -> str:
    env = os.environ.get
    key_file = env("CTG_ALERT_KEY_FILE", DEFAULT_KEY_FILE)
    with open(key_file) as fh:
        key = fh.read().strip()
    if not key:
        raise RuntimeError(f"empty API key in {key_file}")
    payload = {"from": env("CTG_ALERT_FROM", DEFAULT_FROM), "to": to, "subject": subject,
               "text": f"{body}\n\n-- sent by ctg-alert-mail on {socket.gethostname()}"}
    req = urllib.request.Request(
        env("CTG_ALERT_API", DEFAULT_API), data=json.dumps(payload).encode(), method="POST",
        headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
                 "User-Agent": "ctg-alert-mail/2.0"})
    data = _post(req, timeout, "Resend")
    if not data.get("id"):
        raise RuntimeError(f"Resend returned no id: {str(data)[:300]}")
    return data["id"]


def send_ntfy(url: str, subject: str, body: str, priority: str, tags: str, timeout: float) -> str:
    # Plain-text body to the topic URL with Title/Priority/Tags headers - the
    # same publish the senders made themselves before 2026-10-17. Header
    # values must be latin-1 for http.client, so other characters become '?'.
    headers = {"User-Agent": "ctg-alert-mail/2.0", "Title": subject.encode("latin-1", "replace").decode("latin-1"),
               "Priority": priority}
    if tags:
        headers["Tags"] = tags
    req = urllib.request.Request(url, data=body.encode(), headers=headers, method="POST")
    data = _post(req, timeout, "ntfy")
    return str(data.get("id") or "ok")


def send(subject: str, body: str, priority: str = "high", tags: str = "", timeout: float = 20) -> str:
    """Deliver the alert to this host's route; returns the message id. Raises on any failure."""
    route = load_route()
    if route["channel"] == "email":
        return send_email(route["to"], subject, body, timeout)
    return send_ntfy(route["url"], subject, body, priority, tags, timeout)


def main() -> int:
    ap = argparse.ArgumentParser(description="Deliver a host alert to this host's configured destination.")
    ap.add_argument("--subject", required=True)
    ap.add_argument("--body", help="message text (default: read stdin)")
    ap.add_argument("--priority", default="high", help="ntfy priority (ignored for email; default high)")
    ap.add_argument("--tags", default="", help="ntfy tags, comma-separated (ignored for email)")
    ap.add_argument("--dry-run", action="store_true", help="print destination and alert, send nothing")
    args = ap.parse_args()
    if args.priority not in NTFY_PRIORITIES:
        ap.error(f"--priority must be one of {sorted(NTFY_PRIORITIES)}")
    body = args.body if args.body is not None else sys.stdin.read()
    if args.dry_run:
        try:
            route = load_route()
            dest = f"email {', '.join(route['to'])}" if route["channel"] == "email" else f"ntfy {route['url']}"
        except ConfigError as err:
            dest = f"NOWHERE ({err})"
        print(f"[dry-run, not sent; would go to {dest}] {args.subject}: {body}")
        return 0
    try:
        print(send(args.subject, body, args.priority, args.tags))
    except Exception as err:  # noqa: BLE001 - report every failure the same way
        print(f"ctg-alert-mail: send failed: {err}", file=sys.stderr)
        return 1
    return 0


if __name__ == "__main__":
    sys.exit(main())
