"""PRD §5.4 FRFM048 / FRFM049 — bank-statement upload, parse, and auto-match.

The upload view delegates to :func:`import_bank_statement`. The matcher
attempts to link each unmatched line to a DisbursementExecution by exact
amount + value-date proximity (±3 days), then flips ``match_status`` to
``matched`` and records the corresponding GLEntry link.
"""
from __future__ import annotations

import csv
import io
from datetime import date, timedelta
from decimal import Decimal
from typing import Iterable

from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from apps.rims.finance.models import (
    BankStatement,
    BankStatementLine,
    DisbursementExecution,
    GLEntry,
)


@transaction.atomic
def import_bank_statement(
    *,
    actor,
    file,
    name: str,
    period_start: date,
    period_end: date,
    account_reference: str,
    currency: str = "USD",
) -> BankStatement:
    """PRD §5.4 FRFM048 — persist an uploaded bank statement file."""
    if period_end < period_start:
        raise ValidationError("Period end must be on or after period start.")
    statement = BankStatement.objects.create(
        name=name,
        period_start=period_start,
        period_end=period_end,
        currency=currency,
        uploaded_by=actor,
        file=file,
        account_reference=account_reference,
    )
    try:
        from apps.rims.grants.audit_helper import audit_rims

        audit_rims(
            actor=actor,
            action="BANK_STATEMENT_UPLOADED",
            target_model="BankStatement",
            object_id=statement.pk,
            object_repr=str(statement),
            changes={"period": f"{period_start} → {period_end}", "currency": currency},
        )
    except Exception:  # pragma: no cover
        pass
    return statement


def parse_bank_statement_lines(statement: BankStatement) -> list[BankStatementLine]:
    """Parse the uploaded CSV file into BankStatementLine rows.

    Expected CSV columns: value_date (YYYY-MM-DD), description, amount.
    Currency defaults to the statement's currency; existing lines on the
    statement are left in place (idempotent imports just append).
    """
    rows: list[BankStatementLine] = []
    statement.file.open(mode="rb")
    try:
        raw = statement.file.read().decode("utf-8", errors="replace")
    finally:
        statement.file.close()
    reader = csv.DictReader(io.StringIO(raw))
    bulk: list[BankStatementLine] = []
    for r in reader:
        try:
            value_date = date.fromisoformat((r.get("value_date") or "").strip())
        except ValueError:
            continue
        amount_raw = (r.get("amount") or "0").strip()
        try:
            amount = Decimal(amount_raw)
        except Exception:
            continue
        desc = (r.get("description") or "")[:512]
        bulk.append(
            BankStatementLine(
                statement=statement,
                value_date=value_date,
                description=desc,
                amount=amount,
                currency=statement.currency,
            )
        )
    if bulk:
        BankStatementLine.objects.bulk_create(bulk)
        rows = list(statement.lines.all())
    return rows


@transaction.atomic
def auto_match_bank_lines(statement: BankStatement) -> dict[str, int]:
    """PRD §5.4 FRFM048/049 — match unmatched lines against executions.

    Matching rules:
        - amount equals execution.request.amount
        - value_date within ±3 days of execution.executed_at (when set)
        - currency must match

    Returns counts dict: {"matched": N, "unmatched": M}.
    """
    matched = 0
    unmatched = 0
    delta = timedelta(days=3)
    candidates = (
        DisbursementExecution.objects.filter(
            status=DisbursementExecution.Status.EXECUTED,
            executed_at__isnull=False,
        )
        .select_related("request", "request__budget")
    )
    for line in statement.lines.filter(match_status=BankStatementLine.MatchStatus.UNMATCHED):
        match = None
        for exe in candidates:
            if exe.request.budget.currency != line.currency:
                continue
            if Decimal(exe.request.amount or 0) != Decimal(line.amount):
                continue
            exec_date = exe.executed_at.date() if exe.executed_at else None
            if exec_date is None:
                continue
            if abs((exec_date - line.value_date).days) > delta.days:
                continue
            match = exe
            break
        if match:
            line.matched_execution = match
            line.matched_gl_entry = match.gl_entries.first()
            line.match_status = BankStatementLine.MatchStatus.MATCHED
            line.save(
                update_fields=["matched_execution", "matched_gl_entry", "match_status"]
            )
            matched += 1
        else:
            unmatched += 1
    try:
        from apps.rims.grants.audit_helper import audit_rims

        audit_rims(
            actor=None,
            action="BANK_STATEMENT_RECONCILED",
            target_model="BankStatement",
            object_id=statement.pk,
            object_repr=str(statement),
            changes={"matched": matched, "unmatched": unmatched},
        )
    except Exception:  # pragma: no cover
        pass
    return {"matched": matched, "unmatched": unmatched}


@transaction.atomic
def confirm_bank_match(
    line: BankStatementLine,
    *,
    execution: DisbursementExecution,
    actor,
) -> BankStatementLine:
    """PRD §5.4 FRFM048 — manual confirmation path used by the UI."""
    line.matched_execution = execution
    line.matched_gl_entry = execution.gl_entries.first()
    line.match_status = BankStatementLine.MatchStatus.MATCHED
    line.save(update_fields=["matched_execution", "matched_gl_entry", "match_status"])
    try:
        from apps.rims.grants.audit_helper import audit_rims

        audit_rims(
            actor=actor,
            action="BANK_LINE_MATCHED",
            target_model="BankStatementLine",
            object_id=line.pk,
            object_repr=str(line),
            changes={"execution_id": execution.pk},
        )
    except Exception:  # pragma: no cover
        pass
    return line
