"""Microsoft Exchange retrieval handler via Graph API.

Fetches emails from Microsoft Exchange Online using the OAuth2 client
credentials flow. Emails are returned as complete MIME (.eml) files,
base64-encoded.

Required settings attributes:
    TENANT_NAME, ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET
"""

import base64
import email
import logging
import re
from datetime import datetime, timedelta
from email.header import decode_header
from typing import List, Optional

import httpx

from entr_adapter_core.handlers import RetrievalHandler
from entr_adapter_core.schemas import (
    DocumentDownloadResponse,
    DocumentIdentifier,
    RetrievalPayload,
    RetrievalResponse,
    StatusEnum,
)

logger = logging.getLogger(__name__)


class MicrosoftEntraRetrievalHandler(RetrievalHandler):
    """Retrieval handler for Microsoft Exchange via Graph API.

    Uses OAuth2 client credentials flow for authentication.

    Args:
        settings: Must expose TENANT_NAME, ENTRA_TENANT_ID,
            ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET.
        mailbox: Email address of the mailbox to read from.
        folder_path: List of folder names to traverse (default: ["Inbox"]).
    """

    def __init__(self, settings, mailbox: str, folder_path: Optional[list[str]] = None):
        super().__init__(settings)

        self.mailbox = mailbox

        if not all([
            self.settings.ENTRA_TENANT_ID,
            self.settings.ENTRA_CLIENT_ID,
            self.settings.ENTRA_CLIENT_SECRET,
            self.mailbox,
        ]):
            raise ValueError(
                "ENTRA_TENANT_ID, ENTRA_CLIENT_ID, ENTRA_CLIENT_SECRET must be configured, "
                "and mailbox must be provided."
            )

        self.folder_path = folder_path if folder_path else ["Inbox"]

        logger.info("Initialized MicrosoftEntraRetrievalHandler for tenant: %s", self.settings.TENANT_NAME)
        logger.info("Entra Mailbox: %s", self.mailbox)
        logger.info("Target folder path: %s", " / ".join(self.folder_path))

        self._access_token = None
        self._token_expiry = None
        self._target_folder_id = None

    async def _get_access_token(self) -> str:
        """Acquire an OAuth2 access token from Microsoft Entra."""
        if self._access_token and self._token_expiry and datetime.now() < self._token_expiry:
            return self._access_token

        logger.info("Acquiring new access token from Microsoft Entra")

        token_url = (
            f"https://login.microsoftonline.com/{self.settings.ENTRA_TENANT_ID}/oauth2/v2.0/token"
        )
        payload = {
            "grant_type": "client_credentials",
            "client_id": self.settings.ENTRA_CLIENT_ID,
            "client_secret": self.settings.ENTRA_CLIENT_SECRET,
            "scope": "https://graph.microsoft.com/.default",
        }

        try:
            async with httpx.AsyncClient() as client:
                response = await client.post(token_url, data=payload)
                response.raise_for_status()

                token_data = response.json()
                self._access_token = token_data["access_token"]

                # Cache token with expiry (subtract 5 minutes for safety)
                expires_in = token_data.get("expires_in", 3600)
                self._token_expiry = datetime.now() + timedelta(seconds=expires_in - 300)

                logger.info("Successfully acquired access token")
                return self._access_token

        except httpx.HTTPStatusError as e:
            logger.error("Failed to acquire access token: %s", e)
            logger.error("Response: %s", e.response.text)
            raise
        except httpx.RequestError as e:
            logger.error("Failed to acquire access token: %s", e)
            raise

    async def _find_target_folder(self, access_token: str) -> Optional[str]:
        """Traverse the folder path to find the target folder ID."""
        if self._target_folder_id:
            return self._target_folder_id

        logger.info("Searching for folder path: %s", " / ".join(self.folder_path))

        headers = {"Authorization": f"Bearer {access_token}"}
        url = f"https://graph.microsoft.com/v1.0/users/{self.mailbox}/mailFolders"
        parent_folder_id = None

        async with httpx.AsyncClient() as client:
            for i, folder_name in enumerate(self.folder_path):
                logger.info("  -> Searching for '%s'...", folder_name)

                if i > 0 and parent_folder_id:
                    url = (
                        f"https://graph.microsoft.com/v1.0/users/{self.mailbox}"
                        f"/mailFolders/{parent_folder_id}/childFolders"
                    )

                try:
                    response = await client.get(url, headers=headers)
                    response.raise_for_status()
                    folders = response.json().get("value", [])

                    # Find the folder (case-insensitive)
                    found_folder = None
                    for folder in folders:
                        if folder.get("displayName", "").lower() == folder_name.lower():
                            found_folder = folder
                            break

                    if not found_folder:
                        logger.error("Could not find folder '%s' in path", folder_name)
                        logger.error(
                            "Available folders: %s",
                            [f.get("displayName") for f in folders],
                        )
                        return None

                    parent_folder_id = found_folder["id"]
                    logger.info("  Found '%s' with ID: %s", found_folder["displayName"], parent_folder_id)

                except httpx.HTTPStatusError as e:
                    logger.error("Error searching for folder: %s", e)
                    logger.error("Response: %s", e.response.text)
                    return None
                except httpx.RequestError as e:
                    logger.error("Error searching for folder: %s", e)
                    return None

        self._target_folder_id = parent_folder_id
        logger.info("Successfully found target folder with ID: %s", self._target_folder_id)
        return self._target_folder_id

    def _generate_filename_from_message(self, message: dict) -> str:
        """Generate a filename from a Graph API message object using the subject."""
        try:
            subject = message.get("subject", "")
            if subject:
                subject_clean = re.sub(r'[<>:"/\\|?*]', '_', subject).strip()
                return f"{subject_clean}.eml"
            else:
                message_id = message.get("id", "unknown")[:20]
                return f"{message_id}_email.eml"
        except Exception as e:
            logger.warning("Error generating filename: %s", e)
            return f"{message.get('id', 'unknown')[:20]}_email.eml"

    async def list_documents(self, payload: RetrievalPayload) -> RetrievalResponse:
        """List all emails in the target folder since the specified date."""
        logger.info(
            "MicrosoftEntraRetrievalHandler: list_documents called for source: %s",
            payload.source_identifier,
        )

        try:
            access_token = await self._get_access_token()

            folder_id = await self._find_target_folder(access_token)
            if not folder_id:
                return RetrievalResponse(
                    status=StatusEnum.FAILED,
                    message=f"Could not find folder: {' / '.join(self.folder_path)}",
                )

            date_str = payload.date_from.strftime("%Y-%m-%dT00:00:00Z")

            url = (
                f"https://graph.microsoft.com/v1.0/users/{self.mailbox}"
                f"/mailFolders/{folder_id}/messages"
                f"?$filter=receivedDateTime ge {date_str}"
                "&$select=id,subject,from,receivedDateTime"
                "&$orderby=receivedDateTime desc"
                "&$top=999"
            )

            headers = {"Authorization": f"Bearer {access_token}"}

            async with httpx.AsyncClient() as client:
                response = await client.get(url, headers=headers)
                response.raise_for_status()
                messages = response.json().get("value", [])

            logger.info("Found %d messages since %s", len(messages), date_str)

            listed_documents: List[DocumentIdentifier] = []
            for message in messages:
                message_id = message.get("id")
                if not message_id:
                    continue

                filename = self._generate_filename_from_message(message)
                listed_documents.append(
                    DocumentIdentifier(
                        document_identifier=message_id,
                        filename=filename,
                    )
                )

            logger.info("Successfully listed %d email documents", len(listed_documents))
            return RetrievalResponse(
                status=StatusEnum.SUCCESS,
                documents=listed_documents,
            )

        except Exception as e:
            logger.error("Error in list_documents: %s", e, exc_info=True)
            return RetrievalResponse(
                status=StatusEnum.FAILED,
                message=f"Error listing documents: {e}",
            )

    async def get_document(self, document_identifier: str) -> DocumentDownloadResponse:
        """Retrieve a specific email by its message ID and return as .eml file."""
        logger.info(
            "MicrosoftEntraRetrievalHandler: get_document called for ID: %s",
            document_identifier,
        )

        try:
            access_token = await self._get_access_token()

            url = (
                f"https://graph.microsoft.com/v1.0/users/{self.mailbox}"
                f"/messages/{document_identifier}/$value"
            )

            headers = {"Authorization": f"Bearer {access_token}"}

            async with httpx.AsyncClient() as client:
                response = await client.get(url, headers=headers)
                response.raise_for_status()
                email_data = response.content

            logger.info(
                "Successfully fetched email data for ID %s, size: %d bytes",
                document_identifier, len(email_data),
            )

            # Parse email to generate filename from subject
            msg = email.message_from_bytes(email_data)
            subject = msg.get("Subject", "")
            if subject:
                decoded_parts = []
                for bytes_part, charset in decode_header(subject):
                    if isinstance(bytes_part, bytes):
                        try:
                            decoded_parts.append(bytes_part.decode(charset or "utf-8", errors="replace"))
                        except Exception:
                            decoded_parts.append(bytes_part.decode("utf-8", errors="replace"))
                    else:
                        decoded_parts.append(str(bytes_part))
                subject = "".join(decoded_parts)

            if subject:
                subject_clean = re.sub(r'[<>:"/\\|?*]', "_", subject).strip()
                filename = f"{subject_clean}.eml"
            else:
                filename = f"{document_identifier[:20]}_email.eml"

            content_b64 = base64.b64encode(email_data).decode("utf-8")

            logger.info("Successfully retrieved email: %s as %s", document_identifier, filename)
            return DocumentDownloadResponse(
                document_identifier=filename,
                adapter_document_id=document_identifier,
                content_b64=content_b64,
            )

        except Exception as e:
            logger.error("Error in get_document for ID %s: %s", document_identifier, e, exc_info=True)
            raise
