"""
Generic / unknown-template fallback parser.

Invoked only when no registered parser scores above its confidence threshold.
Applies broad heuristics to extract date/amount/description rows from any PDF.
All extracted rows are marked with very low confidence (0.25) and
``review_reason = "unknown_template"`` so reviewers know they need manual
verification before use.
"""
from __future__ import annotations

import re

# ── Pattern library ──────────────────────────────────────────────────────────

# Date formats: YYYY-MM-DD, MM/DD/YYYY, MM-DD-YYYY, Mon DD YYYY, DD Mon YYYY
_DATE_PATTERNS = [
    r"\b(\d{4}-\d{2}-\d{2})\b",                              # 2024-01-15
    r"\b(\d{1,2}/\d{1,2}/\d{4})\b",                          # 1/15/2024 or 01/15/2024
    r"\b(\d{1,2}-\d{1,2}-\d{4})\b",                          # 1-15-2024
    r"\b((?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
    r"\.?\s+\d{1,2},?\s+\d{4})\b",                           # Jan 15, 2024
    r"\b(\d{1,2}\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
    r"\.?\s+\d{4})\b",                                        # 15 Jan 2024
]
_DATE_RE = re.compile("|".join(_DATE_PATTERNS), re.IGNORECASE)

# Amount: optional sign, optional $, digits (with or without thousand separators), decimal
_AMOUNT_RE = re.compile(
    r"(?<!\w)([+-]?\$?\s*(?:\d{1,3}(?:,\d{3})*|\d+)\.\d{2})\b",
    re.IGNORECASE,
)

# Statement period hints
_PERIOD_RE = re.compile(
    r"(?:statement period|period|from|for the period)[:\s]+(\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{4})"
    r"\s*(?:through|to|-)\s*(\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{4})",
    re.IGNORECASE,
)
_ACCOUNT_RE = re.compile(
    r"(?:account(?:\s+number)?|acct\.?)[:\s#]+([0-9X*-]{4,20})",
    re.IGNORECASE,
)


def _normalize_date(raw: str) -> str:
    """Best-effort convert any matched date string to YYYY-MM-DD."""
    raw = raw.strip().rstrip(",")
    # Already ISO
    if re.match(r"^\d{4}-\d{2}-\d{2}$", raw):
        return raw
    # MM/DD/YYYY or MM-DD-YYYY
    m = re.match(r"^(\d{1,2})[/-](\d{1,2})[/-](\d{4})$", raw)
    if m:
        mm, dd, yyyy = m.groups()
        return f"{yyyy}-{mm.zfill(2)}-{dd.zfill(2)}"
    # Jan 15 2024 / 15 Jan 2024
    month_map = {
        "jan": "01", "feb": "02", "mar": "03", "apr": "04",
        "may": "05", "jun": "06", "jul": "07", "aug": "08",
        "sep": "09", "oct": "10", "nov": "11", "dec": "12",
    }
    m2 = re.match(
        r"^(\w{3})\.?\s+(\d{1,2}),?\s+(\d{4})$", raw, re.IGNORECASE
    )
    if m2:
        mon, day, year = m2.groups()
        return f"{year}-{month_map.get(mon.lower()[:3], '00')}-{day.zfill(2)}"
    m3 = re.match(
        r"^(\d{1,2})\s+(\w{3})\.?\s+(\d{4})$", raw, re.IGNORECASE
    )
    if m3:
        day, mon, year = m3.groups()
        return f"{year}-{month_map.get(mon.lower()[:3], '00')}-{day.zfill(2)}"
    return raw


def _clean_amount(raw: str) -> float:
    cleaned = raw.replace("$", "").replace(",", "").replace(" ", "")
    try:
        return float(cleaned)
    except ValueError:
        return 0.0


def detect(full_text: str) -> float:
    """Always returns 0.0 — this parser is invoked explicitly as fallback only."""
    return 0.0


def parse(full_text: str, source_file: str) -> dict:
    """
    Generic heuristic parser. Scans each line for a leading date followed by
    an amount somewhere on the same line, with everything in-between used as
    the description. Returns rows with very low confidence and
    ``review_reason="unknown_template"`` so the reviewer knows to verify each.
    """
    period = _PERIOD_RE.search(full_text)
    statement_start = _normalize_date(period.group(1)) if period else ""
    statement_end = _normalize_date(period.group(2)) if period else ""

    acct = _ACCOUNT_RE.search(full_text)
    account_number = acct.group(1) if acct else ""

    transactions: list[dict] = []
    seen: set[tuple[str, str, float]] = set()

    for line in full_text.splitlines():
        line = line.strip()
        if not line:
            continue

        # Must have a date near the start of the line
        date_match = _DATE_RE.match(line)
        if not date_match:
            # Also try date anywhere in the first 20 chars
            date_match = _DATE_RE.search(line[:32])
            if not date_match:
                continue

        raw_date = next(g for g in date_match.groups() if g)
        post_date = _normalize_date(raw_date)

        # Find all amounts on this line; use the last one as balance, prior as amount
        remainder = line[date_match.end():]
        amounts = _AMOUNT_RE.findall(remainder)
        if not amounts:
            continue

        amount_str = amounts[0]
        amount = _clean_amount(amount_str)
        if amount == 0.0:
            continue

        # Description: text between date and first amount, cleaned up
        first_amount_pos = remainder.find(amount_str)
        raw_desc = remainder[:first_amount_pos].strip()
        # Strip trailing punctuation
        description = re.sub(r"[^\w\s()\-/&']+$", "", raw_desc).strip()
        if not description:
            description = "(unknown)"

        # Deduplicate
        key: tuple[str, str, float] = (post_date, description, amount)
        if key in seen:
            continue
        seen.add(key)

        transactions.append(
            {
                "post_date": post_date,
                "description": description,
                "amount": amount,
                "balance": None,
                "row_confidence": 0.25,
                "review_reason": "unknown_template",
            }
        )

    return {
        "statement_start": statement_start,
        "statement_end": statement_end,
        "account_number": account_number,
        "transactions": transactions,
        "warnings": [
            "Parsed with generic unknown-template fallback. "
            "All rows have low confidence and require manual review."
        ],
    }
