from __future__ import annotations

import asyncio
import base64
import json
import logging
import os
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from email.mime.text import MIMEText
from email.utils import formatdate, parseaddr, parsedate_to_datetime
from typing import Any, Dict, List, Optional, Tuple

from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from .. import settings
from ..database import session_context
from ..models import (
    MailboxIntegration,
    SupportTicket,
    SupportTicketNote,
    TicketNoteDirection,
)
from .admin_alerts import notify_support_inbound_email_received
from .secret_storage import get_saas_secret_backend

log = logging.getLogger(__name__)

GMAIL_SCOPES = [
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.modify",
]

SUPPORT_SECRET_INTEGRATION = "gmail_support"
SUPPORT_SECRET_KEY = "credentials_json"
SUPPORT_SECRET_WORKSPACE_ENV = "GMAIL_SUPPORT_SECRET_WORKSPACE_ID"


class GmailHistoryTooOldError(RuntimeError):
    pass


def is_from_support_mailbox(from_email: Optional[str], support_email: str) -> bool:
    if not from_email:
        return False
    return from_email.strip().lower() == support_email.strip().lower()


def extract_ticket_id_from_subject(subject: str) -> Optional[str]:
    if not subject:
        return None
    match = re.search(r"\bSUP-[A-Z0-9]{4}-\d{2}\b", subject.upper())
    if not match:
        return None
    return match.group(0)


@dataclass
class GmailMessageSummary:
    gmail_message_id: str
    gmail_thread_id: Optional[str]
    rfc822_message_id: Optional[str]
    from_email: Optional[str]
    to_email: Optional[str]
    subject: Optional[str]
    body: str
    date: Optional[datetime]


