import csv, collections, datetime as dt, math, sys
FILE, THRESHOLD, CLOSE_DATE, YEAR_END = sys.argv[1], 10000, dt.date(2026, 1, 20), dt.date(2025, 12, 31)
HOLIDAYS = {dt.date(2025, m, d) for m, d in [(1,1),(1,6),(4,18),(5,1),(8,15),(10,13),(11,1),(12,6),(12,8),(12,25)]}
lines = list(csv.DictReader(open(FILE, encoding="utf-8")))
entries = collections.defaultdict(list)
for l in lines: entries[l["entry"]].append(l)

def amount(e): return round(sum(float(l["debit"]) for l in e), 2)
assert all(abs(sum(float(l["debit"]) - float(l["credit"]) for l in e)) < 0.005 for e in entries.values()), "unbalanced entry"
by_user = collections.Counter(e[0]["user"] for e in entries.values())
flags = collections.defaultdict(list)
for k, e in entries.items():
    h, a = e[0], amount(e)
    acc_date, entered = dt.date.fromisoformat(h["accounting_date"]), dt.date.fromisoformat(h["entered_date"])
    manual = h["source"] == "manual"
    if manual and 0.95 * THRESHOLD <= a < THRESHOLD: flags["just below approval limit"].append(k)
    if manual and (entered.weekday() >= 5 or entered in HOLIDAYS): flags["posted on weekend/holiday"].append(k)
    if acc_date <= YEAR_END and entered > CLOSE_DATE: flags["posted after close into the year"].append(k)
    if manual and a >= 5000 and a % 1000 == 0: flags["round amount"].append(k)
    if by_user[h["user"]] <= 3: flags["user with <= 3 entries"].append(k)

# near-duplicates: same counterparty account and amount within 5 days
seen = collections.defaultdict(list)
for k, e in entries.items():
    for l in e:
        if l["account"].startswith("400") and float(l["credit"]) > 0:
            seen[(l["account"], l["credit"])].append((dt.date.fromisoformat(l["accounting_date"]), k))
for v in seen.values():
    v.sort()
    for (d1, k1), (d2, k2) in zip(v, v[1:]):
        if (d2 - d1).days <= 5: flags["possible duplicate"].append(f"{k1}/{k2}")

# Benford first-digit test on supplier invoice amounts (screening only)
amts = [float(l["credit"]) for l in lines if l["account"].startswith("400") and float(l["credit"]) >= 10]
obs = collections.Counter(int(str(a).lstrip("0.")[0]) for a in amts)
mad = sum(abs(obs[d] / len(amts) - math.log10(1 + 1 / d)) for d in range(1, 10)) / 9

print(f"{len(entries)} entries, {len(lines)} lines, all balanced")
for f, ks in flags.items(): print(f"{f}: {len(ks)} -> {', '.join(ks[:8])}")
print(f"Benford first digit: n={len(amts)}, mean absolute deviation={mad:.4f}")
