"""Reminder dispatcher — multi-channel delivery (client push / email / webhook).

Strategy pattern: one method per channel. External dependencies (WS sender,
SMTP config, webhook URL) are injected so tests can mock them without
patching module imports.
"""

from __future__ import annotations

import ipaddress
import logging
import socket
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from urllib.parse import urlparse

from agentkit.calendar.models import CalendarEvent

logger = logging.getLogger(__name__)


@dataclass
class SmtpConfig:
    """SMTP server configuration for the email reminder channel."""

    host: str = "localhost"
    # ponytail: STARTTLS on 587 is the modern default; plaintext port 25 is
    # only appropriate for local MTA relays. Upgrade: implicit TLS on 465.
    port: int = 587
    username: str | None = None
    password: str | None = None
    use_tls: bool = True
    from_email: str = "noreply@agentkit.local"


def _is_safe_webhook_url(url: str) -> bool:
    """Validate webhook URL to prevent SSRF attacks.

    ponytail: Basic SSRF guard — blocks private/internal IP ranges (RFC 1918),
    loopback, link-local (169.254.x includes cloud metadata endpoints), and
    non-http(s) schemes. Uses blocking socket.getaddrinfo; upgrade to
    asyncio.getaddrinfo if webhook dispatch is on a hot path.
    """
    parsed = urlparse(url)
    if parsed.scheme not in ("http", "https"):
        return False
    hostname = parsed.hostname
    if not hostname:
        return False
    if hostname == "localhost" or hostname.startswith("169.254."):
        return False
    try:
        infos = socket.getaddrinfo(hostname, None)
        for _, _, _, _, sockaddr in infos:
            addr = sockaddr[0]
            try:
                ip = ipaddress.ip_address(addr)
            except ValueError:
                continue
            if ip.is_private or ip.is_loopback or ip.is_link_local:
                return False
    except socket.gaierror:
        pass  # DNS resolution failed — let httpx handle the connection error
    return True


class ReminderDispatcher:
    """Dispatch reminders via client push, email, and webhook channels.

    Args:
        ws_sender: Async callback ``(user_id, message_dict) -> None`` for client
            push.  The callback implementation is responsible for resolving
            ``user_id`` to an active WebSocket session.
        smtp_config: SMTP settings for the email channel. ``None`` disables email.
        webhook_url: URL for the webhook channel. ``None`` disables webhook.
        get_user_email: Async callback ``user_id -> email | None`` used to
            resolve the recipient address for email reminders.
    """

    def __init__(
        self,
        ws_sender: Callable[[str, dict[str, object]], Awaitable[None]] | None = None,
        smtp_config: SmtpConfig | None = None,
        webhook_url: str | None = None,
        get_user_email: Callable[[str], Awaitable[str | None]] | None = None,
    ) -> None:
        self._ws_sender = ws_sender
        self._smtp_config = smtp_config
        self._webhook_url = webhook_url
        self._get_user_email = get_user_email

    async def dispatch(self, channel: str, event: CalendarEvent, user_id: str) -> bool:
        """Send a reminder via *channel*. Returns ``True`` on success."""
        if channel == "client":
            return await self._send_client(event, user_id)
        if channel == "email":
            return await self._send_email(event, user_id)
        if channel == "webhook":
            return await self._send_webhook(event, user_id)
        logger.warning("Unknown reminder channel: %s", channel)
        return False

    async def _send_client(self, event: CalendarEvent, user_id: str) -> bool:
        if self._ws_sender is None:
            return False
        await self._ws_sender(
            user_id,
            {"type": "calendar_reminder", "data": event.to_dict()},
        )
        return True

    async def _send_email(self, event: CalendarEvent, user_id: str) -> bool:
        if self._smtp_config is None or self._get_user_email is None:
            return False
        email = await self._get_user_email(user_id)
        if not email:
            return False
        try:
            import aiosmtplib
        except ImportError:
            # ponytail: aiosmtplib is an optional dep — email channel silently
            # disabled when not installed. Upgrade: add aiosmtplib to pyproject.toml.
            logger.debug("aiosmtplib not installed — skipping email reminder")
            return False
        message = (
            f"From: {self._smtp_config.from_email}\r\n"
            f"To: {email}\r\n"
            f"Subject: Reminder: {event.title}\r\n\r\n"
            f"{event.title} starts at {event.start_time}.\r\n"
        )
        await aiosmtplib.send(
            message,
            hostname=self._smtp_config.host,
            port=self._smtp_config.port,
            username=self._smtp_config.username,
            password=self._smtp_config.password,
            start_tls=self._smtp_config.use_tls,
        )
        return True

    async def _send_webhook(self, event: CalendarEvent, user_id: str) -> bool:
        if self._webhook_url is None:
            return False
        if not _is_safe_webhook_url(self._webhook_url):
            logger.warning("Webhook URL blocked: private/internal address")
            return False
        import httpx

        async with httpx.AsyncClient() as client:
            resp = await client.post(
                self._webhook_url,
                json={"event": event.to_dict(), "user_id": user_id},
            )
            return resp.status_code < 400
