"""
Gmail API client module.

Handles OAuth 2.0 authentication (with automatic token refresh) and
provides high-level methods for searching, reading, and threading emails.
"""

from __future__ import annotations

import base64
import re
from datetime import datetime
from email.message import EmailMessage
from email.utils import parsedate_to_datetime
from pathlib import Path
from typing import Any

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build, Resource

from email_assistant.config import get_settings

# ---------------------------------------------------------------------------
# Gmail API scopes
# ---------------------------------------------------------------------------
SCOPES: list[str] = [
    "https://www.googleapis.com/auth/gmail.readonly",
]
SEND_SCOPE = "https://www.googleapis.com/auth/gmail.send"


class GmailClient:
    """Wrapper around the Gmail API with auto-refreshing OAuth tokens."""

    def __init__(
        self,
        credentials_path: Path | None = None,
        token_path: Path | None = None,
    ) -> None:
        settings = get_settings()
        self._credentials_path = credentials_path or settings.gmail_credentials_path
        self._token_path = token_path or settings.gmail_token_path
        self._service: Resource | None = None
        self._creds: Credentials | None = None

    # ------------------------------------------------------------------
    # Authentication
    # ------------------------------------------------------------------
    def _authenticate(self) -> Credentials:
        """Load or create OAuth credentials, refreshing as needed."""
        creds: Credentials | None = None

        if self._token_path.exists():
            creds = Credentials.from_authorized_user_file(
                str(self._token_path), SCOPES
            )
            if not creds.has_scopes(SCOPES):
                creds = None

        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                try:
                    creds.refresh(Request())
                except Exception:
                    creds = None
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    str(self._credentials_path), SCOPES
                )
                creds = flow.run_local_server(port=0)
            if creds is None:
                flow = InstalledAppFlow.from_client_secrets_file(
                    str(self._credentials_path), SCOPES
                )
                creds = flow.run_local_server(port=0)
            # Persist for next run
            self._token_path.parent.mkdir(parents=True, exist_ok=True)
            self._token_path.write_text(creds.to_json())

        return creds

    @property
    def service(self) -> Resource:
        """Lazily initialised Gmail API service."""
        if self._service is None:
            creds = self._authenticate()
            self._creds = creds
            self._service = build("gmail", "v1", credentials=creds)
        return self._service

    # ------------------------------------------------------------------
    # Public helpers
    # ------------------------------------------------------------------
    def search_messages(
        self,
        query: str,
        max_results: int | None = None,
    ) -> list[dict[str, Any]]:
        """Search Gmail and return lightweight metadata for each hit.

        Args:
            query: Gmail search query (e.g. ``from:amazon newer_than:7d``).
            max_results: Cap on the number of results. Defaults to
                ``settings.max_results``.

        Returns:
            List of dicts with keys: ``message_id``, ``thread_id``,
            ``sender``, ``subject``, ``date``, ``snippet``,
            ``has_attachments``, ``attachment_names``.
        """
        max_results = max_results or get_settings().max_results
        results: list[dict[str, Any]] = []
        page_token: str | None = None

        while len(results) < max_results:
            batch_size = min(max_results - len(results), 100)
            response = (
                self.service.users()
                .messages()
                .list(
                    userId="me",
                    q=query,
                    maxResults=batch_size,
                    pageToken=page_token,
                )
                .execute()
            )

            messages = response.get("messages", [])
            if not messages:
                break

            for msg_stub in messages:
                meta = self.get_message_metadata(msg_stub["id"])
                if meta:
                    results.append(meta)
                if len(results) >= max_results:
                    break

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

        return results

    def get_message_metadata(self, message_id: str) -> dict[str, Any] | None:
        """Fetch a single message's metadata (headers + snippet).

        Returns:
            Dict with parsed header fields, or ``None`` on error.
        """
        try:
            msg = (
                self.service.users()
                .messages()
                .get(userId="me", id=message_id, format="metadata")
                .execute()
            )
            return self._parse_message(msg)
        except Exception as exc:  # noqa: BLE001
            print(f"[warn] Failed to fetch message {message_id}: {exc}")
            return None

    def get_message_full(self, message_id: str) -> dict[str, Any] | None:
        """Fetch a message with its full body content.

        Returns:
            Dict with metadata **plus** a ``body`` key containing the
            plain-text (preferred) or HTML body.
        """
        try:
            msg = (
                self.service.users()
                .messages()
                .get(userId="me", id=message_id, format="full")
                .execute()
            )
            parsed = self._parse_message(msg)
            parsed["body"] = self._extract_body(msg.get("payload", {}))
            return parsed
        except Exception as exc:  # noqa: BLE001
            print(f"[warn] Failed to fetch full message {message_id}: {exc}")
            return None

    def get_thread(self, thread_id: str) -> list[dict[str, Any]]:
        """Fetch all messages in a thread.

        Returns:
            List of parsed message dicts, ordered chronologically.
        """
        try:
            thread = (
                self.service.users()
                .threads()
                .get(userId="me", id=thread_id, format="full")
                .execute()
            )
            messages = []
            for msg in thread.get("messages", []):
                parsed = self._parse_message(msg)
                parsed["body"] = self._extract_body(msg.get("payload", {}))
                messages.append(parsed)
            return messages
        except Exception as exc:  # noqa: BLE001
            print(f"[warn] Failed to fetch thread {thread_id}: {exc}")
            return []

    def send_message(
        self,
        to: str,
        subject: str,
        body: str,
        cc: str = "",
        bcc: str = "",
    ) -> dict[str, Any]:
        """Send an email from the authenticated Gmail account."""
        if not self._has_send_scope():
            raise RuntimeError(
                "Current Gmail token does not include send permission. "
                "Read operations still work. To enable sending, delete token.json, "
                "reauthenticate, and grant send access."
            )

        message = EmailMessage()
        message["To"] = to
        message["Subject"] = subject
        if cc:
            message["Cc"] = cc
        if bcc:
            message["Bcc"] = bcc
        message.set_content(body)

        encoded_message = base64.urlsafe_b64encode(
            message.as_bytes()
        ).decode("utf-8")
        sent = (
            self.service.users()
            .messages()
            .send(userId="me", body={"raw": encoded_message})
            .execute()
        )
        return {
            "status": "sent",
            "message_id": sent.get("id", ""),
            "thread_id": sent.get("threadId", ""),
            "to": to,
            "cc": cc,
            "bcc": bcc,
            "subject": subject,
        }

    def _has_send_scope(self) -> bool:
        """Return True if current credentials include Gmail send scope."""
        _ = self.service
        if self._creds is None:
            return False
        return self._creds.has_scopes([SEND_SCOPE])

    # ------------------------------------------------------------------
    # Internal parsing helpers
    # ------------------------------------------------------------------
    @staticmethod
    def _header(headers: list[dict], name: str) -> str:
        """Extract a header value by name (case-insensitive)."""
        for h in headers:
            if h["name"].lower() == name.lower():
                return h["value"]
        return ""

    def _parse_message(self, msg: dict[str, Any]) -> dict[str, Any]:
        """Parse a raw Gmail API message into a clean dict."""
        headers = msg.get("payload", {}).get("headers", [])
        date_str = self._header(headers, "Date")

        try:
            date_obj = parsedate_to_datetime(date_str)
            formatted_date = date_obj.strftime("%Y-%m-%d %H:%M")
        except Exception:
            formatted_date = date_str

        attachment_names = self._extract_attachment_names(
            msg.get("payload", {})
        )

        return {
            "message_id": msg["id"],
            "thread_id": msg.get("threadId", ""),
            "sender": self._header(headers, "From"),
            "to": self._header(headers, "To"),
            "subject": self._header(headers, "Subject"),
            "date": formatted_date,
            "snippet": msg.get("snippet", ""),
            "has_attachments": len(attachment_names) > 0,
            "attachment_names": attachment_names,
            "labels": msg.get("labelIds", []),
        }

    def _extract_attachment_names(self, payload: dict) -> list[str]:
        """Recursively collect attachment filenames from a message payload."""
        names: list[str] = []
        filename = payload.get("filename", "")
        if filename:
            names.append(filename)
        for part in payload.get("parts", []):
            names.extend(self._extract_attachment_names(part))
        return names

    def _extract_body(self, payload: dict) -> str:
        """Return plain-text body (preferred) or stripped HTML."""
        # Single-part message
        if payload.get("mimeType") == "text/plain":
            data = payload.get("body", {}).get("data", "")
            return self._decode_base64(data)

        # Multipart — look for text/plain first, fall back to text/html
        plain = ""
        html = ""
        for part in payload.get("parts", []):
            mime = part.get("mimeType", "")
            if mime == "text/plain":
                plain = self._decode_base64(
                    part.get("body", {}).get("data", "")
                )
            elif mime == "text/html" and not html:
                html = self._decode_base64(
                    part.get("body", {}).get("data", "")
                )
            # Recurse into nested multipart
            if part.get("parts"):
                nested = self._extract_body(part)
                if nested:
                    plain = plain or nested

        if plain:
            return plain
        if html:
            return self._strip_html(html)
        return ""

    @staticmethod
    def _decode_base64(data: str) -> str:
        """Decode URL-safe base64 content."""
        if not data:
            return ""
        try:
            return base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
        except Exception:
            return ""

    @staticmethod
    def _strip_html(html: str) -> str:
        """Naïve HTML tag stripper for fallback body extraction."""
        clean = re.sub(r"<[^>]+>", " ", html)
        clean = re.sub(r"\s+", " ", clean).strip()
        return clean[:5000]  # Cap length for LLM context
