"""Enrich names for EspoCRM leads — API Recherche d'entreprises + Claude Haiku fallback.

Cible : leads Researching sans vrai firstName/lastName dans EspoCRM.
Deux stratégies (dans l'ordre) :
1. API Recherche d'entreprises (data.gouv.fr) — gratuit, sans auth, 7 req/s
2. Firecrawl scrape + Claude Haiku extraction — fallback si API ne trouve pas

Usage :
    uv run pipeline enrich-names --dry-run
    uv run pipeline enrich-names --limit 50
    uv run pipeline enrich-names --debug
"""

from __future__ import annotations

import asyncio
import json
from dataclasses import dataclass, field
from typing import Literal

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

from src.config import get_llm_config, get_settings
from src.enrichment.models import RateLimiter, _build_rate_limiter, _timed
from src.espocrm import _base_url, _headers, _is_retryable

log = structlog.get_logger()

# --- Data models ---

NameSource = Literal["api_entreprises", "claude_haiku", ""]


@dataclass(frozen=True)
class ExtractedName:
    """Résultat d'extraction de nom de dirigeant."""

    first_name: str
    last_name: str
    title: str = ""
    source: NameSource = ""


@dataclass
class EnrichStats:
    """Compteurs pour le résumé final."""

    total_researching: int = 0
    without_real_name: int = 0
    api_found: int = 0
    api_no_result: int = 0
    api_errors: int = 0
    claude_found: int = 0
    claude_no_result: int = 0
    claude_errors: int = 0
    updated_espo: int = 0
    updated_baserow: int = 0
    skipped_already_named: int = 0
    errors: int = 0


# --- 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 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_name(
    client: httpx.AsyncClient,
    lead_id: str,
    first_name: str,
    last_name: str,
) -> None:
    """Met à jour firstName/lastName d'un lead EspoCRM."""
    payload: dict = {
        "firstName": first_name,
        "lastName": last_name,
    }
    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 _update_baserow_name(
    client: httpx.AsyncClient,
    domain: str,
    first_name: str,
    last_name: str,
) -> bool:
    """Cherche le lead dans Baserow par domaine et met à jour Prenom/Nom."""
    settings = get_settings()
    try:
        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()
        rows = resp.json().get("results", [])
        if not rows:
            log.debug("enrich_names.baserow_not_found", domain=domain)
            return False

        row_id = rows[0]["id"]
        patch_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={"Prenom": first_name, "Nom": last_name},
            timeout=30.0,
        )
        patch_resp.raise_for_status()
        log.info("enrich_names.baserow_updated", domain=domain, row_id=row_id)
        return True
    except Exception:
        log.warning("enrich_names.baserow_error", domain=domain, exc_info=True)
        return False


# --- Stratégie 1 : API Recherche d'entreprises ---

_API_ENTREPRISES_BASE = "https://recherche-entreprises.api.gouv.fr/search"
# 7 req/s max — on met un semaphore + petit sleep
_api_semaphore = asyncio.Semaphore(5)


def _clean_company_name(raw: str) -> str:
    """Nettoie le nom d'entreprise pour l'API (retire descriptions, tirets, suffixes)."""
    import re
    import unicodedata

    name = raw.strip()
    # Retirer les caractères invisibles (ex: \u200e left-to-right mark)
    name = "".join(c for c in name if unicodedata.category(c) != "Cf")
    # Couper au premier " - " ou " | " ou " — " (titre de page web)
    for sep in (" - ", " | ", " — ", " – "):
        if sep in name:
            name = name.split(sep)[0].strip()
    # Retirer les suffixes courants de titres web
    for suffix in ("Reviews, Photos", "Avis, Photos", "Site officiel", "site officiel"):
        if name.endswith(suffix):
            name = name[: -len(suffix)].rstrip(" -|–—")
    # Retirer ponctuation finale
    name = name.strip(" -|–—.,")
    return name


