# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
SMTP Client for sending mail.

Supports:
- Direct sending (requires good IP reputation)
- Relay through external SMTP (Mailgun, SendGrid, SES, etc.)
- DKIM signing
- TLS/STARTTLS
"""

import asyncio
import logging
import ssl
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from typing import Any, Dict, List, Optional

logger = logging.getLogger(__name__)

# Optional aiosmtplib for async sending
try:
    import aiosmtplib

    HAS_AIOSMTPLIB = True
except ImportError:
    HAS_AIOSMTPLIB = False
    logger.debug("aiosmtplib not installed - will use sync smtplib")

from .dkim import DKIMManager  # noqa: E402


class SMTPClient:
    """
    SMTP client for sending outgoing mail.

    Features:
    - Async sending via aiosmtplib
    - Relay support for better deliverability
    - DKIM signing
    - Automatic TLS
    """

    def __init__(self, config, dkim_manager: Optional[DKIMManager] = None):
        self.config = config
        self.dkim_manager = dkim_manager

        # Determine sending method
        self.use_relay = config.relay_enabled and config.relay_host is not None

    async def send(
        self,
        from_address: str,
        to: str,
        subject: str,
        body: str,
        html_body: Optional[str] = None,
        attachments: Optional[List[Dict]] = None,
        reply_to: Optional[str] = None,
        cc: Optional[List[str]] = None,
        bcc: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        """
        Send an email.

        Args:
            from_address: Sender email address
            to: Primary recipient
            subject: Email subject
            body: Plain text body
            html_body: Optional HTML body
            attachments: List of {'filename': str, 'content': bytes, 'mime_type': str}
            reply_to: Reply-to address
            cc: CC recipients
            bcc: BCC recipients

        Returns:
            {'message_id': str} on success

        Raises:
            Exception on failure
        """
        # Build the message
        message = await self._build_message(
            from_address=from_address,
            to=to,
            subject=subject,
            body=body,
            html_body=html_body,
            attachments=attachments,
            reply_to=reply_to,
            cc=cc,
        )

        # Sign with DKIM if available
        if self.dkim_manager:
            message = await self.dkim_manager.sign_message(message)

        # Get all recipients
        recipients = [to]
        if cc:
            recipients.extend(cc)
        if bcc:
            recipients.extend(bcc)

        # Send via relay or direct
        if self.use_relay:
            await self._send_via_relay(from_address, recipients, message)
        else:
            await self._send_direct(from_address, recipients, message)

        message_id = message["Message-ID"]
        logger.info(f"Sent email to {to}: {subject} ({message_id})")

        return {"message_id": message_id}

    async def _build_message(
        self,
        from_address: str,
        to: str,
        subject: str,
        body: str,
        html_body: Optional[str] = None,
        attachments: Optional[List[Dict]] = None,
        reply_to: Optional[str] = None,
        cc: Optional[List[str]] = None,
    ) -> MIMEMultipart:
        """Build MIME message."""
        # Create message container
        if html_body or attachments:
            msg = MIMEMultipart("mixed")
        else:
            msg = MIMEMultipart()

        # Headers
        msg["From"] = from_address
        msg["To"] = to
        msg["Subject"] = subject
        msg["Date"] = formatdate(localtime=True)
        msg["Message-ID"] = make_msgid(domain=self.config.domain)

        if reply_to:
            msg["Reply-To"] = reply_to

        if cc:
            msg["Cc"] = ", ".join(cc)

        # Add custom headers
        msg["X-Mailer"] = "Familiar Mail Server"
        msg["X-Originating-IP"] = "[127.0.0.1]"

        # Body
        if html_body:
            # Multipart alternative for text + HTML
            alt_part = MIMEMultipart("alternative")
            alt_part.attach(MIMEText(body, "plain", "utf-8"))
            alt_part.attach(MIMEText(html_body, "html", "utf-8"))
            msg.attach(alt_part)
        else:
            msg.attach(MIMEText(body, "plain", "utf-8"))

        # Attachments
        if attachments:
            for att in attachments:
                part = MIMEBase("application", "octet-stream")

                content = att.get("content")
                if isinstance(content, str):
                    # Hex-encoded content from storage
                    content = bytes.fromhex(content)

                part.set_payload(content)
                encoders.encode_base64(part)

                part.add_header(
                    "Content-Disposition", "attachment", filename=att.get("filename", "attachment")
                )

                msg.attach(part)

        return msg

    async def _send_via_relay(
        self,
        from_address: str,
        recipients: List[str],
        message: MIMEMultipart,
    ):
        """Send mail through relay server."""
        if HAS_AIOSMTPLIB:
            await self._send_async(
                host=self.config.relay_host,
                port=self.config.relay_port,
                username=self.config.relay_username,
                password=self.config.relay_password,
                from_address=from_address,
                recipients=recipients,
                message=message,
            )
        else:
            await self._send_sync(
                host=self.config.relay_host,
                port=self.config.relay_port,
                username=self.config.relay_username,
                password=self.config.relay_password,
                from_address=from_address,
                recipients=recipients,
                message=message,
            )

    async def _send_direct(
        self,
        from_address: str,
        recipients: List[str],
        message: MIMEMultipart,
    ):
        """Send mail directly to recipient's MX server."""
        import dns.resolver

        for recipient in recipients:
            domain = recipient.split("@")[1]

            # Look up MX records
            try:
                mx_records = dns.resolver.resolve(domain, "MX")
                mx_host = str(sorted(mx_records, key=lambda x: x.preference)[0].exchange).rstrip(
                    "."
                )
            except Exception as e:
                raise Exception(f"MX lookup failed for {domain}: {e}")

            # Send to MX server
            if HAS_AIOSMTPLIB:
                await self._send_async(
                    host=mx_host,
                    port=25,
                    username=None,
                    password=None,
                    from_address=from_address,
                    recipients=[recipient],
                    message=message,
                    use_tls=False,  # Try STARTTLS
                )
            else:
                await self._send_sync(
                    host=mx_host,
                    port=25,
                    username=None,
                    password=None,
                    from_address=from_address,
                    recipients=[recipient],
                    message=message,
                    use_tls=False,
                )

    async def _send_async(
        self,
        host: str,
        port: int,
        username: Optional[str],
        password: Optional[str],
        from_address: str,
        recipients: List[str],
        message: MIMEMultipart,
        use_tls: bool = True,
    ):
        """Send using aiosmtplib (async)."""
        kwargs = {
            "hostname": host,
            "port": port,
            "start_tls": True,
        }

        if use_tls and port == 465:
            kwargs["use_tls"] = True
            kwargs["start_tls"] = False

        if username and password:
            kwargs["username"] = username
            kwargs["password"] = password

        try:
            await aiosmtplib.send(message, sender=from_address, recipients=recipients, **kwargs)
        except aiosmtplib.SMTPException as e:
            logger.error(f"SMTP error: {e}")
            raise

    async def _send_sync(
        self,
        host: str,
        port: int,
        username: Optional[str],
        password: Optional[str],
        from_address: str,
        recipients: List[str],
        message: MIMEMultipart,
        use_tls: bool = True,
    ):
        """Send using smtplib (sync, wrapped in executor)."""
        import smtplib

        def _send():
            if use_tls and port == 465:
                context = ssl.create_default_context()
                server = smtplib.SMTP_SSL(host, port, context=context)
            else:
                server = smtplib.SMTP(host, port)
                server.ehlo()
                if server.has_extn("STARTTLS"):
                    server.starttls()
                    server.ehlo()

            try:
                if username and password:
                    server.login(username, password)

                server.sendmail(from_address, recipients, message.as_string())
            finally:
                server.quit()

        loop = asyncio.get_event_loop()
        await loop.run_in_executor(None, _send)

    async def verify_connection(self) -> Dict[str, Any]:
        """Verify SMTP connection and credentials."""
        if not self.use_relay:
            return {"success": True, "method": "direct"}

        try:
            if HAS_AIOSMTPLIB:
                smtp = aiosmtplib.SMTP(
                    hostname=self.config.relay_host,
                    port=self.config.relay_port,
                )
                await smtp.connect()
                await smtp.starttls()

                if self.config.relay_username:
                    await smtp.login(self.config.relay_username, self.config.relay_password)

                await smtp.quit()
            else:
                import smtplib

                server = smtplib.SMTP(self.config.relay_host, self.config.relay_port)
                server.starttls()

                if self.config.relay_username:
                    server.login(self.config.relay_username, self.config.relay_password)

                server.quit()

            return {
                "success": True,
                "method": "relay",
                "host": self.config.relay_host,
            }

        except Exception as e:
            return {
                "success": False,
                "error": str(e),
            }


