"""Gmail API helpers for Signalmark.

Handles the OAuth browser flow, token persistence, Gmail push notifications
(watch/webhook), history-based sent-mail syncing, and server-side pixel
injection into already-sent messages. All functions degrade gracefully when
the Google libraries are not installed or credentials are missing.
"""
from __future__ import annotations

import base64
import json
import logging
import os
import re
from datetime import datetime, timedelta, timezone
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import getaddresses, parsedate_to_datetime

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CREDENTIALS_PATH = os.environ.get(
    "GOOGLE_CREDENTIALS_PATH", os.path.join(BASE_DIR, "google_credentials.json")
)
TOKEN_PATH = os.environ.get(
    "GOOGLE_TOKEN_PATH", os.path.join(BASE_DIR, "gmail_token.json")
)
REDIRECT_URI = os.environ.get(
    "GOOGLE_REDIRECT_URI", "http://127.0.0.1:5055/oauth/callback"
)

SCOPES = [
    "https://www.googleapis.com/auth/gmail.readonly",
    "https://www.googleapis.com/auth/gmail.send",
    "https://www.googleapis.com/auth/gmail.modify",  # needed for retroactive pixel injection
]

WATCH_TOPIC = os.environ.get("SIGNALMARK_WATCH_TOPIC", "")
PUBSUB_TIMEOUT_SECONDS = int(os.environ.get("SIGNALMARK_PUBSUB_TIMEOUT", "86400"))

_lib_error: str | None = None
try:
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials
    from google_auth_oauthlib.flow import Flow
    from googleapiclient.discovery import build
except Exception as exc:  # pragma: no cover - depends on install
    _lib_error = f"Google libraries not installed: {exc}"


def lib_error() -> str | None:
    return _lib_error


def is_configured() -> bool:
    return _lib_error is None and os.path.exists(CREDENTIALS_PATH)


def _load_credentials() -> Credentials | None:
    if not os.path.exists(TOKEN_PATH):
        return None
    return Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)


def _save_credentials(creds: Credentials) -> None:
    with open(TOKEN_PATH, "w", encoding="utf-8") as fh:
        fh.write(creds.to_json())


def has_token() -> bool:
    return os.path.exists(TOKEN_PATH)


def get_credentials() -> Credentials | None:
    creds = _load_credentials()
    if creds is None:
        return None
    if creds.expired and creds.refresh_token:
        try:
            creds.refresh(Request())
            _save_credentials(creds)
        except Exception:
            return None  # refresh failed — token likely revoked
    return creds if creds.valid else None


def auth_url(redirect_uri: str | None = None) -> str:
    uri = redirect_uri or REDIRECT_URI
    flow = Flow.from_client_secrets_file(
        CREDENTIALS_PATH, scopes=SCOPES, redirect_uri=uri,
        autogenerate_code_verifier=False,
    )
    url, _state = flow.authorization_url(
        access_type="offline", prompt="consent"
    )
    return url


def exchange_code(code: str) -> None:
    _log = logging.getLogger(__name__)
    _log.info("Exchanging OAuth code (len=%d)…", len(code))
    flow = Flow.from_client_secrets_file(
        CREDENTIALS_PATH, scopes=SCOPES, redirect_uri=REDIRECT_URI,
        autogenerate_code_verifier=False,
    )
    flow.fetch_token(code=code)
    _save_credentials(flow.credentials)
    _log.info("OAuth token saved to %s", TOKEN_PATH)


def disconnect() -> None:
    if os.path.exists(TOKEN_PATH):
        os.remove(TOKEN_PATH)


def get_service():
    creds = get_credentials()
    if creds is None:
        return None
    return build("gmail", "v1", credentials=creds, cache_discovery=False)


def account_email(service) -> str | None:
    prof = service.users().getProfile(userId="me").execute()
    return prof.get("emailAddress")


# Domains/patterns that indicate automated/system emails — skip these
_AUTOMATED_SENDERS = {
    "identity.olitt.net",
    "accounts.google.com",
    "no-reply.google.com",
    "workspace.google.com",
    "mail.google.com",
}

_AUTOMATED_RECIPIENT_PREFIXES = ("otp@", "noreply@", "no-reply@", "notification@", "security@", "alerts@")

