import re
from dataclasses import dataclass, field
from typing import Any

from bs4 import BeautifulSoup
from msgraph.generated.models.body_type import BodyType
from msgraph.generated.models.email_address import EmailAddress
from msgraph.generated.models.item_body import ItemBody
from msgraph.generated.models.message import Message as GraphMessage
from msgraph.generated.models.recipient import Recipient as GraphRecipient


@dataclass
class Recipient:
    """A recipient of an email message."""

    email_address: str = ""
    name: str = ""

    @classmethod
    def from_sdk(cls, recipient: GraphRecipient) -> "Recipient":
        """Convert a Microsoft Graph SDK Recipient object to a Recipient dataclass."""
        address = (
            recipient.email_address.address
            if recipient and recipient.email_address and recipient.email_address.address
            else ""
        )
        name = (
            recipient.email_address.name
            if recipient and recipient.email_address and recipient.email_address.name
            else ""
        )
        return cls(email_address=address, name=name)

    def to_dict(self) -> dict[str, str]:
        return {"email_address": self.email_address, "name": self.name}

    def to_sdk(self) -> GraphRecipient:
        """Converts the Recipient dataclass to a Microsoft Graph SDK Recipient object."""
        recipient = GraphRecipient()
        email_address = EmailAddress()
        email_address.address = self.email_address
        email_address.name = self.name
        recipient.email_address = email_address
        return recipient


