from __future__ import annotations

import re
from datetime import datetime
from typing import Iterable

import pandas as pd

from .base import norm


# Shared date support for all bank parsers.  Indian bank statements and the
# other supported banks can be rendered differently by pdftotext/pdfplumber,
# so parsers must not depend on one literal date layout.
SUPPORTED_DATE_FORMATS: tuple[str, ...] = (
    "%d %b %Y",
    "%d %B %Y",
    "%b %d %Y",
    "%B %d %Y",
    "%d %b, %Y",
    "%d %B, %Y",
    "%b %d, %Y",
    "%B %d, %Y",
    "%d-%b-%Y",
    "%d-%B-%Y",
    "%d/%b/%Y",
    "%d/%B/%Y",
    "%d-%m-%Y",
    "%d/%m/%Y",
    "%d.%m.%Y",
    "%Y-%m-%d",
    "%Y/%m/%d",
    "%d %b %y",
    "%d %B %y",
    "%b %d %y",
    "%B %d %y",
    "%d-%b-%y",
    "%d-%B-%y",
    "%d/%b/%y",
    "%d/%B/%y",
    "%d-%m-%y",
    "%d/%m/%y",
    "%d.%m.%y",
)

# A permissive token used only to locate a candidate date at the start of a
# transaction line.  parse_flexible_date() performs the actual validation.
DATE_TOKEN_PATTERN = (
    r"(?:"
    r"\d{1,2}\s+[A-Za-z]{3,9},?\s+\d{2,4}"
    r"|[A-Za-z]{3,9}\s+\d{1,2},?\s+\d{2,4}"
    r"|\d{1,2}[-/.]\d{1,2}[-/.]\d{2,4}"
    r"|\d{4}[-/.]\d{1,2}[-/.]\d{1,2}"
    r"|\d{1,2}[-/.][A-Za-z]{3,9}[-/.]\d{2,4}"
    r")"
)
DATE_TOKEN_RE = re.compile(DATE_TOKEN_PATTERN, re.I)
LEADING_DATE_RE = re.compile(rf"^\s*({DATE_TOKEN_PATTERN})(?:\s+|$)(.*)$", re.I)


def find(pattern, text, group=1, flags=re.I | re.M):
    m = re.search(pattern, text, flags)
    return norm(m.group(group)) if m else ""


def _normalise_date_text(value: str) -> str:
    value = norm(value)
    value = value.replace("–", "-").replace("—", "-")
    value = re.sub(r"\s*,\s*", ", ", value)
    value = re.sub(r"\s+", " ", value)
    return value.strip(" ,")


def parse_flexible_date(value, *, default_year: int | None = None) -> pd.Timestamp | pd.NaT:
    """Parse an Indian bank date without silently swapping day and month.

    Explicit formats are attempted before pandas' parser.  Numeric dates are
    always interpreted day-first because all supported statements are Indian
    banking statements.  A default year may be supplied for legacy DD/MM rows.
    """
    if value is None or (isinstance(value, float) and pd.isna(value)):
        return pd.NaT
    if isinstance(value, (pd.Timestamp, datetime)):
        return pd.Timestamp(value)

    text = _normalise_date_text(str(value))
    if not text:
        return pd.NaT

    if default_year and re.fullmatch(r"\d{1,2}[-/.]\d{1,2}", text):
        text = f"{text}/{default_year}"

    for fmt in SUPPORTED_DATE_FORMATS:
        try:
            return pd.Timestamp(datetime.strptime(text, fmt))
        except ValueError:
            continue

    # Final guarded fallback for extraction noise.  dayfirst=True is explicit
    # and yearfirst is enabled only when the candidate begins with four digits.
    parsed = pd.to_datetime(
        text,
        errors="coerce",
        dayfirst=not bool(re.match(r"^\d{4}[-/.]", text)),
        yearfirst=bool(re.match(r"^\d{4}[-/.]", text)),
    )
    return pd.NaT if pd.isna(parsed) else pd.Timestamp(parsed)


def date_iso(value, *, default_year: int | None = None) -> str:
    parsed = parse_flexible_date(value, default_year=default_year)
    return "" if pd.isna(parsed) else parsed.strftime("%Y-%m-%d")


def leading_dates(line: str, *, maximum: int = 2) -> tuple[list[str], str]:
    """Return up to ``maximum`` validated dates from the start of a line."""
    remaining = line
    dates: list[str] = []
    for _ in range(maximum):
        match = LEADING_DATE_RE.match(remaining)
        if not match:
            break
        candidate = match.group(1)
        if pd.isna(parse_flexible_date(candidate)):
            break
        dates.append(candidate)
        remaining = match.group(2)
    return dates, remaining


def find_date_tokens(text: str) -> list[str]:
    return [m.group(0) for m in DATE_TOKEN_RE.finditer(text or "") if not pd.isna(parse_flexible_date(m.group(0)))]


def split_pages(text):
    return text.split("\f")


def infer_mode(n):
    u = (n or "").upper()
    for k, v in (
        ("UPI", "UPI"),
        ("NEFT", "NEFT"),
        ("IMPS", "IMPS"),
        ("RTGS", "RTGS"),
        ("CASH DEPOSIT", "Cash Deposit"),
        ("CASH WITHDRAWAL", "Cash Withdrawal"),
        ("ATM", "ATM"),
        ("CHEQUE", "Cheque"),
        ("CHQ", "Cheque"),
        ("POS", "POS"),
        ("EDC", "Card Settlement"),
        ("ACH", "ACH"),
    ):
        if k in u:
            return v
    return "Other"