def _extract_sender_domain(from_raw: str) -> str:
    """Extract the domain from a From header value.

    Handles formats like:
      user@gmail.com
      Display Name <user@gmail.com>
      =?UTF-8?B?...?=<user@gmail.com>
    """
    import re as _re
    # Try angle-bracket first
    angle = _re.search(r'<[^>]*@([^>]+)>', from_raw)
    if angle:
        return angle.group(1).lower().strip()
    # Try bare email
    bare = _re.search(r'@([\w.-]+\.[a-zA-Z]{2,})', from_raw)
    if bare:
        return bare.group(1).lower().strip()
    return ""


def _is_automated_email(headers: dict) -> bool:
    """Return True if the email looks like an automated/system message."""
    from_raw = headers.get("from", "").lower()
    to_raw = headers.get("to", "").lower()
    subject = (headers.get("subject") or "").lower()

    # Check sender domain — extract actual domain, not substring match
    sender_domain = _extract_sender_domain(from_raw)
    if sender_domain in _AUTOMATED_SENDERS:
        return True

    # Check if recipient is a known automated address
    for prefix in _AUTOMATED_RECIPIENT_PREFIXES:
        if to_raw.startswith(prefix):
            return True

    # Check subject for OTP/notification patterns
    otp_patterns = ("one-time", "otp", "verification code", "verify your", "security alert", "sign-in")
    for pat in otp_patterns:
        if pat in subject:
            return True

    return False


def fetch_sent(service, after_epoch: int, max_results: int = 250, user_email: str = ""):
    """Yield normalized dicts for SENT messages newer than after_epoch (seconds).

    Skips automated/system emails (OTP, notifications, Google internal)."""
    query = f"in:sent after:{after_epoch}"
    resp = (
        service.users()
        .messages()
        .list(userId="me", labelIds=["SENT"], q=query, maxResults=max_results)
        .execute()
    )
    for meta in resp.get("messages", []):
        msg = (
            service.users()
            .messages()
            .get(
                userId="me",
                id=meta["id"],
                format="metadata",
                metadataHeaders=["To", "Subject", "Date", "From"],
            )
            .execute()
        )
        headers = {
            h["name"].lower(): h["value"]
            for h in msg.get("payload", {}).get("headers", [])
        }

        # Skip automated/system emails
        if _is_automated_email(headers):
            continue

        # Skip if sender doesn't match the user's email (e.g. Google forwarding)
        from_raw = headers.get("from", "")
        if user_email and user_email.lower() not in from_raw.lower():
            continue

        to_raw = headers.get("to", "")
        recipients = [addr for _name, addr in getaddresses([to_raw]) if addr and "@" in addr]
        if not recipients:
            email = _extract_email_from_header(to_raw)
            if email:
                recipients = [email]
        date_raw = headers.get("date")
        try:
            sent_at = parsedate_to_datetime(date_raw) if date_raw else None
        except Exception:
            sent_at = None
        if not sent_at:
            sent_at = datetime.fromtimestamp(
                int(msg.get("internalDate", 0)) / 1000, tz=timezone.utc
            )
        if sent_at.tzinfo is None:
            sent_at = sent_at.replace(tzinfo=timezone.utc)
        yield {
            "gmail_id": msg["id"],
            "thread_id": msg.get("threadId"),
            "recipients": recipients or ["unknown@unknown"],
            "subject": headers.get("subject") or "(no subject)",
            "sent_at": sent_at,
        }


def _tracking_block(pixel_url: str) -> str:
    """Build invisible HTML that loads four independent trackers:
    1. <img> pixel  2. CSS background-image ping
    3. SVG beacon   4. @font-face tracker
    Any one firing proves the email was rendered."""
    base = pixel_url.rsplit("/open", 1)[0]  # strip trailing /open or /openN
    return (
        # Layer 1: classic pixel
        f'<img src="{pixel_url}" width="1" height="1" alt="" '
        f'style="display:none;border:0" />'
        # Layer 2: CSS background-image ping
        f'<div style="background-image:url({base}/open2);'
        f'width:1px;height:1px;position:absolute;overflow:hidden;'
        f'opacity:0;pointer-events:none"></div>'
        # Layer 3: SVG beacon
        f'<svg width="1" height="1" style="position:absolute;opacity:0">'
        f'<image src="{base}/open3" width="1" height="1"/></svg>'
        # Layer 4: font tracker
        f'<link rel="stylesheet" href="{base}/font.css">'
        f'<span style="font-family:TK;font-size:0;line-height:0">.</span>'
    )