@dataclass
class Message:
    """An email message in Outlook."""

    bcc_recipients: list[Recipient] = field(default_factory=list)
    cc_recipients: list[Recipient] = field(default_factory=list)
    reply_to: list[Recipient] = field(default_factory=list)
    to_recipients: list[Recipient] = field(default_factory=list)
    from_: Recipient = field(default_factory=Recipient)
    subject: str = ""
    body: str = ""
    conversation_id: str = ""
    conversation_index: str = ""
    flag: dict[str, str] = field(default_factory=dict)
    has_attachments: bool = False
    importance: str = ""
    is_read: bool = False
    received_date_time: str = ""
    web_link: str = ""
    is_draft: bool = True
    message_id: str = ""  # The unique identifier of the email message. Read-only.

    @staticmethod
    def _safe_str(value: Any) -> str:
        if not value:
            return ""
        if isinstance(value, bytes | bytearray):
            return value.decode("utf-8", errors="ignore")
        return str(value)

    @staticmethod
    def _safe_bool(value: Any) -> bool:
        return bool(value)

    @staticmethod
    def _parse_body(mime: str) -> str:
        if not mime:
            return ""
        soup = BeautifulSoup(mime, "html.parser")
        text = soup.get_text(separator=" ")
        # Replace multiple newlines with a single newline
        text = re.sub(r"\n+", "\n", text)
        # Replace multiple spaces with a single space
        text = re.sub(r"\s+", " ", text)
        # Remove leading/trailing whitespace from each line
        text = "\n".join(line.strip() for line in text.split("\n"))

        return text

    @staticmethod
    def _parse_importance(value: Any) -> str:
        return value.value if getattr(value, "value", None) else ""

    @staticmethod
    def _parse_flag(flag: Any) -> dict[str, str]:
        if not flag:
            return {"flag_status": "", "due_date_time": ""}
        status = flag.flag_status.value if getattr(flag, "flag_status", None) else ""
        due = ""
        if getattr(flag, "due_date_time", None) and getattr(flag.due_date_time, "date_time", None):
            due = Message._safe_str(flag.due_date_time.date_time)
        return {"flag_status": status, "due_date_time": due}

    @classmethod
    def from_sdk(cls, msg: GraphMessage) -> "Message":
        """Convert a Microsoft Graph SDK Message object to a Message dataclass."""
        text = cls._parse_body(msg.body.content if msg.body and msg.body.content else "")
        return cls(
            bcc_recipients=[
                Recipient.from_sdk(recipient) for recipient in msg.bcc_recipients or []
            ],
            cc_recipients=[Recipient.from_sdk(recipient) for recipient in msg.cc_recipients or []],
            reply_to=[Recipient.from_sdk(recipient) for recipient in msg.reply_to or []],
            to_recipients=[Recipient.from_sdk(recipient) for recipient in msg.to_recipients or []],
            from_=Recipient.from_sdk(msg.from_) if msg.from_ else Recipient(),
            subject=cls._safe_str(msg.subject),
            body=text,
            conversation_id=cls._safe_str(msg.conversation_id),
            conversation_index=(
                msg.conversation_index.decode("utf-8", errors="ignore")
                if isinstance(msg.conversation_index, bytes | bytearray)
                else cls._safe_str(msg.conversation_index)
            ),
            flag=cls._parse_flag(msg.flag),
            has_attachments=cls._safe_bool(msg.has_attachments),
            importance=cls._parse_importance(msg.importance),
            is_read=cls._safe_bool(msg.is_read),
            received_date_time=(
                msg.received_date_time.isoformat() if msg.received_date_time else ""
            ),
            web_link=cls._safe_str(msg.web_link),
            is_draft=cls._safe_bool(msg.is_draft),
            message_id=cls._safe_str(msg.id),
        )

    def to_sdk(self) -> GraphMessage:
        """Converts the Message dataclass to a Microsoft Graph SDK Message object."""
        sdk_msg = GraphMessage()
        sdk_msg.subject = self.subject
        body_obj = ItemBody()
        body_obj.content = self.body
        body_obj.content_type = BodyType.Text
        sdk_msg.body = body_obj
        sdk_msg.is_draft = self.is_draft
        sdk_msg.to_recipients = [r.to_sdk() for r in self.to_recipients]
        sdk_msg.cc_recipients = [r.to_sdk() for r in self.cc_recipients]
        sdk_msg.bcc_recipients = [r.to_sdk() for r in self.bcc_recipients]
        sdk_msg.reply_to = [r.to_sdk() for r in self.reply_to]

        return sdk_msg

    def to_dict(self) -> dict[str, Any]:
        """Converts the Message dataclass to a dictionary."""
        return {
            "bcc_recipients": [recipient.to_dict() for recipient in self.bcc_recipients],
            "cc_recipients": [recipient.to_dict() for recipient in self.cc_recipients],
            "reply_to": [recipient.to_dict() for recipient in self.reply_to],
            "to_recipients": [recipient.to_dict() for recipient in self.to_recipients],
            "from": self.from_.to_dict(),
            "subject": self.subject,
            "body": self.body,
            "conversation_id": self.conversation_id,
            "conversation_index": self.conversation_index,
            "flag": self.flag,
            "has_attachments": self.has_attachments,
            "importance": self.importance,
            "is_read": self.is_read,
            "received_date_time": self.received_date_time,
            "web_link": self.web_link,
            "is_draft": self.is_draft,
            "message_id": self.message_id,
        }

    def update_recipient_lists(
        self,
        to_add: list[str] | None = None,
        to_remove: list[str] | None = None,
        cc_add: list[str] | None = None,
        cc_remove: list[str] | None = None,
        bcc_add: list[str] | None = None,
        bcc_remove: list[str] | None = None,
    ) -> None:
        """Update each recipient list of the message.

        This function updates the recipient lists of the message by first adding new recipients
        and then removing existing recipients. Therefore, if an email address is both
        added and removed, then it will not be included in the returned list.
        """
        for attr, add_emails_input, remove_emails_input in (
            ("to_recipients", to_add, to_remove),
            ("cc_recipients", cc_add, cc_remove),
            ("bcc_recipients", bcc_add, bcc_remove),
        ):
            current_recipients = getattr(self, attr) or []
            # Add recipients
            existing_emails = {r.email_address.lower() for r in current_recipients}
            new_additions = [
                Recipient(email_address=email)
                for email in (add_emails_input or [])
                if email.lower() not in existing_emails
            ]
            # Remove recipients
            updated_list = current_recipients + new_additions
            remove_emails = {email.lower() for email in (remove_emails_input or [])}
            updated_list = [
                recipient
                for recipient in updated_list
                if recipient.email_address.lower() not in remove_emails
            ]
            # Update the message's attribute with the new list
            setattr(self, attr, updated_list)
