"""Contrat commun à tous les collecteurs."""

from __future__ import annotations

import time
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import date

from pipeline.journal import logger
from pipeline.modeles import DecisionCC, Evenement, Source, Statut, TypeTexte

log = logger("collecteur")


@dataclass(slots=True)
class TexteCollecte:
    """Un texte observé sur une source officielle.

    Volontairement plus pauvre qu'un `Texte` : un collecteur rapporte ce qu'il
    voit, il ne classe pas. La classification et la cotation Guadeloupe sont
    faites ensuite, et signalées comme automatiques.
    """

    titre: str
    source_url: str
    collecteur: str

    numero_officiel: str | None = None
    type: TypeTexte | None = None
    statut: Statut | None = None
    date_promulgation: date | None = None
    date_adoption: date | None = None
    date_publication_jo: date | None = None
    identifiant_externe: str | None = None      # cid Légifrance, dossier AN…
    resume: str | None = None

    evenements: list[Evenement] = field(default_factory=list)
    decisions_cc: list[DecisionCC] = field(default_factory=list)
    sources: list[Source] = field(default_factory=list)


@dataclass(slots=True)
class ResultatCollecte:
    """Ce qu'un collecteur rapporte d'un passage."""

    collecteur: str
    textes: list[TexteCollecte] = field(default_factory=list)
    decisions_cc: list[DecisionCC] = field(default_factory=list)
    erreurs: list[str] = field(default_factory=list)
    duree_s: float = 0.0
    ignore: bool = False
    motif_ignore: str | None = None

    @property
    def a_reussi(self) -> bool:
        return not self.erreurs and not self.ignore


class Collecteur(ABC):
    """Un collecteur de source officielle.

    Les implémentations ne lèvent jamais : `collecter()` capture ses propres
    erreurs et les range dans le résultat. C'est ce qui permet au run complet de
    se poursuivre quand une source est indisponible.
    """

    nom: str = "inconnu"

    def est_disponible(self) -> tuple[bool, str | None]:
        """Indique si le collecteur peut travailler, et sinon pourquoi.

        Sert à documenter les dégradations : un collecteur privé de clé d'API
        doit le dire clairement plutôt que d'échouer silencieusement.
        """
        return True, None

    @abstractmethod
    def _collecter(self) -> ResultatCollecte:
        """Collecte effective, susceptible de lever."""

    def collecter(self) -> ResultatCollecte:
        depart = time.monotonic()
        disponible, motif = self.est_disponible()

        if not disponible:
            log.info("collecteur ignoré", collecteur=self.nom, motif=motif)
            return ResultatCollecte(
                collecteur=self.nom, ignore=True, motif_ignore=motif,
                duree_s=time.monotonic() - depart,
            )

        try:
            resultat = self._collecter()
        except Exception as erreur:  # noqa: BLE001 — un collecteur ne fait pas tomber le run
            log.warning("collecteur en échec", collecteur=self.nom, erreur=str(erreur))
            resultat = ResultatCollecte(collecteur=self.nom, erreurs=[str(erreur)])

        resultat.duree_s = time.monotonic() - depart
        log.info(
            "collecte terminée",
            collecteur=self.nom,
            textes=len(resultat.textes),
            decisions=len(resultat.decisions_cc),
            erreurs=len(resultat.erreurs),
            duree_s=round(resultat.duree_s, 2),
        )
        return resultat
