from __future__ import annotations

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

OPEN_CLOSE_PERIOD_RE = re.compile(
    r"Opening/Closing Date\s+(\d{2}/\d{2}/\d{2})\s*-\s*(\d{2}/\d{2}/\d{2})"
)
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)

DATE_AT_START_RE = re.compile(r"^(\d{2}/\d{2})\b")
HEADER_LINE_RE = re.compile(r"^DATE\s+DESCRIPTION\s+AMOUNT\s+BALANCE$")

_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*$")
_CC_AMOUNT_RE = re.compile(r"(-?\$?\d[\d,]*\.\d{2})\s*$")

EXCLUDE_PREFIXES = (
    "Beginning Balance",
    "Ending Balance",
    "TRANSACTION DETAIL",
    "Page ",
    "Chase Sapphire",
    "*start*",
    "*end*",
    "(continued)",
    "\u00b7",
)

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


def detect(full_text: str) -> float:
    """Detect if this is a Chase Sapphire credit card statement."""
    score = 0.0
    text = full_text.upper()
    
    if "SAPPHIRE" in text:
        score += 0.4
    if "CHASE" in text:
        score += 0.2
    if OPEN_CLOSE_PERIOD_RE.search(full_text):
        score += 0.3
    if "Date of" in full_text and "Transaction Merchant" in full_text:
        score += 0.15
    
    return min(score, 1.0)


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


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_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_statement_period(full_text: str) -> tuple[date, date]:
    """Extract statement period from Sapphire statement."""
    # Try Sapphire format first
    match = OPEN_CLOSE_PERIOD_RE.search(full_text)
    if match:
        start_dt = _parse_date(match.group(1), "%m/%d/%y")
        end_dt = _parse_date(match.group(2), "%m/%d/%y")
        return start_dt, end_dt
    
    # Try generic Chase format
    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
    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 _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:
        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 _should_ignore_line(line: str) -> bool:
    """Check if a line should be ignored."""
    if not line:
        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."""
    return bool(
        _DEBIT_SPACED_RE.search(line)
        or _TWO_NUMS_RE.search(line)
        or _ONE_NUM_RE.search(line)
        or _CC_AMOUNT_RE.search(line)
    )


def _collect_transaction_lines(full_text: str) -> list[str]:
    """Collect transaction lines, handling Sapphire's two-line header."""
    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: "Date of" followed by "Transaction Merchant..."
        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_transaction_line(
    line: str,
    statement_start: date,
    statement_end: date,
    source_file: str,
    prev_balance: Optional[Decimal] = None,
) -> dict[str, Any]:
    """Parse a single credit card 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 to extract amount (credit cards typically only show amount, not balance)
    m = _CC_AMOUNT_RE.search(rest)
    if not m:
        return {}
    
    description = rest[:m.start()].strip()
    amount = _parse_decimal(m.group(1))
    
    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": None,
        "row_confidence": 0.92,
        "review_reason": "",
    }


def parse(full_text: str, source_file: str) -> dict[str, Any]:
    """Parse Chase Sapphire credit card 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)
    
    transactions = []
    for raw_line in raw_txn_lines:
        tx = _parse_transaction_line(
            raw_line,
            statement_start=statement_start,
            statement_end=statement_end,
            source_file=source_file,
        )
        if tx:
            transactions.append(tx)
    
    return {
        "statement_start": statement_start.isoformat(),
        "statement_end": statement_end.isoformat(),
        "account_number": account_number,
        "transactions": transactions,
    }
