"""Typed extraction into FDI line items, assertions, parties."""

from __future__ import annotations

import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from app.fdi.classify import Classification, classify_document
from app.fdi.identifiers import FoundIdentifier, extract_identifiers
from app.fdi.types import DocType


@dataclass
class ExtractedLine:
    page_no: int | None = None
    row_idx: int | None = None
    event_date: str | None = None
    description_raw: str = ""
    description_norm: str = ""
    amount: float | None = None
    debit: float | None = None
    credit: float | None = None
    balance_after: float | None = None
    direction: str = "unknown"  # in|out|neutral|unknown
    currency: str | None = None
    counterparty_name: str | None = None
    category: str | None = None
    event_class: str = "other"
    confidence: float = 0.7
    extras: dict[str, Any] = field(default_factory=dict)


@dataclass
class ExtractedAssertion:
    kind: str
    label: str
    value_numeric: float | None = None
    value_text: str | None = None
    unit: str | None = None
    currency: str | None = None
    source: str = "extracted"
    confidence: float = 0.8


@dataclass
class StructuredExtract:
    doc_type: DocType
    doc_type_confidence: float
    source_type: str
    ocr_used: bool
    parse_confidence: float
    page_count: int
    currency_primary: str | None
    period_start: str | None
    period_end: str | None
    issuer_name: str | None
    holder_name: str | None
    lines: list[ExtractedLine]
    assertions: list[ExtractedAssertion]
    identifiers: list[FoundIdentifier]
    parties: list[tuple[str, str]]  # (entity_type, name)
    document_text: str
    warnings: list[str] = field(default_factory=list)
    classification: Classification | None = None


_HOLDER_RE = re.compile(
    r"(?i)(?:account\s+holder|customer\s+name|employee\s+name|insured|"
    r"policy\s+holder|name\s+of\s+(?:the\s+)?(?:customer|account\s+holder))\s*[:\-]\s*"
    r"([A-Za-z][A-Za-z .']{2,60})"
)
_ISSUER_RE = re.compile(
    r"(?i)(?:bank\s+name|issued\s+by|insurer|employer|sold\s+by|from)\s*[:\-]\s*"
    r"([A-Za-z0-9][A-Za-z0-9 .,&']{2,60})"
)
_PERIOD_RE = re.compile(
    r"(?i)(?:statement\s+period|period|from)\s*[:\-]?\s*"
    r"([0-9]{1,2}[\s\-/][A-Za-z]{3,9}[\s\-/']?[0-9]{2,4})"
    r".{0,20}?(?:to|[-–])\s*"
    r"([0-9]{1,2}[\s\-/][A-Za-z]{3,9}[\s\-/']?[0-9]{2,4})"
)


def _norm_desc(text: str) -> str:
    t = re.sub(r"\s+", " ", (text or "").strip())
    return t[:500]


def _guess_counterparty(description: str) -> str | None:
    d = description or ""
    m = re.search(
        r"(?i)(?:paid\s+to|sent\s+to|received\s+from|transfer(?:red)?\s+to|"
        r"neft-|imps-|upi-|bought\s+from)\s*([A-Za-z0-9][A-Za-z0-9 &.'\-]{1,50})",
        d,
    )
    if m:
        return m.group(1).strip(" -")[:80]
    return None


def _direction_from_row(debit: float | None, credit: float | None, amount: float | None) -> tuple[str, float | None]:
    if debit and debit > 0:
        return "out", float(debit)
    if credit and credit > 0:
        return "in", float(credit)
    if amount is None:
        return "unknown", None
    if amount < 0:
        return "out", abs(float(amount))
    if amount > 0:
        # ambiguous without column role — treat positive lone amount as out for wallet signed layouts handled upstream
        return "unknown", abs(float(amount))
    return "neutral", 0.0


def _event_class_for(doc_type: str, description: str, direction: str) -> str:
    low = (description or "").lower()
    if doc_type in {"invoice", "bill", "purchase_order"}:
        return "invoice_line"
    if doc_type == "payslip":
        return "salary"
    if doc_type == "insurance_policy":
        return "premium"
    if doc_type == "loan_statement" or "emi" in low:
        return "emi"
    if doc_type in {"gst_return", "tax_document"} or "gst" in low or "tds" in low:
        return "tax"
    if "transfer" in low or "self" in low:
        return "transfer"
    if direction == "in":
        return "receipt"
    if direction == "out":
        return "payment"
    return "other"