async def _search_api_entreprises(
    client: httpx.AsyncClient,
    company_name: str,
    *,
    debug: bool = False,
) -> ExtractedName | None:
    """Cherche le dirigeant via l'API Recherche d'entreprises (data.gouv.fr).

    Returns ExtractedName si trouvé, None sinon.
    """
    if not company_name or len(company_name) < 3:
        return None

    # Nettoyer le nom avant la requête
    cleaned = _clean_company_name(company_name)
    if len(cleaned) < 3:
        return None

    if debug and cleaned != company_name:
        log.debug("enrich_names.api_cleaned", raw=company_name, cleaned=cleaned)

    async with _api_semaphore:
        try:
            async with _timed("api_entreprises"):
                resp = await client.get(
                    _API_ENTREPRISES_BASE,
                    params={"q": cleaned, "per_page": "3", "page": "1"},
                    timeout=15.0,
                )
                resp.raise_for_status()
                data = resp.json()

            results = data.get("results", [])
            if debug:
                log.debug(
                    "enrich_names.api_response",
                    company=company_name,
                    total=data.get("total_results", 0),
                    results_count=len(results),
                )

            # Rôles exclus (auditeurs, pas des dirigeants opérationnels)
            _EXCLUDED_ROLES = {"commissaire aux comptes titulaire", "commissaire aux comptes suppléant"}
            # Rôles prioritaires (dirigeants opérationnels)
            _PRIORITY_ROLES = {"gérant", "président", "président de sas", "président de sarl",
                               "directeur général", "fondateur", "co-gérant", "président du directoire"}

            all_candidates: list[tuple[dict, str, int]] = []  # (dir_entry, entreprise_name, priority)

            for entreprise in results:
                dirigeants = entreprise.get("dirigeants", [])
                nom_complet = entreprise.get("nom_complet", "")
                if debug:
                    log.debug(
                        "enrich_names.api_dirigeants",
                        company=company_name,
                        nom_complet=nom_complet,
                        dirigeants_count=len(dirigeants),
                        dirigeants_raw=dirigeants[:3],
                    )

                for dir_entry in dirigeants:
                    type_dir = dir_entry.get("type_dirigeant", "")
                    if type_dir != "personne physique":
                        continue

                    qualite = (dir_entry.get("qualite", "") or "").strip()
                    qualite_lower = qualite.lower()

                    # Exclure les auditeurs
                    if qualite_lower in _EXCLUDED_ROLES:
                        continue

                    nom = (dir_entry.get("nom", "") or "").strip()
                    prenoms = (dir_entry.get("prenoms", "") or "").strip()
                    if not nom or not prenoms:
                        continue

                    # Priorité : 0 = rôle clé, 1 = autre
                    priority = 0 if qualite_lower in _PRIORITY_ROLES else 1
                    all_candidates.append((dir_entry, nom_complet, priority))

            # Trier par priorité (0 d'abord)
            all_candidates.sort(key=lambda x: x[2])

            if all_candidates:
                dir_entry, _ent_name, _prio = all_candidates[0]
                nom = (dir_entry.get("nom", "") or "").strip()
                prenoms = (dir_entry.get("prenoms", "") or "").strip()
                qualite = (dir_entry.get("qualite", "") or "").strip()

                # Prendre le premier prénom si multiples
                first = prenoms.split(",")[0].strip().split(" ")[0].strip()
                first = first.capitalize()
                # Nettoyer le nom : retirer parenthèses (ex: "JOMBART (AMASSE)" → "Jombart")
                import re
                last = re.sub(r"\s*\(.*?\)\s*", "", nom).strip().capitalize()

                log.info(
                    "enrich_names.api_found",
                    company=company_name,
                    first_name=first,
                    last_name=last,
                    title=qualite,
                )
                return ExtractedName(
                    first_name=first,
                    last_name=last,
                    title=qualite,
                    source="api_entreprises",
                )

            # Aucun dirigeant personne physique trouvé
            return None

        except httpx.HTTPStatusError as exc:
            log.warning(
                "enrich_names.api_http_error",
                company=company_name,
                status=exc.response.status_code,
            )
            return None
        except Exception as exc:
            log.warning(
                "enrich_names.api_error",
                company=company_name,
                error=f"{type(exc).__name__}: {exc}",
            )
            return None
        finally:
            # Rate limit : 7 req/s max → ~150ms entre chaque
            await asyncio.sleep(0.15)


# --- Stratégie 2 : Firecrawl scrape + Claude Haiku ---