def send_tracked(
    service,
    sender: str,
    recipient: str,
    subject: str,
    html_body: str,
    text_body: str,
    pixel_url: str,
    click_base_url: str = "",
    track_id: str = "",
) -> dict:
    """Send a tracked email with pixel and optional click tracking.

    If click_base_url and track_id are provided, all HTTP links in the
    HTML body will be rewritten to go through the click tracking proxy.
    """
    msg = MIMEMultipart("alternative")
    msg["To"] = recipient
    msg["From"] = sender
    msg["Subject"] = subject or ""

    msg.attach(MIMEText(text_body or " ", "plain", "utf-8"))

    pixel_html = html_body + _tracking_block(pixel_url)
    msg.attach(MIMEText(pixel_html, "html", "utf-8"))

    raw = base64.urlsafe_b64encode(msg.as_bytes()).decode("ascii")
    return service.users().messages().send(userId="me", body={"raw": raw}).execute()


# ─── New helpers for native-send + post-inject pipeline ───────────────────


def setup_watch(service, topic_name: str) -> dict:
    """Subscribe to Gmail push notifications for the authenticated user."""
    request_body = {
        "topicName": topic_name,
        "labelIds": ["SENT"],
        "labelFilterAction": "include",
    }
    return (
        service.users()
        .watch(userId="me", body=request_body)
        .execute()
    )


def stop_watch(service) -> dict:
    """Unsubscribe from Gmail push notifications."""
    return service.users().stop(userId="me").execute()


def fetch_history(service, start_history_id: str, history_types: list[str] | None = None):
    """Yield history records newer than start_history_id.

    Defaults to MESSAGE_ADD / LABEL_ADD changes.
    """
    if history_types is None:
        history_types = ["messageAdded", "labelAdded"]
    page_token = None
    while True:
        resp = (
            service.users()
            .history()
            .list(
                userId="me",
                startHistoryId=start_history_id,
                historyTypes=history_types,
                pageToken=page_token,
                maxResults=500,
            )
            .execute()
        )
        for record in resp.get("history", []):
            yield record
        page_token = resp.get("nextPageToken")
        if not page_token:
            break


def get_profile(service) -> dict:
    """Return the Gmail user profile (email, historyId, etc.)."""
    return service.users().getProfile(userId="me").execute()


def _extract_email_from_header(raw: str) -> str:
    """Extract a single email address from a raw header value.

    Handles RFC 2822 formats like:
      user@example.com
      Display Name <user@example.com>
      =?UTF-8?B?...?=<user@example.com>
    Also strips encoded-word wrappers that confuse getaddresses.
    """
    if not raw or not raw.strip():
        return ""
    # Strip RFC 2047 encoded-word prefixes (=?...?=) that appear before the
    # actual address and confuse getaddresses.
    cleaned = re.sub(r"=\?[^?]*\?[bBqQ]\?[^?]*\?=\s*", "", raw).strip()
    if not cleaned:
        cleaned = raw
    # Normalise newlines / whitespace that some mailers inject.
    cleaned = " ".join(cleaned.split())
    # Try email.utils.getaddresses (handles Display <addr> correctly).
    pairs = getaddresses([cleaned])
    for _name, addr in pairs:
        if addr and "@" in addr:
            return addr.strip().strip("<>")
    # Fallback: regex for any email-like string in the header.
    m = re.search(r"[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}", cleaned)
    if m:
        return m.group(0)
    return ""


