"""
Proveedor Gmail (API de Google).

Reescritura del `api/utils/gmail.py` original. Los tres problemas que se
corrigen:

1. `flow.run_local_server()` en el camino de envio. En un servidor eso abre
   un socket y espera a que alguien complete un consentimiento OAuth en un
   navegador que no existe: la peticion se queda colgada hasta el timeout.
   Aqui la autorizacion es un paso previo de operacion, nunca algo que ocurra
   sirviendo una peticion.
2. El token se reescribia dentro del repositorio. Ahora la ruta se configura
   por entorno y apunta fuera del arbol de codigo.
3. No habia tiempo de espera. Una llamada sin timeout puede dejar bloqueado
   un worker indefinidamente.

Se admiten dos formas de autenticacion:

* Cuenta de servicio con delegacion en todo el dominio (recomendado en
  Workspace): `GMAIL_SERVICE_ACCOUNT_FILE` + `GMAIL_SENDER`.
* Credenciales de usuario OAuth ya autorizadas: `GMAIL_TOKEN_FILE`. Solo se
  refrescan, nunca se pide consentimiento interactivo.
"""

from __future__ import annotations

import base64
import logging
import os
import threading
from email.message import EmailMessage as MIMEEmailMessage
from email.utils import formataddr

from django.conf import settings

from notifications.channels.email.base import (
    EmailPayload,
    EmailProvider,
    ProviderConfigurationError,
    SendResult,
    is_retryable_status,
)

logger = logging.getLogger("notifications.providers.gmail")

SCOPES = ["https://www.googleapis.com/auth/gmail.send"]

# httplib2 no es seguro entre hilos, asi que el servicio se protege con un
# cerrojo y se reutiliza dentro del mismo proceso.
_service_lock = threading.Lock()
_service_cache: dict[str, object] = {}


class GmailProvider(EmailProvider):
    name = "gmail"
    supports_templates = False

    def __init__(self):
        self.sender = settings.GMAIL_SENDER or "me"
        self.timeout = settings.GMAIL_TIMEOUT_SECONDS

    def check_configuration(self) -> None:
        service_account = settings.GMAIL_SERVICE_ACCOUNT_FILE
        token_file = settings.GMAIL_TOKEN_FILE

        if service_account:
            if not os.path.exists(service_account):
                raise ProviderConfigurationError(
                    f"No existe el archivo de cuenta de servicio: {service_account}"
                )
            if not settings.GMAIL_SENDER or settings.GMAIL_SENDER == "me":
                raise ProviderConfigurationError(
                    "Con una cuenta de servicio hay que indicar GMAIL_SENDER con "
                    "la direccion del buzon que se va a suplantar."
                )
            return

        if token_file:
            if not os.path.exists(token_file):
                raise ProviderConfigurationError(
                    f"No existe el archivo de token de Gmail: {token_file}. "
                    "Genera las credenciales fuera de linea y despliegalas."
                )
            return

        raise ProviderConfigurationError(
            "Gmail no esta configurado: define GMAIL_SERVICE_ACCOUNT_FILE o "
            "GMAIL_TOKEN_FILE."
        )

    # -- Credenciales -------------------------------------------------------

    def _credentials(self):
        from google.auth.transport.requests import Request
        from google.oauth2.credentials import Credentials

        if settings.GMAIL_SERVICE_ACCOUNT_FILE:
            from google.oauth2 import service_account

            creds = service_account.Credentials.from_service_account_file(
                settings.GMAIL_SERVICE_ACCOUNT_FILE, scopes=SCOPES
            )
            # Delegacion en todo el dominio: la cuenta de servicio actua en
            # nombre del buzon indicado.
            return creds.with_subject(settings.GMAIL_SENDER)

        creds = Credentials.from_authorized_user_file(
            settings.GMAIL_TOKEN_FILE, SCOPES
        )
        if not creds.valid:
            if not (creds.expired and creds.refresh_token):
                raise ProviderConfigurationError(
                    "Las credenciales de Gmail no son validas y no se pueden "
                    "refrescar. Hay que volver a autorizar la aplicacion "
                    "fuera de linea."
                )
            creds.refresh(Request())
            _persist_token(settings.GMAIL_TOKEN_FILE, creds)
        return creds

    def _service(self):
        import httplib2
        from google_auth_httplib2 import AuthorizedHttp
        from googleapiclient.discovery import build

        cache_key = settings.GMAIL_SERVICE_ACCOUNT_FILE or settings.GMAIL_TOKEN_FILE
        with _service_lock:
            service = _service_cache.get(cache_key)
            if service is None:
                authorized_http = AuthorizedHttp(
                    self._credentials(), http=httplib2.Http(timeout=self.timeout)
                )
                service = build(
                    "gmail",
                    "v1",
                    http=authorized_http,
                    cache_discovery=False,
                    static_discovery=True,
                )
                _service_cache[cache_key] = service
            return service

    # -- Envio --------------------------------------------------------------

    def send(self, payload: EmailPayload) -> SendResult:
        from googleapiclient.errors import HttpError

        self.check_configuration()

        if payload.template_id:
            return SendResult.failure(
                self.name,
                error_code="templates_unsupported",
                error_message=(
                    "Gmail no expande plantillas del proveedor. Usa SendGrid o "
                    "envia el HTML ya renderizado."
                ),
                retryable=False,
            )

        try:
            mime = build_mime_message(payload)
            raw = base64.urlsafe_b64encode(mime.as_bytes()).decode("ascii")
        except Exception as exc:  # noqa: BLE001 - error al armar el MIME
            logger.exception("No se pudo construir el mensaje MIME para Gmail")
            return SendResult.failure(
                self.name,
                error_code="build_error",
                error_message=str(exc),
                retryable=False,
            )

        try:
            # `_service()` toma el cerrojo por su cuenta; hay que obtener el
            # servicio antes de volver a tomarlo aqui, porque `threading.Lock`
            # no es reentrante y anidarlo bloquearia el hilo para siempre.
            service = self._service()
            with _service_lock:
                sent = (
                    service.users()
                    .messages()
                    .send(userId="me", body={"raw": raw})
                    .execute()
                )
        except HttpError as exc:
            status_code = getattr(getattr(exc, "resp", None), "status", None)
            logger.warning(
                "Gmail rechazo el envio (HTTP %s)",
                status_code,
                extra={"status_code": status_code, "provider": self.name},
            )
            return SendResult.failure(
                self.name,
                error_code=f"http_{status_code}",
                error_message=_decode(getattr(exc, "content", b"")),
                status_code=status_code,
                retryable=is_retryable_status(status_code),
            )
        except ProviderConfigurationError:
            raise
        except Exception as exc:  # noqa: BLE001 - red, TLS, credenciales
            logger.warning(
                "Fallo al contactar con Gmail: %s", exc.__class__.__name__
            )
            return SendResult.failure(
                self.name,
                error_code="network_error",
                error_message=f"{exc.__class__.__name__}: {exc}",
                retryable=True,
            )

        message_id = (sent or {}).get("id", "")
        logger.info(
            "Correo aceptado por Gmail",
            extra={
                "provider": self.name,
                "provider_message_id": message_id,
                "recipients": len(payload.all_recipients),
            },
        )
        return SendResult.ok(self.name, message_id, status_code=200)