_NAME_EXTRACTION_PROMPT = """\
Tu analyses le contenu de plusieurs pages d'un site web (accueil, contact, à propos, mentions légales).
Extrais UNIQUEMENT le nom du dirigeant/gérant/fondateur/président de l'entreprise.

Retourne UNIQUEMENT du JSON valide :
{
  "first_name": "Prénom du dirigeant",
  "last_name": "Nom de famille du dirigeant",
  "title": "Titre exact (Gérant, Président, Fondateur, Directeur, etc.)"
}

INDICES pour trouver le dirigeant :
- Mentions légales : "Directeur de la publication", "Responsable", "Éditeur"
- Page à propos : fondateur, gérant, CEO, dirigeant
- Signature ou bio dans le contenu
- "L'équipe" : chercher le rôle le plus haut

Si tu ne trouves AUCUN nom de dirigeant, retourne : {"first_name": "", "last_name": "", "title": ""}

Contenu du site :
"""


async def _claude_extract_name(
    markdown: str,
    rate_limiter: RateLimiter,
    *,
    debug: bool = False,
) -> ExtractedName | None:
    """Extrait le nom du dirigeant via Claude Haiku."""
    if not markdown or len(markdown) < 50:
        return None

    await rate_limiter.acquire()
    settings = get_settings()
    llm = get_llm_config()
    aclient = anthropic.AsyncAnthropic(api_key=settings.anthropic_api_key)

    truncated = markdown[: llm.extraction_max_input_chars]

    try:
        async with _timed("claude_extract_name"):
            message = await aclient.messages.create(
                model=llm.extraction_model,
                max_tokens=256,
                messages=[
                    {"role": "user", "content": _NAME_EXTRACTION_PROMPT + truncated},
                    {"role": "assistant", "content": "{"},
                ],
            )

        text_block = next((b for b in message.content if b.type == "text"), None)
        if not text_block:
            return None

        json_text = "{" + text_block.text
        if debug:
            log.debug("enrich_names.claude_raw", response=json_text[:300])

        # Extraire le premier objet JSON (Haiku ajoute parfois du texte après)
        try:
            data = json.loads(json_text)
        except json.JSONDecodeError:
            # Trouver la fin du premier objet JSON
            brace_count = 0
            end_idx = -1
            for idx, char in enumerate(json_text):
                if char == "{":
                    brace_count += 1
                elif char == "}":
                    brace_count -= 1
                    if brace_count == 0:
                        end_idx = idx
                        break
            if end_idx > 0:
                try:
                    data = json.loads(json_text[: end_idx + 1])
                except json.JSONDecodeError:
                    log.warning("enrich_names.claude_json_error", raw=json_text[:200])
                    return None
            else:
                log.warning("enrich_names.claude_json_error", raw=json_text[:200])
                return None

        first = (data.get("first_name") or "").strip()
        last = (data.get("last_name") or "").strip()
        title = (data.get("title") or "").strip()

        if first and last:
            return ExtractedName(
                first_name=first,
                last_name=last,
                title=title,
                source="claude_haiku",
            )
        return None

    except Exception as exc:
        log.warning("enrich_names.claude_error", error=f"{type(exc).__name__}: {exc}")
        return None


async def _scrape_and_extract_name(
    website: str,
    rate_limiter: RateLimiter,
    *,
    debug: bool = False,
) -> ExtractedName | None:
    """Scrape le site web puis extrait le nom via Claude Haiku."""
    from firecrawl import AsyncFirecrawl

    from src.enrichment.scraping import _scrape_multi_pages

    settings = get_settings()
    fc = AsyncFirecrawl(
        api_key=settings.firecrawl_api_key,
        api_url=settings.firecrawl_base_url,
    )

    try:
        async with _timed("firecrawl_scrape"):
            markdown = await _scrape_multi_pages(fc, website)

        if debug:
            log.debug(
                "enrich_names.scrape_result",
                website=website,
                markdown_len=len(markdown),
                preview=markdown[:200] if markdown else "(vide)",
            )

        if not markdown:
            log.info("enrich_names.scrape_empty", website=website)
            return None

        return await _claude_extract_name(markdown, rate_limiter, debug=debug)

    except Exception as exc:
        log.warning(
            "enrich_names.scrape_error",
            website=website,
            error=f"{type(exc).__name__}: {exc}",
        )
        return None


# --- Helpers ---