class GmailSupportService:
    def __init__(self, credentials_json: str, support_email: str) -> None:
        info = json.loads(credentials_json)
        self.support_email = support_email
        self._credentials = Credentials(
            token=info.get("token"),
            refresh_token=info.get("refresh_token"),
            token_uri=info.get("token_uri", "https://oauth2.googleapis.com/token"),
            client_id=info.get("client_id"),
            client_secret=info.get("client_secret"),
            scopes=info.get("scopes", GMAIL_SCOPES),
        )
        self._service = build("gmail", "v1", credentials=self._credentials, cache_discovery=False)

    @classmethod
    async def from_secret(cls, db: AsyncSession) -> "GmailSupportService":
        credentials_json = await _load_gmail_credentials(db)
        if not credentials_json:
            raise RuntimeError("Gmail credentials not configured")
        support_email = os.getenv("GMAIL_SUPPORT_EMAIL") or settings.SUPPORT_EMAIL
        return cls(credentials_json, support_email)

    async def send_new_ticket_email(
        self,
        to_email: str,
        ticket_public_id: str,
        subject: str,
        body: str,
    ) -> Dict[str, Optional[str]]:
        message = self._build_message(to_email, subject, body)
        request = self._service.users().messages().send(userId="me", body=message)
        response = await self._execute(request)
        message_id = response.get("id")
        thread_id = response.get("threadId")
        rfc822_id = None
        if message_id:
            fetched = await self.get_message(message_id)
            rfc822_id = fetched.rfc822_message_id
            thread_id = thread_id or fetched.gmail_thread_id
        return {
            "gmail_message_id": message_id,
            "gmail_thread_id": thread_id,
            "rfc822_message_id": rfc822_id,
        }

    async def send_reply(
        self,
        to_email: str,
        ticket_public_id: str,
        subject: str,
        body: str,
        gmail_thread_id: str,
        in_reply_to: Optional[str] = None,
        references: Optional[List[str]] = None,
    ) -> Dict[str, Optional[str]]:
        message = self._build_message(
            to_email,
            subject,
            body,
            in_reply_to=in_reply_to,
            references=references,
        )
        payload = {"raw": message["raw"], "threadId": gmail_thread_id}
        request = self._service.users().messages().send(userId="me", body=payload)
        response = await self._execute(request)
        message_id = response.get("id")
        thread_id = response.get("threadId") or gmail_thread_id
        rfc822_id = None
        if message_id:
            fetched = await self.get_message(message_id)
            rfc822_id = fetched.rfc822_message_id
            thread_id = thread_id or fetched.gmail_thread_id
        return {
            "gmail_message_id": message_id,
            "gmail_thread_id": thread_id,
            "rfc822_message_id": rfc822_id,
        }

    async def fetch_history_delta(
        self, start_history_id: str
    ) -> Tuple[List[Dict[str, Optional[str]]], str]:
        items: List[Dict[str, Optional[str]]] = []
        next_page_token = None
        latest_history_id = start_history_id

        while True:
            request = self._service.users().history().list(
                userId="me",
                startHistoryId=str(start_history_id),
                historyTypes=["messageAdded"],
                pageToken=next_page_token,
            )
            try:
                response = await self._execute(request)
            except HttpError as exc:
                # Gmail returns 404 or 410 when historyId is too old
                if exc.resp is not None and exc.resp.status in (404, 410):
                    raise GmailHistoryTooOldError("historyId too old") from exc
                raise

            history = response.get("history", []) or []
            latest_history_id = response.get("historyId", latest_history_id)

            for entry in history:
                for added in entry.get("messagesAdded", []) or []:
                    msg = added.get("message", {})
                    msg_id = msg.get("id")
                    if not msg_id:
                        continue
                    items.append(
                        {
                            "gmail_message_id": msg_id,
                            "gmail_thread_id": msg.get("threadId"),
                        }
                    )

            next_page_token = response.get("nextPageToken")
            if not next_page_token:
                break

        # Deduplicate message ids
        deduped: Dict[str, Dict[str, Optional[str]]] = {}
        for item in items:
            msg_id = item.get("gmail_message_id")
            if not msg_id:
                continue
            deduped[msg_id] = item

        return list(deduped.values()), str(latest_history_id)

    async def get_message(self, message_id: str) -> GmailMessageSummary:
        request = self._service.users().messages().get(
            userId="me", id=message_id, format="full"
        )
        response = await self._execute(request)
        payload = response.get("payload") or {}
        headers = payload.get("headers") or []
        header_map = {h.get("name", "").lower(): h.get("value", "") for h in headers}

        from_email = parseaddr(header_map.get("from", ""))[1] or None
        to_email = parseaddr(header_map.get("to", ""))[1] or None
        subject = header_map.get("subject")
        rfc822_message_id = header_map.get("message-id") or None

        date_value = header_map.get("date")
        parsed_date = None
        if date_value:
            try:
                parsed_date = parsedate_to_datetime(date_value)
                if parsed_date and parsed_date.tzinfo is None:
                    parsed_date = parsed_date.replace(tzinfo=timezone.utc)
            except Exception:
                parsed_date = None

        body = _extract_body(payload)

        return GmailMessageSummary(
            gmail_message_id=response.get("id") or message_id,
            gmail_thread_id=response.get("threadId"),
            rfc822_message_id=rfc822_message_id,
            from_email=from_email,
            to_email=to_email,
            subject=subject,
            body=body,
            date=parsed_date,
        )

    async def mark_as_read(self, message_id: str) -> None:
        request = self._service.users().messages().modify(
            userId="me",
            id=message_id,
            body={"removeLabelIds": ["UNREAD"]},
        )
        await self._execute(request)

    async def register_watch(
        self, topic_name: str, label_ids: Optional[List[str]] = None
    ) -> Dict[str, Any]:
        request = self._service.users().watch(
            userId="me",
            body={
                "topicName": topic_name,
                "labelIds": label_ids or ["INBOX"],
                "labelFilterAction": "include",
            },
        )
        return await self._execute(request)

    async def _execute(self, request: Any) -> Dict[str, Any]:
        def _run():
            if self._credentials and self._credentials.expired and self._credentials.refresh_token:
                self._credentials.refresh(Request())
            return request.execute()

        return await asyncio.to_thread(_run)

    def _build_message(
        self,
        to_email: str,
        subject: str,
        body: str,
        in_reply_to: Optional[str] = None,
        references: Optional[List[str]] = None,
    ) -> Dict[str, str]:
        message = MIMEText(body or "", "plain", "utf-8")
        message["To"] = to_email
        message["From"] = self.support_email
        message["Subject"] = subject
        message["Date"] = formatdate(localtime=True)

        if in_reply_to:
            message["In-Reply-To"] = _normalize_message_id(in_reply_to)
        if references:
            refs = [_normalize_message_id(r) for r in references if r]
            if refs:
                message["References"] = " ".join(refs)

        raw_bytes = message.as_bytes()
        raw = base64.urlsafe_b64encode(raw_bytes).decode("utf-8")
        return {"raw": raw}


