from __future__ import annotations

import re
from pathlib import Path

import pandas as pd

from .base import BaseParser, StatementMeta, amount, extract_text, finalize, norm
from .common import DATE_TOKEN_PATTERN, date_iso, find, parse_flexible_date


def _signed_amount(value: str | None, suffix: str | None) -> float | None:
    parsed = amount(value)
    if parsed is None:
        return None
    return -parsed if (suffix or "").upper() == "DR" else parsed


def _nearest_column(position: int, debit_pos: int, credit_pos: int, balance_pos: int) -> str:
    distances = {
        "debit": abs(position - debit_pos),
        "credit": abs(position - credit_pos),
        "balance": abs(position - balance_pos),
    }
    return min(distances, key=distances.get)


class IndianBankModernParser(BaseParser):
    bank_name = "Indian Bank"
    parser_name = "IndianBankModernParser"

    @classmethod
    def detect(cls, text):
        u = norm(text).upper()
        return 0.98 if "ACCOUNT STATEMENT" in u and "TRANSACTION DETAILS" in u and "TOTAL CREDITS" in u else 0

    def parse(self, path, text=None):
        text = text or extract_text(path)
        meta = StatementMeta(
            bank_name=self.bank_name,
            source_file=Path(path).name,
            parser_name=self.parser_name,
            confidence="High",
        )

        customer = norm(find(r"Account Holder Name[ \t]*([^\n]*)", text))
        if customer and not re.search(r"^(Opening Balance|Account Type|Account Number|Customer)", customer, re.I):
            meta.customer_name = customer
        meta.account_number = find(r"Account Number[ \t]*([0-9X*]{4,})", text)
        meta.ifsc = find(r"IFSC[ \t]*([A-Z0-9]{8,})", text)

        period = re.search(rf"For period:\s*({DATE_TOKEN_PATTERN})\s*(?:-|to)\s*({DATE_TOKEN_PATTERN})", text, re.I)
        if period:
            meta.period_from = date_iso(period.group(1))
            meta.period_to = date_iso(period.group(2))

        opening = re.search(r"Opening Balance\s+INR\s*([\d,]+\.\d{2})\s*(CR|DR)?", text, re.I)
        if opening:
            meta.opening_balance = _signed_amount(opening.group(1), opening.group(2))
        meta.total_credit = amount(find(r"Total Credits\s+\+\s*INR\s*([\d,]+\.\d{2})", text))
        meta.total_debit = amount(find(r"Total Debits\s+-\s*INR\s*([\d,]+\.\d{2})", text))
        closing = re.search(r"Ending Balance\s+INR\s*([\d,]+\.\d{2})\s*(CR|DR)?", text, re.I)
        if closing:
            meta.closing_balance = _signed_amount(closing.group(1), closing.group(2))

        date_re = re.compile(rf"^\s*({DATE_TOKEN_PATTERN})\s+(.*)$", re.I)
        amount_re = re.compile(r"INR\s*([\d,]+\.\d{2})\s*(CR|DR)?", re.I)

        rows: list[dict] = []
        current: dict | None = None
        page = 1
        # Defaults match the wide first-page layout.  They are replaced from
        # every page header, which also supports narrower subsequent pages.
        debit_pos, credit_pos, balance_pos = 56, 80, 94

        for raw_line in text.splitlines():
            if "\f" in raw_line:
                page += raw_line.count("\f")
            line = raw_line.rstrip("\n")
            upper = line.upper()

            if "TRANSACTION DETAILS" in upper and "DEBITS" in upper and "CREDITS" in upper and "BALANCE" in upper:
                debit_pos = line.upper().find("DEBITS")
                credit_pos = line.upper().find("CREDITS")
                balance_pos = line.upper().rfind("BALANCE")
                continue

            match = date_re.match(line)
            if match and not pd.isna(parse_flexible_date(match.group(1))):
                if current:
                    rows.append(current)
                current = {
                    "transaction_date": date_iso(match.group(1)),
                    "value_date": date_iso(match.group(1)),
                    "first_line": line,
                    "detail_start": match.start(2),
                    "continuation": [],
                    "source_page": page,
                    "debit_pos": debit_pos,
                    "credit_pos": credit_pos,
                    "balance_pos": balance_pos,
                }
                continue

            if not current or not line.strip():
                continue
            if re.match(r"^(\s*Date\s+Transaction|\s*ACCOUNT STATEMENT|\s*Indian Bank\s*\||\s*Ending Balance|\s*Total\s+INR)", line, re.I):
                continue
            current["continuation"].append(line.strip())

        if current:
            rows.append(current)

        output: list[dict] = []
        previous_balance = meta.opening_balance

        for row in rows:
            first_line = row["first_line"]
            tokens = list(amount_re.finditer(first_line))
            if not tokens:
                continue

            debit = credit = balance = None
            balance_suffix = None
            token_columns: list[tuple[str, re.Match]] = []
            for token in tokens:
                column = _nearest_column(token.start(), row["debit_pos"], row["credit_pos"], row["balance_pos"])
                token_columns.append((column, token))

            # The running balance is the rightmost amount and is also checked
            # against the header-derived balance column.
            balance_token = tokens[-1]
            balance_suffix = balance_token.group(2)
            balance = _signed_amount(balance_token.group(1), balance_suffix)

            for column, token in token_columns[:-1]:
                value = amount(token.group(1))
                if column == "debit":
                    debit = value
                elif column == "credit":
                    credit = value

            # When text extraction collapses spacing, infer the transaction side
            # from the running balance rather than guessing by token position.
            if debit is None and credit is None and len(tokens) >= 2:
                txn = amount(tokens[-2].group(1))
                if txn is not None and previous_balance is not None and balance is not None:
                    if abs((previous_balance - txn) - balance) < 0.05:
                        debit = txn
                    elif abs((previous_balance + txn) - balance) < 0.05:
                        credit = txn

            # If the balance has no explicit DR/CR suffix, preserve continuity.
            if balance is not None and previous_balance is not None:
                expected = previous_balance + (credit or 0) - (debit or 0)
                if abs(expected - balance) >= 0.05 and abs(expected + balance) < 0.05:
                    balance = -balance

            if debit is None and credit is None:
                continue

            first_detail_end = min((m.start() for m in tokens), default=len(first_line))
            first_detail = first_line[row["detail_start"]:first_detail_end].strip()
            narration_parts = [first_detail] + row["continuation"]
            narration = norm(" ".join(part for part in narration_parts if part))

            reference_no = ""
            reference = re.search(r"(?:NEFT|IMPS|UPI|RTGS)[/A-Z0-9-]{6,}", narration, re.I)
            if reference:
                reference_no = reference.group(0)

            output.append(
                {
                    "transaction_date": row["transaction_date"],
                    "value_date": row["value_date"],
                    "narration": narration,
                    "reference_no": reference_no,
                    "debit": debit,
                    "credit": credit,
                    "balance": balance,
                    "source_page": row["source_page"],
                }
            )
            if balance is not None:
                previous_balance = balance

        frame = finalize(pd.DataFrame(output), meta)
        if not frame.empty:
            # Validate and repair metadata from the parsed rows only when the
            # statement summary did not supply it.
            first = frame.iloc[0]
            calculated_opening = (first["balance"] or 0) + (first["debit"] or 0) - (first["credit"] or 0)
            if meta.opening_balance is None:
                meta.opening_balance = round(calculated_opening, 2)
            if meta.closing_balance is None:
                meta.closing_balance = float(frame["balance"].dropna().iloc[-1])
        return meta, frame


