"""The mail seam: a `Mailer` sends one plain-text message; `none` is the launch provider.

Sources of truth: this module, `app/config.py` (`MAIL_PROVIDER`, `MAIL_FROM`, `RESEND_API_KEY`),
`app/models/discussion.py` (`EmailSend`, the cap ledger), `app/cli_notify.py` (the digest),
`docs/ACCOUNTS.md`, `docs/RUNBOOK.md` (Mail). Design: `.logs/planning/accounts-2026-09.md` §4.5
and §8.

With no provider every send raises `MailNotConfigured` before anything is written, which the
directory turns into a refusal the People panel can show. There is deliberately no console
provider: printing a link to stdout is logging a token, and the container's stdout is an
access log. The recipient is always the address stored on the account, never one from a
request. The test suite injects a recording mailer through `set_mailer`; nothing under test
reads `MAIL_PROVIDER`.

Every send counts against a cap in `email_sends` (plan §8): 6 auth mails per account per day
(welcome, reset, access notices), 25 invites per host per day, 40 notification mails per
account per day. A cap reached raises `MailCapReached` before the provider is called; the
ledger row is written after a successful send. The caps bound what a compromised session, a
stuck loop or a typo in a cron line can send in anyone's name.
"""

from __future__ import annotations

import logging
from datetime import UTC, datetime, timedelta
from typing import Protocol

from sqlalchemy import func, select

from app import db as appdb
from app.config import settings

log = logging.getLogger(__name__)

#: kind -> (limit, window). The bucket key is `<kind>:<account id>` or `invite:host`.
CAPS: dict[str, tuple[int, timedelta]] = {
    "auth": (6, timedelta(days=1)),
    "invite": (25, timedelta(days=1)),
    "notify": (40, timedelta(days=1)),
}


class MailNotConfigured(RuntimeError):
    """No provider on this host: invites and resets go by CLI link or generated password."""


class MailCapReached(RuntimeError):
    """The daily cap for this bucket is spent; nothing was sent."""


def bucket(kind: str, key: str | int) -> str:
    assert kind in CAPS, kind
    return f"{kind}:{key}"


def sent_in_window(db, name: str, *, now: datetime | None = None) -> int:
    from app.models.discussion import EmailSend

    kind = name.split(":", 1)[0]
    _, window = CAPS[kind]
    since = (now or datetime.now(UTC)) - window
    return db.scalar(select(func.count()).select_from(EmailSend)
                     .where(EmailSend.bucket == name, EmailSend.sent_at >= since)) or 0


def allowed(db, name: str, *, now: datetime | None = None) -> bool:
    limit, _ = CAPS[name.split(":", 1)[0]]
    return sent_in_window(db, name, now=now) < limit


def record(db, name: str, *, now: datetime | None = None) -> None:
    from app.models.discussion import EmailSend

    db.add(EmailSend(bucket=name, sent_at=now or datetime.now(UTC)))


def send_capped(name: str, to: str, subject: str, text: str) -> None:
    """One capped send on the store's own short session: check the ledger, send, record. The
    caller's own transaction is already committed; a refused send leaves it as it is."""
    with appdb.SessionLocal() as db:
        if not allowed(db, name):
            limit, window = CAPS[name.split(":", 1)[0]]
            raise MailCapReached(f"The mail cap for {name.split(':', 1)[0]} ({limit} per {window.days} day) is spent.")
    mailer().send(to, subject, text)
    with appdb.SessionLocal() as db:
        record(db, name)
        db.commit()


class Mailer(Protocol):
    def send(self, to: str, subject: str, text: str) -> None: ...


class NoMailer:
    def send(self, to: str, subject: str, text: str) -> None:
        raise MailNotConfigured(
            "No mail provider is configured on this host. Use `accounts invite-link` or "
            "`accounts reset-link` from the CLI, or add the person with a generated password."
        )


_override: Mailer | None = None


def set_mailer(mailer: Mailer | None) -> None:
    """Tests inject a recorder; None restores the configured provider."""
    global _override
    _override = mailer


def mailer() -> Mailer:
    if _override is not None:
        return _override
    provider = (settings.mail_provider or "none").strip().lower()
    if provider == "resend":
        from app.services.mail_resend import ResendMailer

        return ResendMailer()
    return NoMailer()


def configured() -> bool:
    """Whether a send could succeed, for the CLI's advice; never sends."""
    if _override is not None:
        return True
    return (settings.mail_provider or "none").strip().lower() == "resend" and settings.mail_ready