async def _load_gmail_credentials(db: AsyncSession) -> Optional[str]:
    workspace_id = os.getenv(SUPPORT_SECRET_WORKSPACE_ENV)
    backend = get_saas_secret_backend()
    if workspace_id:
        try:
            secret = await backend.get_secret(
                db, SUPPORT_SECRET_INTEGRATION, SUPPORT_SECRET_KEY, workspace_id
            )
            if secret and secret.value:
                return secret.value
        except Exception as exc:
            log.warning("Failed to load Gmail credentials from secret storage: %s", exc)

    return os.getenv("GMAIL_CREDENTIALS_JSON")


async def ensure_mailbox_integration(
    db: AsyncSession, email: str
) -> MailboxIntegration:
    stmt = select(MailboxIntegration).where(MailboxIntegration.email == email)
    mailbox = (await db.execute(stmt)).scalar_one_or_none()
    if mailbox:
        return mailbox
    mailbox = MailboxIntegration(email=email)
    db.add(mailbox)
    await db.commit()
    await db.refresh(mailbox)
    return mailbox


async def process_gmail_history_delta(
    mailbox_id: str, history_id_hint: Optional[str] = None
) -> Dict[str, Any]:
    processed = 0
    matched = 0
    skipped = 0
    duplicates = 0

    async with session_context() as db:
        mailbox = await db.get(MailboxIntegration, mailbox_id)
        if not mailbox:
            log.warning("Mailbox integration not found: %s", mailbox_id)
            return {
                "processed": 0,
                "matched": 0,
                "skipped": 0,
                "duplicates": 0,
            }

        if not mailbox.last_history_id:
            if history_id_hint:
                mailbox.last_history_id = str(history_id_hint)
                db.add(mailbox)
                await db.commit()
            return {
                "processed": 0,
                "matched": 0,
                "skipped": 0,
                "duplicates": 0,
                "initialized": True,
            }

        gmail = await GmailSupportService.from_secret(db)

        try:
            items, new_history_id = await gmail.fetch_history_delta(
                mailbox.last_history_id
            )
        except GmailHistoryTooOldError:
            log.warning("History too old; resetting to latest hint")
            if history_id_hint:
                mailbox.last_history_id = str(history_id_hint)
                db.add(mailbox)
                await db.commit()
            return {
                "processed": 0,
                "matched": 0,
                "skipped": 0,
                "duplicates": 0,
                "reset": True,
            }

        for item in items:
            msg_id = item.get("gmail_message_id")
            if not msg_id:
                continue

            existing = await db.execute(
                select(SupportTicketNote.id).where(
                    SupportTicketNote.gmail_message_id == msg_id
                )
            )
            if existing.scalar_one_or_none():
                duplicates += 1
                continue

            try:
                message = await gmail.get_message(msg_id)
            except HttpError as exc:
                # Message was deleted/trashed/spam before we could fetch it
                if exc.resp is not None and exc.resp.status == 404:
                    log.debug("Message %s not found (deleted/spam), skipping", msg_id)
                    skipped += 1
                    continue
                raise

            if is_from_support_mailbox(message.from_email, gmail.support_email):
                skipped += 1
                continue

            ticket = None
            if message.gmail_thread_id:
                result = await db.execute(
                    select(SupportTicket).where(
                        SupportTicket.gmail_thread_id == message.gmail_thread_id
                    )
                )
                ticket = result.scalar_one_or_none()

            if not ticket and message.subject:
                public_id = extract_ticket_id_from_subject(message.subject)
                if public_id:
                    result = await db.execute(
                        select(SupportTicket).where(SupportTicket.public_id == public_id)
                    )
                    ticket = result.scalar_one_or_none()

            if not ticket:
                try:
                    await notify_support_inbound_email_received(
                        from_email=message.from_email,
                        to_email=message.to_email,
                        subject_text=message.subject,
                        body_snippet=message.body,
                        ticket_public_id=None,
                        gmail_thread_id=message.gmail_thread_id,
                        gmail_message_id=message.gmail_message_id,
                        received_at=message.date,
                    )
                except Exception as exc:
                    log.warning(
                        "Failed to send unmatched support email alert for %s: %s",
                        msg_id,
                        exc,
                    )
                skipped += 1
                continue

            note = SupportTicketNote(
                ticket_id=ticket.id,
                author_email=message.from_email,
                content=message.body or "",
                is_internal=False,
                direction=TicketNoteDirection.INBOUND,
                gmail_message_id=message.gmail_message_id,
                rfc822_message_id=message.rfc822_message_id,
                gmail_thread_id=message.gmail_thread_id,
            )
            db.add(note)

            if message.gmail_thread_id and not ticket.gmail_thread_id:
                ticket.gmail_thread_id = message.gmail_thread_id
                db.add(ticket)

            await db.commit()
            await gmail.mark_as_read(msg_id)

            try:
                await notify_support_inbound_email_received(
                    from_email=message.from_email,
                    to_email=message.to_email,
                    subject_text=message.subject,
                    body_snippet=message.body,
                    ticket_public_id=ticket.public_id,
                    gmail_thread_id=message.gmail_thread_id,
                    gmail_message_id=message.gmail_message_id,
                    received_at=message.date,
                )
            except Exception as exc:
                log.warning(
                    "Failed to send support inbound alert for ticket %s: %s",
                    ticket.public_id,
                    exc,
                )

            processed += 1
            matched += 1

        mailbox.last_history_id = str(new_history_id)
        mailbox.updated_at = datetime.now(timezone.utc)
        db.add(mailbox)
        await db.commit()

    return {
        "processed": processed,
        "matched": matched,
        "skipped": skipped,
        "duplicates": duplicates,
    }