# Relay provider configurations
RELAY_PROVIDERS = {
    "mailgun": {
        "host": "smtp.mailgun.org",
        "port": 587,
        "docs": "https://documentation.mailgun.com/en/latest/quickstart-sending.html",
    },
    "sendgrid": {
        "host": "smtp.sendgrid.net",
        "port": 587,
        "docs": "https://docs.sendgrid.com/for-developers/sending-email/integrating-with-the-smtp-api",
    },
    "ses": {
        "host": "email-smtp.us-east-1.amazonaws.com",  # Region varies
        "port": 587,
        "docs": "https://docs.aws.amazon.com/ses/latest/dg/smtp-credentials.html",
    },
    "postmark": {
        "host": "smtp.postmarkapp.com",
        "port": 587,
        "docs": "https://postmarkapp.com/developer/user-guide/sending-email/sending-with-smtp",
    },
    "mailjet": {
        "host": "in-v3.mailjet.com",
        "port": 587,
        "docs": "https://dev.mailjet.com/email/guides/send-email-with-smtp/",
    },
}


def get_relay_config(provider: str) -> Dict[str, Any]:
    """Get relay configuration for a provider."""
    if provider.lower() in RELAY_PROVIDERS:
        return RELAY_PROVIDERS[provider.lower()]
    return None
