"""The WhatsApp provider port, and the rules that apply whatever is behind it.

**The constraints here are Meta's, and they are enforced for every provider.**

Evolution imposes no session window and needs no template approval. If the
abstraction were shaped around that, everything built in development would
assume it can send anything at any time — and would break the day it moved to
the official platform, with a failure mode of "messages silently not delivered
to clients".

So the stricter rule is the shared rule. Development is deliberately as
constrained as production.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Protocol, runtime_checkable

from app.runtime.metrics import METRICS
from app.whatsapp.messages import InboundMessage, MediaReference, OutboundMessage, SendResult

SESSION_WINDOW = timedelta(hours=24)
"""Meta permits free-form replies only within 24 hours of the customer's last
message. Outside it, only pre-approved templates may be sent."""


class SessionWindowError(RuntimeError):
    """Raised when a free-form message is attempted outside the 24-hour window."""


class TemplateNotApprovedError(RuntimeError):
    """Raised when a template that was never registered is used."""


class SessionWindow:
    """Tracks when each customer last wrote to us.

    Held per deployment (ADR-0001: one installation, one number). The window is
    checked before sending rather than after a rejection, so a workflow can
    choose a template instead of discovering the problem from a provider error.
    """

    def __init__(self) -> None:
        self._last_inbound: dict[str, datetime] = {}

    def record_inbound(self, sender: str, at: datetime | None = None) -> None:
        self._last_inbound[sender] = at or datetime.now(UTC)

    def is_open(self, recipient: str, now: datetime | None = None) -> bool:
        last = self._last_inbound.get(recipient)

        if last is None:
            return False

        return (now or datetime.now(UTC)) - last < SESSION_WINDOW

    def closes_at(self, recipient: str) -> datetime | None:
        last = self._last_inbound.get(recipient)

        return last + SESSION_WINDOW if last else None


@runtime_checkable
class WhatsAppProvider(Protocol):
    """What any WhatsApp implementation must provide."""

    name: str

    def send(self, message: OutboundMessage) -> SendResult: ...

    def parse_webhook(self, payload: dict) -> list[InboundMessage]:
        """Turn a provider's webhook body into messages.

        Returns a list: providers batch, and a payload carrying three messages
        must not lose two of them.
        """
        ...

    def download_media(self, media: MediaReference, destination: Path) -> Path:
        """Fetch a message's file to local disk.

        Local because of ADR-0002: the document is Level 3 and is processed on
        this installation. A provider that hands back a URL and one that issues
        an expiring id both reduce to this.
        """
        ...


class MessageSender:
    """Sends through a provider, enforcing the platform rules first.

    Every outbound message goes through here rather than calling a provider
    directly, so the window and template checks cannot be bypassed by a caller
    who did not know about them.
    """

    def __init__(
        self,
        provider: WhatsAppProvider,
        window: SessionWindow | None = None,
        approved_templates: set[str] | None = None,
        sent: object | None = None,
    ) -> None:
        self._provider = provider
        self._window = window if window is not None else SessionWindow()
        # Empty means none approved yet — the correct state for a deployment
        # that has not been through Meta's review.
        self._approved = approved_templates if approved_templates is not None else set()

        # The receiver's SeenMessages, when one is shared. Every id we send is
        # recorded there, so the provider's echo of our own reply is recognised
        # as already handled.
        #
        # This is what makes the note-to-self inbox possible: `fromMe` cannot be
        # used to filter, because the documents the account owner sends
        # themselves are also fromMe. An id we generated is the only reliable
        # way to tell our messages from theirs.
        self._sent = sent

    @property
    def window(self) -> SessionWindow:
        return self._window

    def approve_template(self, name: str) -> None:
        """Register a template Meta has approved."""
        self._approved.add(name)

    def send(self, message: OutboundMessage, now: datetime | None = None) -> SendResult:
        if message.is_template:
            if message.template not in self._approved:
                # Sending an unregistered template fails at the platform with a
                # generic error; refusing here says which template and why.
                raise TemplateNotApprovedError(
                    f"Template '{message.template}' has not been approved for this deployment."
                )

            # Templates are permitted at any time — that is what they are for.
            return self._remember(self._provider.send(message))

        if not self._window.is_open(message.recipient, now):
            raise SessionWindowError(
                f"The 24-hour window for {message.recipient} has closed. "
                "Send a pre-approved template instead."
            )

        return self._remember(self._provider.send(message))

    def _remember(self, result: SendResult) -> SendResult:
        """Record what we sent, so its echo is not treated as a new message."""
        # A message that fails to send is invisible to the person waiting for it,
        # so the failure rate is the signal that matters here.
        METRICS.counter(
            "taxpilot_whatsapp_sent_total", "Outbound WhatsApp messages.",
            outcome="ok" if result.ok else "failed",
        )

        if self._sent is not None and result.ok and result.provider_message_id:
            self._sent.add(result.provider_message_id)

        return result

    def record_inbound(self, message: InboundMessage) -> None:
        """Open (or extend) the window for whoever just wrote to us."""
        self._window.record_inbound(message.sender, message.received_at)
