"""Find emails for EspoCRM leads — Reacher ou email-sleuth.

Cible : leads Researching (ou New) sans emailAddress dans EspoCRM.
2 backends disponibles :
- reacher : Reacher SMTP API (permutations une par une)
- sleuth  : email-sleuth CLI Rust (batch, 3 modes, confidence scoring)

Usage :
    uv run pipeline find-emails --dry-run
    uv run pipeline find-emails --limit 50
    uv run pipeline find-emails --thorough                    # Reacher + plus de variantes
    uv run pipeline find-emails --local                       # Reacher Docker local
    uv run pipeline find-emails --backend sleuth              # email-sleuth enhanced
    uv run pipeline find-emails --backend sleuth --mode comprehensive
"""

from __future__ import annotations

import asyncio
import unicodedata

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

from src.config import get_settings, get_reacher_config
from src.enrichment.email_finder import (
    _email_generic_ping,
    _email_guess_reacher,
    _generate_email_permutations,
    _normalize_name,
    _reacher_check_email,
)
from src.espocrm import _base_url, _headers, _is_retryable

log = structlog.get_logger()


# --- Extended permutations (thorough mode) ---

def _generate_thorough_permutations(first: str, last: str, domain: str) -> list[str]:
    """Permutations étendues — ~60-80 variations pour maximiser les chances.

    En plus des ~22 patterns standard, ajoute :
    - Premières 2/3 lettres du prénom + nom
    - Variations compound (noms composés : dupont-martin → dupont, martin, dm)
    - Patterns avec suffixes numériques (prenom.nom1, prenom.nom2)
    - Inversions rares (nom_prenom, nomprénom)
    - Patterns courants en France (p.nom, pnom, nom.p)
    """
    f = _normalize_name(first)
    l = _normalize_name(last)
    if not f or not l:
        return []

    # Commencer par les patterns standard
    base = _generate_email_permutations(first, last, domain)

    fi = f[0]
    li = l[0]
    f2 = f[:2] if len(f) >= 2 else f
    f3 = f[:3] if len(f) >= 3 else f
    l2 = l[:2] if len(l) >= 2 else l
    l3 = l[:3] if len(l) >= 3 else l

    # Variantes pour noms composés (dupont-martin → dupont, martin, dm)
    l_parts = l.split("-")
    l_nohyphen = l.replace("-", "")
    l_initials = "".join(p[0] for p in l_parts) if len(l_parts) > 1 else ""
    l_first_part = l_parts[0] if len(l_parts) > 1 else ""

    f_nohyphen = f.replace("-", "")

    extra: list[str | None] = [
        # Premières 2 lettres du prénom + nom
        f"{f2}{l}@{domain}",
        f"{f2}.{l}@{domain}",
        f"{f2}-{l}@{domain}",
        f"{f2}_{l}@{domain}",
        # Premières 3 lettres du prénom + nom
        f"{f3}{l}@{domain}" if f3 != f2 else None,
        f"{f3}.{l}@{domain}" if f3 != f2 else None,
        f"{f3}-{l}@{domain}" if f3 != f2 else None,
        # Premières 2 lettres du nom + prénom
        f"{l2}{f}@{domain}",
        f"{l2}.{f}@{domain}",
        # Premières 3 lettres du nom + prénom
        f"{l3}{f}@{domain}" if l3 != l2 else None,
        f"{l3}.{f}@{domain}" if l3 != l2 else None,
        # Prénom + 2/3 premières lettres du nom
        f"{f}{l2}@{domain}",
        f"{f}.{l2}@{domain}",
        f"{f}{l3}@{domain}" if l3 != l2 else None,
        f"{f}.{l3}@{domain}" if l3 != l2 else None,
        # Nom composé : première partie seule
        f"{f}.{l_first_part}@{domain}" if l_first_part else None,
        f"{f}{l_first_part}@{domain}" if l_first_part else None,
        f"{fi}{l_first_part}@{domain}" if l_first_part else None,
        f"{fi}.{l_first_part}@{domain}" if l_first_part else None,
        # Nom composé : initiales du nom
        f"{f}.{l_initials}@{domain}" if l_initials else None,
        f"{f}{l_initials}@{domain}" if l_initials else None,
        # Nom composé sans tiret
        f"{f}.{l_nohyphen}@{domain}" if l_nohyphen != l else None,
        f"{f}{l_nohyphen}@{domain}" if l_nohyphen != l else None,
        f"{fi}{l_nohyphen}@{domain}" if l_nohyphen != l else None,
        f"{fi}.{l_nohyphen}@{domain}" if l_nohyphen != l else None,
        # Patterns avec suffixes numériques (courant chez les PME)
        f"{f}.{l}1@{domain}",
        f"{f}{l}1@{domain}",
        # Patterns France courants
        f"{l}.{fi}@{domain}",
        f"{l}{fi}@{domain}",
        f"{l}_{fi}@{domain}",
        f"{l}-{fi}@{domain}",
        # Double initiales avec séparateurs
        f"{fi}-{li}@{domain}",
        f"{fi}_{li}@{domain}",
        f"{li}-{fi}@{domain}",
        f"{li}_{fi}@{domain}",
        # Prénom sans tiret + nom (jean-pierre → jeanpierre.dupont)
        f"{f_nohyphen}.{l}@{domain}" if f_nohyphen != f else None,
        f"{f_nohyphen}-{l}@{domain}" if f_nohyphen != f else None,
        f"{f_nohyphen}_{l}@{domain}" if f_nohyphen != f else None,
        f"{l}.{f_nohyphen}@{domain}" if f_nohyphen != f else None,
        f"{l}{f_nohyphen}@{domain}" if f_nohyphen != f else None,
    ]

    # Merge base + extra, dédupliquer
    seen = set(base)
    result = list(base)
    for p in extra:
        if p is not None and p not in seen:
            seen.add(p)
            result.append(p)

    return result


