from __future__ import annotations

import re
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from typing import Any, Optional

DATE_AT_START_RE = re.compile(r"^(\d{2}/\d{2})\b")
STATEMENT_PERIOD_RE = re.compile(
    r"([A-Za-z]+\s+\d{1,2},\s+\d{4})\s+through\s+([A-Za-z]+\s+\d{1,2},\s+\d{4})"
)
ACCOUNT_NUMBER_RE = re.compile(r"Account\s+Number:[\s]*([0-9xX \-*]+)", re.IGNORECASE)
BEGINNING_BALANCE_RE = re.compile(r"Beginning Balance\s+\$?([\d,]+\.\d{2})")
HEADER_LINE_RE = re.compile(r"^\s*DATE\s+DESCRIPTION\s+AMOUNT\s+BALANCE\s*$")
HEADER_ANYWHERE_RE = re.compile(r"DATE\s+DESCRIPTION\s+AMOUNT\s+BALANCE", re.IGNORECASE)

_DEBIT_SPACED_RE = re.compile(r"\s+-\s+(\d[\d,]*\.\d{2})\s+(\d[\d,]*\.\d{2})\s*$")
_TWO_NUMS_RE = re.compile(r"(-?\$?\d[\d,]*\.\d{2})\s+(-?\$?\d[\d,]*\.\d{2})\s*$")
_ONE_NUM_RE = re.compile(r"(-?\$?\d[\d,]*\.\d{2})\s*$")

EXCLUDE_PREFIXES = (
    "Beginning Balance",
    "Ending Balance",
    "TRANSACTION DETAIL",
    "TRANSACTION DETAIL (continued)",
    "Page ",
    "IN CASE OF ERRORS OR QUESTIONS",
    "Call us at",
    "For personal accounts only:",
    "For business accounts,",
    "For business accounts",
    "JPMorgan Chase Bank, N.A.",
    "*start*",
    "*end*",
    "(continued)",
    "\u00b7",
    "December ",
    "January ",
    "February ",
    "March ",
    "April ",
    "May ",
    "June ",
    "July ",
    "August ",
    "September ",
    "October ",
    "November ",
)

_PAGE_NUMBER_RE = re.compile(r"^\d+\s+\d+Page")

EXACT_EXCLUDES = {
    "*start*transactiondetail",
    "*start*transaction detail",
    "*end*transaction detail",
    "*end*transactiondetail",
    "DATE DESCRIPTION AMOUNT BALANCE",
}


def detect(full_text: str) -> float:
    """Detect if this is a Chase checking statement."""
    score = 0.0
    text = full_text.upper()
    
    if "JPMORGAN CHASE" in text:
        score += 0.4
    if "CHECKING ACCOUNT" in text or "ACCOUNT NUMBER:" in text:
        score += 0.3
    if "STATEMENT PERIOD:" in text or "through" in full_text:
        score += 0.25
    if "DATE DESCRIPTION AMOUNT BALANCE" in text:
        score += 0.1
    
    return min(score, 1.0)


def _parse_date(date_str: str, fmt: str) -> date:
    """Parse a date string, handling errors gracefully."""
    try:
        return datetime.strptime(date_str, fmt).date()
    except ValueError:
        return date.today()


def _parse_statement_period(full_text: str) -> tuple[date, date]:
    """Extract statement period from text."""
    match = STATEMENT_PERIOD_RE.search(full_text)
    if match:
        start_dt = _parse_date(match.group(1), "%B %d, %Y")
        end_dt = _parse_date(match.group(2), "%B %d, %Y")
        return start_dt, end_dt
    
    # Fallback: use today as both dates
    today = date.today()
    return today, today


def _parse_account_number(full_text: str) -> str:
    """Extract account number from statement."""
    match = ACCOUNT_NUMBER_RE.search(full_text)
    if match:
        return re.sub(r"[^0-9]", "", match.group(1))
    return ""


def _parse_beginning_balance(full_text: str) -> Optional[Decimal]:
    """Extract beginning balance for inferring credit amounts."""
    match = BEGINNING_BALANCE_RE.search(full_text)
    if not match:
        return None
    try:
        return Decimal(match.group(1).replace(",", ""))
    except (InvalidOperation, ValueError):
        return None


def _infer_post_date(mmdd: str, statement_start: date, statement_end: date) -> date:
    """Infer full date from MM/DD given the statement period."""
    try:
        month = int(mmdd[:2])
        day = int(mmdd[3:5])
    except (ValueError, IndexError):
        return date.today()
    
    if statement_start.year == statement_end.year:
        year = statement_start.year
    else:
        # If statement crosses year boundary, months >= start month belong to start year
        year = statement_start.year if month >= statement_start.month else statement_end.year
    
    try:
        return date(year, month, day)
    except ValueError:
        return date.today()


def _clean_line(raw: str) -> str:
    """Normalize whitespace in a line."""
    return " ".join(raw.split())


