from __future__ import annotations

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

ALLY_STATEMENT_DATE_RE = re.compile(r"Statement Date\s*(\d{2}/\d{2}/\d{4})")
ALLY_BEGIN_AS_OF_RE = re.compile(r"Beginning Balance, as of\s*(\d{2}/\d{2}/\d{4})")
ALLY_ACTIVITY_HEADER = "ActivityDateDescription CreditsDebitsBalance"
ALLY_ACCOUNT_ACTIVITY_RE = re.compile(
    r"Account Number:\s*([xX0-9]+).*?ActivityDateDescription CreditsDebitsBalance",
    re.DOTALL,
)
ALLY_DATE_TOKEN_RE = re.compile(r"\d{2}/\d{2}/\d{4}")
ALLY_ACTIVITY_CHUNK_RE = re.compile(
    r"^(\d{2}/\d{2}/\d{4})(.*?)([+-]?\$[+-]?\d[\d,]*\.\d{2})\s*"
    r"([+-]?\$[+-]?\d[\d,]*\.\d{2})\s*([+-]?\$[+-]?\d[\d,]*\.\d{2})",
    re.DOTALL,
)
_LOWER_TO_UPPER_RE = re.compile(r"([a-z\)])([A-Z0-9])")
_UPPER_WORD_BOUNDARY_RE = re.compile(r"([A-Z]{2,})([A-Z][a-z])")
_CHECKING_SUFFIX_RE = re.compile(r"\s+Checking\s+X{6}\d{4}\)$")
_PAREN_AFTER_WORD_RE = re.compile(r"([A-Za-z])\(")
_PAREN_LINEBREAK_RE = re.compile(r"([A-Za-z])\s+\(")
_TRAILING_LINE_SPLIT_RE = {
    "ONLINE PMT": re.compile(r"^(.*\S)\s+(ONLINE PMT)$"),
    "ACH": re.compile(r"^(.*\S)\s+(ACH)$"),
    "TRANSFER": re.compile(r"^(.*\S)\s+(TRANSFER)$"),
}
_DESCRIPTION_PREFIXES = (
    "ACH Withdrawal",
    "WEB Funds Transfer",
    "Direct Deposit",
    "NOW Withdrawal",
    "Interest Paid",
)


def detect(full_text: str) -> float:
    """Detect if this is an Ally Bank statement."""
    score = 0.0
    text = full_text.upper()
    
    if "ALLY BANK" in text or "ALLY" in text:
        score += 0.3
    if ALLY_ACTIVITY_HEADER in full_text:
        score += 0.3
    if ALLY_STATEMENT_DATE_RE.search(full_text):
        score += 0.2
    if "ACCOUNT NUMBER:" in 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 _clean_description(raw: str) -> str:
    """Repair common Ally PDF extraction artifacts in transaction descriptions."""
    cleaned = _clean_line(raw)
    cleaned = _LOWER_TO_UPPER_RE.sub(r"\1 \2", cleaned)
    cleaned = _UPPER_WORD_BOUNDARY_RE.sub(r"\1 \2", cleaned)
    cleaned = cleaned.strip()

    for prefix in _DESCRIPTION_PREFIXES:
        prefix_with_space = f"{prefix} "
        if cleaned.startswith(prefix_with_space):
            cleaned = f"{prefix}\n{cleaned[len(prefix_with_space):]}"
            break

    cleaned = cleaned.replace(" account XXXXXX", " account\nXXXXXX")
    cleaned = _PAREN_AFTER_WORD_RE.sub(r"\1 (", cleaned)
    cleaned = _PAREN_LINEBREAK_RE.sub(r"\1\n(", cleaned)
    cleaned = _CHECKING_SUFFIX_RE.sub("", cleaned)

    for pattern in _TRAILING_LINE_SPLIT_RE.values():
        maybe_split = pattern.match(cleaned)
        if maybe_split:
            cleaned = f"{maybe_split.group(1)}\n{maybe_split.group(2)}"
            break

    return cleaned.strip()


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(",", "").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 Ally statement."""
    match = ALLY_STATEMENT_DATE_RE.search(full_text)
    if match:
        end_dt = _parse_date(match.group(1), "%m/%d/%Y")
        begin_matches = ALLY_BEGIN_AS_OF_RE.findall(full_text)
        if begin_matches:
            start_dt = min(
                _parse_date(token, "%m/%d/%Y") for token in begin_matches
            )
            return start_dt, end_dt
    
    # Fallback
    today = date.today()
    return today, today


def parse(full_text: str, source_file: str) -> dict[str, Any]:
    """Parse Ally checking statement and extract transactions."""
    statement_start, statement_end = _parse_statement_period(full_text)
    
    transactions = []
    markers = list(ALLY_ACCOUNT_ACTIVITY_RE.finditer(full_text))
    
    for i, marker in enumerate(markers):
        account_raw = marker.group(1)
        account_number = re.sub(r"[^0-9]", "", account_raw)
        account_last4 = account_number[-4:] if len(account_number) >= 4 else ""

        context_start = max(0, marker.start() - 200)
        marker_context = full_text[context_start:marker.start()]
        if "Spending Account Summary" not in marker_context:
            continue
        
        section_start = marker.end()
        section_end = markers[i + 1].start() if i + 1 < len(markers) else len(full_text)
        section_text = full_text[section_start:section_end]
        
        date_matches = list(ALLY_DATE_TOKEN_RE.finditer(section_text))
        for j, date_match in enumerate(date_matches):
            row_start = date_match.start()
            row_end = date_matches[j + 1].start() if j + 1 < len(date_matches) else len(section_text)
            row_chunk = section_text[row_start:row_end]
            
            row_match = ALLY_ACTIVITY_CHUNK_RE.match(row_chunk)
            if not row_match:
                continue
            
            post_date = _parse_date(row_match.group(1), "%m/%d/%Y")
            description = _clean_description(row_match.group(2))
            
            if not description or description.startswith("Beginning Balance") or description.startswith("Ending Balance"):
                continue
            
            credits = _parse_decimal(row_match.group(3))
            debits = _parse_decimal(row_match.group(4))
            balance = _parse_decimal(row_match.group(5))
            
            amount = credits + debits
            
            transactions.append({
                "post_date": post_date.isoformat(),
                "description": description,
                "amount": float(amount),
                "balance": float(balance),
                "row_confidence": 0.90,
                "review_reason": "",
            })
    
    return {
        "statement_start": statement_start.isoformat(),
        "statement_end": statement_end.isoformat(),
        "account_number": account_number if markers else "",
        "transactions": transactions,
    }