def _has_real_name(lead: dict) -> bool:
    """Vérifie si le lead a un vrai nom (pas juste accountName en fallback)."""
    first = (lead.get("firstName") or "").strip()
    last = (lead.get("lastName") or "").strip()
    company = (lead.get("accountName") or "").strip()

    # Pas de nom du tout
    if not first and not last:
        return False

    # lastName == accountName = fallback EspoCRM (pas un vrai nom)
    if not first and last == company:
        return False

    return True


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."""
    company = lead.get("accountName", "")
    domain = _domain_from_website(lead.get("website", ""))
    return company or domain or lead.get("id", "?")


# --- Main ---

async def enrich_names(
    *,
    dry_run: bool = False,
    limit: int = 500,
    debug: bool = False,
) -> EnrichStats:
    """Enrichit les noms des leads Researching.

    Flow par lead :
    1. API Recherche d'entreprises (par accountName)
    2. Si pas trouvé → Firecrawl scrape + Claude Haiku
    3. Update EspoCRM + Baserow

    Returns EnrichStats avec tous les compteurs.
    """
    stats = EnrichStats()
    rate_limiter = _build_rate_limiter("anthropic")

    async with httpx.AsyncClient() as client:
        # 1. Fetch tous les leads Researching (paginé)
        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("enrich_names.fetched", total=len(all_leads))

        # 2. Filtrer ceux sans vrai nom
        no_name = [lead for lead in all_leads if not _has_real_name(lead)]
        stats.without_real_name = len(no_name)
        stats.skipped_already_named = len(all_leads) - len(no_name)

        targets = no_name[:limit]
        log.info(
            "enrich_names.targets",
            without_name=len(no_name),
            already_named=stats.skipped_already_named,
            processing=len(targets),
        )

        if not targets:
            log.info("enrich_names.nothing_to_do")
            return stats

        # 3. Enrichir chaque lead
        total_targets = len(targets)
        for i, lead in enumerate(targets, 1):
            lead_id = lead["id"]
            company = (lead.get("accountName") or "").strip()
            website = (lead.get("website") or "").strip()
            domain = _domain_from_website(website)
            label = _lead_label(lead)

            structlog.contextvars.bind_contextvars(
                lead_domain=domain,
                lead_company=company,
                progress=f"[{i}/{total_targets}]",
            )

            log.info(
                "enrich_names.processing",
                lead_id=lead_id,
                company=company,
                domain=domain,
                website=website,
            )

            extracted: ExtractedName | None = None

            # Stratégie 1 : API Recherche d'entreprises
            if company:
                extracted = await _search_api_entreprises(client, company, debug=debug)
                if extracted:
                    stats.api_found += 1
                else:
                    stats.api_no_result += 1
                    log.info("enrich_names.api_miss", company=company)

            # Stratégie 2 : Firecrawl + Claude Haiku (fallback)
            if not extracted and website:
                log.info("enrich_names.fallback_claude", website=website)
                extracted = await _scrape_and_extract_name(
                    website, rate_limiter, debug=debug,
                )
                if extracted:
                    stats.claude_found += 1
                else:
                    stats.claude_no_result += 1
                    log.info("enrich_names.claude_miss", website=website)

            if not extracted:
                log.info("enrich_names.no_name_found", label=label)
                continue

            # 4. Update EspoCRM + Baserow
            log.info(
                "enrich_names.found",
                first_name=extracted.first_name,
                last_name=extracted.last_name,
                title=extracted.title,
                source=extracted.source,
                label=label,
            )

            if dry_run:
                log.info(
                    "enrich_names.would_update",
                    label=label,
                    first=extracted.first_name,
                    last=extracted.last_name,
                )
                stats.updated_espo += 1
                continue

            try:
                await _update_lead_name(
                    client, lead_id, extracted.first_name, extracted.last_name,
                )
                stats.updated_espo += 1
                log.info(
                    "enrich_names.espo_updated",
                    lead_id=lead_id,
                    first=extracted.first_name,
                    last=extracted.last_name,
                )
            except Exception as exc:
                stats.errors += 1
                log.exception(
                    "enrich_names.espo_update_error",
                    lead_id=lead_id,
                    error=f"{type(exc).__name__}: {exc}",
                )
                continue

            # Update Baserow aussi
            if domain:
                ok = await _update_baserow_name(
                    client, domain, extracted.first_name, extracted.last_name,
                )
                if ok:
                    stats.updated_baserow += 1

            structlog.contextvars.unbind_contextvars("lead_domain", "lead_company", "progress")

    log.info(
        "enrich_names.done",
        api_found=stats.api_found,
        claude_found=stats.claude_found,
        updated_espo=stats.updated_espo,
        updated_baserow=stats.updated_baserow,
        errors=stats.errors,
    )
    return stats