# --- Extended generic emails (thorough mode) ---

_GENERIC_EMAILS_THOROUGH = [
    "contact",
    "info",
    "hello",
    "bonjour",
    "accueil",
    "commercial",
    "direction",
    "vente",
    "ventes",
    "devis",
    "admin",
    "support",
    "service",
    "bienvenue",
    "courrier",
    "secretariat",
    "comptabilite",
    "rh",
    "communication",
    "marketing",
    "webmaster",
]


# --- EspoCRM helpers ---

@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 _fetch_researching_leads(
    client: httpx.AsyncClient, *, offset: int = 0, max_size: int = 200,
) -> tuple[list[dict], int]:
    """Fetch leads Researching (sans email) depuis EspoCRM."""
    params = {
        "where[0][type]": "equals",
        "where[0][attribute]": "status",
        "where[0][value]": "Researching",
        "select": "id,firstName,lastName,accountName,emailAddress,website,status",
        "orderBy": "createdAt",
        "order": "asc",
        "maxSize": str(max_size),
        "offset": str(offset),
    }
    resp = await client.get(
        f"{_base_url()}/api/v1/Lead",
        headers=_headers(),
        params=params,
        timeout=30.0,
    )
    resp.raise_for_status()
    data = resp.json()
    return data.get("list", []), data.get("total", 0)


@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_lead_email(
    client: httpx.AsyncClient,
    lead_id: str,
    email: str,
    *,
    confidence: str = "",
    source: str = "",
) -> None:
    """Met à jour l'email et passe le lead en Planned."""
    payload: dict = {
        "emailAddress": email,
        "status": "Planned",
    }
    if confidence:
        payload["cEmailConfidence"] = confidence
    if source:
        payload["cEmailSource"] = source

    resp = await client.put(
        f"{_base_url()}/api/v1/Lead/{lead_id}",
        headers=_headers(),
        json=payload,
        timeout=30.0,
    )
    resp.raise_for_status()


# --- Baserow helpers ---

