"""Sync Resend events → EspoCRM + Baserow.

- bounced/suppressed/complained → Dead
- delivered/opened/clicked → Contacted (tout sauf Dead)
"""

from __future__ import annotations

import asyncio
import re

import httpx
import structlog
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception

from src.config import get_settings
from src.espocrm import _base_url, _headers, _is_retryable

log = structlog.get_logger()

DEAD_EVENTS = frozenset({"bounced", "suppressed", "complained"})
CONTACTED_EVENTS = frozenset({"delivered", "opened", "clicked"})
_EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")

RESEND_API = "https://api.resend.com"


# --- Resend API ---


async def _fetch_all_resend_emails(client: httpx.AsyncClient, api_key: str) -> list[dict]:
    """Pagine toute la liste des emails envoyés via Resend API."""
    all_emails: list[dict] = []
    after: str | None = None

    while True:
        params: dict[str, str | int] = {"limit": 100}
        if after:
            params["after"] = after

        # Retry sur 429 (rate limit Resend)
        for attempt in range(5):
            resp = await client.get(
                f"{RESEND_API}/emails",
                headers={"Authorization": f"Bearer {api_key}"},
                params=params,
                timeout=30.0,
            )
            if resp.status_code == 429:
                wait = 2 ** attempt  # 1s, 2s, 4s, 8s, 16s
                log.warning("sync_resend.rate_limited", attempt=attempt + 1, wait_s=wait)
                await asyncio.sleep(wait)
                continue
            break

        resp.raise_for_status()
        body = resp.json()

        data = body.get("data", [])
        all_emails.extend(data)

        if not body.get("has_more") or not data:
            break
        after = data[-1]["id"]
        # Petit délai entre pages pour éviter le 429
        await asyncio.sleep(0.5)

    return all_emails


def _extract_bounced_emails(emails: list[dict]) -> list[dict[str, str]]:
    """Filtre les emails bounced/suppressed et extrait les adresses."""
    bounces: list[dict[str, str]] = []
    for email in emails:
        event = (email.get("last_event") or "").lower()
        if event not in DEAD_EVENTS:
            continue

        to_list = email.get("to", [])
        for to_addr in to_list:
            # Peut être "user@domain.com" ou """Nom"" <user@domain.com>"
            match = _EMAIL_RE.search(to_addr)
            addr = match.group(0) if match else to_addr.strip()
            if addr:
                bounces.append({"email": addr, "event": event, "resend_id": email.get("id", "")})

    return bounces


def _extract_contacted_emails(emails: list[dict]) -> list[dict[str, str]]:
    """Filtre les emails delivered/opened/clicked et extrait les adresses."""
    contacted: list[dict[str, str]] = []
    for email in emails:
        event = (email.get("last_event") or "").lower()
        if event not in CONTACTED_EVENTS:
            continue

        to_list = email.get("to", [])
        for to_addr in to_list:
            match = _EMAIL_RE.search(to_addr)
            addr = match.group(0) if match else to_addr.strip()
            if addr:
                contacted.append({"email": addr, "event": event, "resend_id": email.get("id", "")})

    return contacted


# --- EspoCRM ---


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=2, max=10),
    retry=retry_if_exception(_is_retryable),
    reraise=True,
)
async def _search_espo_lead_by_email(client: httpx.AsyncClient, email: str) -> list[dict]:
    """Cherche un Lead EspoCRM par emailAddress."""
    params = {
        "where[0][type]": "equals",
        "where[0][attribute]": "emailAddress",
        "where[0][value]": email,
        "select": "id,firstName,lastName,emailAddress,status",
        "maxSize": "5",
    }
    resp = await client.get(
        f"{_base_url()}/api/v1/Lead",
        headers=_headers(),
        params=params,
        timeout=30.0,
    )
    resp.raise_for_status()
    return resp.json().get("list", [])


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=2, max=10),
    retry=retry_if_exception(_is_retryable),
    reraise=True,
)
async def _update_espo_lead_dead(client: httpx.AsyncClient, lead_id: str) -> None:
    """Met à jour le status d'un lead EspoCRM à Dead."""
    resp = await client.put(
        f"{_base_url()}/api/v1/Lead/{lead_id}",
        headers=_headers(),
        json={"status": "Dead"},
        timeout=30.0,
    )
    resp.raise_for_status()