def extract_structured(path: Path, filename: str) -> StructuredExtract:
    """Layout-lite extract using existing PDF pipeline + identifier/party layer."""
    from app.pdf.extract import extract_pdf

    result = extract_pdf(path, filename=filename)
    text = result.document_text or result.text_preview or ""
    classification = classify_document(text, filename=filename)

    # Map legacy kind into classifier if stronger
    if result.document_kind == "bank_statement" and classification.doc_type == "unknown":
        classification = Classification("bank_statement", 0.7, (), ("legacy_kind",))
    if result.document_kind == "balance_sheet" and classification.doc_type == "unknown":
        classification = Classification("balance_sheet", 0.75, (), ("legacy_kind",))

    # UPI refinement when wallet cues present
    if classification.doc_type == "bank_statement" and re.search(
        r"(?i)\b(upi|paytm|phonepe|gpay)\b", f"{filename}\n{text[:2000]}"
    ):
        classification = Classification("upi_statement", max(0.8, classification.confidence), (), ("upi_refine",))

    lines: list[ExtractedLine] = []
    for i, row in enumerate(result.rows or []):
        debit = row.debit
        credit = row.credit
        amount = row.amount
        direction, amt = _direction_from_row(debit, credit, amount)
        # Wallet rows often put signed amount in amount with empty debit/credit
        if direction == "unknown" and amount is not None:
            if amount < 0:
                direction, amt = "out", abs(amount)
                debit = amt
            elif amount > 0 and not credit and not debit:
                # Prefer description cues
                if re.search(r"(?i)\breceived|credit|from\b", row.description or ""):
                    direction, amt = "in", float(amount)
                    credit = amt
                else:
                    direction, amt = "out", float(amount)
                    debit = amt
        desc = row.description or row.raw or ""
        counterparty = _guess_counterparty(desc)
        # Drop absurd "amounts" that are clearly reference numbers (UTR etc.)
        if amt is not None and amt >= 5_000_000 and not (debit or credit):
            amt = None
            direction = "unknown"
        if debit is not None and debit >= 5_000_000:
            debit = None
        if credit is not None and credit >= 5_000_000:
            credit = None
        if (debit is None and credit is None and amt is None) or (
            direction == "unknown" and not (debit or credit or amt)
        ):
            # keep the row for searchability but mark neutral if no usable money
            if direction == "unknown":
                direction = "neutral"
        lines.append(
            ExtractedLine(
                page_no=None,
                row_idx=i,
                event_date=row.date,
                description_raw=desc,
                description_norm=_norm_desc(desc),
                amount=amt,
                debit=debit,
                credit=credit,
                direction=direction,
                currency="INR",
                counterparty_name=counterparty,
                event_class=_event_class_for(classification.doc_type, desc, direction),
                confidence=0.75 if result.trusted else 0.55,
                extras={"account": row.account} if row.account else {},
            )
        )

    assertions: list[ExtractedAssertion] = []
    totals = result.statement_totals or {}
    if totals.get("money_out") is not None:
        assertions.append(
            ExtractedAssertion(
                kind="total_spend",
                label="Total money out",
                value_numeric=float(totals["money_out"]),
                currency="INR",
                source="header",
                confidence=0.9,
            )
        )
    if totals.get("money_in") is not None:
        assertions.append(
            ExtractedAssertion(
                kind="total_income",
                label="Total money in",
                value_numeric=float(totals["money_in"]),
                currency="INR",
                source="header",
                confidence=0.9,
            )
        )
    if totals.get("payments_made") is not None:
        assertions.append(
            ExtractedAssertion(
                kind="payment_count",
                label="Payments made",
                value_numeric=float(totals["payments_made"]),
                unit="count",
                source="header",
                confidence=0.9,
            )
        )
    if totals.get("payments_received") is not None:
        assertions.append(
            ExtractedAssertion(
                kind="receipt_count",
                label="Payments received",
                value_numeric=float(totals["payments_received"]),
                unit="count",
                source="header",
                confidence=0.9,
            )
        )

    # Computed fallbacks from lines when header missing
    if not any(a.kind == "total_spend" for a in assertions):
        out_sum = sum(float(x.debit or 0) for x in lines if (x.debit or 0) > 0)
        if out_sum > 0:
            assertions.append(
                ExtractedAssertion(
                    kind="total_spend",
                    label="Sum of outflow lines",
                    value_numeric=round(out_sum, 2),
                    currency="INR",
                    source="sum_of_rows",
                    confidence=0.65,
                )
            )
    if not any(a.kind == "total_income" for a in assertions):
        in_sum = sum(float(x.credit or 0) for x in lines if (x.credit or 0) > 0)
        if in_sum > 0:
            assertions.append(
                ExtractedAssertion(
                    kind="total_income",
                    label="Sum of inflow lines",
                    value_numeric=round(in_sum, 2),
                    currency="INR",
                    source="sum_of_rows",
                    confidence=0.65,
                )
            )

    holder = None
    m = _HOLDER_RE.search(text)
    if m:
        holder = m.group(1).strip()
    issuer = None
    m = _ISSUER_RE.search(text[:3000])
    if m:
        issuer = m.group(1).strip()

    period_start = period_end = None
    m = _PERIOD_RE.search(text[:4000])
    if m:
        period_start, period_end = m.group(1).strip(), m.group(2).strip()

    ids = extract_identifiers(text)
    parties: list[tuple[str, str]] = []
    if holder:
        parties.append(("person", holder))
    if issuer:
        parties.append(("organization", issuer))
    for line in lines:
        if line.counterparty_name:
            parties.append(("merchant", line.counterparty_name))

    # Coverage / parse confidence
    coverage = 1.0
    header_count = totals.get("payments_made")
    if header_count and lines:
        try:
            coverage = min(1.0, len([x for x in lines if x.direction == "out"]) / max(float(header_count), 1))
        except Exception:
            coverage = 0.5
    parse_confidence = 0.85 if result.trusted else 0.55
    parse_confidence *= 0.5 + 0.5 * coverage
    if classification.doc_type == "unknown":
        parse_confidence *= 0.8

    warnings = list(result.warnings or [])
    if header_count and lines and coverage < 0.85:
        warnings.append(
            f"Coverage gap: header payments_made={header_count}, extracted_outflows="
            f"{len([x for x in lines if x.direction == 'out'])}"
        )

    base = StructuredExtract(
        doc_type=classification.doc_type,
        doc_type_confidence=classification.confidence,
        source_type=result.source_type,
        ocr_used=bool(result.ocr_used),
        parse_confidence=round(parse_confidence, 3),
        page_count=int((result.validation or {}).get("pages") or 0),
        currency_primary="INR",
        period_start=period_start,
        period_end=period_end,
        issuer_name=issuer,
        holder_name=holder,
        lines=lines,
        assertions=assertions,
        identifiers=ids,
        parties=parties,
        document_text=text[:100000],
        warnings=warnings,
        classification=classification,
    )
    from app.fdi.extractors import apply_type_enrichment

    return apply_type_enrichment(base)
