# _1099_k_2025_parser.py
"""
1099-K (2025/2026 revision) parser.

Consumes page dictionaries produced by extract_pages() in app.py:
    {page, text, text_alt, width, height, source, image}

Returns a flat dictionary compatible with FIELDS_1099_K_2025.
Only page 1 is used.
"""

from __future__ import annotations

import re
from collections import defaultdict
from typing import Any


# ---------------------------------------------------------------------
# Regex helpers
# ---------------------------------------------------------------------

_EIN_RE = re.compile(r"(?<!\d)\d{2}[-\s]?\d{7}(?!\d)")
_SSN_RE = re.compile(r"(?<!\d)\d{3}[-\s]?\d{2}[-\s]?\d{4}(?!\d)")

_MONEY_RE = re.compile(
    r"(?<![\w./-])\$?\s*"
    r"(?P<amount>(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d{1,2})?)"
    r"(?![\w./-])"
)

_BOX_NUMBER_FOLLOW_RE = re.compile(
    r"^\s*(?:gross\s+amount|cash\s+tips|card\s+not\s+present|"
    r"merchant\s+category|number\s+of\s+payment|federal\s+income\s+tax|"
    r"state\s+income\s+tax|state\s+identification|state\b|"
    r"january|february|march|april|may|june|july|august|september|"
    r"october|november|december|TTO)",
    re.IGNORECASE,
)

_BOX_ANCHORS = {
    "box1a_gross_amount": [
        r"\b1a\s*[.\-:]?\s*Gross\s+amount\s+of\s+payment\s+card",
        r"\b1a\s*[.\-:]?\s*Gross\s+amount",
    ],
    "box1b_card_not_present": [
        r"\b1b\s*[.\-:]?\s*Card\s+not\s+present",
        r"\b1a\s*[.\-:]?\s*Cash\s+tips",
        r"\b1a\s*[.\-:]?\s*Card\s+not\s+present",
        r"\bcard\s+not\s+present\s+transactions?\b",
    ],
    "box1d_tto": [
        r"\b1d\s*[.\-:]?\s*TTO\b",
        r"\b1d\s*[.\-:]?\s*TIN\s+not\s+provided\b",
    ],
    "box2_merchant_category_code": [
        r"\b2\s*[.\-:]?\s*Merchant\s+category\s+code\b",
    ],
    "box3_num_payment_transactions": [
        r"\b3\s*[.\-:]?\s*Number\s+of\s+payment\s+transactions\b",
    ],
    "box4_fed_tax_withheld": [
        r"\b4\s*[.\-:]?\s*Federal\s+income\s+tax\s+withheld\b",
    ],
    "box5a_january":  [r"\b5a\s*[.\-:]?\s*January\b"],
    "box5b_february": [r"\b5b\s*[.\-:]?\s*February\b"],
    "box5c_march":    [r"\b5c\s*[.\-:]?\s*March\b"],
    "box5d_april":    [r"\b5d\s*[.\-:]?\s*April\b"],
    "box5e_may":      [r"\b5e\s*[.\-:]?\s*May\b"],
    "box5f_june":     [r"\b5f\s*[.\-:]?\s*June\b"],
    "box5g_july":     [r"\b5g\s*[.\-:]?\s*July\b"],
    "box5h_august":   [r"\b5h\s*[.\-:]?\s*August\b"],
    "box5i_september":[r"\b5i\s*[.\-:]?\s*September\b"],
    "box5j_october":  [r"\b5j\s*[.\-:]?\s*October\b"],
    "box5k_november": [r"\b5k\s*[.\-:]?\s*November\b"],
    "box5l_december": [r"\b5l\s*[.\-:]?\s*December\b"],
    "box6_state_tax_withheld": [
        r"\b6\s*[.\-:]?\s*State\s+income\s+tax\s+withheld\b",
    ],
    "box7_state_id_no": [
        r"\b7\s*[.\-:]?\s*State\s+identification\s+no\.?",
        r"\b7\s*[.\-:]?\s*State\s*/\s*Payer'?s?\s+state\s+no\.?",
    ],
    "box8_state": [
        r"\b8\s*[.\-:]?\s*State\b",
    ],
}