def _should_ignore_line(line: str) -> bool:
    """Check if a line should be ignored (not a transaction)."""
    if not line:
        return True
    if line in EXACT_EXCLUDES:
        return True
    if HEADER_LINE_RE.match(line):
        return True
    if _PAGE_NUMBER_RE.match(line):
        return True
    return any(line.startswith(prefix) for prefix in EXCLUDE_PREFIXES)


def _transaction_is_complete(line: str) -> bool:
    """Check if a line is a complete transaction (has amount/balance)."""
    return bool(
        _DEBIT_SPACED_RE.search(line)
        or _TWO_NUMS_RE.search(line)
        or _ONE_NUM_RE.search(line)
    )


def _collect_transaction_lines(full_text: str) -> list[str]:
    """Collect transaction lines, handling wrapped descriptions."""
    lines = [_clean_line(line) for line in full_text.splitlines()]
    
    transactions: list[str] = []
    current: Optional[str] = None
    in_txn_section = False
    cc_header_pending = False
    
    for line in lines:
        if not line:
            continue
        
        if line.startswith("TRANSACTION DETAIL"):
            if in_txn_section and current:
                transactions.append(current)
                current = None
            continue
        
        if HEADER_LINE_RE.match(line):
            in_txn_section = True
            cc_header_pending = False
            continue
        
        # Sapphire two-line header handling
        if line == "Date of":
            cc_header_pending = True
            continue
        if cc_header_pending and line.startswith("Transaction Merchant") and "$ Amount" in line:
            in_txn_section = True
            cc_header_pending = False
            continue
        if cc_header_pending and line.startswith("Page "):
            cc_header_pending = False
            continue
        
        if not in_txn_section:
            continue
        
        if _should_ignore_line(line):
            continue
        
        if DATE_AT_START_RE.match(line):
            if current:
                transactions.append(current)
            current = line
            if _transaction_is_complete(current):
                transactions.append(current)
                current = None
        else:
            if current:
                current = f"{current} {line}"
                if _transaction_is_complete(current):
                    transactions.append(current)
                    current = None
    
    if current:
        transactions.append(current)
    
    return transactions


def _parse_decimal(token: str) -> Decimal:
    """Parse a decimal value from a token."""
    normalized = token.replace("$", "").replace(",", "")
    try:
        return Decimal(normalized)
    except (InvalidOperation, ValueError):
        return Decimal("0")


def _parse_transaction_line(
    line: str,
    statement_start: date,
    statement_end: date,
    account_number: str,
    source_file: str,
    prev_balance: Optional[Decimal] = None,
    has_running_balance: bool = True,
) -> dict[str, Any]:
    """Parse a single transaction line."""
    date_match = DATE_AT_START_RE.match(line)
    if not date_match:
        return {}
    
    mmdd = date_match.group(1)
    rest = line[date_match.end():].strip()
    
    # Try format with spaced dash (newer)
    m = _DEBIT_SPACED_RE.search(rest)
    if m:
        description = rest[:m.start()].strip()
        amount = -_parse_decimal(m.group(1))
        balance = _parse_decimal(m.group(2))
    else:
        # Try format with sign attached to amount
        m = _TWO_NUMS_RE.search(rest)
        if m:
            description = rest[:m.start()].strip()
            amount = _parse_decimal(m.group(1))
            balance = _parse_decimal(m.group(2))
        else:
            # Format with balance only (credit)
            m = _ONE_NUM_RE.search(rest)
            if not m:
                return {}
            description = rest[:m.start()].strip()
            if has_running_balance:
                balance = _parse_decimal(m.group(1))
                if prev_balance is not None:
                    amount = balance - prev_balance
                else:
                    amount = Decimal("0")
            else:
                amount = _parse_decimal(m.group(1))
                balance = prev_balance if prev_balance is not None else Decimal("0")
    
    if not description:
        return {}
    
    post_date = _infer_post_date(mmdd, statement_start, statement_end)
    
    return {
        "post_date": post_date.isoformat(),
        "description": description,
        "amount": float(amount),
        "balance": float(balance),
        "row_confidence": 0.95,
        "review_reason": "",
    }


def parse(full_text: str, source_file: str) -> dict[str, Any]:
    """Parse Chase checking statement and extract transactions."""
    statement_start, statement_end = _parse_statement_period(full_text)
    account_number = _parse_account_number(full_text)
    account_last4 = account_number[-4:] if len(account_number) >= 4 else ""
    
    raw_txn_lines = _collect_transaction_lines(full_text)
    has_running_balance = bool(HEADER_ANYWHERE_RE.search(full_text))
    beginning_balance = _parse_beginning_balance(full_text)
    
    transactions = []
    prev_balance = beginning_balance
    
    for raw_line in raw_txn_lines:
        tx = _parse_transaction_line(
            raw_line,
            statement_start=statement_start,
            statement_end=statement_end,
            account_number=account_number,
            source_file=source_file,
            prev_balance=prev_balance,
            has_running_balance=has_running_balance,
        )
        if tx:
            prev_balance = Decimal(str(tx["balance"]))
            transactions.append(tx)
    
    return {
        "statement_start": statement_start.isoformat(),
        "statement_end": statement_end.isoformat(),
        "account_number": account_number,
        "transactions": transactions,
    }
