"""Email configuration for Brynq runtime notifications.

Stores SMTP settings in ~/.brynq/email_config.json with the password
field encrypted using the runtime vault's Fernet key (machine-bound).
"""

from __future__ import annotations

import base64
import json
import logging
import smtplib
from email.message import EmailMessage
from pathlib import Path
from typing import Any, Optional

from cryptography.fernet import Fernet, InvalidToken

from runtime.vault.key_vault import _derive_fernet_key, VAULT_FILE

log = logging.getLogger("brynq.runtime.integrations.email")

CONFIG_FILE = Path.home() / ".brynq" / "email_config.json"


class EmailConfig:
    """SMTP configuration backed by ~/.brynq/email_config.json.

    The password field is encrypted at rest using the same Fernet key
    derivation as the runtime vault (machine-bound, PBKDF2).

    Usage::

        cfg = EmailConfig()
        cfg.configure("smtp.gmail.com", 587, "user@gmail.com", "app-password", "user@gmail.com")
        print(cfg.host, cfg.port)
    """

    def __init__(self, config_path: Optional[Path] = None) -> None:
        self._config_path = config_path or CONFIG_FILE
        self._host: Optional[str] = None
        self._port: Optional[int] = None
        self._user: Optional[str] = None
        self._from_addr: Optional[str] = None
        self._use_tls: bool = True
        self._encrypted_password: Optional[str] = None
        self._load()

    # ── Public properties ────────────────────────────────────────────

    @property
    def host(self) -> Optional[str]:
        return self._host

    @property
    def port(self) -> Optional[int]:
        return self._port

    @property
    def user(self) -> Optional[str]:
        return self._user

    @property
    def from_addr(self) -> Optional[str]:
        return self._from_addr

    @property
    def use_tls(self) -> bool:
        return self._use_tls

    @property
    def is_configured(self) -> bool:
        return self._host is not None and self._encrypted_password is not None

    # ── Public API ───────────────────────────────────────────────────

    def configure(
        self,
        host: str,
        port: int,
        user: str,
        password: str,
        from_addr: str,
        use_tls: bool = True,
    ) -> None:
        """Save SMTP configuration with an encrypted password.

        Args:
            host: SMTP server hostname.
            port: SMTP server port.
            user: SMTP login username.
            password: SMTP login password (encrypted before storing).
            from_addr: Sender email address.
            use_tls: Whether to use STARTTLS (default True).

        Raises:
            ValueError: If any required field is empty.
        """
        if not host or not host.strip():
            raise ValueError("SMTP host cannot be empty")
        if not user or not user.strip():
            raise ValueError("SMTP user cannot be empty")
        if not password:
            raise ValueError("SMTP password cannot be empty")
        if not from_addr or not from_addr.strip():
            raise ValueError("From address cannot be empty")

        encrypted_b64 = self._encrypt_password(password)

        self._host = host.strip()
        self._port = port
        self._user = user.strip()
        self._from_addr = from_addr.strip()
        self._use_tls = use_tls
        self._encrypted_password = encrypted_b64

        self._save()
        log.info("Email configuration saved (password encrypted)")

    def get_password(self) -> Optional[str]:
        """Decrypt and return the stored SMTP password.

        Returns:
            The plaintext password, or None if not configured or decryption fails.
        """
        if not self._encrypted_password:
            return None

        salt = self._get_vault_salt()
        if salt is None:
            log.error("Cannot decrypt email password — vault not initialized")
            return None

        fernet_key = _derive_fernet_key(salt)
        f = Fernet(fernet_key)

        try:
            encrypted = base64.b64decode(self._encrypted_password)
            return f.decrypt(encrypted).decode("utf-8")
        except InvalidToken:
            log.error("Failed to decrypt email password — vault key mismatch")
            return None
        except Exception:
            log.error("Failed to decrypt email password — unexpected error")
            return None

    def get_config(self) -> dict[str, Any] | None:
        """Return the stored SMTP config with the password decrypted.

        Returns:
            A dict with keys ``host``, ``port``, ``user``, ``from_addr``,
            ``use_tls``, and ``password`` (plaintext), or ``None`` if
            the email is not configured or the password cannot be decrypted.
        """
        if not self.is_configured:
            return None

        password = self.get_password()
        if password is None:
            return None

        return {
            "host": self._host,
            "port": self._port,
            "user": self._user,
            "from_addr": self._from_addr,
            "use_tls": self._use_tls,
            "password": password,
        }

    # ── Private helpers ──────────────────────────────────────────────

    def _encrypt_password(self, password: str) -> str:
        """Encrypt a password using the vault's Fernet key."""
        salt = self._get_vault_salt()
        if salt is None:
            # Vault doesn't exist yet — initialize it by importing KeyVault
            from runtime.vault.key_vault import KeyVault

            KeyVault()  # creates empty vault with fresh salt
            salt = self._get_vault_salt()
            if salt is None:
                raise RuntimeError("Failed to initialize vault for encryption")

        fernet_key = _derive_fernet_key(salt)
        f = Fernet(fernet_key)
        encrypted = f.encrypt(password.encode("utf-8"))
        return base64.b64encode(encrypted).decode("utf-8")

    def _get_vault_salt(self) -> Optional[bytes]:
        """Read the salt from the runtime vault file."""
        vault_path = VAULT_FILE
        if not vault_path.exists():
            return None

        try:
            raw = vault_path.read_text("utf-8")
            data = json.loads(raw)
            return base64.b64decode(data["salt"])
        except (json.JSONDecodeError, KeyError, OSError):
            return None

    def _load(self) -> None:
        """Load config from disk."""
        if not self._config_path.exists():
            return

        try:
            raw = self._config_path.read_text("utf-8")
            data: dict[str, Any] = json.loads(raw)
        except (json.JSONDecodeError, OSError):
            log.warning("Corrupted email config — ignoring")
            return

        self._host = data.get("host")
        self._port = data.get("port")
        self._user = data.get("user")
        self._from_addr = data.get("from_addr")
        self._use_tls = data.get("use_tls", True)
        self._encrypted_password = data.get("encrypted_password")

    def _save(self) -> None:
        """Persist config to disk."""
        self._config_path.parent.mkdir(parents=True, exist_ok=True)

        data = {
            "host": self._host,
            "port": self._port,
            "user": self._user,
            "from_addr": self._from_addr,
            "use_tls": self._use_tls,
            "encrypted_password": self._encrypted_password,
        }

        self._config_path.write_text(
            json.dumps(data, indent=2) + "\n", encoding="utf-8"
        )