_BOX_REGEXES = {
    key: [re.compile(p, re.IGNORECASE) for p in patterns]
    for key, patterns in _BOX_ANCHORS.items()
}

_ANY_BOX_LABEL_RE = re.compile(
    r"\b(?:box\s*)?(?:1a|1b|1c|1d|1e|[2-8]|5[a-l])\s*[.\-:]?\s*"
    r"(?:gross\s+amount|cash\s+tips|card\s+not\s+present|merchant\s+category|"
    r"number\s+of\s+payment|federal\s+income\s+tax|january|february|march|"
    r"april|may|june|july|august|september|october|november|december|"
    r"state\s+income\s+tax|state\s+identification|state\b|TTO)",
    re.IGNORECASE,
)


# ---------------------------------------------------------------------
# Text utilities
# ---------------------------------------------------------------------

def _normalise_text(value: str) -> str:
    value = (value or "").replace("\x00", " ")
    value = value.replace("\u00a0", " ")
    value = value.replace("\r\n", "\n").replace("\r", "\n")
    value = re.sub(r"[ \t]+", " ", value)
    value = re.sub(r"\n[ \t]+", "\n", value)
    value = re.sub(r"\n{3,}", "\n\n", value)
    return value.strip()


def _clean_line(value: str) -> str:
    value = re.sub(r"\s+", " ", value or "").strip()
    value = value.strip(" |:;,_")
    value = re.sub(r"^[•*]+", "", value).strip()
    return value


def _join_first_page_text(pages: list[dict]) -> str:
    if not pages:
        return ""
    first_page = pages[0] or {}
    primary = _normalise_text(first_page.get("text") or "")
    alternate = _normalise_text(first_page.get("text_alt") or "")
    if not primary:
        return alternate
    if not alternate:
        return primary
    if len(alternate) > len(primary) * 1.20:
        return alternate
    return primary


def _clean_amount(raw: str) -> float | None:
    if not raw:
        return None
    raw = raw.replace("$", "").replace(",", "").strip()
    try:
        amount = float(raw)
    except (ValueError, TypeError):
        return None
    return int(amount) if amount.is_integer() else amount