@retry(
    stop=stop_after_attempt(3),
    wait=wait_exponential(multiplier=2, min=2, max=10),
    retry=retry_if_exception(_is_retryable),
    reraise=True,
)
async def _update_espo_lead_contacted(client: httpx.AsyncClient, lead_id: str) -> None:
    """Met à jour le status d'un lead EspoCRM à Contacted."""
    resp = await client.put(
        f"{_base_url()}/api/v1/Lead/{lead_id}",
        headers=_headers(),
        json={"status": "Contacted"},
        timeout=30.0,
    )
    resp.raise_for_status()


# --- Baserow ---


async def _search_baserow_by_email(client: httpx.AsyncClient, email: str) -> list[dict]:
    """Cherche un lead Baserow par Email Contact."""
    settings = get_settings()
    resp = await client.get(
        f"{settings.baserow_base_url}/api/database/rows/table/{settings.baserow_table_id}/",
        headers={"Authorization": f"Token {settings.baserow_token}"},
        params={
            "user_field_names": "true",
            "filter__Email Contact__equal": email,
            "size": 5,
        },
        timeout=30.0,
    )
    resp.raise_for_status()
    return resp.json().get("results", [])


async def _update_baserow_row_dead(client: httpx.AsyncClient, row_id: int) -> None:
    """Met à jour le Statut d'une row Baserow à Dead."""
    settings = get_settings()
    resp = await client.patch(
        f"{settings.baserow_base_url}/api/database/rows/table/{settings.baserow_table_id}/{row_id}/",
        headers={
            "Authorization": f"Token {settings.baserow_token}",
            "Content-Type": "application/json",
        },
        params={"user_field_names": "true"},
        json={"Statut": "Dead"},
        timeout=30.0,
    )
    resp.raise_for_status()


async def _update_baserow_row_contacted(client: httpx.AsyncClient, row_id: int) -> None:
    """Met à jour le Statut d'une row Baserow à Contacted."""
    settings = get_settings()
    resp = await client.patch(
        f"{settings.baserow_base_url}/api/database/rows/table/{settings.baserow_table_id}/{row_id}/",
        headers={
            "Authorization": f"Token {settings.baserow_token}",
            "Content-Type": "application/json",
        },
        params={"user_field_names": "true"},
        json={"Statut": "Contacted"},
        timeout=30.0,
    )
    resp.raise_for_status()


# --- Orchestration ---