async def _search_baserow_by_domain(client: httpx.AsyncClient, domain: str) -> list[dict]:
    """Cherche un lead Baserow par Domaine."""
    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__Domaine__equal": domain,
            "size": 5,
        },
        timeout=30.0,
    )
    resp.raise_for_status()
    return resp.json().get("results", [])


async def _update_baserow_email(
    client: httpx.AsyncClient, row_id: int, email: str, confidence: str = "",
) -> None:
    """Met à jour l'email + confidence dans Baserow."""
    settings = get_settings()
    payload: dict = {"Email Contact": email}
    if confidence:
        payload["Email Confidence"] = confidence
    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=payload,
        timeout=30.0,
    )
    resp.raise_for_status()


async def _sync_baserow(client: httpx.AsyncClient, domain: str, email: str, confidence: str = "") -> bool:
    """Cherche le lead dans Baserow par domaine et met à jour l'email. Returns True si mis à jour."""
    try:
        rows = await _search_baserow_by_domain(client, domain)
        if not rows:
            log.debug("find_emails.baserow_not_found", domain=domain)
            return False
        row_id = rows[0]["id"]
        await _update_baserow_email(client, row_id, email, confidence)
        log.info("find_emails.baserow_updated", domain=domain, row_id=row_id, email=email)
        return True
    except Exception:
        log.warning("find_emails.baserow_error", domain=domain, exc_info=True)
        return False


def _domain_from_website(website: str) -> str:
    """Extrait le domaine d'une URL website."""
    if not website:
        return ""
    d = website.lower().strip().rstrip("/")
    for prefix in ("https://www.", "http://www.", "https://", "http://"):
        if d.startswith(prefix):
            d = d[len(prefix):]
            break
    return d.split("/")[0]


def _lead_label(lead: dict) -> str:
    """Label lisible pour les logs."""
    name = f"{lead.get('firstName', '')} {lead.get('lastName', '')}".strip()
    company = lead.get("accountName", "")
    return f"{name} ({company})" if company else (name or "?")


# --- Backend: email-sleuth ---

async def _find_emails_sleuth(
    targets: list[dict],
    client: httpx.AsyncClient,
    *,
    mode: str = "enhanced",
    dry_run: bool = False,
    stats: dict,
) -> None:
    """Backend email-sleuth : batch CLI Rust."""
    from src.enrichment.sleuth import (
        sleuth_batch,
        parse_sleuth_result,
        sleuth_confidence_to_label,
    )

    # Préparer les contacts pour sleuth (seulement ceux avec nom)
    contacts_with_name: list[tuple[dict, dict]] = []
    contacts_without_name: list[dict] = []

    for lead in targets:
        first = (lead.get("firstName") or "").strip()
        last = (lead.get("lastName") or "").strip()
        website = (lead.get("website") or "").strip()
        domain = _domain_from_website(website)

        if not domain:
            stats["not_found"] += 1
            continue

        if first and last:
            contacts_with_name.append((lead, {"first_name": first, "last_name": last, "domain": domain}))
        else:
            contacts_without_name.append(lead)
            stats["without_name"] += 1

    if not contacts_with_name:
        log.info("sleuth.no_contacts_with_name")
        return

    # Lancer le batch
    sleuth_contacts = [c for _, c in contacts_with_name]
    log.info("sleuth.batch_start", contacts=len(sleuth_contacts), mode=mode)

    results = await sleuth_batch(sleuth_contacts, mode=mode)

    # Traiter les résultats
    for i, (lead, _contact) in enumerate(contacts_with_name):
        result = results[i] if i < len(results) else {}
        label = _lead_label(lead)
        lead_id = lead["id"]

        email, source, confidence_score = parse_sleuth_result(result)

        if email:
            confidence_label = sleuth_confidence_to_label(confidence_score)
            stats["email_found_nominative"] += 1

            log.info(
                "sleuth.found",
                name=label,
                email=email,
                confidence=confidence_score,
                source=source,
            )

            if dry_run:
                log.info("sleuth.would_update", name=label, email=email)
                stats["updated_espo"] += 1
            else:
                try:
                    await _update_lead_email(
                        client, lead_id, email,
                        confidence=confidence_label,
                        source=f"sleuth-{source}",
                    )
                    stats["updated_espo"] += 1
                    log.info("sleuth.updated", name=label, email=email)
                    # Sync Baserow aussi
                    domain = _contact.get("domain", "")
                    if domain:
                        await _sync_baserow(client, domain, email, confidence_label)
                except Exception:
                    stats["errors"] += 1
                    log.exception("sleuth.update_error", lead_id=lead_id)
        else:
            stats["not_found"] += 1
            log.info("sleuth.not_found", name=label)


