"""Lookupline — IOC index over the case corpus.

Turns the collected cases into a reverse index: indicator -> which cases,
feeds, and severities mention it. This is the shared primitive behind
"paste an indicator, is it known-bad?", asset matching, and blocklist export.
Indicators are normalized so lookups are case- and format-insensitive.
"""

from __future__ import annotations

from typing import Dict, Iterable, List, Optional, Tuple

from psyc import db, log
from psyc.models import Case


_log = log.get(__name__)

# severity ordering for min-severity filters
_SEVERITY_RANK: Dict[str, int] = {"low": 0, "medium": 1, "high": 2, "critical": 3}

IOC_TYPES = ("url", "domain", "ip", "hash", "cve")


def normalize(value: str, ioc_type: str) -> str:
    """Normalize an indicator for storage + lookup. CVEs upper, everything else lower."""
    v = value.strip()
    if ioc_type == "cve":
        return v.upper()
    return v.lower()


def iter_case_iocs(case: Case) -> Iterable[Tuple[str, str]]:
    """Yield (normalized_value, ioc_type) for every observable on a case."""
    obs = case.observables
    for u in obs.urls:
        yield normalize(u, "url"), "url"
    for d in obs.domains:
        yield normalize(d, "domain"), "domain"
    for ip in obs.ips:
        yield normalize(ip, "ip"), "ip"
    for h in obs.hashes:
        yield normalize(h, "hash"), "hash"
    for c in obs.cves:
        yield normalize(c, "cve"), "cve"


def reindex(cases: Iterable[Case]) -> int:
    """Rebuild the whole IOC index from the given cases. Returns rows written."""
    rows: List[dict] = []
    seen: set = set()
    for case in cases:
        feed = case.source_metadata.get("feed")
        sev = case.classification.severity.value if case.classification.severity else None
        first_seen = case.observed_at.isoformat() if case.observed_at else None
        for value, ioc_type in iter_case_iocs(case):
            if not value:
                continue
            key = (value, ioc_type, case.case_id)
            if key in seen:
                continue
            seen.add(key)
            rows.append(dict(
                value=value, ioc_type=ioc_type, case_id=case.case_id,
                feed=feed, severity=sev, first_seen=first_seen,
            ))
    written = db.replace_iocs(rows)
    _log.info("lookup.reindexed", iocs=written, cases=len(seen))
    return written


def lookup(value: str) -> List[dict]:
    """Look up one indicator across all types. Returns matching index rows (may be empty)."""
    # Try every type's normalization so callers don't need to know the type.
    candidates = {normalize(value, t) for t in IOC_TYPES}
    out: List[dict] = []
    seen_ids: set = set()
    for cand in candidates:
        for row in db.find_iocs(cand):
            if row["id"] not in seen_ids:
                seen_ids.add(row["id"])
                out.append(row)
    return out


def export_blocklist(ioc_type: str, min_severity: Optional[str] = None) -> List[str]:
    """Distinct indicator values of one type, optionally filtered by min severity."""
    if ioc_type not in IOC_TYPES:
        raise ValueError(f"unknown ioc_type: {ioc_type}; choices: {', '.join(IOC_TYPES)}")
    floor = _SEVERITY_RANK.get(min_severity, -1) if min_severity else -1
    values: List[str] = []
    seen: set = set()
    for row in db.iocs_by_type(ioc_type):
        if floor >= 0:
            rank = _SEVERITY_RANK.get(row["severity"] or "", -1)
            if rank < floor:
                continue
        v = row["value"]
        if v not in seen:
            seen.add(v)
            values.append(v)
    return values