class IndianBankLegacyParser(BaseParser):
    bank_name = "Indian Bank"
    parser_name = "IndianBankLegacyParser"

    @classmethod
    def detect(cls, text):
        u = text.upper()
        return 0.97 if "STATEMENT OF ACCOUNT FROM" in u and "REMITTER" in u and "CHEQUE NO" in u else 0

    def parse(self, path, text=None):
        text = text or extract_text(path)
        meta = StatementMeta(
            bank_name=self.bank_name,
            source_file=Path(path).name,
            parser_name=self.parser_name,
            confidence="Medium",
        )
        meta.account_number = find(r"for Account Number\s*\.?\s*([0-9X*]+)", text)
        period = re.search(rf"STATEMENT OF ACCOUNT from\s*({DATE_TOKEN_PATTERN})\s*to\s*({DATE_TOKEN_PATTERN})", text, re.I)
        if period:
            meta.period_from = date_iso(period.group(1))
            meta.period_to = date_iso(period.group(2))

        # Legacy files may omit the year on each transaction.  Preserve the
        # previous behaviour while accepting every supported full-date format.
        default_year = int(meta.period_from[:4]) if meta.period_from else None
        full_date_re = re.compile(rf"^\s*({DATE_TOKEN_PATTERN})\s+({DATE_TOKEN_PATTERN})\s+(.*)$", re.I)
        short_date_re = re.compile(r"^\s*(\d{1,2}[-/.]\d{1,2})\s+(\d{1,2}[-/.]\d{1,2})\s+(.*)$")
        rows = []
        current = None
        page = 1
        for line in text.splitlines():
            if "\f" in line:
                page += line.count("\f")
            match = full_date_re.match(line) or short_date_re.match(line)
            if match:
                if current:
                    rows.append(current)
                transaction_date = date_iso(match.group(1), default_year=default_year)
                value_date = date_iso(match.group(2), default_year=default_year)
                current = {
                    "transaction_date": transaction_date,
                    "value_date": value_date,
                    "lines": [match.group(3)],
                    "source_page": page,
                }
            elif current and line.strip() and not re.match(r"^(Value Post|Date Date|STATEMENT OF ACCOUNT|Page No)", line.strip(), re.I):
                current["lines"].append(line)
        if current:
            rows.append(current)

        output = []
        previous_balance = None
        for row in rows:
            first = row["lines"][0]
            all_text = norm(" ".join(row["lines"]))
            balance_match = re.search(r"([\d,]+\.\d{2})(CR|DR)\s*$", first, re.I)
            if not balance_match:
                continue
            balance = _signed_amount(balance_match.group(1), balance_match.group(2))
            prefix = first[: balance_match.start()]
            numbers = list(re.finditer(r"(?<!\d)([\d,]+\.\d{2})(?!\d)", prefix))
            transaction_amount = amount(numbers[-1].group(1)) if numbers else None
            debit = credit = None
            if transaction_amount is not None and previous_balance is not None:
                if abs((previous_balance - transaction_amount) - balance) < 0.05:
                    debit = transaction_amount
                elif abs((previous_balance + transaction_amount) - balance) < 0.05:
                    credit = transaction_amount
            if transaction_amount is not None and debit is None and credit is None:
                if numbers[-1].start() > 70:
                    credit = transaction_amount
                else:
                    debit = transaction_amount
            if debit is None and credit is None:
                continue
            reference = re.search(r"(?:NEFT|IMPS|UPI|RTGS)[/A-Z0-9-]{6,}", all_text, re.I)
            output.append(
                {
                    **row,
                    "narration": norm(prefix[: numbers[-1].start()] + " " + " ".join(row["lines"][1:])),
                    "reference_no": reference.group(0) if reference else "",
                    "debit": debit,
                    "credit": credit,
                    "balance": balance,
                }
            )
            previous_balance = balance
        return meta, finalize(pd.DataFrame(output), meta)
