"""Exact identifier extractors — never use embeddings for these."""

from __future__ import annotations

import re
from dataclasses import dataclass


@dataclass(frozen=True)
class FoundIdentifier:
    id_type: str
    value_raw: str
    value_norm: str
    confidence: float


# GSTIN: 2 digit state + 5 PAN letters + 4 digits + 1 letter + 1 alnum + Z + 1 checksum
_GSTIN_RE = re.compile(
    r"\b([0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][A-Z0-9]Z[A-Z0-9])\b",
    re.IGNORECASE,
)
_PAN_RE = re.compile(r"\b([A-Z]{5}[0-9]{4}[A-Z])\b", re.IGNORECASE)
_IFSC_RE = re.compile(r"\b([A-Z]{4}0[A-Z0-9]{6})\b", re.IGNORECASE)
_UTR_RE = re.compile(
    r"(?i)\b(?:utr|rrn|ref(?:erence)?\s*(?:no|num|number|#)?)\s*[:\-]?\s*([A-Z0-9]{8,30})\b"
)
_INVOICE_RE = re.compile(
    r"(?i)\b(?:invoice\s*(?:no|num|number|#)|inv\s*(?:no|num|number|#))\s*[:\-]?\s*"
    r"([A-Z0-9][A-Z0-9\-/]{3,30})\b"
)
_ACCOUNT_RE = re.compile(
    r"(?i)\b(?:a/?c|account)\s*(?:no|num|number|#)?\s*[:\-]?\s*([0-9]{6,18})\b"
)
_POLICY_RE = re.compile(
    r"(?i)\b(?:policy)\s*(?:no|num|number|#)?\s*[:\-]?\s*([A-Z0-9][A-Z0-9\-/]{4,30})\b"
)
_LOAN_RE = re.compile(
    r"(?i)\b(?:loan)\s*(?:no|num|number|#|account)?\s*[:\-]?\s*([A-Z0-9][A-Z0-9\-/]{4,30})\b"
)
_CIN_RE = re.compile(r"\b([UL][0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6})\b", re.IGNORECASE)
_UPI_REF_RE = re.compile(
    r"(?i)\b(?:upi\s*(?:ref|reference|txn)|transaction\s*id)\s*[:\-]?\s*([A-Z0-9]{8,30})\b"
)
_SWIFT_RE = re.compile(r"\b([A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\b")
_CARD_MASK_RE = re.compile(r"\b((?:\d{4}[\s\-]?){3}\d{4}|\*{4,}\d{4})\b")


def normalize_id(id_type: str, value: str) -> str:
    v = (value or "").strip().upper()
    v = re.sub(r"[\s\-]+", "", v)
    return v


def validate_gstin(value: str) -> bool:
    v = normalize_id("GSTIN", value)
    if not re.fullmatch(r"[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z][A-Z0-9]Z[A-Z0-9]", v):
        return False
    return True


def validate_pan(value: str) -> bool:
    v = normalize_id("PAN", value)
    return bool(re.fullmatch(r"[A-Z]{5}[0-9]{4}[A-Z]", v))


