"""DRE (Diário da República) integration via the official RSS feeds.

The DRE site itself is an OutSystems SPA with no public JSON API, but the
publishing back-end exposes RSS feeds with the day's publications:

  - Série I (laws, government acts):
      https://files.diariodarepublica.pt/rss/serie1-html.xml
  - Série II (notices, assemblies, dissolutions, contracts):
      https://files.diariodarepublica.pt/rss/serie2-html.xml

Each RSS item carries a title, description (entity + content summary), and
a link to the publication detail. We fetch both feeds once per scrape, then
match each item locally against every monitored company's name (substring,
case-insensitive, with a short distinctive search term).

Note: the RSS feeds only contain the *current day's* publications. Historical
deep search would require browser automation. For ongoing monitoring this is
exactly what we need.
"""
import hashlib
import json
import logging
import re
from datetime import date, datetime
from typing import Any

import httpx
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.scrapers.distribuicao import short_search_term

logger = logging.getLogger(__name__)

FEEDS = {
    "I": "https://files.diariodarepublica.pt/rss/serie1-html.xml",
    "II": "https://files.diariodarepublica.pt/rss/serie2-html.xml",
}

# Série II Parte E carries éditos and citações judiciais (court summons
# from Caixa de Previdência, notificações a beneficiários, convocatórias de
# herdeiros, etc). Niche but useful for cross-reference: when a newly added
# company's distintivo appears as subject of such a notice, the retro match
# surfaces it even though it never hit our CITIUS feeds.
#
# NOTE — the original plan was to archive "atos societários" (nomeações,
# alterações de capital, liquidações) here, but that data is NOT in DRE
# Parte E in the modern DRE (post-2006). Those acts are published via
# publicacoes.mj.pt, which is blocked by reCAPTCHA v3 — see the dormant
# implementation in app/services/mj_publications.py. Revive when a LLM/
# 2Captcha stack is available.
RAW_ARCHIVE_FEED = (
    "https://files.diariodarepublica.pt/rss/serie2&parte=e-html.xml"
)
RAW_ARCHIVE_SERIES = "II"
RAW_ARCHIVE_PART = "E"

ITEM_RE = re.compile(r"<item>(.*?)</item>", re.DOTALL)
TITLE_RE = re.compile(r"<title>\s*(.*?)\s*</title>", re.DOTALL)
DESC_RE = re.compile(
    r"<description[^>]*>\s*(?:<!\[CDATA\[(.*?)\]\]>|(.*?))\s*</description>", re.DOTALL
)
LINK_RE = re.compile(r"<link>\s*(?:<!\[CDATA\[(.*?)\]\]>|(.*?))\s*</link>", re.DOTALL)
DATE_IN_TITLE_RE = re.compile(r"de\s+(\d{4})-(\d{2})-(\d{2})")
TYPE_PREFIX_RE = re.compile(r"^([A-Za-zÇçãõéáíúâêôÉÁÍÚÂÊÔ]+(?:\s\(extrato\))?)\s+n\.")


def _parse_feed(xml: str) -> list[dict[str, Any]]:
    out: list[dict[str, Any]] = []
    for raw in ITEM_RE.findall(xml):
        title_m = TITLE_RE.search(raw)
        desc_m = DESC_RE.search(raw)
        link_m = LINK_RE.search(raw)
        title = (title_m.group(1) if title_m else "").strip()
        desc = ""
        if desc_m:
            desc = (desc_m.group(1) or desc_m.group(2) or "").strip()
        link = ""
        if link_m:
            link = (link_m.group(1) or link_m.group(2) or "").strip()
        if not title:
            continue
        # Date inside title: "...Série II de 2026-04-17"
        d_m = DATE_IN_TITLE_RE.search(title)
        pub_date: date | None = None
        if d_m:
            try:
                pub_date = date(int(d_m.group(1)), int(d_m.group(2)), int(d_m.group(3)))
            except ValueError:
                pub_date = None
        # Document type prefix: "Despacho (extrato) n.º 5039/2026 ..." -> "Despacho (extrato)"
        type_m = TYPE_PREFIX_RE.match(title)
        doc_type = type_m.group(1) if type_m else None
        out.append(
            {
                "title": title,
                "summary": desc[:1000] if desc else None,
                "date": pub_date,
                "type": doc_type,
                "source_url": link,
                "raw": {"title": title, "description": desc, "link": link},
            }
        )
    return out


async def _fetch_all_feeds() -> list[dict[str, Any]]:
    async with httpx.AsyncClient(
        timeout=httpx.Timeout(10.0, connect=5.0),
        headers={"User-Agent": "Mozilla/5.0 (judicial-monitor)"},
    ) as client:
        items: list[dict[str, Any]] = []
        for series, url in FEEDS.items():
            try:
                r = await client.get(url)
                r.raise_for_status()
                parsed = _parse_feed(r.text)
                for it in parsed:
                    it["raw"]["series"] = series
                items.extend(parsed)
                logger.info("DRE Série %s: %d items", series, len(parsed))
            except httpx.HTTPError as e:
                logger.warning("DRE Série %s fetch failed: %s", series, e)
    return items