def send_chain_result(config: EmailConfig, to: str, chain_result: dict) -> bool:
    """Format a chain execution result and email it.

    Builds a readable plain-text email with a section per step showing
    the model used, its output, and timing when available, then delegates
    to :func:`send_email`.

    Args:
        config: An :class:`EmailConfig` instance with SMTP settings.
        to: Recipient email address.
        chain_result: Dict as returned by the ``/chain`` endpoint, expected
            keys: ``plan_id``, ``steps`` (list of step dicts), ``success``,
            ``total_duration_ms``.

    Returns:
        True if the email was sent successfully, False on any error.
    """
    plan_id = chain_result.get("plan_id", "unknown")
    steps = chain_result.get("steps", [])
    success = chain_result.get("success", False)
    total_ms = chain_result.get("total_duration_ms", 0)

    lines: list[str] = []
    lines.append(f"Brynq Chain Result — {'SUCCESS' if success else 'FAILED'}")
    lines.append(f"Plan: {plan_id}")
    lines.append(f"Total duration: {total_ms}ms")
    lines.append("")

    for i, step in enumerate(steps, 1):
        model = step.get("model", "unknown")
        output = step.get("content", step.get("output", ""))
        step_id = step.get("step_id", f"step_{i}")
        step_ok = step.get("success", True)
        error = step.get("error")
        duration = step.get("duration_ms")

        lines.append(f"{'=' * 50}")
        lines.append(f"Step {i}: {step_id}")
        lines.append(f"Model: {model}")
        if duration is not None:
            lines.append(f"Duration: {duration}ms")
        lines.append(f"Status: {'OK' if step_ok else 'FAILED'}")
        if error:
            lines.append(f"Error: {error}")
        lines.append("")
        lines.append(str(output))
        lines.append("")

    lines.append(f"{'=' * 50}")
    lines.append("Sent by Brynq Runtime")

    subject = f"Brynq Chain Result — {plan_id}"
    body = "\n".join(lines)
    return send_email(config, to, subject, body)


def send_email(config: EmailConfig, to: str, subject: str, body: str) -> bool:
    """Send a plain-text email using the provided SMTP configuration.

    Args:
        config: An :class:`EmailConfig` instance with SMTP settings.
        to: Recipient email address.
        subject: Email subject line.
        body: Plain-text email body.

    Returns:
        True if the email was sent successfully, False on any error.
    """
    if not config.is_configured:
        log.error("Cannot send email — email is not configured")
        return False

    password = config.get_password()
    if password is None:
        log.error("Cannot send email — failed to decrypt password")
        return False

    host: str = config.host  # type: ignore[assignment]  # guarded by is_configured
    port: int = config.port  # type: ignore[assignment]
    user: str = config.user  # type: ignore[assignment]

    msg = EmailMessage()
    msg["From"] = config.from_addr or user
    msg["To"] = to
    msg["Subject"] = subject
    msg.set_content(body)

    try:
        if config.use_tls:
            with smtplib.SMTP(host, port) as smtp:
                smtp.ehlo()
                smtp.starttls()
                smtp.ehlo()
                smtp.login(user, password)
                smtp.send_message(msg)
        else:
            with smtplib.SMTP_SSL(host, port) as smtp:
                smtp.login(user, password)
                smtp.send_message(msg)

        log.info("Email sent to %s: %s", to, subject)
        return True
    except Exception:
        log.exception("Failed to send email to %s", to)
        return False
