"""
Mail Sources API endpoints.

Provides CRUD operations for MailSource objects stored in the database, plus
a *test-connection* action that validates the supplied credentials without
persisting anything.  Gmail API sources additionally have OAuth2 helper
endpoints (authorize-url, callback, fetch).
"""

import json
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional

from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel
from sqlalchemy.orm import Session

from app.core.database import get_db
from app.core.security import require_admin_auth
from app.models.mail_source import MailSource
from app.models.mail_source_import import MailSourceImport
from app.services.gmail_client import GmailClient
from app.services.imap_client import IMAPClient
from app.services.import_history import record_import_attempt

router = APIRouter()
logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Pydantic schemas
# ---------------------------------------------------------------------------


class MailSourceBase(BaseModel):
    """Fields shared by create and update payloads."""

    name: str
    method: str = "IMAP"  # IMAP | POP3 | GMAIL_API
    server: Optional[str] = None
    port: int = 993
    username: Optional[str] = None
    password: Optional[str] = None
    use_ssl: bool = True
    folder: str = "INBOX"
    polling_interval: int = 60
    enabled: bool = True
    # Gmail API OAuth2 fields (only relevant when method == GMAIL_API)
    gmail_client_id: Optional[str] = None
    gmail_client_secret: Optional[str] = None


class MailSourceCreate(MailSourceBase):
    """Payload for creating a new mail source."""


class MailSourceUpdate(BaseModel):
    """Payload for partial updates – all fields optional."""

    name: Optional[str] = None
    method: Optional[str] = None
    server: Optional[str] = None
    port: Optional[int] = None
    username: Optional[str] = None
    password: Optional[str] = None
    use_ssl: Optional[bool] = None
    folder: Optional[str] = None
    polling_interval: Optional[int] = None
    enabled: Optional[bool] = None
    gmail_client_id: Optional[str] = None
    gmail_client_secret: Optional[str] = None


class MailSourceResponse(MailSourceBase):
    """Response schema – exposes the stored row without exposing raw password."""

    id: int
    last_checked: Optional[datetime] = None
    created_at: Optional[datetime] = None
    updated_at: Optional[datetime] = None
    # Mask the stored password in responses
    password: Optional[str] = None
    # Gmail: show the authorised email address but not tokens
    gmail_email: Optional[str] = None
    # Indicate whether OAuth tokens are present (without exposing them)
    gmail_connected: bool = False

    class Config:
        from_attributes = True


class TestConnectionRequest(BaseModel):
    """Credentials for an ad-hoc connection test (not persisted)."""

    server: Optional[str] = None
    port: int = 993
    username: Optional[str] = None
    password: Optional[str] = None
    ssl: bool = True
    method: str = "IMAP"


class GmailCallbackRequest(BaseModel):
    """Payload for the Gmail OAuth2 callback endpoint."""

    code: str
    redirect_uri: str


class MailSourceImportResponse(BaseModel):
    """Sanitized import-history entry for a mail source."""

    id: int
    mail_source_id: int
    trigger: str
    status: str
    processed: int
    reports_found: int
    duplicate_reports: int
    error_count: int
    new_domains: List[str]
    errors: List[str]
    details: List[Dict[str, str]]
    started_at: datetime
    finished_at: datetime
    created_at: datetime


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------


def _sanitize_for_log(value: object) -> str:
    """Remove CR/LF characters from a value to prevent log injection attacks."""
    return str(value).replace("\r", "").replace("\n", " ")


def _get_source_or_404(source_id: int, db: Session) -> MailSource:
    source = db.query(MailSource).filter(MailSource.id == source_id).first()
    if source is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"Mail source {source_id} not found",
        )
    return source


def _source_to_response(source: MailSource) -> MailSourceResponse:
    """Convert ORM row to response schema, masking the stored password."""
    return MailSourceResponse(
        id=source.id,
        name=source.name,
        method=source.method,
        server=source.server,
        port=source.port or 993,
        username=source.username,
        password="**redacted**" if source.password else None,
        use_ssl=source.use_ssl if source.use_ssl is not None else True,
        folder=source.folder or "INBOX",
        polling_interval=source.polling_interval or 60,
        enabled=source.enabled if source.enabled is not None else True,
        last_checked=source.last_checked,
        created_at=source.created_at,
        updated_at=source.updated_at,
        gmail_client_id=source.gmail_client_id,
        gmail_client_secret="**redacted**" if source.gmail_client_secret else None,
        gmail_email=source.gmail_email,
        gmail_connected=bool(source.gmail_access_token),
    )


