"""Candidate cross-reference service — powers /api/v1/candidates/check.

Given a candidate name (fuzzy) or NIF (exact), find every judicial
process where that person appears as a party AND a monitored
competitor also appears in the same process. Classify each hit by an
`alert_level`:

  high   — distribuição + candidate is autor/requerente/exequente,
           competitor is réu/requerido/executado
  medium — distribuição + candidate is réu/requerido/executado,
           competitor is autor/requerente/exequente
  low    — CIRE + candidate is credor

Anything outside these shapes is dropped. The service does all the
heavy lifting in one SQL and post-processes into the grouped output
the API returns.

Reuses the trigram index on `process_parties.name` (migration 0003)
and the legal-entity exclusion predicate already used in
app/services/relations.py so corporate parties don't pollute the
candidate matches.
"""
import re
from datetime import datetime, timezone
from typing import Any

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


# Role categorisation. role_in_process is free text stored as whatever
# the scraper pulled, so we normalise case-insensitively via substring.
_ACTIVE_ROLES = ("autor", "requerente", "exequente", "recorrente")
_PASSIVE_ROLES = ("réu", "reu", "requerido", "executado", "recorrido")


def _role_category(role: str | None) -> str:
    if not role:
        return "other"
    low = role.lower()
    if any(k in low for k in _ACTIVE_ROLES):
        return "active"
    if any(k in low for k in _PASSIVE_ROLES):
        return "passive"
    if "credor" in low:
        return "credor"
    return "other"


def _classify(
    source: str, candidate_role: str | None, comp_role: str | None,
) -> tuple[str, str] | None:
    """Return (alert_level, alert_reason) or None when the hit doesn't
    match any of the three patterns we care about."""
    cc = _role_category(candidate_role)
    mc = _role_category(comp_role)
    if source == "distribuicao":
        if cc == "active" and mc == "passive":
            return "high", "candidate_sues_competitor"
        if cc == "passive" and mc == "active":
            return "medium", "competitor_sues_candidate"
    if source == "cire" and cc == "credor":
        return "low", "candidate_creditor_in_competitor_insolvency"
    return None


# Copied from app/services/relations.py — heuristic that trims corporate
# names from the candidate match set. Candidates are natural persons;
# these tokens nearly always signal a legal entity in the party string.
_LEGAL_ENTITY_SQL = """
    position(' s.a' in lower(pp.name)) > 0 OR
    position(' s a' in lower(pp.name)) > 0 OR
    position(' sa,' in lower(pp.name)) > 0 OR
    position(' sa ' in lower(pp.name)) > 0 OR
    position('lda' in lower(pp.name)) > 0 OR
    position('unipessoal' in lower(pp.name)) > 0 OR
    position('sgps' in lower(pp.name)) > 0 OR
    position('banco' in lower(pp.name)) > 0 OR
    position('caixa' in lower(pp.name)) > 0 OR
    position('fundo' in lower(pp.name)) > 0 OR
    position('instituto' in lower(pp.name)) > 0 OR
    position('ministério' in lower(pp.name)) > 0 OR
    position('ministerio' in lower(pp.name)) > 0 OR
    position('autoridade tribut' in lower(pp.name)) > 0 OR
    position('fazenda' in lower(pp.name)) > 0 OR
    position('sociedade' in lower(pp.name)) > 0 OR
    position('cooperativa' in lower(pp.name)) > 0 OR
    position('município' in lower(pp.name)) > 0 OR
    position('municipio' in lower(pp.name)) > 0 OR
    position('câmara municipal' in lower(pp.name)) > 0 OR
    position('segurança social' in lower(pp.name)) > 0
"""


_CANDIDATES_SQL = text(
    f"""
    WITH cand AS (
        SELECT pp.process_id, pp.name, pp.nif,
               pp.role AS candidate_role,
               similarity(pp.name, :q) AS sim
        FROM process_parties pp
        WHERE pp.name % :q
          AND similarity(pp.name, :q) >= :mins
          AND NOT ({_LEGAL_ENTITY_SQL})
    )
    SELECT cand.process_id::text, cand.name, cand.nif,
           cand.candidate_role, cand.sim,
           p.process_number, p.tribunal, p.source, p.date_filed,
           c.nif AS comp_nif, c.legal_name, c.monitoring_type,
           (SELECT role FROM process_parties
            WHERE process_id = p.id AND nif = c.nif
            LIMIT 1) AS comp_role
    FROM cand
    JOIN processes p ON p.id = cand.process_id
    JOIN companies c ON c.id = p.company_id
    WHERE c.monitoring_type = 'competitor'
    ORDER BY cand.sim DESC, p.date_filed DESC NULLS LAST
    LIMIT :lim
    """
)