def _haystack(item: dict[str, Any]) -> str:
    return f"{item.get('title') or ''}\n{item.get('summary') or ''}".lower()


def _matches(haystack_lo: str, search_term: str, nif: str | None) -> bool:
    if nif and nif in haystack_lo:
        return True
    if search_term:
        # Word-boundary substring to avoid e.g. "SOL" matching "solitário"
        pattern = r"\b" + re.escape(search_term.lower()) + r"\b"
        if re.search(pattern, haystack_lo):
            return True
    return False


def _company_search_term(legal_name: str) -> str:
    """Distinctive search token derived from legal_name (≥4 chars to limit
    false positives)."""
    term = short_search_term(legal_name).strip()
    if len(term) < 4:
        return ""
    return term


async def fetch_dre_publications(company: dict[str, Any]) -> list[dict[str, Any]]:
    """Single-company entry point (kept for API/CLI parity).

    Inefficient when called for many companies — prefer
    `fetch_dre_for_companies` in the scheduler hot path.
    """
    items = await _fetch_all_feeds()
    return _filter_for_company(items, company)


def _filter_for_company(
    items: list[dict[str, Any]], company: dict[str, Any]
) -> list[dict[str, Any]]:
    term = _company_search_term(company.get("legal_name") or "")
    if not term and not company.get("nif"):
        return []
    out: list[dict[str, Any]] = []
    for it in items:
        h = _haystack(it)
        if _matches(h, term, company.get("nif")):
            out.append(it)
    return out


async def fetch_dre_for_companies(
    companies: list[dict[str, Any]],
) -> dict[str, list[dict[str, Any]]]:
    """Bulk: fetch the daily feeds once, then return per-company-id matches."""
    items = await _fetch_all_feeds()
    by_company: dict[str, list[dict[str, Any]]] = {}
    for c in companies:
        matches = _filter_for_company(items, c)
        if matches:
            by_company[c["id"]] = matches
    return by_company


async def fetch_and_archive_parte_e() -> int:
    """Fetch Série II Parte E RSS, store every item in `dre_raw` (regardless
    of company match). Returns count of new rows inserted.

    Called alongside the daily DRE match run. The raw archive feeds the
    retroactive match on new company creation — see
    `retroactively_match_company` below."""
    from app.db import AsyncSessionLocal
    from app.services.dre_classifier import classify

    async with httpx.AsyncClient(
        timeout=httpx.Timeout(10.0, connect=5.0),
        headers={"User-Agent": "Mozilla/5.0 (segunor-intel)"},
    ) as client:
        try:
            r = await client.get(RAW_ARCHIVE_FEED)
            r.raise_for_status()
        except httpx.HTTPError as e:
            logger.warning("DRE parte E fetch failed: %s", e)
            return 0
    items = _parse_feed(r.text)

    new_count = 0
    async with AsyncSessionLocal() as session:
        for it in items:
            date_iso = it["date"].isoformat() if it.get("date") else ""
            # Hash includes series + part so the same title in Série I wouldn't
            # collide with Parte E (hypothetical — they won't, but safe).
            key = f"{RAW_ARCHIVE_SERIES}|{RAW_ARCHIVE_PART}|{(it.get('title') or '').strip().lower()}|{date_iso}"
            h = hashlib.sha256(key.encode("utf-8")).hexdigest()
            cls = classify(it.get("title"), it.get("summary"))
            res = await session.execute(
                text(
                    """
                    INSERT INTO dre_raw
                        (series, part, title, summary, date, type, source_url,
                         raw_json, dedup_hash, relevance, change_kind)
                    VALUES
                        (:ser, :part, :t, :s, :d, :ty, :u, CAST(:r AS JSONB), :h,
                         :rel, :ck)
                    ON CONFLICT (dedup_hash) DO NOTHING
                    RETURNING id
                    """
                ),
                {
                    "ser": RAW_ARCHIVE_SERIES, "part": RAW_ARCHIVE_PART,
                    "t": it.get("title") or "",
                    "s": it.get("summary"),
                    "d": it.get("date"),
                    "ty": it.get("type"),
                    "u": it.get("source_url"),
                    "r": json.dumps(it.get("raw") or {}, default=str),
                    "h": h,
                    "rel": cls.relevance, "ck": cls.change_kind,
                },
            )
            if res.first():
                new_count += 1
        await session.commit()
    logger.info(
        "dre_raw archive: Parte E fetched=%d new=%d", len(items), new_count,
    )
    return new_count