def _decode_json_list(value: Optional[str]) -> List[str]:
    """Decode a JSON list stored on import history rows."""
    if not value:
        return []
    try:
        decoded = json.loads(value)
    except (json.JSONDecodeError, TypeError):
        return []
    if not isinstance(decoded, list):
        return []
    return [str(item) for item in decoded]


def _decode_json_details(value: Optional[str]) -> List[Dict[str, str]]:
    """Decode a sanitized JSON detail list stored on import history rows."""
    if not value:
        return []
    try:
        decoded = json.loads(value)
    except (json.JSONDecodeError, TypeError):
        return []
    if not isinstance(decoded, list):
        return []

    details: List[Dict[str, str]] = []
    for item in decoded:
        if isinstance(item, dict):
            details.append({str(key): str(val) for key, val in item.items()})
    return details


def _import_to_response(row: MailSourceImport) -> MailSourceImportResponse:
    """Convert an import-history ORM row to an API response."""
    return MailSourceImportResponse(
        id=row.id,
        mail_source_id=row.mail_source_id,
        trigger=row.trigger,
        status=row.status,
        processed=row.processed,
        reports_found=row.reports_found,
        duplicate_reports=row.duplicate_reports,
        error_count=row.error_count,
        new_domains=_decode_json_list(row.new_domains),
        errors=_decode_json_list(row.errors),
        details=_decode_json_details(row.details),
        started_at=row.started_at,
        finished_at=row.finished_at,
        created_at=row.created_at,
    )


def _fetch_response(source: MailSource, results: Dict[str, Any]) -> Dict[str, Any]:
    """Build the common response payload for a manual source fetch."""
    return {
        "source_id": source.id,
        "name": source.name,
        "success": bool(results.get("success", False)),
        "processed": int(results.get("processed", 0)),
        "reports_found": int(results.get("reports_found", 0)),
        "duplicate_reports": int(results.get("duplicate_reports", 0)),
        "new_domains": [str(d) for d in results.get("new_domains", [])],
        "error_count": len(results.get("errors", [])),
        "timestamp": datetime.now().isoformat(),
    }


def _fetch_gmail_source(source: MailSource, db: Session) -> Dict[str, Any]:
    """Run one Gmail API import and persist source/import metadata."""
    if not source.gmail_access_token:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Gmail account not yet authorised. Complete OAuth2 flow first.",
        )

    already = GmailClient.load_ingested_ids(source.gmail_ingested_ids)
    client = GmailClient(
        client_id=source.gmail_client_id or "",
        client_secret=source.gmail_client_secret or "",
        access_token=source.gmail_access_token,
        refresh_token=source.gmail_refresh_token or "",
        already_ingested_ids=already,
        db=db,
    )

    started_at = datetime.utcnow()
    results = client.fetch_reports()

    if results.get("new_ingested_ids"):
        all_ids = list(dict.fromkeys(already + results["new_ingested_ids"]))
        source.gmail_ingested_ids = GmailClient.dump_ingested_ids(all_ids)

    refreshed = client.get_refreshed_tokens()
    if refreshed:
        source.gmail_access_token = refreshed["access_token"]
        if "refresh_token" in refreshed:
            source.gmail_refresh_token = refreshed["refresh_token"]

    source.last_checked = datetime.utcnow()
    record_import_attempt(db, source, results, started_at=started_at, trigger="manual")
    db.commit()
    return results


def _fetch_imap_source(source: MailSource, db: Session, days: int) -> Dict[str, Any]:
    """Run one IMAP import and persist source/import metadata."""
    client = IMAPClient(
        server=source.server,
        port=source.port or 993,
        username=source.username,
        password=source.password,
        delete_emails=False,
        db=db,
    )
    started_at = datetime.utcnow()
    results = client.fetch_reports(days=days)
    source.last_checked = datetime.utcnow()
    record_import_attempt(db, source, results, started_at=started_at, trigger="manual")
    db.commit()
    return results