def _extract_tax_year(text: str) -> int | None:
    patterns = [
        r"(?:tax\s+year|calendar\s+year|for\s+calendar\s+year|for\s+the\s+year)"
        r"\s*[:\-]?\s*(20\d{2})",
        r"FORM\s+1099[-\s]?K.{0,80}?\b(20\d{2})\b",
        r"\b(20\d{2})\s+FORM\s+1099[-\s]?K\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE | re.DOTALL)
        if match:
            year = int(match.group(1))
            if 2000 <= year <= 2100:
                return year
    return None


# ---------------------------------------------------------------------
# TIN extraction (FILER / PAYEE; falls back to PAYER / RECIPIENT)
# ---------------------------------------------------------------------

_TIN_TOKEN = (
    r"(?:\d{3}[-\s.]?\d{2}[-\s.]?\d{4}"
    r"|\d{2}[-\s.]?\d{7})"
)
_TIN_RE = re.compile(rf"(?<!\d){_TIN_TOKEN}(?!\d)")


def _normalise_tin(value: str) -> str:
    if not value:
        return ""
    original = value.strip()
    digits = re.sub(r"\D", "", original)
    if len(digits) != 9:
        return original
    if re.match(r"^\d{3}[\-\s.]\d{2}[\-\s.]\d{4}$", original):
        return f"{digits[:3]}-{digits[3:5]}-{digits[5:]}"
    if re.match(r"^\d{2}[\-\s.]\d{7}$", original):
        return f"{digits[:2]}-{digits[2:]}"
    return f"{digits[:2]}-{digits[2:]}"


def _tin_digits(value: str) -> str:
    return re.sub(r"\D", "", value or "")


def _extract_tins(text: str) -> tuple[str, str]:
    filer_tin = ""
    payee_tin = ""

    filer_patterns = [
        r"(?:FILER'?S?|PAYER'?S?)\s+(?:TIN|taxpayer\s+identification\s+number)"
        rf"[\s\S]{{0,100}}?({_TIN_TOKEN})",
        r"\bfiler\b[^\n]{0,120}?" rf"({_TIN_TOKEN})",
    ]
    for pattern in filer_patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            candidate = _normalise_tin(match.group(1))
            if candidate:
                filer_tin = candidate
                break

    filer_digits = _tin_digits(filer_tin)

    payee_patterns = [
        r"(?:PAYEE'?S?|RECIPIENT'?S?)\s+(?:TIN|taxpayer\s+identification\s+number)"
        rf"[\s\S]{{0,100}}?({_TIN_TOKEN})",
        r"\bpayee\b[^\n]{0,120}?" rf"({_TIN_TOKEN})",
    ]
    for pattern in payee_patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            candidate = _normalise_tin(match.group(1))
            if candidate and _tin_digits(candidate) != filer_digits:
                payee_tin = candidate
                break

    all_tins: list[str] = []
    all_tin_digits: set[str] = set()
    for match in _TIN_RE.finditer(text):
        candidate = _normalise_tin(match.group(0))
        digits = _tin_digits(candidate)
        if candidate and digits not in all_tin_digits:
            all_tins.append(candidate)
            all_tin_digits.add(digits)

    if not filer_tin and all_tins:
        filer_tin = all_tins[0]
    if not payee_tin:
        for candidate in all_tins:
            if _tin_digits(candidate) != _tin_digits(filer_tin):
                payee_tin = candidate
                break

    return filer_tin, payee_tin


# ---------------------------------------------------------------------
# Name extraction (FILER / PAYEE)
# ---------------------------------------------------------------------

_NAME_CAPS_RE = re.compile(r"\b[A-Z][A-Z'.\-]{1,}(?:\s+[A-Z][A-Z'.\-]{1,}){1,4}\b")
_NAME_TITLE_RE = re.compile(r"\b[A-Z][a-z]+(?:\s+[A-Z][a-z]+){1,4}\b")

_FORM_WORD_RE = re.compile(
    r"\b(?:payment|card|third|party|network|gross|amount|transactions|"
    r"merchant|category|code|number|federal|income|tax|withheld|state|"
    r"identification|no|filer|payee|pse|epf|electronic|facilitator|"
    r"settlement|entity|other|name|address|street|city|town|province|"
    r"country|postal|telephone|apt|room|suite|account|january|february|"
    r"march|april|may|june|july|august|september|october|november|"
    r"december|cash|tips|tto|form|rev|department|treasury|internal|"
    r"revenue|service|and|or|of|the|to|for|check|indicate|is|an)\b",
    re.IGNORECASE,
)

_ADDRESS_SUFFIXES = {
    "st", "street", "ave", "avenue", "rd", "road", "ct", "court",
    "dr", "drive", "ln", "lane", "blvd", "boulevard", "way", "pl",
    "place", "ter", "terrace", "pkwy", "parkway", "cir", "circle",
    "hwy", "highway", "sq", "square",
}

_NOISE_RE = re.compile(
    r"^(?:"
    r"filer'?s?\s+(?:name|tin|federal)|"
    r"payee'?s?\s+(?:name|tin|account)|"
    r"street\s+address|city\s+or\s+town|"
    r"state\s+or\s+province|foreign\s+postal|"
    r"account\s+number|2nd\s+TIN|"
    r"form\s+1099|copy\s+[a-z]|"
    r"corrected|void|"
    r"department\s+of\s+the\s+treasury|"
    r"internal\s+revenue\s+service|"
    r"for\s+calendar\s+year|"
    r"OMB\s+No\.?"
    r")\b",
    re.IGNORECASE,
)


def _is_noise_line(line: str) -> bool:
    if not line:
        return True
    if _NOISE_RE.search(line):
        return True
    if len(line) > 120:
        return True
    if re.fullmatch(r"[\W\d_]+", line):
        return True
    return False


def _lines(text: str) -> list[str]:
    return [_clean_line(line) for line in text.splitlines() if _clean_line(line)]


def _extract_clean_name(value: str) -> str:
    if not value:
        return ""
    for pattern in (_NAME_CAPS_RE, _NAME_TITLE_RE):
        for match in pattern.finditer(value):
            candidate = match.group(0).strip()
            if not candidate:
                continue
            words = candidate.split()
            if not (2 <= len(words) <= 5):
                continue
            if _FORM_WORD_RE.fullmatch(candidate):
                continue
            last_word = re.sub(r"[^A-Za-z]", "", words[-1]).lower()
            if last_word in _ADDRESS_SUFFIXES:
                continue
            if any(_FORM_WORD_RE.fullmatch(w) for w in words):
                continue
            return candidate
    return ""


def _extract_name(text: str, party: str) -> str:
    if party == "filer":
        label = r"(?:FILER'?S?|PAYER'?S?)\s+(?:name\s+and\s+address|name)"
        fallback = r"(?:FILER'?S?|PAYER'?S?)"
    else:
        label = r"(?:PAYEE'?S?|RECIPIENT'?S?)\s+(?:name\s+and\s+address|name)"
        fallback = r"(?:PAYEE'?S?|RECIPIENT'?S?)"

    lines = _lines(text)

    for index, line in enumerate(lines):
        if not re.search(fallback, line, re.IGNORECASE):
            continue
        for candidate in lines[index:index + 8]:
            if _is_noise_line(candidate):
                continue
            if _EIN_RE.search(candidate) or _SSN_RE.search(candidate):
                continue
            name = _extract_clean_name(candidate)
            if name:
                return name[:100]
    return ""


# ---------------------------------------------------------------------
# Address extraction
# ---------------------------------------------------------------------

_STREET_RE = re.compile(
    r"\b\d+\s+[A-Za-z][A-Za-z.'\-]*"
    r"(?:\s+[A-Za-z][A-Za-z.'\-]*){0,4}"
    r"\s+(?:St|Street|Ave|Avenue|Rd|Road|Ct|Court|Dr|Drive|"
    r"Ln|Lane|Blvd|Boulevard|Way|Pl|Place|Ter|Terrace|Pkwy|"
    r"Parkway|Cir|Circle|Hwy|Highway|Sq|Square)\b\.?",
    re.IGNORECASE,
)

_CITY_STATE_ZIP_RE = re.compile(
    r"\b([A-Z][A-Za-z]+(?:\s+[A-Z][A-Za-z]+){0,3})\s+"
    r"([A-Z]{2})\s+(\d{5}(?:-\d{4})?)\b"
)


def _extract_us_address(value: str) -> str:
    if not value:
        return ""
    for city_match in _CITY_STATE_ZIP_RE.finditer(value):
        city = city_match.group(1).strip()
        state = city_match.group(2)
        zipcode = city_match.group(3)
        before = value[:city_match.start()]
        street_matches = list(_STREET_RE.finditer(before))
        if street_matches:
            street = street_matches[-1].group(0).strip()
            return f"{street}, {city} {state} {zipcode}"
    street_match = _STREET_RE.search(value)
    if street_match:
        return street_match.group(0).strip()
    return ""


def _extract_address(text: str, party: str) -> str:
    if party == "filer":
        label = r"(?:FILER'?S?|PAYER'?S?)\s+(?:name\s+and\s+address|name)"
        other = r"(?:PAYEE'?S?|RECIPIENT'?S?)\s+"
    else:
        label = r"(?:PAYEE'?S?|RECIPIENT'?S?)\s+(?:name\s+and\s+address|name)"
        other = r"(?:FILER'?S?|PAYER'?S?)\s+"

    lines = _lines(text)
    label_rx = re.compile(label, re.IGNORECASE)

    for index, line in enumerate(lines):
        match = label_rx.search(line)
        if not match:
            continue

        candidates: list[str] = []
        remainder = _clean_line(line[match.end():])
        remainder = re.sub(r"^[\s:|.\-]+", "", remainder)
        if remainder and not _is_noise_line(remainder):
            candidates.append(remainder)

        for next_line in lines[index + 1:index + 14]:
            if re.search(other, next_line, re.IGNORECASE):
                break
            if _is_noise_line(next_line):
                continue
            if _EIN_RE.fullmatch(next_line) or _SSN_RE.fullmatch(next_line):
                continue
            candidates.append(next_line)

        if not candidates:
            continue

        joined = "\n".join(candidates)
        us_addr = _extract_us_address(joined)
        if us_addr:
            return us_addr[:200]

        addr_lines = [c for c in candidates if re.search(r"\d", c)]
        if addr_lines:
            return ", ".join(addr_lines[:3])[:200]

        return ", ".join(candidates[:3])[:200]

    return ""


# ---------------------------------------------------------------------
# Phone extraction
# ---------------------------------------------------------------------

def _extract_phone(text: str) -> str:
    patterns = [
        r"(?:filer'?s?|payee'?s?|payer'?s?)?\s*(?:telephone|phone)\s*"
        r"(?:number|no\.?)?[^0-9]{0,30}"
        r"(\+?1?[-.\s]?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4})",
        r"\b(\d{3}[-.\s]\d{3}[-.\s]\d{4})\b",
    ]
    for pattern in patterns:
        match = re.search(pattern, text, re.IGNORECASE)
        if match:
            return match.group(1).strip()
    return ""


# ---------------------------------------------------------------------
# Amount extraction
# ---------------------------------------------------------------------

def _find_valid_amount(snippet: str) -> float | None:
    for match in _MONEY_RE.finditer(snippet):
        raw = match.group("amount")
        if re.fullmatch(r"20\d{2}", raw):
            continue
        after = snippet[match.end():match.end() + 60]
        if _BOX_NUMBER_FOLLOW_RE.match(after):
            continue
        amount = _clean_amount(raw)
        if amount is not None:
            return amount
    return None


def _find_box_value(text: str, key: str) -> float | None:
    """
    Find a box amount.

    Strategy:
      1. Same-line snippet after the label, truncated at the next box label.
      2. Fallback: next line if it contains no box label (handles tables where
         the value sits on the row below the label).
    """
    for rx in _BOX_REGEXES.get(key, []):
        match = rx.search(text)
        if not match:
            continue

        # --- 1) Same-line snippet ---
        snippet_end = min(len(text), match.end() + 250)
        snippet = text[match.end():snippet_end]
        next_box = _ANY_BOX_LABEL_RE.search(snippet)
        if next_box:
            snippet = snippet[:next_box.start()]
        value = _find_valid_amount(snippet)
        if value is not None:
            return value

        # --- 2) Next-line fallback (no other box label on that line) ---
        line_end = text.find("\n", match.end())
        if line_end == -1:
            continue
        next_line_end = text.find("\n", line_end + 1)
        if next_line_end == -1:
            next_line_end = len(text)
        next_line = text[line_end + 1:next_line_end]
        if _ANY_BOX_LABEL_RE.search(next_line):
            continue
        value = _find_valid_amount(next_line)
        if value is not None:
            return value

    return None


def _find_box_text(text: str, key: str, max_chars: int = 40) -> str:
    for rx in _BOX_REGEXES.get(key, []):
        match = rx.search(text)
        if not match:
            continue
        snippet = text[match.end():match.end() + max_chars * 3]
        snippet = snippet.split("\n")[0]
        cleaned = re.sub(r"^[\s:|.\-]+", "", snippet).strip()
        if cleaned:
            return cleaned[:max_chars]
    return ""


# ---------------------------------------------------------------------
# Box 5 (monthly) — group-aware extraction
# ---------------------------------------------------------------------

_BOX5_MONTHS = [
    "january", "february", "march", "april", "may", "june",
    "july", "august", "september", "october", "november", "december",
]
_BOX5_KEYS = [
    "box5a_january", "box5b_february", "box5c_march",
    "box5d_april", "box5e_may", "box5f_june",
    "box5g_july", "box5h_august", "box5i_september",
    "box5j_october", "box5k_november", "box5l_december",
]
_BOX5_LETTERS = "abcdefghijkl"


def _extract_box5_values(text: str) -> dict[str, float | None]:
    """
    Extract 5a-5l monthly values by pairing labels with numbers.

    On the actual form, months are printed two per row with values on the
    row below. This routine groups labels by line and assigns numbers from
    the next line 1-to-1 (or to the last labels when there are fewer values
    than labels, matching the form's right-aligned layout).
    """
    result: dict[str, float | None] = {k: None for k in _BOX5_KEYS}

    label_positions: list[dict] = []
    for letter, month, key in zip(_BOX5_LETTERS, _BOX5_MONTHS, _BOX5_KEYS):
        pattern = rf"\b5{letter}\s*[.\-:]?\s*{month}\b"
        m = re.search(pattern, text, re.IGNORECASE)
        if m:
            label_positions.append({
                "start": m.start(),
                "end": m.end(),
                "key": key,
            })

    if not label_positions:
        return result

    text_lines = text.split("\n")
    line_offsets: list[int] = []
    offset = 0
    for line in text_lines:
        line_offsets.append(offset)
        offset += len(line) + 1

    def line_index_of(pos: int) -> int:
        for i in range(len(line_offsets) - 1, -1, -1):
            if line_offsets[i] <= pos:
                return i
        return 0

    by_line: dict[int, list] = defaultdict(list)
    for lp in label_positions:
        by_line[line_index_of(lp["start"])].append(lp)

    for line_idx, labels in sorted(by_line.items()):
        labels.sort(key=lambda x: x["start"])
        line_start = line_offsets[line_idx]
        line_end = line_start + len(text_lines[line_idx])

        # (1) Same-line values, between labels
        for i, label in enumerate(labels):
            region_start = label["end"]
            region_end = labels[i + 1]["start"] if i + 1 < len(labels) else line_end
            region = text[region_start:region_end]
            value = _find_valid_amount(region)
            if value is not None:
                result[label["key"]] = value

        # (2) Still-missing labels: look at next line
        missing = [l for l in labels if result[l["key"]] is None]
        if not missing:
            continue

        next_line_idx = line_idx + 1
        if next_line_idx >= len(text_lines):
            continue
        next_line = text_lines[next_line_idx]
        if _ANY_BOX_LABEL_RE.search(next_line):
            continue

        next_values: list[float] = []
        for m in _MONEY_RE.finditer(next_line):
            raw = m.group("amount")
            if re.fullmatch(r"20\d{2}", raw):
                continue
            amount = _clean_amount(raw)
            if amount is not None:
                next_values.append(amount)

        if not next_values:
            continue

        if len(next_values) == len(missing):
            for label, val in zip(missing, next_values):
                result[label["key"]] = val
        elif len(next_values) < len(missing):
            # Fewer values than labels → assign to the LAST labels
            # (right-aligned table: value column sits under the trailing label)
            offset_idx = len(missing) - len(next_values)
            for i, val in enumerate(next_values):
                result[missing[offset_idx + i]["key"]] = val
        else:
            for i, label in enumerate(missing):
                result[label["key"]] = next_values[i]

    return result


# ---------------------------------------------------------------------
# Checkbox detection
# ---------------------------------------------------------------------

_CHECKED_RE = re.compile(
    r"(?:\[\s*[xX✓✔]\s*\]|\b[xX✓✔]\b|\bchecked\b|\byes\b|\btrue\b)",
)


def _detect_filer_type(text: str) -> dict[str, bool]:
    """Detect which filer-type checkbox is marked (PSE / EPF / Other)."""
    out = {"filer_type_pse": False, "filer_type_epf": False, "filer_type_other": False}

    match = re.search(
        r"check\s+to\s+indicate\s+if\s+FILER\s+is\s+a\s*\(an\)"
        r"[\s\S]{0,200}?"
        r"(payment\s+settlement\s+entity|electronic\s+payment\s+facilitator"
        r"|other\s+third\s+party)",
        text,
        re.IGNORECASE,
    )
    if not match:
        return out

    label = match.group(1).lower()
    if "payment settlement" in label or "pse" in label:
        out["filer_type_pse"] = True
    elif "electronic payment" in label or "epf" in label:
        out["filer_type_epf"] = True
    else:
        out["filer_type_other"] = True

    return out


def _detect_transaction_type(text: str) -> dict[str, bool]:
    """Detect which transaction-type checkbox is marked (Payment card / Third party network)."""
    out = {
        "transactions_payment_card": False,
        "transactions_third_party_network": False,
    }

    match = re.search(
        r"check\s+to\s+indicate\s+transactions\s+reported\s+are"
        r"[\s\S]{0,120}?"
        r"(payment\s+card|third\s+party\s+network)",
        text,
        re.IGNORECASE,
    )
    if not match:
        return out

    label = match.group(1).lower()
    if "payment card" in label:
        out["transactions_payment_card"] = True
    else:
        out["transactions_third_party_network"] = True

    return out


# ---------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------

_MONEY_FIELDS = [
    "box1a_gross_amount",
    "box1b_card_not_present",
    "box4_fed_tax_withheld",
    "box6_state_tax_withheld",
]


def parse_1099_k_2025(pages: list[dict], filename: str = "") -> dict[str, Any]:
    """Parse a 1099-K using page 1 only."""

    text = _join_first_page_text(pages)

    if not text:
        raise ValueError(
            "The uploaded 1099-K file contained no readable text on page 1. "
            "If this is a scanned PDF, OCR must run in app.py."
        )

    warnings: list[str] = []

    filer_tin, payee_tin = _extract_tins(text)
    filer_name = _extract_name(text, "filer")
    payee_name = _extract_name(text, "payee")
    filer_address = _extract_address(text, "filer")
    payee_address = _extract_address(text, "payee")

    if not filer_tin:
        warnings.append("Could not locate the filer's TIN.")
    if not payee_tin:
        warnings.append("Could not locate the payee's TIN.")
    if not filer_name:
        warnings.append("Could not confidently locate the filer's name.")
    if not payee_name:
        warnings.append("Could not confidently locate the payee's name.")

    result: dict[str, Any] = {
        "form_type": "1099_k_2025",
        "filename": filename,
        "tax_year": _extract_tax_year(text) or 2025,
        "account_number": "",
        "filer_tin": filer_tin,
        "filer_name": filer_name,
        "filer_address": filer_address,
        "filer_phone": _extract_phone(text),
        "pse_name": "",
        "pse_phone": "",
        "payee_tin": payee_tin,
        "payee_name": payee_name,
        "payee_address": payee_address,
        "box2_merchant_category_code": _find_box_text(
            text, "box2_merchant_category_code"
        ),
        "box3_num_payment_transactions": None,
        "box7_state_id_no": _find_box_text(text, "box7_state_id_no"),
        "box8_state": _find_box_text(text, "box8_state"),
    }

    # Money fields that use the standard same-line + next-line fallback
    for key in _MONEY_FIELDS:
        result[key] = _find_box_value(text, key)

    # Monthly boxes (special two-column layout)
    result.update(_extract_box5_values(text))

    # Box 1d is free-form text ("TIN not provided" etc.)
    result["box1d_tto"] = _find_box_text(text, "box1d_tto")

    # Box 3 is a count, not money.
    for rx in _BOX_REGEXES["box3_num_payment_transactions"]:
        match = rx.search(text)
        if not match:
            continue
        snippet = text[match.end():match.end() + 80]
        snippet = _ANY_BOX_LABEL_RE.split(snippet, maxsplit=1)[0]
        count_match = re.search(r"(?<!\d)(\d{1,6})(?!\d)", snippet)
        if count_match:
            result["box3_num_payment_transactions"] = int(count_match.group(1))
        else:
            # Next-line fallback (value often on the row below the label)
            line_end = text.find("\n", match.end())
            if line_end != -1:
                next_line_end = text.find("\n", line_end + 1)
                if next_line_end == -1:
                    next_line_end = len(text)
                next_line = text[line_end + 1:next_line_end]
                if not _ANY_BOX_LABEL_RE.search(next_line):
                    count_match = re.search(r"(?<!\d)(\d{1,6})(?!\d)", next_line)
                    if count_match:
                        result["box3_num_payment_transactions"] = int(
                            count_match.group(1)
                        )
        break

    result.update(_detect_filer_type(text))
    result.update(_detect_transaction_type(text))

    all_money_keys = _MONEY_FIELDS + list(_BOX5_KEYS)
    if not any(result.get(k) is not None for k in all_money_keys):
        warnings.append(
            "No monetary box values were detected. The PDF may be scanned "
            "at low resolution or use a non-standard layout."
        )

    result["parse_warnings"] = warnings
    return result