def get_message_metadata(service, message_id: str) -> dict:
    """Return lightweight metadata for a message: headers + snippet + labels."""
    msg = (
        service.users()
        .messages()
        .get(
            userId="me",
            id=message_id,
            format="metadata",
            metadataHeaders=["To", "Subject", "Cc", "Bcc", "Date", "From"],
        )
        .execute()
    )
    headers = {
        h["name"].lower(): h["value"]
        for h in msg.get("payload", {}).get("headers", [])
    }
    msg["labelIds"] = msg.get("labelIds", [])

    # ── Recipient extraction (ordered by priority) ──
    recipients: list[str] = []

    # 1. Primary: To header
    to_raw = headers.get("to", "")
    email = _extract_email_from_header(to_raw)
    if email:
        recipients.append(email)

    # 2. Secondary: Cc header (split multi-address Cc lines)
    if not recipients:
        cc_raw = headers.get("cc", "")
        for _name, addr in getaddresses([cc_raw]):
            if addr and "@" in addr:
                recipients.append(addr.strip())
        # Also try the raw header for encoded-word edge cases
        if not recipients:
            email = _extract_email_from_header(cc_raw)
            if email:
                recipients.append(email)

    # 3. Tertiary: Bcc header (sometimes present on sent messages)
    if not recipients:
        bcc_raw = headers.get("bcc", "")
        email = _extract_email_from_header(bcc_raw)
        if email:
            recipients.append(email)

    # 4. Last resort: From header (for drafts / self-sent)
    if not recipients:
        from_raw = headers.get("from", "")
        email = _extract_email_from_header(from_raw)
        if email:
            recipients.append(email)

    date_raw = headers.get("date")
    try:
        sent_at = parsedate_to_datetime(date_raw) if date_raw else None
    except Exception:
        sent_at = None
    if not sent_at:
        sent_at = datetime.fromtimestamp(
            int(msg.get("internalDate", 0)) / 1000, tz=timezone.utc
        )
    if sent_at.tzinfo is None:
        sent_at = sent_at.replace(tzinfo=timezone.utc)
    return {
        "gmail_id": msg["id"],
        "thread_id": msg.get("threadId"),
        "recipients": recipients or ["unknown@unknown"],
        "subject": headers.get("subject") or "(no subject)",
        "from": headers.get("from", ""),
        "snippet": msg.get("snippet", ""),
        "sent_at": sent_at,
        "labelIds": msg.get("labelIds", []),
    }


def get_message_raw(service, message_id: str) -> str:
    """Return the base64url-encoded raw MIME of a message."""
    msg = (
        service.users()
        .messages()
        .get(userId="me", id=message_id, format="raw")
        .execute()
    )
    return msg.get("raw", "")


def inject_pixel_into_raw(raw_b64: str, pixel_url: str,
                         click_base_url: str = "", track_id: str = "") -> str:
    """Decode raw MIME, append tracking layers and rewrite links, re-encode."""
    raw_bytes = base64.urlsafe_b64decode(raw_b64)
    mime_text = raw_bytes.decode("utf-8", errors="replace")

    block = _tracking_block(pixel_url)

    # Rewrite links if click tracking is requested
    if click_base_url and track_id:
        import tracking as _trk
        mime_text = _trk.rewrite_html_links_safe(
            mime_text, click_base_url, track_id,
        )

    if "Content-Type: text/html" in mime_text:
        mime_text = mime_text.replace(
            "</body>",
            f"{block}</body>",
        )
    else:
        boundary = None
        for line in mime_text.splitlines():
            if line.startswith("Content-Type: multipart/alternative; boundary="):
                boundary = line.split("boundary=")[1].strip().strip('"')
                break
        if boundary:
            closing = f"--{boundary}--".encode()
            html_part = (
                f"Content-Type: text/html; charset=UTF-8\r\n"
                f"\r\n"
                f"<div>{block}</div>\r\n"
                f"\r\n"
                f"--{boundary}--\r\n"
            ).encode("utf-8")
            raw_bytes = raw_bytes.rstrip(closing) + html_part
            return base64.urlsafe_b64encode(raw_bytes).decode("ascii")

    return base64.urlsafe_b64encode(mime_text.encode("utf-8")).decode("ascii")


def is_sent_message(service, message_id: str) -> bool:
    """Check whether a message has the SENT label."""
    try:
        msg = (
            service.users()
            .messages()
            .get(userId="me", id=message_id, format="minimal")
            .execute()
        )
        return "SENT" in msg.get("labelIds", [])
    except Exception:
        return False


def update_message_with_pixel(service, message_id: str, pixel_url: str,
                             click_base_url: str = "", track_id: str = "") -> dict:
    """Post-inject a tracking pixel and optionally rewrite links."""
    raw_b64 = get_message_raw(service, message_id)
    new_raw = inject_pixel_into_raw(
        raw_b64, pixel_url,
        click_base_url=click_base_url, track_id=track_id,
    )
    return (
        service.users()
        .messages()
        .update(userId="me", id=message_id, body={"raw": new_raw})
        .execute()
    )