# ---------------------------------------------------------------------------
# Construccion del mensaje MIME
# ---------------------------------------------------------------------------


def build_mime_message(payload: EmailPayload) -> MIMEEmailMessage:
    """
    Arma el mensaje MIME.

    Estructura resultante cuando hay de todo:

        multipart/mixed
        |-- multipart/alternative
        |   |-- text/plain
        |   `-- multipart/related
        |       |-- text/html
        |       `-- imagenes incrustadas
        `-- adjuntos

    Las direcciones ya vienen validadas por el serializer, que garantiza que
    no contienen saltos de linea. Sin esa validacion, un asunto o un
    destinatario con `\\r\\n` permitiria inyectar cabeceras arbitrarias en el
    mensaje (por ejemplo un `Bcc` oculto hacia el atacante).
    """
    message = MIMEEmailMessage()

    message["From"] = (
        formataddr((payload.from_name, payload.from_email))
        if payload.from_name
        else payload.from_email
    )
    message["To"] = ", ".join(payload.to)
    if payload.cc:
        message["Cc"] = ", ".join(payload.cc)
    if payload.bcc:
        message["Bcc"] = ", ".join(payload.bcc)
    if payload.reply_to:
        message["Reply-To"] = payload.reply_to
    message["Subject"] = payload.subject

    for key, value in payload.headers.items():
        message[key] = value

    inline = [a for a in payload.attachments if a.inline and a.content_id]
    regular = [a for a in payload.attachments if not (a.inline and a.content_id)]

    text_body = payload.text or _fallback_text(payload.html)
    message.set_content(text_body, subtype="plain", charset="utf-8")

    if payload.html:
        message.add_alternative(payload.html, subtype="html", charset="utf-8")
        html_part = message.get_payload()[-1]
        for attachment in inline:
            maintype, _, subtype = attachment.content_type.partition("/")
            html_part.add_related(
                attachment.content,
                maintype=maintype or "application",
                subtype=subtype or "octet-stream",
                cid=f"<{attachment.content_id}>",
                filename=attachment.filename,
            )
    else:
        # Sin HTML no hay donde incrustar: se adjuntan de forma normal.
        regular = list(payload.attachments)

    for attachment in regular:
        maintype, _, subtype = attachment.content_type.partition("/")
        message.add_attachment(
            attachment.content,
            maintype=maintype or "application",
            subtype=subtype or "octet-stream",
            filename=attachment.filename,
        )

    return message


def _fallback_text(html: str) -> str:
    """
    Texto plano minimo cuando solo se envio HTML.

    Un mensaje sin parte de texto puntua peor en los filtros antispam.
    """
    if not html:
        return ""
    import re

    text = re.sub(r"<br\s*/?>|</p>|</div>|</tr>", "\n", html, flags=re.IGNORECASE)
    text = re.sub(r"<[^>]+>", "", text)
    from html import unescape

    return re.sub(r"\n{3,}", "\n\n", unescape(text)).strip()


def _persist_token(path: str, creds) -> None:
    """Guarda el token refrescado con permisos restrictivos."""
    try:
        fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
        with os.fdopen(fd, "w") as handle:
            handle.write(creds.to_json())
    except OSError as exc:
        # No es fatal: el token en memoria sirve para esta ejecucion.
        logger.warning("No se pudo guardar el token de Gmail refrescado: %s", exc)


def _decode(body) -> str:
    if isinstance(body, bytes):
        return body.decode("utf-8", errors="replace")
    return str(body or "")
