"""
placsp_fetcher.py

Detector de licitaciones del PLACSP (Plataforma de Contratación del
Sector Público) mediante consumo del feed Atom oficial.

Filtros soportados:
  · CPV (códigos del Common Procurement Vocabulary, prefijo permitido)
  · Palabras clave (cualquier subcadena en título o resumen)
  · Importe mínimo / máximo (€)

Standard library only — sin dependencias externas. Pensado para correr
en cron Linux, GitHub Actions o Vercel Cron sin instalación adicional.

Fuente del feed Atom público:
  https://contrataciondelestado.es/sindicacion

Autor:       LloretIA (https://lloretia.com)
Licencia:    MIT
Versión:     1.0.0
Python:      3.10+
"""

from __future__ import annotations
import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
import re
import json
import sys
from datetime import datetime, timezone
from typing import Optional
from dataclasses import dataclass, asdict


# Endpoint oficial del feed Atom del PLACSP (licitaciones publicadas)
PLACSP_ATOM_URL = (
    "https://contrataciondelestado.es/sindicacion/sindicacion_1143/"
    "licitacionesPerfilesContratanteCompleto3.atom"
)

# Namespaces XML del PLACSP
NS = {"atom": "http://www.w3.org/2005/Atom"}


@dataclass
class Licitacion:
    """Representación normalizada de una licitación del PLACSP."""

    titulo: str
    enlace: str
    resumen: str
    actualizado: str
    importe: Optional[float] = None
    cpv_detectados: Optional[list[str]] = None


# ── Fetching ──────────────────────────────────────────────


def fetch_atom_feed(url: str = PLACSP_ATOM_URL, timeout: int = 30) -> str:
    """Descarga el feed Atom del PLACSP como texto UTF-8."""
    request = urllib.request.Request(
        url,
        headers={
            "User-Agent": "LloretIA-PLACSP-Fetcher/1.0 (+https://lloretia.com)",
            "Accept": "application/atom+xml",
        },
    )
    try:
        with urllib.request.urlopen(request, timeout=timeout) as response:
            return response.read().decode("utf-8")
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"PLACSP HTTP {e.code}: {e.reason}") from e
    except urllib.error.URLError as e:
        raise RuntimeError(f"PLACSP unreachable: {e.reason}") from e


def parse_atom(xml_data: str) -> list[Licitacion]:
    """Parsea el Atom feed y normaliza cada entry en Licitacion."""
    root = ET.fromstring(xml_data)
    licitaciones = []
    for entry in root.findall("atom:entry", NS):
        titulo = (entry.findtext("atom:title", default="", namespaces=NS) or "").strip()
        resumen = (entry.findtext("atom:summary", default="", namespaces=NS) or "").strip()
        actualizado = (entry.findtext("atom:updated", default="", namespaces=NS) or "").strip()
        link_el = entry.find("atom:link", NS)
        enlace = link_el.get("href", "") if link_el is not None else ""

        lic = Licitacion(
            titulo=titulo,
            enlace=enlace,
            resumen=resumen,
            actualizado=actualizado,
            importe=extract_importe(resumen),
            cpv_detectados=extract_cpvs(resumen),
        )
        licitaciones.append(lic)
    return licitaciones


# ── Extracción ───────────────────────────────────────────


def extract_importe(texto: str) -> Optional[float]:
    """Intenta extraer el importe base (en €) del resumen."""
    match = re.search(r"([\d\.]+(?:,\d{1,2})?)\s*€", texto)
    if not match:
        return None
    raw = match.group(1).replace(".", "").replace(",", ".")
    try:
        return float(raw)
    except ValueError:
        return None


def extract_cpvs(texto: str) -> list[str]:
    """Extrae códigos CPV (formato XXXXXXXX-X) mencionados en el texto."""
    return re.findall(r"\b\d{8}-\d\b", texto)


# ── Filtros ──────────────────────────────────────────────


