"""Rule-based bank transaction ↔ invoice matching (no AI)."""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime

from app.services.bank_workflow import SUGGEST_THRESHOLD
from app.store import BankTx, Invoice


@dataclass
class MatchCandidate:
    invoice_id: str
    score: float
    signals: list[str]
    allocated_amount: float
    outstanding: float
    is_partial: bool

    @property
    def confidence_label(self) -> str:
        from app.services.bank_workflow import confidence_label

        return confidence_label(self.score)


def _norm(text: str) -> str:
    return re.sub(r"\s+", " ", (text or "").strip().lower())


def _parse_date(value: str) -> datetime | None:
    if not value:
        return None
    for fmt in ("%Y-%m-%d", "%d-%m-%Y", "%Y/%m/%d"):
        try:
            return datetime.strptime(value[:10], fmt)
        except ValueError:
            continue
    return None


def _days_apart(a: str, b: str) -> int | None:
    da = _parse_date(a)
    db = _parse_date(b)
    if not da or not db:
        return None
    return abs((da.date() - db.date()).days)


def invoice_outstanding(invoice: Invoice) -> float:
    paid = max(float(getattr(invoice, "paidAmount", 0) or 0), 0)
    total = max(float(invoice.total or 0), 0)
    return max(total - paid, 0)


def _name_in_text(name: str, *texts: str) -> bool:
    n = _norm(name)
    if len(n) < 3:
        return False
    blob = " ".join(_norm(t) for t in texts if t)
    return n in blob


def score_match(tx: BankTx, invoice: Invoice) -> MatchCandidate | None:
    """Score one invoice against a bank transaction for the same client."""
    if invoice.clientId != tx.clientId:
        return None

    outstanding = invoice_outstanding(invoice)
    if outstanding <= 0 and (invoice.total or 0) > 0:
        return None

    tx_amount = abs(float(tx.amount or 0))
    if tx_amount <= 0:
        return None

    score = 0.0
    signals: list[str] = []

    inv_total = float(invoice.total or 0)
    compare_outstanding = outstanding if outstanding > 0 else inv_total

    # Amount signals
    if compare_outstanding > 0 and abs(tx_amount - compare_outstanding) < 0.01:
        score += 40
        signals.append("exact_amount")
    elif compare_outstanding > 0 and tx_amount < compare_outstanding:
        score += 28
        signals.append("partial_amount")
    elif inv_total > 0 and abs(tx_amount - inv_total) < 0.01:
        score += 35
        signals.append("exact_invoice_total")

    # Invoice number / reference in description or payment reference
    inv_num = (invoice.number or "").strip()
    if inv_num and inv_num not in {"—", "-"}:
        num_l = inv_num.lower()
        desc_l = _norm(tx.description)
        ref_l = _norm(getattr(tx, "paymentReference", "") or "")
        pay_ref = _norm(invoice.paymentRef or "")
        if num_l in desc_l or num_l in ref_l:
            score += 30
            signals.append("invoice_number")
        if pay_ref and (pay_ref in desc_l or pay_ref == ref_l):
            score += 15
            signals.append("payment_reference")

    # Supplier / customer name
    supplier = (invoice.supplier or "").strip()
    if supplier and _name_in_text(supplier, tx.description, getattr(tx, "paymentReference", "")):
        score += 20
        signals.append("supplier_name")

    # IBAN if present on both sides (future SnelStart field)
    tx_iban = _norm(getattr(tx, "iban", "") or "")
    if tx_iban and len(tx_iban) >= 8 and tx_iban in _norm(tx.description):
        score += 5
        signals.append("iban_in_description")

    # Date proximity
    gap = _days_apart(tx.date, invoice.invoiceDate or invoice.dueDate or invoice.uploadedAt)
    if gap is not None:
        if gap <= 3:
            score += 10
            signals.append("date_close")
        elif gap <= 14:
            score += 5
            signals.append("date_near")

    score = min(score, 100)
    if score <= 0:
        return None

    allocated = min(tx_amount, compare_outstanding if compare_outstanding > 0 else tx_amount)
    is_partial = compare_outstanding > 0 and allocated + 0.01 < compare_outstanding

    return MatchCandidate(
        invoice_id=invoice.id,
        score=score,
        signals=signals,
        allocated_amount=allocated,
        outstanding=compare_outstanding,
        is_partial=is_partial,
    )


def find_matches(tx: BankTx, invoices: list[Invoice], *, limit: int = 10) -> list[MatchCandidate]:
    candidates: list[MatchCandidate] = []
    for invoice in invoices:
        if invoice.clientId != tx.clientId:
            continue
        hit = score_match(tx, invoice)
        if hit:
            candidates.append(hit)
    candidates.sort(key=lambda c: (-c.score, c.invoice_id))
    return candidates[:limit]


def best_match(tx: BankTx, invoices: list[Invoice]) -> MatchCandidate | None:
    matches = find_matches(tx, invoices, limit=1)
    return matches[0] if matches else None


def apply_suggestion(tx: BankTx, candidate: MatchCandidate | None) -> dict:
    """Patch fields to store on BankTx after running the matcher."""
    from app.services.bank_workflow import STATUS_UNMATCHED, status_from_confidence

    if not candidate or candidate.score < SUGGEST_THRESHOLD:
        status = STATUS_UNMATCHED
        if candidate and candidate.score >= 35:
            status = status_from_confidence(candidate.score)
        return {
            "status": status,
            "suggestedInvoiceId": candidate.invoice_id if candidate else None,
            "confidence": candidate.score if candidate else None,
            "matchedRecordId": None,
            "matchedRecordType": None,
        }

    status = status_from_confidence(candidate.score)
    return {
        "status": status,
        "suggestedInvoiceId": candidate.invoice_id,
        "confidence": candidate.score,
        "matchedRecordId": None,
        "matchedRecordType": None,
    }
