#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Lee códigos OTP de Bucéfalo (Login security code) desde IMAP.

Bucéfalo manda el código 2FA desde `noreply@donotreply.acct-mgmt.com` con un cuerpo
tipo: `Your one time login code for logging into crm.bucefalocrm.io is 929711.`

Uso:
    from scripts.email_otp_reader import wait_for_bucefalo_otp
    code = wait_for_bucefalo_otp(since=datetime.now())  # bloquea hasta verlo o timeout

Configuración: variables de entorno (carga `.env` si existe).
    EMAIL_IMAP_HOST, EMAIL_IMAP_PORT, EMAIL_IMAP_USER, EMAIL_IMAP_PASSWORD,
    EMAIL_IMAP_FOLDER (default INBOX), OTP_TIMEOUT_SECONDS, OTP_POLL_INTERVAL_SECONDS.
"""

import os
import re
import sys
import time
from datetime import datetime, timedelta, timezone

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if ROOT_DIR not in sys.path:
    sys.path.insert(0, ROOT_DIR)

# Cargar .env si existe (silencioso si falta python-dotenv).
try:
    from dotenv import load_dotenv
    load_dotenv(os.path.join(ROOT_DIR, ".env"))
except Exception:
    pass


# Remitente esperado del correo de 2FA de Bucéfalo / GHL.
EXPECTED_SENDERS = (
    "noreply@donotreply.acct-mgmt.com",
    "donotreply.acct-mgmt.com",
)

# Patrones para extraer el código. El correo dice "is 929711." pero por robustez
# probamos varios formatos.
CODE_PATTERNS = (
    re.compile(r"\bis\s+(\d{6})\b", re.IGNORECASE),       # "is 929711"
    re.compile(r"\bc[oó]digo[^\d]{0,20}(\d{6})\b", re.IGNORECASE),  # español
    re.compile(r"\bcode[^\d]{0,20}(\d{6})\b", re.IGNORECASE),       # inglés
    re.compile(r"(?<!\d)(\d{6})(?!\d)"),                  # cualquier grupo aislado de 6 dígitos (último recurso)
)


class OtpReaderError(Exception):
    pass


def _config():
    """Lee variables de entorno y normaliza."""
    host = (os.environ.get("EMAIL_IMAP_HOST") or "").strip()
    port = int(os.environ.get("EMAIL_IMAP_PORT") or "993")
    user = (os.environ.get("EMAIL_IMAP_USER") or "").strip()
    pwd = os.environ.get("EMAIL_IMAP_PASSWORD") or ""
    folder = (os.environ.get("EMAIL_IMAP_FOLDER") or "INBOX").strip()
    timeout = int(os.environ.get("OTP_TIMEOUT_SECONDS") or "120")
    poll = int(os.environ.get("OTP_POLL_INTERVAL_SECONDS") or "3")
    if not host or not user or not pwd:
        raise OtpReaderError(
            "Faltan variables IMAP en .env: EMAIL_IMAP_HOST / EMAIL_IMAP_USER / EMAIL_IMAP_PASSWORD"
        )
    return host, port, user, pwd, folder, timeout, poll


def _extract_code(text):
    """Devuelve el primer código de 6 dígitos que matchee algún patrón, o None."""
    if not text:
        return None
    for pat in CODE_PATTERNS:
        m = pat.search(text)
        if m:
            return m.group(1)
    return None


def wait_for_bucefalo_otp(since=None, timeout_sec=None, poll_sec=None, debug=False):
    """Espera el próximo OTP de Bucéfalo y devuelve el código (string de 6 dígitos).

    Args:
        since: datetime con timezone; ignora correos anteriores a esta fecha.
               Si es None, usa "ahora - 60s" para tolerar relojes desfasados.
        timeout_sec: override del timeout (default desde .env).
        poll_sec: override del intervalo (default desde .env).
        debug: imprime info de los correos vistos por consola.

    Returns:
        str: código de 6 dígitos.

    Raises:
        OtpReaderError: si caduca o falla la conexión.
    """
    try:
        from imap_tools import MailBox, AND
    except ImportError as e:
        raise OtpReaderError(
            "Falta la librería imap-tools. Instálala con: pip install imap-tools"
        ) from e

    host, port, user, pwd, folder, default_timeout, default_poll = _config()

    if since is None:
        since = datetime.now(timezone.utc) - timedelta(seconds=60)
    if since.tzinfo is None:
        # Asumimos UTC si no viene con tz.
        since = since.replace(tzinfo=timezone.utc)
    # Margen para tolerar skew entre el reloj local y el del servidor IMAP/SMTP
    # (vimos correos que tardan 20-30s y a veces el server stamp viene desfasado).
    effective_since = since - timedelta(seconds=45)
    since = effective_since

    timeout_sec = timeout_sec if timeout_sec is not None else default_timeout
    poll_sec = poll_sec if poll_sec is not None else default_poll

    deadline = time.time() + timeout_sec
    seen_uids = set()

    if debug:
        print(f"[OTP] Conectando a IMAP {host}:{port} como {user}...")
    try:
        with MailBox(host, port=port).login(user, pwd, initial_folder=folder) as mailbox:
            if debug:
                print(f"[OTP] Conectado. Esperando correo de {EXPECTED_SENDERS[0]} (timeout {timeout_sec}s)...")
            while time.time() < deadline:
                # imap-tools acepta filtros AND; buscamos correos desde la fecha indicada.
                # IMAP `SINCE` ignora la hora, solo mira fecha → la usamos como filtro burdo
                # y luego filtramos por timestamp real en Python.
                since_date = since.date()
                try:
                    candidates = list(mailbox.fetch(
                        AND(date_gte=since_date),
                        mark_seen=False,
                        reverse=True,  # más nuevo primero
                        limit=10,
                    ))
                except Exception as fe:
                    if debug:
                        print(f"[OTP] Error fetch: {fe}")
                    candidates = []

                for msg in candidates:
                    if msg.uid in seen_uids:
                        continue
                    seen_uids.add(msg.uid)

                    sender = (msg.from_ or "").lower()
                    if not any(s in sender for s in EXPECTED_SENDERS):
                        if debug:
                            print(f"[OTP]   skip (otro remitente): {sender}")
                        continue

                    # Filtro por timestamp real (no solo fecha).
                    msg_dt = msg.date
                    if msg_dt is None:
                        if debug:
                            print(f"[OTP]   skip (sin fecha): uid={msg.uid}")
                        continue
                    if msg_dt.tzinfo is None:
                        msg_dt = msg_dt.replace(tzinfo=timezone.utc)
                    if msg_dt < since:
                        if debug:
                            print(f"[OTP]   skip (anterior a since={since.isoformat()}): {msg_dt.isoformat()}")
                        continue

                    body = (msg.text or "") + " " + (msg.subject or "")
                    code = _extract_code(body)
                    if code:
                        if debug:
                            print(f"[OTP] ¡Código encontrado! {code} (de mensaje {msg_dt.isoformat()})")
                        return code
                    if debug:
                        print(f"[OTP]   matcheó remitente pero no extrajo código de: {body[:160]}")

                time.sleep(poll_sec)
    except OtpReaderError:
        raise
    except Exception as e:
        raise OtpReaderError(f"Error IMAP ({type(e).__name__}): {e}") from e

    raise OtpReaderError(f"Tiempo agotado ({timeout_sec}s) esperando el código OTP de Bucéfalo.")


def main():
    """CLI de prueba: imprime el próximo código OTP que llegue."""
    print("=== email_otp_reader: prueba CLI ===")
    print("Esperando el próximo correo OTP de Bucéfalo (provócalo intentando loggearte ahora)...")
    try:
        code = wait_for_bucefalo_otp(debug=True)
        print(f"\n[ÉXITO] Código recibido: {code}")
        sys.exit(0)
    except OtpReaderError as e:
        print(f"\n[ERROR] {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()