async def retroactively_match_company(
    session: AsyncSession, company_id: str, legal_name: str,
    months_back: int = 12,
) -> int:
    """Run the newly-created company's distintivo against the `dre_raw`
    archive of the last N months, copying matches into `dre_publications`.

    This is what gives a new company an instant DRE timeline instead of
    starting from zero on the day monitoring begins."""
    term = _company_search_term(legal_name)
    if not term:
        logger.info(
            "retro-match: skip %s (distintivo too short/ambiguous)", legal_name,
        )
        return 0
    # ILIKE %term% hits the GIN trigram indexes for fast scan. Word-boundary
    # logic happens post-fetch in Python because PG trigrams ignore it.
    rows = (
        await session.execute(
            text(
                """
                SELECT title, summary, date, type, source_url, raw_json,
                       relevance, change_kind
                FROM dre_raw
                WHERE date >= (CURRENT_DATE - (:m || ' months')::interval)
                  AND (title ILIKE :q OR summary ILIKE :q)
                ORDER BY date DESC NULLS LAST
                """
            ),
            {"q": f"%{term}%", "m": months_back},
        )
    ).all()
    if not rows:
        logger.info("retro-match: 0 candidates for %s (term=%r)", legal_name, term)
        return 0

    # Re-check word-boundary to drop prefix false positives ("SOL" in "solidário")
    pattern = re.compile(r"\b" + re.escape(term.lower()) + r"\b")
    accepted: list[dict[str, Any]] = []
    for r in rows:
        hay = f"{r[0] or ''}\n{r[1] or ''}".lower()
        if pattern.search(hay):
            accepted.append({
                "title": r[0], "summary": r[1], "date": r[2],
                "type": r[3], "source_url": r[4], "raw": r[5],
                "relevance": r[6], "change_kind": r[7],
            })
    if not accepted:
        return 0

    inserted = 0
    for it in accepted:
        date_iso = it["date"].isoformat() if it.get("date") else ""
        h = _dedup(company_id, it.get("title") or "", date_iso)
        res = await session.execute(
            text(
                """
                INSERT INTO dre_publications
                    (company_id, title, summary, date, type, source_url,
                     raw_json, dedup_hash, relevance, change_kind, source)
                VALUES
                    (:cid, :t, :s, :d, :ty, :u, CAST(:r AS JSONB), :h,
                     :rel, :ck, 'dre')
                ON CONFLICT (dedup_hash) DO NOTHING
                RETURNING id
                """
            ),
            {
                "cid": company_id,
                "t": it.get("title") or "", "s": it.get("summary"),
                "d": it.get("date"), "ty": it.get("type"),
                "u": it.get("source_url"),
                "r": json.dumps(it.get("raw") or {}, default=str),
                "h": h,
                "rel": it.get("relevance") or "low",
                "ck": it.get("change_kind"),
            },
        )
        if res.first():
            inserted += 1
    await session.commit()
    logger.info(
        "retro-match: %s (term=%r) → %d candidates, %d inserted",
        legal_name, term, len(accepted), inserted,
    )
    return inserted


def _dedup(company_id: str, title: str, date_iso: str) -> str:
    key = f"{company_id}|{(title or '').strip().lower()}|{date_iso or ''}"
    return hashlib.sha256(key.encode("utf-8")).hexdigest()


async def save_new_publications(
    session: AsyncSession,
    company_id: str,
    items: list[dict[str, Any]],
) -> int:
    from app.services.dre_classifier import classify

    new_count = 0
    for it in items:
        date_iso = it["date"].isoformat() if it.get("date") else ""
        h = _dedup(company_id, it.get("title") or "", date_iso)
        cls = classify(it.get("title"), it.get("summary"))
        res = await session.execute(
            text(
                """
                INSERT INTO dre_publications
                    (company_id, title, summary, date, type, source_url,
                     raw_json, dedup_hash, relevance, change_kind)
                VALUES
                    (:cid, :t, :s, :d, :ty, :u, CAST(:r AS JSONB), :h,
                     :rel, :ck)
                ON CONFLICT (dedup_hash) DO NOTHING
                RETURNING id
                """
            ),
            {
                "cid": company_id,
                "t": it.get("title") or "",
                "s": it.get("summary"),
                "d": it.get("date"),
                "ty": it.get("type"),
                "u": it.get("source_url"),
                "r": json.dumps(it.get("raw") or {}, default=str),
                "h": h,
                "rel": cls.relevance,
                "ck": cls.change_kind,
            },
        )
        if res.first():
            new_count += 1
    await session.commit()
    return new_count


async def reclassify_all(session: AsyncSession) -> int:
    """Re-run the classifier over every dre_publications row. Used once after
    deploying classifier improvements."""
    from app.services.dre_classifier import classify

    rows = (
        await session.execute(
            text("SELECT id::text, title, summary FROM dre_publications")
        )
    ).all()
    updated = 0
    for rid, title, summary in rows:
        cls = classify(title, summary)
        await session.execute(
            text(
                "UPDATE dre_publications SET relevance = :rel, change_kind = :ck WHERE id = :id"
            ),
            {"rel": cls.relevance, "ck": cls.change_kind, "id": rid},
        )
        updated += 1
    await session.commit()
    return updated