async def sync_bounces(*, dry_run: bool = False) -> dict[str, int]:
    """Fetch events from Resend API, update EspoCRM + Baserow + SQLite.

    - bounced/suppressed/complained → Dead
    - delivered/opened/clicked → Contacted (tout sauf Dead)

    Utilise un cache SQLite (processed_resend_emails) pour ne pas re-traiter
    les emails Resend déjà processés. Seuls les nouveaux sont traités.
    """
    settings = get_settings()
    stats = {
        "total_emails": 0, "new_emails": 0, "cached": 0,
        "bounced": 0, "contacted": 0,
        "espo_dead": 0, "espo_contacted": 0,
        "baserow_dead": 0, "baserow_contacted": 0,
        "sqlite_updated": 0, "already_dead": 0, "already_contacted": 0,
        "not_found": 0, "errors": 0,
    }

    async with httpx.AsyncClient() as client:
        # 1. Fetch all sent emails from Resend
        log.info("sync_resend.fetch_resend")
        all_emails = await _fetch_all_resend_emails(client, settings.resend_api_key)
        stats["total_emails"] = len(all_emails)

        # 2. Charger le cache des IDs déjà traités
        from src.db import init_db, mark_processed, get_processed_resend_ids, mark_resend_processed
        await init_db()

        cached_ids = await get_processed_resend_ids()
        new_emails = [e for e in all_emails if e.get("id", "") not in cached_ids]
        stats["cached"] = len(all_emails) - len(new_emails)
        stats["new_emails"] = len(new_emails)

        log.info(
            "sync_resend.filtered",
            total=len(all_emails),
            cached=stats["cached"],
            new=len(new_emails),
        )

        if not new_emails:
            log.info("sync_resend.nothing_new")
            return stats

        # 3. Classifier les nouveaux emails
        bounces = _extract_bounced_emails(new_emails)
        contacted = _extract_contacted_emails(new_emails)
        stats["bounced"] = len(bounces)
        stats["contacted"] = len(contacted)
        log.info(
            "sync_resend.classified",
            bounced=len(bounces),
            contacted=len(contacted),
        )

        # Collecter les emails dead pour éviter de les marquer Contacted ensuite
        dead_emails: set[str] = set()

        # 4. Process bounces → Dead
        for bounce in bounces:
            email = bounce["email"]
            resend_id = bounce["resend_id"]
            dead_emails.add(email)
            domain = email.split("@")[-1] if "@" in email else ""

            try:
                # 4a. EspoCRM — search + update Dead
                if settings.espocrm_url and settings.espocrm_api_key:
                    leads = await _search_espo_lead_by_email(client, email)
                    for lead in leads:
                        if lead.get("status") == "Dead":
                            stats["already_dead"] += 1
                            log.debug("sync_resend.already_dead", email=email, lead_id=lead["id"])
                            continue
                        if dry_run:
                            log.info("sync_resend.would_dead_espo", email=email, lead_id=lead["id"])
                        else:
                            await _update_espo_lead_dead(client, lead["id"])
                            log.info("sync_resend.espo_dead", email=email, lead_id=lead["id"])
                        stats["espo_dead"] += 1

                    if not leads:
                        stats["not_found"] += 1
                        log.debug("sync_resend.espo_not_found", email=email)

                # 4b. Baserow — search + update Dead
                baserow_rows = await _search_baserow_by_email(client, email)
                for row in baserow_rows:
                    statut = row.get("Statut")
                    if isinstance(statut, dict):
                        statut = statut.get("value", "")
                    if statut == "Dead":
                        continue
                    if dry_run:
                        log.info("sync_resend.would_dead_baserow", email=email, row_id=row["id"])
                    else:
                        await _update_baserow_row_dead(client, row["id"])
                        log.info("sync_resend.baserow_dead", email=email, row_id=row["id"])
                    stats["baserow_dead"] += 1

                # 4c. SQLite — marquer le domaine comme dead
                if domain and not dry_run:
                    await mark_processed(domain, status="dead")
                    stats["sqlite_updated"] += 1

                # 4d. Marquer le resend_id comme traité
                if not dry_run and resend_id:
                    await mark_resend_processed(resend_id, email, "dead")

            except Exception:
                stats["errors"] += 1
                log.exception("sync_resend.error", email=email)

        # 5. Process contacted → Contacted (tout sauf Dead)
        for entry in contacted:
            email = entry["email"]
            resend_id = entry["resend_id"]

            # Ne pas promouvoir un email qui a aussi bouncé
            if email in dead_emails:
                if not dry_run and resend_id:
                    await mark_resend_processed(resend_id, email, "skipped_dead")
                continue

            try:
                # 5a. EspoCRM — tout sauf Dead/Contacted → Contacted
                if settings.espocrm_url and settings.espocrm_api_key:
                    leads = await _search_espo_lead_by_email(client, email)
                    for lead in leads:
                        status = lead.get("status", "")
                        if status == "Contacted":
                            stats["already_contacted"] += 1
                            continue
                        if status == "Dead":
                            continue
                        if dry_run:
                            log.info("sync_resend.would_contact_espo", email=email, lead_id=lead["id"], from_status=status)
                        else:
                            await _update_espo_lead_contacted(client, lead["id"])
                            log.info("sync_resend.espo_contacted", email=email, lead_id=lead["id"], from_status=status)
                        stats["espo_contacted"] += 1

                # 5b. Baserow — tout sauf Dead/Contacted → Contacted
                baserow_rows = await _search_baserow_by_email(client, email)
                for row in baserow_rows:
                    statut = row.get("Statut")
                    if isinstance(statut, dict):
                        statut = statut.get("value", "")
                    if statut == "Contacted":
                        stats["already_contacted"] += 1
                        continue
                    if statut == "Dead":
                        continue
                    if dry_run:
                        log.info("sync_resend.would_contact_baserow", email=email, row_id=row["id"], from_status=statut)
                    else:
                        await _update_baserow_row_contacted(client, row["id"])
                        log.info("sync_resend.baserow_contacted", email=email, row_id=row["id"], from_status=statut)
                    stats["baserow_contacted"] += 1

                # 5c. Marquer le resend_id comme traité
                if not dry_run and resend_id:
                    await mark_resend_processed(resend_id, email, "contacted")

            except Exception:
                stats["errors"] += 1
                log.exception("sync_resend.error_contacted", email=email)

    log.info("sync_resend.done", **stats)
    return stats