def filter_by_cpv_prefix(
    licitaciones: list[Licitacion], prefijo: str
) -> list[Licitacion]:
    """Filtra licitaciones cuyo CPV detectado empiece por el prefijo dado.

    Ejemplos de prefijos comunes (CPV-2008):
      "45"     → Trabajos de construcción
      "4521"   → Calderería
      "4525"   → Trabajos de cubiertas
      "504100" → Servicios mantenimiento equipos médicos
    """
    return [
        lic
        for lic in licitaciones
        if lic.cpv_detectados
        and any(cpv.startswith(prefijo) for cpv in lic.cpv_detectados)
    ]


def filter_by_keywords(
    licitaciones: list[Licitacion], keywords: list[str]
) -> list[Licitacion]:
    """Filtra por palabras clave en título o resumen (case-insensitive)."""
    kw_lower = [k.lower() for k in keywords]
    matches = []
    for lic in licitaciones:
        text = (lic.titulo + " " + lic.resumen).lower()
        if any(k in text for k in kw_lower):
            matches.append(lic)
    return matches


def filter_by_importe(
    licitaciones: list[Licitacion],
    minimo: Optional[float] = None,
    maximo: Optional[float] = None,
) -> list[Licitacion]:
    """Filtra licitaciones cuyo importe esté en el rango [minimo, maximo]."""
    def en_rango(lic: Licitacion) -> bool:
        if lic.importe is None:
            return False
        if minimo is not None and lic.importe < minimo:
            return False
        if maximo is not None and lic.importe > maximo:
            return False
        return True

    return [lic for lic in licitaciones if en_rango(lic)]


# ── CLI ──────────────────────────────────────────────────
# Uso:
#   python placsp_fetcher.py
#   python placsp_fetcher.py --cpv 4521
#   python placsp_fetcher.py --keyword "calderería" --keyword "metalurgia"
#   python placsp_fetcher.py --min 50000 --max 500000

def main():
    import argparse

    parser = argparse.ArgumentParser(
        description="Detecta licitaciones del PLACSP filtradas por CPV / keywords / importe.",
    )
    parser.add_argument("--cpv", help="Prefijo de CPV (ej. 4521)", default=None)
    parser.add_argument(
        "--keyword",
        action="append",
        default=[],
        help="Palabra clave (acumulable; --keyword X --keyword Y).",
    )
    parser.add_argument("--min", type=float, default=None, help="Importe mínimo €.")
    parser.add_argument("--max", type=float, default=None, help="Importe máximo €.")
    parser.add_argument(
        "--limit", type=int, default=20, help="Máximo resultados a imprimir."
    )
    parser.add_argument(
        "--json", action="store_true", help="Salida en JSON (default: tabla)."
    )
    args = parser.parse_args()

    print(f"[{datetime.now(timezone.utc).isoformat()}] Fetching PLACSP feed...", file=sys.stderr)
    try:
        xml = fetch_atom_feed()
    except RuntimeError as e:
        print(f"ERROR: {e}", file=sys.stderr)
        sys.exit(1)

    licitaciones = parse_atom(xml)
    print(f"  → {len(licitaciones)} licitaciones recientes.", file=sys.stderr)

    resultados = licitaciones
    if args.cpv:
        resultados = filter_by_cpv_prefix(resultados, args.cpv)
    if args.keyword:
        resultados = filter_by_keywords(resultados, args.keyword)
    if args.min is not None or args.max is not None:
        resultados = filter_by_importe(resultados, args.min, args.max)

    resultados = resultados[: args.limit]
    print(f"  → {len(resultados)} tras filtros (limitado a {args.limit}).", file=sys.stderr)

    if args.json:
        out = [asdict(lic) for lic in resultados]
        print(json.dumps(out, indent=2, ensure_ascii=False))
    else:
        for i, lic in enumerate(resultados, 1):
            imp = f"{lic.importe:,.2f} €" if lic.importe else "—"
            print(f"\n{i}. {lic.titulo}")
            print(f"   Importe: {imp}")
            if lic.cpv_detectados:
                print(f"   CPV: {', '.join(lic.cpv_detectados)}")
            print(f"   Link: {lic.enlace}")


if __name__ == "__main__":
    main()