# --- Backend: Reacher ---

async def _find_emails_reacher(
    targets: list[dict],
    client: httpx.AsyncClient,
    *,
    thorough: bool = False,
    local: bool = False,
    dry_run: bool = False,
    stats: dict,
) -> None:
    """Backend Reacher : permutations SMTP une par une."""
    rcfg = get_reacher_config(local=local)
    reacher_url = rcfg["url"]
    reacher_secret = rcfg["secret"]
    reacher_from_email = rcfg["from_email"]

    async def _check_email(candidate: str, idx: int, total: int, label: str) -> dict:
        stats["reacher_checks"] += 1
        result = await _reacher_check_email(
            client, candidate,
            reacher_url=reacher_url,
            reacher_secret=reacher_secret,
            reacher_from_email=reacher_from_email,
        )
        reachable = result.get("is_reachable", "unknown")
        smtp_info = result.get("smtp", {})
        is_catch_all = smtp_info.get("is_catch_all", False)

        log.info(
            "find_emails.check",
            progress=f"[{idx}/{total}]",
            email=candidate,
            result=reachable,
            catch_all=is_catch_all,
            name=label,
        )
        return result

    total_targets = len(targets)
    for lead_num, lead in enumerate(targets, 1):
        lead_id = lead["id"]
        first = (lead.get("firstName") or "").strip()
        last = (lead.get("lastName") or "").strip()
        website = (lead.get("website") or "").strip()
        domain = _domain_from_website(website)
        label = _lead_label(lead)

        log.info(
            "find_emails.lead_start",
            progress=f"[{lead_num}/{total_targets}]",
            name=label,
            domain=domain,
            has_name=bool(first and last),
        )

        if not domain:
            log.info("find_emails.no_domain", name=label)
            stats["not_found"] += 1
            continue

        email = ""
        confidence = ""
        source = ""

        # 1. Permutations nominatives via Reacher
        if first and last:
            if thorough:
                permutations = _generate_thorough_permutations(first, last, domain)
            else:
                permutations = _generate_email_permutations(first, last, domain)

            total_perms = len(permutations)
            log.info(
                "find_emails.perms_start",
                domain=domain,
                count=total_perms,
                mode="thorough" if thorough else "normal",
            )

            for idx, candidate in enumerate(permutations, 1):
                result = await _check_email(candidate, idx, total_perms, label)
                reachable = result.get("is_reachable", "unknown")

                if reachable == "safe":
                    email = candidate
                    confidence = "high"
                    source = "reacher"
                    stats["email_found_nominative"] += 1
                    log.info("find_emails.found_nominative", email=email, name=label)
                    break

                smtp_info = result.get("smtp", {})
                if smtp_info.get("is_catch_all"):
                    if thorough:
                        email = permutations[0]
                        confidence = "catch-all"
                        source = "reacher"
                        stats["catch_all"] += 1
                        log.info("find_emails.catch_all_kept", email=email, name=label)
                    else:
                        stats["catch_all"] += 1
                        log.info("find_emails.catch_all_skip", name=label, domain=domain)
                    break

                mx_info = result.get("mx", {})
                if mx_info and not mx_info.get("accepts_mail", True):
                    log.info("find_emails.no_mx", domain=domain)
                    break
        else:
            stats["without_name"] += 1
            log.info("find_emails.no_name", name=label, domain=domain)

        # 2. Fallback : emails génériques
        if not email:
            generics = _GENERIC_EMAILS_THOROUGH if thorough else [
                "contact", "info", "hello", "bonjour", "accueil", "commercial", "direction",
            ]
            total_gen = len(generics)
            log.info("find_emails.generic_start", domain=domain, count=total_gen)

            for idx, prefix in enumerate(generics, 1):
                candidate = f"{prefix}@{domain}"
                result = await _check_email(candidate, idx, total_gen, label)
                reachable = result.get("is_reachable", "unknown")

                if reachable == "safe":
                    email = candidate
                    confidence = "low" if prefix in ("contact", "info", "hello", "bonjour") else "medium"
                    source = "generic"
                    stats["email_found_generic"] += 1
                    log.info("find_emails.found_generic", email=email, name=label)
                    break

                smtp_info = result.get("smtp", {})
                if smtp_info.get("is_catch_all"):
                    break

                mx_info = result.get("mx", {})
                if mx_info and not mx_info.get("accepts_mail", True):
                    break

        if not email:
            stats["not_found"] += 1
            log.info("find_emails.not_found", name=label, domain=domain)
            continue

        # 3. Update EspoCRM + Baserow
        if dry_run:
            log.info(
                "find_emails.would_update",
                name=label,
                email=email,
                confidence=confidence,
            )
            stats["updated_espo"] += 1
        else:
            try:
                await _update_lead_email(
                    client, lead_id, email,
                    confidence=confidence,
                    source=source,
                )
                stats["updated_espo"] += 1
                log.info(
                    "find_emails.updated",
                    name=label,
                    email=email,
                    confidence=confidence,
                )
                # Sync Baserow aussi
                if domain:
                    await _sync_baserow(client, domain, email, confidence)
            except Exception:
                stats["errors"] += 1
                log.exception("find_emails.update_error", lead_id=lead_id)


