import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

from attrs import define, field
from fastapi.templating import Jinja2Templates
from pydantic import EmailStr
from rotoger import get_logger

from app.config import settings as global_settings
from app.utils.singleton import SingletonMetaNoArgs

logger = get_logger()


@define
class SMTPEmailService(metaclass=SingletonMetaNoArgs):
    """
    SMTPEmailService provides a reusable interface to send emails via an SMTP server.

    This service supports plaintext and HTML emails, and also allows
    sending template-based emails using the Jinja2 template engine.

    It is implemented as a singleton to ensure that only one SMTP connection is maintained
    throughout the application lifecycle, optimizing resource usage.

    Attributes:
        server_host (str): SMTP server hostname or IP address.
        server_port (int): Port number for the SMTP connection.
        username (str): SMTP username for authentication.
        password (str): SMTP password for authentication.
        templates (Jinja2Templates): Jinja2Templates instance for loading and rendering email templates.
        server (smtplib.SMTP): An SMTP object for sending emails, initialized after object creation.
    """

    # SMTP configuration
    server_host: str = field(default=global_settings.smtp.server)
    server_port: int = field(default=global_settings.smtp.port)
    username: str = field(default=global_settings.smtp.username)
    password: str = field(default=global_settings.smtp.password)

    # Dependencies
    templates: Jinja2Templates = field(
        factory=lambda: Jinja2Templates(global_settings.smtp.template_path)
    )
    server: smtplib.SMTP = field(init=False)  # Deferred initialization in post-init

    def __attrs_post_init__(self):
        """
        Initializes the SMTP server connection after the object is created.

        This method sets up a secure connection to the SMTP server, including STARTTLS encryption
        and logs in using the provided credentials.
        """
        self.server = smtplib.SMTP(self.server_host, self.server_port)
        self.server.starttls()  # Upgrade the connection to secure TLS
        self.server.login(self.username, self.password)
        logger.info(
            "SMTPEmailService initialized successfully and connected to SMTP server."
        )

    def _prepare_email(
        self,
        sender: EmailStr,
        recipients: list[EmailStr],
        subject: str,
        body_text: str,
        body_html: str,
    ) -> MIMEMultipart:
        """
        Prepares a MIME email message with the given plaintext and HTML content.

        Args:
            sender (EmailStr): The email address of the sender.
            recipients (list[EmailStr]): A list of recipient email addresses.
            subject (str): The subject line of the email.
            body_text (str): The plaintext content of the email.
            body_html (str): The HTML content of the email (optional).

        Returns:
            MIMEMultipart: A MIME email object ready to be sent.
        """
        msg = MIMEMultipart()
        msg["From"] = sender
        msg["To"] = ",".join(recipients)
        msg["Subject"] = subject
        # Add plain text and HTML content (if provided)
        msg.attach(MIMEText(body_text, "plain"))
        if body_html:
            msg.attach(MIMEText(body_html, "html"))
        logger.debug(f"Prepared email from {sender} to {recipients}.")
        return msg

    def send_email(
        self,
        sender: EmailStr,
        recipients: list[EmailStr],
        subject: str,
        body_text: str = "",
        body_html: str = None,
    ):
        """
        Sends an email to the specified recipients.

        Supports plaintext and HTML email content. This method constructs
        the email message using `_prepare_email` and sends it using the SMTP server.

        Args:
            sender (EmailStr): The email address of the sender.
            recipients (list[EmailStr]): A list of recipient email addresses.
            subject (str): The subject line of the email.
            body_text (str): The plaintext content of the email.
            body_html (str): The HTML content of the email (optional).

        Raises:
            smtplib.SMTPException: If the email cannot be sent.
        """
        try:
            msg = self._prepare_email(sender, recipients, subject, body_text, body_html)
            self.server.sendmail(sender, recipients, msg.as_string())
            logger.info(f"Email sent successfully to {recipients} from {sender}.")
        except smtplib.SMTPException as e:
            logger.error("Failed to send email", exc_info=e)
            raise

    def send_template_email(
        self,
        recipients: list[EmailStr],
        subject: str,
        template: str,
        context: dict,
        sender: EmailStr,
    ):
        """
        Sends an email using a Jinja2 template.

        This method renders the template with the provided context and sends it
        to the specified recipients.

        Args:
            recipients (list[EmailStr]): A list of recipient email addresses.
            subject (str): The subject line of the email.
            template (str): The name of the template file in the templates' directory.
            context (dict): A dictionary of values to render the template with.
            sender (EmailStr): The email address of the sender.

        Raises:
            jinja2.TemplateNotFound: If the specified template is not found.
            smtplib.SMTPException: If the email cannot be sent.
        """
        try:
            template_str = self.templates.get_template(template)
            body_html = template_str.render(
                context
            )  # Render the HTML using context variables
            self.send_email(sender, recipients, subject, body_html=body_html)
            logger.info(
                f"Template email sent successfully to {recipients} using template {template}."
            )
        except Exception as e:
            logger.error("Failed to send template email", exc_info=e)
            raise