_EXACT_NIF_SQL = text(
    """
    SELECT pp.process_id::text, pp.name, pp.nif,
           pp.role AS candidate_role, 1.0 AS sim,
           p.process_number, p.tribunal, p.source, p.date_filed,
           c.nif AS comp_nif, c.legal_name, c.monitoring_type,
           (SELECT role FROM process_parties
            WHERE process_id = p.id AND nif = c.nif
            LIMIT 1) AS comp_role
    FROM process_parties pp
    JOIN processes p ON p.id = pp.process_id
    JOIN companies c ON c.id = p.company_id
    WHERE pp.nif = :nif
      AND c.monitoring_type = 'competitor'
    ORDER BY p.date_filed DESC NULLS LAST
    LIMIT :lim
    """
)


def _group(rows: list[Any]) -> list[dict[str, Any]]:
    """Group the flat result set by (name, nif) so the API can show one
    entry per candidate identity with its processes underneath."""
    groups: dict[tuple[str, str | None], dict[str, Any]] = {}
    for r in rows:
        (pid, p_name, p_nif, cand_role, sim,
         pnum, trib, source, date_filed,
         comp_nif, comp_legal, _comp_type, comp_role) = r
        cls = _classify(source, cand_role, comp_role)
        if cls is None:
            continue
        alert_level, alert_reason = cls
        key = (p_name, p_nif)
        if key not in groups:
            groups[key] = {
                "party_name": p_name,
                "party_nif": p_nif,
                "confidence": float(sim),
                "processes": [],
                "summary": {
                    "processes_total": 0, "high": 0, "medium": 0, "low": 0,
                    "unique_competitors": 0,
                },
                "_competitors": set(),
            }
        g = groups[key]
        g["confidence"] = max(g["confidence"], float(sim))
        g["processes"].append({
            "process_number": pnum,
            "tribunal": trib,
            "source": source,
            "date_filed": date_filed.isoformat() if date_filed else None,
            "candidate_role": cand_role,
            "monitored_company": {
                "nif": comp_nif,
                "legal_name": comp_legal,
                "monitoring_type": "competitor",
                "role": comp_role,
            },
            "alert_level": alert_level,
            "alert_reason": alert_reason,
        })
        g["summary"]["processes_total"] += 1
        g["summary"][alert_level] += 1
        g["_competitors"].add(comp_nif)

    out: list[dict[str, Any]] = []
    for g in groups.values():
        g["summary"]["unique_competitors"] = len(g["_competitors"])
        del g["_competitors"]
        levels = {p["alert_level"] for p in g["processes"]}
        g["top_alert"] = (
            "high" if "high" in levels
            else "medium" if "medium" in levels
            else "low" if "low" in levels
            else None
        )
        out.append(g)
    # Sort groups by top_alert priority then by confidence desc
    _priority = {"high": 0, "medium": 1, "low": 2, None: 3}
    out.sort(key=lambda g: (_priority[g["top_alert"]], -g["confidence"]))
    return out


async def check_by_name(
    session: AsyncSession,
    name: str,
    min_similarity: float = 0.4,
    limit: int = 50,
) -> dict[str, Any]:
    rows = (
        await session.execute(
            _CANDIDATES_SQL,
            {"q": name, "mins": min_similarity, "lim": limit},
        )
    ).all()
    matches = _group(rows)
    return {
        "query": {
            "name": name,
            "min_similarity": min_similarity,
            "limit": limit,
        },
        "searched_at": datetime.now(tz=timezone.utc).isoformat(),
        "total_matches": len(matches),
        "matches": matches,
    }


async def check_by_nif(
    session: AsyncSession, nif: str, limit: int = 50,
) -> dict[str, Any]:
    if not re.fullmatch(r"\d{9}", nif):
        return {
            "query": {"nif": nif, "limit": limit},
            "searched_at": datetime.now(tz=timezone.utc).isoformat(),
            "total_matches": 0, "matches": [],
        }
    rows = (
        await session.execute(_EXACT_NIF_SQL, {"nif": nif, "lim": limit})
    ).all()
    matches = _group(rows)
    return {
        "query": {"nif": nif, "limit": limit},
        "searched_at": datetime.now(tz=timezone.utc).isoformat(),
        "total_matches": len(matches),
        "matches": matches,
    }