def _normalize_message_id(value: str) -> str:
    normalized = value.strip()
    if not normalized:
        return normalized
    if normalized.startswith("<") and normalized.endswith(">"):
        return normalized
    return f"<{normalized}>"


def _extract_body(payload: Dict[str, Any]) -> str:
    if not payload:
        return ""

    if payload.get("parts"):
        for part in payload.get("parts", []):
            mime_type = part.get("mimeType", "")
            if mime_type == "text/plain":
                return _decode_part_body(part)
        for part in payload.get("parts", []):
            mime_type = part.get("mimeType", "")
            if mime_type == "text/html":
                return _strip_html(_decode_part_body(part))
        # Fallback: try first part
        return _decode_part_body(payload.get("parts", [{}])[0])

    return _decode_part_body(payload)


def _decode_part_body(part: Dict[str, Any]) -> str:
    body = part.get("body") or {}
    data = body.get("data")
    if not data:
        return ""
    try:
        decoded = base64.urlsafe_b64decode(data.encode("utf-8")).decode(
            "utf-8", errors="replace"
        )
        return decoded.strip()
    except Exception:
        return ""


def _strip_html(value: str) -> str:
    if not value:
        return ""
    # Minimal HTML strip for emails (avoid heavy deps)
    text = re.sub(r"<[^>]+>", " ", value)
    return re.sub(r"\s+", " ", text).strip()
