"""Discord delivery — currently just the daily "most unusual right now" post.

A once-daily message to a Discord channel via an **incoming webhook** (a single
secret URL; no bot, no gateway, no persistent process). It rides the notifier
daemon like the homepage feed refresh does, so it inherits the single-leader
invariant and costs no new infrastructure. Entirely optional: with no webhook
configured, everything here no-ops.

The message is built from the same `data/homepage.json` feed the homepage renders
(homepage.build/refresh), so the Discord post and the site always agree.
"""
from __future__ import annotations

import datetime
import os
import time

import httpx

from api import homepage

# The webhook URL IS the credential — env only, never the repo. Unset => disabled.
WEBHOOK_URL = os.environ.get("THERMOGRAPH_DISCORD_WEBHOOK", "").strip()

# Bot token (a credential) for sending DMs; public site URL for absolute deep links
# in a DM (a webhook/DM can't resolve a relative path the way the SW-backed push can).
BOT_TOKEN = os.environ.get("THERMOGRAPH_DISCORD_BOT_TOKEN", "").strip()
PUBLIC_URL = os.environ.get("THERMOGRAPH_BASE_URL", "https://thermograph.org").rstrip("/")
_API = "https://discord.com/api/v10"

# Channel IDs the bot posts into (numeric snowflakes, from env; empty => that feed
# is off). SUBSCRIPTION is this deployment's dev/uat/prod test feed — it mirrors
# every notification the notifier creates so a run can be watched live. WEATHER is
# the prod "notable weather" channel the daily digest broadcasts into.
SUBSCRIPTION_CHANNEL_ID = os.environ.get("THERMOGRAPH_DISCORD_SUBSCRIPTION_CHANNEL", "").strip()
WEATHER_CHANNEL_ID = os.environ.get("THERMOGRAPH_DISCORD_WEATHER_CHANNEL", "").strip()

# One 429 retry, capped so a rate-limit can't stall the notifier pass for long.
_MAX_BACKOFF_S = 5.0

# How many cities the daily post lists.
POST_LIMIT = 8

# Embed accent: warm when the top anomaly is hot, cool when it's cold.
_HOT = 0xE8663B
_COLD = 0x4393C3

_TIMEOUT = httpx.Timeout(10.0)

# One shared client instead of a bare httpx.post per call — same reused-client
# pattern as web/app.py's _frontend_client, so every webhook/bot-REST call in
# this module reuses one connection pool instead of paying a fresh TCP+TLS
# handshake per notification.
_client = httpx.Client(timeout=_TIMEOUT)


def enabled() -> bool:
    return bool(WEBHOOK_URL) or weather_enabled()


def weather_enabled() -> bool:
    """The daily digest has a bot channel (prod's notable-weather feed) to post into."""
    return bool(BOT_TOKEN and WEATHER_CHANNEL_ID)


def _f_c(value) -> str:
    """A Fahrenheit reading as '104°F (40°C)' — Discord's audience is global, so
    the post carries both, even though the site picks one per city."""
    if value is None:
        return "—"
    f = round(value)
    c = round((value - 32) * 5 / 9)
    return f"{f}°F ({c}°C)"


def _line(card: dict) -> str:
    """One city's line in the post."""
    display = card.get("display") or card.get("name") or "—"
    metric = (card.get("metric_label") or "").lower() or "reading"
    val = _f_c(card.get("value"))
    normal = _f_c(card.get("normal")) if card.get("normal") is not None else None
    pct = card.get("pct_label") or ""
    grade = card.get("grade_label") or card.get("grade") or ""
    tag = " · ".join(p for p in (pct, grade) if p)
    when = f" ({card['date_label']})" if card.get("date_label") else ""
    normal_txt = f", normal {normal}" if normal else ""
    return f"**{display}**: {metric} {val}{normal_txt}{when}" + (f" · {tag}" if tag else "")


def build_embed(feed: dict) -> dict:
    """A Discord rich embed for the day's most unusual cities, or raise ValueError
    if the feed has nothing to show."""
    ranked = (feed or {}).get("ranked") or []
    if not ranked:
        raise ValueError("empty feed")
    top = ranked[:POST_LIMIT]
    color = _COLD if top[0].get("tail") == "cold" else _HOT
    date_s = feed.get("date") or datetime.date.today().isoformat()
    description = "\n".join(_line(c) for c in top)
    embed = {
        "title": "🌡️ Most unusual weather right now",
        "description": description,
        "url": "https://thermograph.org/",
        "color": color,
        "footer": {"text": "thermograph.org · every day graded against ~45 years of local history"},
    }
    # ISO8601 timestamp from the feed's build time, when available.
    gen = feed.get("generated_at")
    if gen:
        embed["timestamp"] = datetime.datetime.fromtimestamp(
            float(gen), tz=datetime.timezone.utc
        ).isoformat()
    else:
        embed["footer"]["text"] += f" · {date_s}"
    return embed


