#!/usr/bin/env python3
"""Checks the late-payers figures against Find a Tender's own rows, without trusting the Actor.

1. Every Actor row must match the raw notice (same id, period, % within 30 days, 31-60, 61+, average days, not paid).
2. The current set must be exactly: every raw UK17 notice minus the notices a correction replaced.
3. Headline numbers in results.json are recomputed from the raw statistics alone.
4. Every organisation named in a results table must appear in the raw rows with the same figure.
Run after late-payers-2026-fetch.py and late-payers-2026.py. Exits 1 on any mismatch.
"""
import json
import statistics as st
import sys
from pathlib import Path

OUT = Path(__file__).resolve().parent / 'out' / 'late-payers-2026'
rows = json.loads((OUT / 'raw.json').read_text())
fts = json.loads((OUT / 'fts-statistics.json').read_text())
res = json.loads((OUT / 'results.json').read_text())
errors = []


def num(v):
    return None if v in (None, '') else float(v)


def raw_fig(nid):
    c = fts[nid]['compliance'][0]
    s = c['statistics']
    p = lambda k: None if num(s.get(k)) is None else round(100 * num(s[k]), 2)  # noqa: E731
    return {
        'start': c['period']['startDate'][:10], 'end': c['period']['endDate'][:10],
        'within30': p('proportionPaidIn30orFewerDays'), 'd31_60': p('proportionPaidIn31to60Days'),
        'd61': p('proportionPaidIn61orMoreDays'), 'notPaid': p('proportionNotPaid'),
        'avg': num(s.get('averagePaymentTimeInDays')), 'dimension': c['dimension'],
    }


# 1. row-by-row
close = lambda a, b, tol=0.02: (a is None and b is None) or (a is not None and b is not None and abs(a - b) <= tol)  # noqa: E731
for r in rows:
    if r['noticeId'] not in fts:
        errors.append(f"missing raw notice {r['noticeId']}")
        continue
    f = raw_fig(r['noticeId'])
    checks = [('start', r['paymentPeriodStart'], f['start']), ('end', r['paymentPeriodEnd'], f['end'])]
    for name, a, b in checks:
        if a != b:
            errors.append(f"{r['noticeId']} {name}: actor {a} raw {b}")
    for name, a, b in [('within30', r['paidWithin30DaysPct'], f['within30']), ('31-60', r['paid31To60DaysPct'], f['d31_60']),
                       ('61+', r['paid61PlusDaysPct'], f['d61']), ('notPaid', r['duePaymentsNotMadePct'], f['notPaid']),
                       ('avg', r['averageDaysToPay'], f['avg'])]:
        if not r['emptyReport'] and not close(a, b, 0.6 if name == 'avg' else 0.02):
            errors.append(f"{r['noticeId']} {name}: actor {a} raw {b}")

# 2. current set = raw minus corrected
replaced = {v['previousNotice'] for v in fts.values() if v['previousNotice']}
current_raw = set(fts) - replaced
actor_ids = {r['noticeId'] for r in rows}
if current_raw != actor_ids:
    errors.append(f"current set differs: raw-only {sorted(current_raw - actor_ids)[:5]}, actor-only {sorted(actor_ids - current_raw)[:5]}")
print(f'raw notices {len(fts)}, replaced by corrections {len(replaced)}, current {len(current_raw)}, actor rows {len(rows)}')

# 3. headline recomputed from raw only (main period, reports with a within-30 figure and not all-zero)
main_raw, main_all_raw = [], []
for nid in current_raw:
    f = raw_fig(nid)
    vals = [f['within30'], f['d31_60'], f['d61'], f['avg'], f['notPaid']]
    if f['within30'] is None or all((v or 0) == 0 for v in vals):
        continue
    if (f['start'], f['end']) == ('2025-10-01', '2026-03-31'):
        main_raw.append(f)
        if f['dimension'] == 'all':
            main_all_raw.append(f)
for label, got, want in [
    ('main reports', len(main_raw), res['late']['reports']),
    ('main all-contracts reports', len(main_all_raw), res['lateAllContracts']['reports']),
    ('median within 30', round(st.median(f['within30'] for f in main_raw), 1), res['late']['medianPaidWithin30']),
    ('median avg days', round(st.median(f['avg'] for f in main_raw if f['avg'] is not None), 1), res['late']['medianAvgDays']),
    ('>=95%', sum(f['within30'] >= 95 for f in main_raw), res['late']['atLeast95']),
    ('<80%', sum(f['within30'] < 80 for f in main_raw), res['late']['under80']),
    ('<50%', sum(f['within30'] < 50 for f in main_raw), res['late']['under50']),
    ('=100%', sum(f['within30'] == 100 for f in main_raw), res['late']['all100']),
    ('mean within 30', round(st.mean(f['within30'] for f in main_raw), 1), res['late']['meanPaidWithin30']),
]:
    ok = abs(got - want) < 0.051
    print(f"{'ok ' if ok else 'BAD'} {label}: raw {got} results {want}")
    if not ok:
        errors.append(label)

# 4. every named organisation's figure matches its raw notice
named = res['worst'] + res['best'] + res['slowestAverage'] + res['changes'] + res['latestLowest'] + res['narrowScopeZero']
for r in named:
    f = raw_fig(r['noticeId'])
    if not close(r['paidWithin30DaysPct'], f['within30']) or not close(r['averageDaysToPay'], f['avg'], 0.6):
        errors.append(f"named {r['authorityName']} {r['noticeId']}")
    if r['authorityName'].lower().split()[0] not in (fts[r['noticeId']]['buyerName'] or '').lower():
        errors.append(f"name {r['authorityName']} vs raw {fts[r['noticeId']]['buyerName']}")
for r in res['changes']:
    prev = [x for x in rows if x['noticeId'] == r['noticeId']][0]['previousNoticeId']
    if prev and prev in fts and not close(r['previousPaidWithin30DaysPct'], raw_fig(prev)['within30']):
        errors.append(f"previous figure {r['authorityName']}")
print(f'named organisations checked: {len(named)}')

if errors:
    print('\n'.join(errors[:40]))
    print(f'{len(errors)} problems')
    sys.exit(1)
print('all checks passed')