# --- Main ---

async def find_emails(
    *,
    dry_run: bool = False,
    limit: int = 500,
    thorough: bool = False,
    concurrency: int = 3,
    local: bool = False,
    backend: str = "reacher",
    sleuth_mode: str = "enhanced",
) -> dict[str, int]:
    """Cherche les emails des leads Researching.

    Args:
        backend: "reacher" ou "sleuth"
        sleuth_mode: basic, enhanced, comprehensive (pour backend=sleuth)
        local: Reacher Docker local (pour backend=reacher)
        thorough: permutations étendues (pour backend=reacher)

    Returns dict avec les compteurs.
    """
    log.info(
        "find_emails.config",
        backend=backend,
        local=local,
        thorough=thorough,
        sleuth_mode=sleuth_mode if backend == "sleuth" else None,
    )

    stats: dict[str, int] = {
        "total_researching": 0,
        "without_email": 0,
        "without_name": 0,
        "email_found_nominative": 0,
        "email_found_generic": 0,
        "catch_all": 0,
        "not_found": 0,
        "updated_espo": 0,
        "errors": 0,
        "reacher_checks": 0,
    }

    # Fetch all Researching leads
    async with httpx.AsyncClient() as client:
        all_leads: list[dict] = []
        offset = 0

        while True:
            leads, total = await _fetch_researching_leads(client, offset=offset)
            all_leads.extend(leads)
            if not leads or len(all_leads) >= total:
                break
            offset += len(leads)

        stats["total_researching"] = len(all_leads)
        log.info("find_emails.fetched", total=len(all_leads))

        # Filtrer ceux sans email
        no_email = [
            l for l in all_leads
            if not (l.get("emailAddress") or "").strip()
        ]
        stats["without_email"] = len(no_email)

        # Appliquer limit
        targets = no_email[:limit]
        log.info("find_emails.targets", count=len(targets), backend=backend)

        if backend == "sleuth":
            await _find_emails_sleuth(
                targets, client,
                mode=sleuth_mode,
                dry_run=dry_run,
                stats=stats,
            )
        else:
            await _find_emails_reacher(
                targets, client,
                thorough=thorough,
                local=local,
                dry_run=dry_run,
                stats=stats,
            )

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