def post_daily_feed(feed: dict | None = None) -> bool:
    """Post the daily feed to the configured webhook. Best-effort: returns True on
    a 2xx, False otherwise (disabled, empty/stale feed, or any HTTP error) and
    never raises — a Discord failure must not disturb the notifier pass."""
    if not enabled():
        return False
    if feed is None:
        feed = homepage.load()
    if not feed or homepage.is_stale(feed):
        return False
    try:
        embed = build_embed(feed)
    except ValueError:
        return False
    ok = False
    if WEBHOOK_URL:
        payload = {
            "username": "Thermograph",
            "avatar_url": "https://thermograph.org/logo.png?v=3",
            "embeds": [embed],
        }
        try:
            resp = _client.post(WEBHOOK_URL, json=payload, timeout=_TIMEOUT)
            ok = 200 <= resp.status_code < 300
        except Exception:  # noqa: BLE001 - best-effort side channel
            pass
    if weather_enabled():
        # Broadcast the same digest into the prod notable-weather channel via the
        # bot — independent of the webhook, so either destination can run alone.
        ok = _post_channel_embed(WEATHER_CHANNEL_ID, embed) or ok
    return ok


# --- direct messages ---------------------------------------------------------
def dm_enabled() -> bool:
    return bool(BOT_TOKEN)


def _retry_after(resp) -> float:
    """Seconds to wait after a 429, capped. Reads the JSON body Discord returns."""
    try:
        return min(float(resp.json().get("retry_after", 1.0)), _MAX_BACKOFF_S)
    except Exception:  # noqa: BLE001
        return 1.0


def _bot_post(path: str, json_body: dict) -> httpx.Response | None:
    """POST to the bot REST API with one 429 retry. None on a transport error."""
    headers = {"Authorization": f"Bot {BOT_TOKEN}"}
    try:
        resp = _client.post(f"{_API}{path}", json=json_body, headers=headers, timeout=_TIMEOUT)
        if resp.status_code == 429:
            time.sleep(_retry_after(resp))
            resp = _client.post(f"{_API}{path}", json=json_body, headers=headers, timeout=_TIMEOUT)
        return resp
    except Exception:  # noqa: BLE001 - best-effort side channel
        return None


def _alert_embed(title: str, body: str | None, path: str, footer: str) -> dict:
    """A single-alert embed (DM or channel): title, body text, a deep link back to
    the site, warm accent, and a channel-appropriate footer."""
    return {
        "title": title,
        "description": body or "",
        "url": f"{PUBLIC_URL}{path}" if path else PUBLIC_URL,
        "color": _HOT,
        "footer": {"text": footer},
    }


def _post_channel_embed(channel_id: str, embed: dict) -> bool:
    """POST an embed into a channel by id via the bot. False on any failure."""
    msg = _bot_post(f"/channels/{channel_id}/messages", {"embeds": [embed]})
    return msg is not None and 200 <= msg.status_code < 300


def subscription_enabled() -> bool:
    """The notifier has a bot channel (this env's dev/uat/prod test feed) to post into."""
    return bool(BOT_TOKEN and SUBSCRIPTION_CHANNEL_ID)


def post_subscription_alert(title: str, body: str | None, path: str = "") -> bool:
    """Mirror one just-created notification into this deployment's subscription
    channel (dev/uat/prod), so a notifier run can be watched live. Best-effort:
    returns True on a 2xx, False otherwise (unconfigured or any HTTP error), and
    never raises — the in-app row and per-user DM are unaffected either way."""
    if not subscription_enabled():
        return False
    embed = _alert_embed(title, body, path, "thermograph.org · subscription notifier")
    return _post_channel_embed(SUBSCRIPTION_CHANNEL_ID, embed)


def send_dm(discord_id: str, title: str, body: str | None, path: str = "") -> bool:
    """DM one linked user an alert. Opens (or reuses) the DM channel, then posts an
    embed. Best-effort: returns True on success, False on anything else, never
    raises. Discord only delivers to users who share the server / installed the app;
    an undeliverable DM just returns False and the user keeps their other channels."""
    if not BOT_TOKEN or not discord_id:
        return False
    chan = _bot_post("/users/@me/channels", {"recipient_id": str(discord_id)})
    if chan is None or chan.status_code >= 300:
        return False
    try:
        channel_id = chan.json().get("id")
    except Exception:  # noqa: BLE001
        return False
    if not channel_id:
        return False
    embed = _alert_embed(
        title, body, path,
        "thermograph.org · mute Discord alerts anytime in your account",
    )
    return _post_channel_embed(channel_id, embed)
