import csv
import io
from datetime import date, datetime

_DATE_ALIASES = {"fecha", "date", "fecha_movimiento", "fecha movimiento"}
_AMOUNT_ALIASES = {"monto", "amount", "valor", "importe"}
_REFERENCE_ALIASES = {
    "referencia", "reference", "glosa", "descripcion", "descripción", "detalle", "concepto",
}
_DATE_FORMATS = ["%Y-%m-%d", "%d-%m-%Y", "%d/%m/%Y", "%d.%m.%Y"]


def parse_ledger_csv(csv_text: str) -> list:
    """Parsea un CSV de libro contable con columnas flexibles (fecha, monto, referencia)."""
    reader = csv.DictReader(io.StringIO(csv_text.strip()))
    if not reader.fieldnames:
        raise ValueError("El CSV del libro contable está vacío o no tiene encabezados")

    col_map = {}
    for key, aliases in (
        ("date", _DATE_ALIASES),
        ("amount", _AMOUNT_ALIASES),
        ("reference", _REFERENCE_ALIASES),
    ):
        for h in reader.fieldnames:
            if h.strip().lower() in aliases:
                col_map[key] = h
                break

    if "date" not in col_map or "amount" not in col_map:
        raise ValueError(
            "El CSV debe tener columnas de fecha y monto reconocibles "
            "(ej: fecha, monto, referencia)"
        )

    entries = []
    for row_num, row in enumerate(reader, start=2):
        raw_date = (row.get(col_map["date"]) or "").strip()
        raw_amount = (row.get(col_map["amount"]) or "").strip()
        raw_ref = (row.get(col_map.get("reference", "")) or "").strip() if "reference" in col_map else ""
        if not raw_date or not raw_amount:
            continue
        entries.append(
            {
                "date": _parse_flexible_date(raw_date, row_num),
                "amount": _parse_flexible_amount(raw_amount, row_num),
                "reference": raw_ref,
            }
        )

    if not entries:
        raise ValueError("El CSV no contiene filas válidas (revisa fecha y monto en cada fila)")

    return entries


def _parse_flexible_date(raw: str, row_num: int) -> str:
    for fmt in _DATE_FORMATS:
        try:
            return datetime.strptime(raw, fmt).date().isoformat()
        except ValueError:
            continue
    raise ValueError(f"Fecha inválida en fila {row_num}: '{raw}' (usa YYYY-MM-DD o DD-MM-YYYY)")


def _parse_flexible_amount(raw: str, row_num: int) -> float:
    s = raw.replace(" ", "").replace("$", "")
    negative = False
    if s.startswith("(") and s.endswith(")"):
        negative = True
        s = s[1:-1]
    if s.startswith("-"):
        negative = True
        s = s[1:]
    if "," in s and "." in s:
        if s.rfind(",") > s.rfind("."):
            s = s.replace(".", "").replace(",", ".")
        else:
            s = s.replace(",", "")
    elif "," in s:
        s = s.replace(",", ".")
    try:
        value = float(s)
    except ValueError:
        raise ValueError(f"Monto inválido en fila {row_num}: '{raw}'")
    return -value if negative else value


def reconcile(bank_txs: list, ledger_entries: list, tolerance_days: int = 2) -> dict:
    """Concilia transacciones bancarias (MT940 parseado) contra entradas de libro contable.

    Estrategia en dos pasadas: primero monto+fecha exacta+referencia coincidente (alta
    confianza), luego monto exacto+fecha dentro de tolerancia (media confianza). El resto
    queda como no conciliado en cada lado.
    """
    used_bank = set()
    used_ledger = set()
    matched = []

    for bi, b in enumerate(bank_txs):
        b_haystack = f"{b.get('reference', '')} {b.get('bank_reference', '')} {b.get('transaction_details', '')}".lower()
        for li, l in enumerate(ledger_entries):
            if li in used_ledger:
                continue
            if abs(b["signed_amount"] - l["amount"]) > 0.01:
                continue
            if b["value_date"] != l["date"]:
                continue
            l_ref = (l.get("reference") or "").strip().lower()
            if l_ref and l_ref in b_haystack:
                matched.append(
                    {
                        "bank": b,
                        "ledger": l,
                        "confidence": "alta",
                        "match_reason": "monto + fecha + referencia coincidente",
                    }
                )
                used_bank.add(bi)
                used_ledger.add(li)
                break

    for bi, b in enumerate(bank_txs):
        if bi in used_bank:
            continue
        best_li, best_diff = None, None
        b_date = date.fromisoformat(b["value_date"])
        for li, l in enumerate(ledger_entries):
            if li in used_ledger:
                continue
            if abs(b["signed_amount"] - l["amount"]) > 0.01:
                continue
            diff = abs((b_date - date.fromisoformat(l["date"])).days)
            if diff <= tolerance_days and (best_diff is None or diff < best_diff):
                best_li, best_diff = li, diff
        if best_li is not None:
            matched.append(
                {
                    "bank": b,
                    "ledger": ledger_entries[best_li],
                    "confidence": "media",
                    "match_reason": f"monto exacto, fecha con diferencia de {best_diff} día(s)",
                }
            )
            used_bank.add(bi)
            used_ledger.add(best_li)

    unmatched_bank = [b for bi, b in enumerate(bank_txs) if bi not in used_bank]
    unmatched_ledger = [l for li, l in enumerate(ledger_entries) if li not in used_ledger]

    bank_count = len(bank_txs)
    ledger_count = len(ledger_entries)
    matched_count = len(matched)
    total = bank_count + ledger_count
    match_rate = round((2 * matched_count / total), 4) if total else 0.0

    return {
        "matched": matched,
        "unmatched_bank": unmatched_bank,
        "unmatched_ledger": unmatched_ledger,
        "summary": {
            "bank_count": bank_count,
            "ledger_count": ledger_count,
            "matched_count": matched_count,
            "unmatched_bank_count": len(unmatched_bank),
            "unmatched_ledger_count": len(unmatched_ledger),
            "match_rate": match_rate,
        },
    }