def _fetch_source(source: MailSource, db: Session, days: int) -> Dict[str, Any]:
    """Dispatch a manual fetch for one configured mail source."""
    if source.method == "GMAIL_API":
        return _fetch_gmail_source(source, db)
    if source.method == "IMAP":
        return _fetch_imap_source(source, db, days)
    raise HTTPException(
        status_code=status.HTTP_400_BAD_REQUEST,
        detail=f"Manual fetch is not available for method '{source.method}'.",
    )


# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------


@router.get("", response_model=List[MailSourceResponse])
async def list_mail_sources(
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> List[MailSourceResponse]:
    """Return all configured mail sources (passwords redacted)."""
    sources = db.query(MailSource).order_by(MailSource.id).all()
    return [_source_to_response(s) for s in sources]


@router.post("", response_model=MailSourceResponse, status_code=status.HTTP_201_CREATED)
async def create_mail_source(
    payload: MailSourceCreate,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> MailSourceResponse:
    """Create a new mail source."""
    source = MailSource(
        name=payload.name,
        method=payload.method.upper(),
        server=payload.server,
        port=payload.port,
        username=payload.username,
        password=payload.password,
        use_ssl=payload.use_ssl,
        folder=payload.folder,
        polling_interval=payload.polling_interval,
        enabled=payload.enabled,
        gmail_client_id=payload.gmail_client_id,
        gmail_client_secret=payload.gmail_client_secret,
    )
    db.add(source)
    db.commit()
    db.refresh(source)
    logger.info(
        "Created mail source id=%d name=%r method=%r", source.id, source.name, source.method
    )
    return _source_to_response(source)


@router.get("/{source_id}", response_model=MailSourceResponse)
async def get_mail_source(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> MailSourceResponse:
    """Return a single mail source by ID (password redacted)."""
    source = _get_source_or_404(source_id, db)
    return _source_to_response(source)


@router.get("/{source_id}/imports", response_model=List[MailSourceImportResponse])
async def list_mail_source_imports(
    source_id: int,
    limit: int = 20,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> List[MailSourceImportResponse]:
    """Return recent sanitized import attempts for one mail source."""
    _get_source_or_404(source_id, db)
    safe_limit = min(max(limit, 1), 100)
    rows = (
        db.query(MailSourceImport)
        .filter(MailSourceImport.mail_source_id == source_id)
        .order_by(MailSourceImport.started_at.desc(), MailSourceImport.id.desc())
        .limit(safe_limit)
        .all()
    )
    return [_import_to_response(row) for row in rows]


@router.post("/{source_id}/fetch", response_model=Dict[str, Any])
async def fetch_mail_source(
    source_id: int,
    days: int = 7,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
    """Manually fetch DMARC reports for one configured mail source."""
    if days < 1 or days > 365:
        raise HTTPException(status_code=400, detail="Days parameter must be between 1 and 365")

    source = _get_source_or_404(source_id, db)
    results = _fetch_source(source, db, days)
    logger.info(
        "Manual fetch for source id=%d: processed=%d reports_found=%d duplicates=%d",
        int(source_id),
        int(results.get("processed", 0)),
        int(results.get("reports_found", 0)),
        int(results.get("duplicate_reports", 0)),
    )
    for err in results.get("errors", []):
        logger.warning(
            "Manual fetch warning for source id=%d: %s",
            int(source_id),
            _sanitize_for_log(err),
        )

    return _fetch_response(source, results)


@router.put("/{source_id}", response_model=MailSourceResponse)
async def update_mail_source(
    source_id: int,
    payload: MailSourceUpdate,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> MailSourceResponse:
    """Update one or more fields of an existing mail source."""
    source = _get_source_or_404(source_id, db)

    update_data = payload.model_dump(exclude_unset=True)
    if "method" in update_data and update_data["method"]:
        update_data["method"] = update_data["method"].upper()

    for field, value in update_data.items():
        setattr(source, field, value)

    source.updated_at = datetime.utcnow()
    db.commit()
    db.refresh(source)
    logger.info("Updated mail source id=%d", source.id)
    return _source_to_response(source)


@router.delete("/{source_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_mail_source(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> None:
    """Delete a mail source permanently."""
    source = _get_source_or_404(source_id, db)
    db.delete(source)
    db.commit()
    logger.info("Deleted mail source id=%s", _sanitize_for_log(source_id))


@router.post("/{source_id}/toggle", response_model=MailSourceResponse)
async def toggle_mail_source(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> MailSourceResponse:
    """Toggle the *enabled* flag of a mail source."""
    source = _get_source_or_404(source_id, db)
    source.enabled = not source.enabled
    source.updated_at = datetime.utcnow()
    db.commit()
    db.refresh(source)
    return _source_to_response(source)


@router.post("/{source_id}/test", response_model=Dict[str, Any])
async def test_stored_mail_source(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
    """Test the connection for an already-stored mail source using its saved credentials."""
    source = _get_source_or_404(source_id, db)

    if source.method == "GMAIL_API":
        if not source.gmail_access_token:
            return {
                "success": False,
                "message": "Gmail API source is not yet authorised. "
                "Use the 'Connect Gmail' button to complete OAuth2 authorisation.",
                "timestamp": datetime.now().isoformat(),
            }
        try:
            gmail_client = GmailClient(
                client_id=source.gmail_client_id or "",
                client_secret=source.gmail_client_secret or "",
                access_token=source.gmail_access_token,
                refresh_token=source.gmail_refresh_token or "",
            )
            # Attempt to list one message to verify the credentials work
            service = gmail_client._build_service()  # pylint: disable=protected-access
            service.users().getProfile(userId="me").execute()
            source.last_checked = datetime.utcnow()
            db.commit()
            return {
                "success": True,
                "message": f"Gmail API credentials are valid (account: {source.gmail_email or 'unknown'}).",
                "timestamp": datetime.now().isoformat(),
            }
        except Exception as exc:  # pylint: disable=broad-exception-caught
            logger.error(
                "Gmail API test failed for source id=%d: %s",
                int(source_id),
                _sanitize_for_log(exc),
            )
            return {
                "success": False,
                "message": "Gmail API test failed. Check server logs for details.",
                "timestamp": datetime.now().isoformat(),
            }

    if source.method != "IMAP":
        return {
            "success": False,
            "message": f"Connection testing for method '{source.method}' is not yet implemented.",
            "timestamp": datetime.now().isoformat(),
        }

    imap_client = IMAPClient(
        server=source.server,
        port=source.port or 993,
        username=source.username,
        password=source.password,
    )
    success, message, stats = imap_client.test_connection()

    if success:
        source.last_checked = datetime.utcnow()
        db.commit()

    return {
        "success": success,
        "message": message,
        "message_count": stats.get("message_count", 0),
        "unread_count": stats.get("unread_count", 0),
        "dmarc_count": stats.get("dmarc_count", 0),
        "available_mailboxes": stats.get("available_mailboxes", []),
        "timestamp": datetime.now().isoformat(),
    }


@router.post("/test-connection", response_model=Dict[str, Any])
async def test_connection_adhoc(
    request: TestConnectionRequest,
    _auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
    """
    Test a connection using ad-hoc credentials (not stored in the database).

    Useful when filling out the *add/edit mail source* form before saving.
    """
    method = request.method.upper()

    if method != "IMAP":
        return {
            "success": False,
            "message": f"Connection testing for method '{method}' is not yet implemented.",
            "timestamp": datetime.now().isoformat(),
        }

    imap_client = IMAPClient(
        server=request.server,
        port=request.port,
        username=request.username,
        password=request.password,
    )
    success, message, stats = imap_client.test_connection()

    return {
        "success": success,
        "message": message,
        "message_count": stats.get("message_count", 0),
        "unread_count": stats.get("unread_count", 0),
        "dmarc_count": stats.get("dmarc_count", 0),
        "available_mailboxes": stats.get("available_mailboxes", []),
        "timestamp": datetime.now().isoformat(),
    }


# ---------------------------------------------------------------------------
# Gmail API OAuth2 routes
# ---------------------------------------------------------------------------


@router.get("/{source_id}/gmail/authorize-url", response_model=Dict[str, Any])
async def gmail_authorize_url(
    source_id: int,
    request: Request,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
    """
    Return a Google OAuth2 authorization URL for the given GMAIL_API source.

    The frontend should redirect the user to this URL.  After the user
    grants access Google redirects back to
    ``<origin>/mail-sources/<id>/gmail/callback`` with a ``code`` parameter.
    """
    source = _get_source_or_404(source_id, db)

    if source.method != "GMAIL_API":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="This endpoint is only available for GMAIL_API sources.",
        )
    if not source.gmail_client_id:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="gmail_client_id is not configured for this source.",
        )

    # Build a redirect_uri that points back to this server's callback endpoint
    base_url = str(request.base_url).rstrip("/")
    redirect_uri = f"{base_url}/api/v1/mail-sources/{source_id}/gmail/callback"

    auth_url = GmailClient.build_authorization_url(
        client_id=source.gmail_client_id,
        redirect_uri=redirect_uri,
        state=str(source_id),
    )
    return {
        "authorization_url": auth_url,
        "redirect_uri": redirect_uri,
    }


@router.get("/{source_id}/gmail/callback")
async def gmail_oauth_callback(
    source_id: int,
    request: Request,
    db: Session = Depends(get_db),
) -> Any:
    """
    Handle the Google OAuth2 redirect after the user grants Gmail access.

    Exchanges the authorization ``code`` query parameter for access/refresh
    tokens and stores them on the MailSource row.  This endpoint is called
    directly by Google's redirect, so it does not require the usual API key
    authentication; it is protected instead by the state/code being
    single-use and bound to the source_id in the URL.
    """
    from fastapi.responses import HTMLResponse

    code = request.query_params.get("code")
    error = request.query_params.get("error")

    if error or not code:
        html = (
            "<html><body><p>Gmail authorisation failed: "
            f"{error or 'no code received'}. "
            "You may close this window.</p></body></html>"
        )
        return HTMLResponse(content=html, status_code=400)

    source = db.query(MailSource).filter(MailSource.id == source_id).first()
    if source is None or source.method != "GMAIL_API":
        return HTMLResponse(
            content="<html><body><p>Mail source not found.</p></body></html>",
            status_code=404,
        )

    base_url = str(request.base_url).rstrip("/")
    redirect_uri = f"{base_url}/api/v1/mail-sources/{source_id}/gmail/callback"

    try:
        token_data = GmailClient.exchange_code_for_tokens(
            client_id=source.gmail_client_id or "",
            client_secret=source.gmail_client_secret or "",
            code=code,
            redirect_uri=redirect_uri,
        )
    except ValueError as exc:
        logger.error(
            "Gmail token exchange error for source id=%d: %s",
            int(source_id),
            _sanitize_for_log(exc),
        )
        html = (
            "<html><body><p>Token exchange failed. "
            "Please close this window and try again.</p></body></html>"
        )
        return HTMLResponse(content=html, status_code=400)

    access_token = token_data.get("access_token")
    refresh_token = token_data.get("refresh_token")

    if not access_token:
        return HTMLResponse(
            content="<html><body><p>No access token returned by Google.</p></body></html>",
            status_code=400,
        )

    gmail_email = GmailClient.get_gmail_email(access_token)

    source.gmail_access_token = access_token
    if refresh_token:
        source.gmail_refresh_token = refresh_token
    if gmail_email:
        source.gmail_email = gmail_email
    source.updated_at = datetime.utcnow()
    db.commit()

    logger.info(
        "Gmail OAuth2 authorisation complete for source id=%d (account=%s)",
        int(source_id),
        _sanitize_for_log(gmail_email or "unknown"),
    )

    html = (
        "<html><body>"
        "<p>✅ Gmail account connected successfully"
        f"{(' (' + gmail_email + ')') if gmail_email else ''}. "
        "You may close this window.</p>"
        "<script>window.close();</script>"
        "</body></html>"
    )
    return HTMLResponse(content=html)


@router.post("/{source_id}/gmail/callback", response_model=MailSourceResponse)
async def gmail_oauth_callback_post(
    source_id: int,
    payload: GmailCallbackRequest,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> MailSourceResponse:
    """
    Exchange an OAuth2 authorization code for tokens (JSON / programmatic flow).

    This POST variant is for clients that handle the OAuth2 redirect
    themselves and post the code here as JSON.  Requires the standard
    admin authentication.
    """
    source = _get_source_or_404(source_id, db)

    if source.method != "GMAIL_API":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="This endpoint is only available for GMAIL_API sources.",
        )

    try:
        token_data = GmailClient.exchange_code_for_tokens(
            client_id=source.gmail_client_id or "",
            client_secret=source.gmail_client_secret or "",
            code=payload.code,
            redirect_uri=payload.redirect_uri,
        )
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail=str(exc),
        ) from exc

    access_token = token_data.get("access_token")
    refresh_token = token_data.get("refresh_token")

    if not access_token:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Google did not return an access token.",
        )

    gmail_email = GmailClient.get_gmail_email(access_token)

    source.gmail_access_token = access_token
    if refresh_token:
        source.gmail_refresh_token = refresh_token
    if gmail_email:
        source.gmail_email = gmail_email
    source.updated_at = datetime.utcnow()
    db.commit()
    db.refresh(source)

    logger.info(
        "Gmail OAuth2 tokens saved for source id=%d (account=%s)",
        int(source_id),
        _sanitize_for_log(gmail_email or "unknown"),
    )
    return _source_to_response(source)


@router.post("/{source_id}/gmail/fetch", response_model=Dict[str, Any])
async def gmail_fetch_reports(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
    """
    Manually trigger a Gmail DMARC report fetch for the given source.

    Searches Gmail for emails matching the DMARC report heuristic, ingests
    any attachments not yet seen, and returns a summary.
    """
    source = _get_source_or_404(source_id, db)

    if source.method != "GMAIL_API":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="This endpoint is only available for GMAIL_API sources.",
        )
    if not source.gmail_access_token:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Gmail account not yet authorised. Complete OAuth2 flow first.",
        )

    already = GmailClient.load_ingested_ids(source.gmail_ingested_ids)
    client = GmailClient(
        client_id=source.gmail_client_id or "",
        client_secret=source.gmail_client_secret or "",
        access_token=source.gmail_access_token,
        refresh_token=source.gmail_refresh_token or "",
        already_ingested_ids=already,
        db=db,
    )

    started_at = datetime.utcnow()
    results = client.fetch_reports()

    # Persist updated ingested IDs and any refreshed tokens
    if results.get("new_ingested_ids"):
        all_ids = list(dict.fromkeys(already + results["new_ingested_ids"]))
        source.gmail_ingested_ids = GmailClient.dump_ingested_ids(all_ids)

    refreshed = client.get_refreshed_tokens()
    if refreshed:
        source.gmail_access_token = refreshed["access_token"]
        if "refresh_token" in refreshed:
            source.gmail_refresh_token = refreshed["refresh_token"]

    source.last_checked = datetime.utcnow()
    record_import_attempt(db, source, results, started_at=started_at, trigger="manual")
    db.commit()

    logger.info(
        "Gmail fetch for source id=%d: processed=%d reports_found=%d",
        int(source_id),
        int(results.get("processed", 0)),
        int(results.get("reports_found", 0)),
    )

    for err in results.get("errors", []):
        logger.warning(
            "Gmail fetch warning for source id=%d: %s",
            int(source_id),
            _sanitize_for_log(err),
        )

    return {
        "success": bool(results.get("success", False)),
        "processed": int(results.get("processed", 0)),
        "reports_found": int(results.get("reports_found", 0)),
        "new_domains": [str(d) for d in results.get("new_domains", [])],
        "error_count": len(results.get("errors", [])),
        "timestamp": datetime.now().isoformat(),
    }


@router.delete("/{source_id}/gmail/connection", status_code=status.HTTP_204_NO_CONTENT)
async def gmail_disconnect(
    source_id: int,
    db: Session = Depends(get_db),
    _auth: dict = Depends(require_admin_auth),
) -> None:
    """Revoke / clear the stored Gmail OAuth2 tokens for this source."""
    source = _get_source_or_404(source_id, db)

    if source.method != "GMAIL_API":
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="This endpoint is only available for GMAIL_API sources.",
        )

    source.gmail_access_token = None
    source.gmail_refresh_token = None
    source.gmail_email = None
    source.updated_at = datetime.utcnow()
    db.commit()
    logger.info("Gmail tokens cleared for source id=%d", int(source_id))
