#!/usr/bin/env python3
"""Build an explainable review queue from synthetic transactions; not a fraud model.
Run: python3 analyze.py --input input.csv --output results
Flags mean 'review this record', never a finding of fraud. Standard library only.
"""
import argparse, csv, json
from collections import defaultdict
from datetime import datetime, timedelta
from decimal import Decimal
from html import escape
from pathlib import Path

def analyze(rows):
    ordered = sorted(rows, key=lambda row: (datetime.fromisoformat(row['timestamp']), row['transaction_id']))
    seen, history, flags = set(), defaultdict(list), []
    for row in ordered:
        if row['transaction_id'] in seen: raise ValueError('Duplicate transaction ID')
        seen.add(row['transaction_id'])
        when = datetime.fromisoformat(row['timestamp'])
        amount = Decimal(row['amount_eur'])
        if not amount.is_finite() or amount <= 0: raise ValueError('Invalid transaction amount')
        recent = [t for t in history[row['account_id']] if when-t <= timedelta(minutes=10)]
        reasons = []
        if amount >= 1000 and row['new_device'] == 'true': reasons.append('large_amount_new_device')
        if len(recent) >= 2: reasons.append('third_or_later_payment_within_10_minutes')
        if reasons: flags.append({'transaction_id':row['transaction_id'], 'account_id':row['account_id'],
                                  'amount_eur':row['amount_eur'], 'reasons':';'.join(reasons)})
        history[row['account_id']] = recent + [when]
    counts = {reason: sum(reason in row['reasons'].split(';') for row in flags) for reason in ['large_amount_new_device','third_or_later_payment_within_10_minutes']}
    return {'data_type':'synthetic teaching records', 'transactions':len(ordered), 'flagged_for_review':len(flags),
            'rule_counts':counts, 'flagged_transactions':flags,
            'limitation':'No verified fraud labels; precision, recall and fraud-detection accuracy cannot be calculated.'}

def main():
    parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--input',default='input.csv');parser.add_argument('--output',default='results');args=parser.parse_args()
    with open(args.input,newline='',encoding='utf-8') as file: rows=list(csv.DictReader(file))
    result=analyze(rows);out=Path(args.output);out.mkdir(parents=True,exist_ok=True)
    (out/'summary.json').write_text(json.dumps(result,indent=2)+'\n',encoding='utf-8')
    with (out/'review-queue.csv').open('w',newline='',encoding='utf-8') as file:
        writer=csv.DictWriter(file,fieldnames=['transaction_id','account_id','amount_eur','reasons']);writer.writeheader();writer.writerows(result['flagged_transactions'])
    svg=['<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 300" role="img" aria-labelledby="title desc"><title id="title">Transaction review signals</title><desc id="desc">Calculated signal counts from synthetic teaching records. A flag is not proof of fraud.</desc><rect width="900" height="300" fill="#fff"/><text x="30" y="40" font-size="23" font-family="sans-serif">Review signals · synthetic data</text>']
    for i,(rule,count) in enumerate(result['rule_counts'].items()):
        label=['Large amount + new device','Repeated payments within 10 min'][i];y=90+i*84
        svg.append(f'<text x="30" y="{y+24}" font-size="17" font-family="sans-serif">{escape(label)}</text><rect x="365" y="{y}" width="{count*75}" height="36" rx="4" fill="#008c76"/><text x="{380+count*75}" y="{y+24}" font-size="20" font-family="sans-serif">{count}</text>')
    svg.append('<text x="30" y="270" font-size="16" font-family="sans-serif">Signals may overlap. Flags prioritize manual review; they do not establish fraud.</text></svg>')
    (out/'chart.svg').write_text(''.join(svg),encoding='utf-8');print(json.dumps(result))

if __name__ == '__main__': main()