def extract_identifiers(text: str) -> list[FoundIdentifier]:
    """Pull exact identifiers from document text."""
    text = text or ""
    found: list[FoundIdentifier] = []
    seen: set[tuple[str, str]] = set()

    def add(id_type: str, raw: str, conf: float) -> None:
        norm = normalize_id(id_type, raw)
        if not norm or len(norm) < 4:
            return
        key = (id_type, norm)
        if key in seen:
            return
        if id_type == "GSTIN" and not validate_gstin(norm):
            conf = min(conf, 0.4)
        if id_type == "PAN" and not validate_pan(norm):
            return
        seen.add(key)
        found.append(FoundIdentifier(id_type, raw.strip(), norm, conf))

    for m in _GSTIN_RE.finditer(text):
        add("GSTIN", m.group(1), 0.95 if validate_gstin(m.group(1)) else 0.5)
    for m in _PAN_RE.finditer(text):
        # Avoid double-counting GSTIN-embedded PAN-like spans already captured
        add("PAN", m.group(1), 0.9)
    for m in _IFSC_RE.finditer(text):
        add("IFSC", m.group(1), 0.92)
    for m in _CIN_RE.finditer(text):
        add("CIN", m.group(1), 0.9)
    for m in _UTR_RE.finditer(text):
        add("UTR", m.group(1), 0.85)
    for m in _UPI_REF_RE.finditer(text):
        add("UPI_REF", m.group(1), 0.85)
    for m in _INVOICE_RE.finditer(text):
        add("INVOICE_NO", m.group(1), 0.8)
    for m in _ACCOUNT_RE.finditer(text):
        add("ACCOUNT", m.group(1), 0.8)
    for m in _POLICY_RE.finditer(text):
        add("POLICY", m.group(1), 0.8)
    for m in _LOAN_RE.finditer(text):
        add("LOAN", m.group(1), 0.75)
    for m in _CARD_MASK_RE.finditer(text):
        add("CARD", m.group(1), 0.7)
    # SWIFT is noisy — only when labeled
    for m in re.finditer(
        r"(?i)\b(?:swift|bic)\s*(?:code)?\s*[:\-]?\s*([A-Z]{4}[A-Z]{2}[A-Z0-9]{2}(?:[A-Z0-9]{3})?)\b",
        text,
    ):
        add("SWIFT", m.group(1), 0.85)

    return found


def detect_id_query(question: str) -> tuple[str, str] | None:
    """If the user asks for / quotes an identifier, return (id_type, value_norm)."""
    q = question or ""
    # Explicit value in question
    for id_type, pattern in (
        ("GSTIN", _GSTIN_RE),
        ("PAN", _PAN_RE),
        ("IFSC", _IFSC_RE),
        ("CIN", _CIN_RE),
    ):
        m = pattern.search(q)
        if m:
            return id_type, normalize_id(id_type, m.group(1))

    m = _UTR_RE.search(q)
    if m:
        return "UTR", normalize_id("UTR", m.group(1))
    m = _INVOICE_RE.search(q)
    if m:
        return "INVOICE_NO", normalize_id("INVOICE_NO", m.group(1))
    m = _ACCOUNT_RE.search(q)
    if m:
        return "ACCOUNT", normalize_id("ACCOUNT", m.group(1))
    m = _POLICY_RE.search(q)
    if m:
        return "POLICY", normalize_id("POLICY", m.group(1))
    m = _LOAN_RE.search(q)
    if m:
        return "LOAN", normalize_id("LOAN", m.group(1))
    m = _UPI_REF_RE.search(q)
    if m:
        return "UPI_REF", normalize_id("UPI_REF", m.group(1))

    # "what is the GSTIN / invoice number / IFSC"
    ask = re.search(
        r"(?i)\b(?:what|find|show|get|lookup|look\s*up)\b.{0,40}\b("
        r"gstin|pan|ifsc|utr|invoice\s*(?:no|number|#)?|account\s*(?:no|number)?|"
        r"policy\s*(?:no|number)?|loan\s*(?:no|number)?|swift|cin|upi\s*ref"
        r")\b",
        q,
    )
    if ask:
        label = re.sub(r"\s+", " ", ask.group(1).lower())
        mapping = {
            "gstin": "GSTIN",
            "pan": "PAN",
            "ifsc": "IFSC",
            "utr": "UTR",
            "swift": "SWIFT",
            "cin": "CIN",
        }
        if label in mapping:
            return mapping[label], ""
        if label.startswith("invoice"):
            return "INVOICE_NO", ""
        if label.startswith("account"):
            return "ACCOUNT", ""
        if label.startswith("policy"):
            return "POLICY", ""
        if label.startswith("loan"):
            return "LOAN", ""
        if label.startswith("upi"):
            return "UPI_REF", ""
    return None
