#!/usr/bin/env python3
"""
csv_to_ledger.py — reshape a bank/ledger CSV (Date,Amount,Description|Memo — the same
shape run_reconcile.sh reads) into the ledger-JSON list-of-records the vendored CPA
Audit Pack modules (anomaly/match/envelope) read: one record per row with
_transaction_ref, _supplier, amount, date ("DD-Mon-YY"), description, category.

This performs NO audit computation. It converts column shapes and re-encodes dates and
amounts so the vendored modules can read a CSV export instead of their research fixture.
Every audit finding still comes from the vendored run.py that reads this file's output.

Amounts are taken as absolute value: the audit-pack tests operate on transaction
magnitude (Benford digits, round-number clustering, duplicate-amount matching), and the
source CSVs encode debits/credits as sign or parentheses, not as two ledgers. If you need
signed amounts preserved, do not use this converter — write your own reshape.

Usage: csv_to_ledger.py <in.csv> <out.json>
"""
import csv
import json
import sys
from datetime import datetime


def parse_amount_pence(raw: str) -> int:
    """Exact integer-pence parse — no float ever touches an amount here.
    Amounts with more than 2 decimal places are refused (ValueError), not
    silently rounded, mirroring the reconciler's own typed-refusal posture."""
    s = raw.strip()
    neg = s.startswith("(") and s.endswith(")")
    s = s.strip("()").replace(",", "").replace("$", "").lstrip("+")
    if s.startswith("-"):
        neg = True
        s = s[1:]
    if not s:
        return 0
    whole, _, frac = s.partition(".")
    if len(frac) > 2:
        raise ValueError(f"more than 2 decimal places: {raw!r}")
    if not (whole or frac) or not (whole or "0").isdigit() or not (frac or "0").isdigit():
        raise ValueError(f"unparseable amount: {raw!r}")
    pence = int(whole or "0") * 100 + int((frac or "0").ljust(2, "0"))
    return -pence if neg else pence


def main() -> None:
    if len(sys.argv) != 3:
        print("usage: csv_to_ledger.py <in.csv> <out.json>", file=sys.stderr)
        raise SystemExit(2)
    in_path, out_path = sys.argv[1], sys.argv[2]

    with open(in_path, newline="", encoding="utf-8") as f:
        reader = csv.DictReader(f)
        fieldnames = reader.fieldnames or []
        desc_col = next((c for c in ("Description", "Memo", "description", "memo") if c in fieldnames), None)
        date_col = next((c for c in ("Date", "date") if c in fieldnames), None)
        amount_col = next((c for c in ("Amount", "amount") if c in fieldnames), None)
        if not (date_col and amount_col):
            print(f"csv_to_ledger.py: expected Date and Amount columns, got {fieldnames}", file=sys.stderr)
            raise SystemExit(3)

        rows = []
        refused = 0
        for i, row in enumerate(reader):
            # Typed, explicit failure per row (mirrors apostol-reconcile's own
            # refusal-by-name behavior): a malformed amount or date is skipped
            # and reported, never silently coerced or allowed to crash the run.
            try:
                pence = abs(parse_amount_pence(row.get(amount_col, "0")))
                # Single exact boundary encode into the vendored modules' own
                # float-JSON wire format (int pence / 100). All parsing and
                # validation above is integer-exact; no float arithmetic is
                # performed on any amount in this converter.
                amount = pence / 100
                dt = datetime.strptime(row.get(date_col, "").strip(), "%Y-%m-%d")
            except ValueError as e:
                print(f"csv_to_ledger.py: refused row {i} (unparseable amount/date): {e}", file=sys.stderr)
                refused += 1
                continue
            desc = (row.get(desc_col, "") if desc_col else "").strip()
            desc_lower = desc.lower()
            supplier = desc
            for marker in (" invoice", " inv ", " pmt", " wire", " remittance"):
                pos = desc_lower.find(marker)
                if pos != -1:
                    supplier = desc[:pos]
                    break
            supplier = supplier.strip() or f"ROW-{i}"
            rows.append({
                "_transaction_ref": f"CSVROW-{i}",
                "_supplier": supplier,
                "amount": amount,
                "date": dt.strftime("%d-%b-%y"),
                "description": desc,
                "category": "GENERAL",
            })

    with open(out_path, "w", encoding="utf-8") as f:
        json.dump(rows, f, indent=2)
    print(f"csv_to_ledger.py: wrote {len(rows)} rows -> {out_path} ({refused} row(s) refused, unparseable)")


if __name__ == "__main__":
    main()
