#!/usr/bin/env python3
"""Reconcile the fictional contract exercise; no network or VICIdial writes."""
import argparse
from collections import defaultdict
import csv
from datetime import datetime, timezone
from decimal import Decimal
import json
from pathlib import Path
import re

START = datetime(2026,11,6,tzinfo=timezone.utc)
END = datetime(2026,11,7,tzinfo=timezone.utc)
RATES = {"SOURCE_A": Decimal("12.50"), "SOURCE_B": Decimal("18.00")}


def reconcile(calls, attribution):
    if any(not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,79}', r['call_id']) for r in calls):
        raise ValueError('Use stable nonempty call identifiers, not a spreadsheet formula or free text.')
    # A duplicate call key would inflate the base population before any join.
    if len({r['call_id'] for r in calls}) != len(calls):
        raise ValueError('Call IDs must be unique; investigate repeated export rows first.')
    known = {r['call_id'] for r in calls}
    matches = defaultdict(list)
    for row in attribution:
        if not re.fullmatch(r'[A-Za-z][A-Za-z0-9_-]{0,63}', row['source']):
            raise ValueError('Source labels must be simple identifiers, not free text.')
        if row['call_id'] not in known:
            raise ValueError('Attribution contains an unknown call; reconcile export scopes first.')
        matches[row['call_id']].append(row['source'])
    decisions = []
    counts = dict(raw_rows=len(calls), excluded_window=0, in_window=0,
                  unique_attribution=0, ambiguous_attribution=0, unmatched_attribution=0,
                  candidate_billable=0)
    for row in calls:
        when = datetime.fromisoformat(row['started_utc'].replace('Z','+00:00'))
        if when.tzinfo is None:
            raise ValueError('Timestamps must carry a timezone.')
        duration = int(row['connected_seconds'])
        if duration < 0 or any(row[k] not in ('Y','N') for k in ['qualified','duplicate','consent_reviewed']):
            raise ValueError('Duration or review flags are missing/invalid; unknown does not mean eligible.')
        source_matches = matches[row['call_id']]
        source = source_matches[0] if len(source_matches)==1 else ''
        reasons = []
        if not START <= when < END:
            counts['excluded_window'] += 1
            reasons.append('outside_window')
        else:
            counts['in_window'] += 1
            if not source_matches:
                counts['unmatched_attribution'] += 1
                reasons.append('unmatched_attribution')
            elif len(source_matches) != 1:
                counts['ambiguous_attribution'] += 1
                reasons.append('ambiguous_attribution')
            else:
                counts['unique_attribution'] += 1
                if source not in RATES:
                    reasons.append('unreviewed_source')
            if duration < 60: reasons.append('below_60_connected_seconds')
            if row['qualified'] != 'Y': reasons.append('not_qualified')
            if row['duplicate'] != 'N': reasons.append('duplicate')
            if row['consent_reviewed'] != 'Y': reasons.append('review_missing')
        eligible = not reasons
        amount = RATES[source] if eligible else Decimal('0.00')
        if eligible: counts['candidate_billable'] += 1
        decisions.append(dict(call_id=row['call_id'],source=source,
                              attribution_rows=len(source_matches),
                              decision='candidate_billable' if eligible else 'excluded',
                              reason='all_example_predicates_pass' if eligible else '|'.join(reasons),
                              currency='USD', amount=f'{amount:.2f}'))
    counts['candidate_amount_usd'] = f"{sum(Decimal(r['amount']) for r in decisions):.2f}"
    assert counts['in_window'] == counts['unique_attribution'] + counts['ambiguous_attribution'] + counts['unmatched_attribution']
    assert counts['raw_rows'] == counts['in_window'] + counts['excluded_window']
    return decisions, counts


def main():
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--calls',type=Path,default=Path('calls-fictional.csv'))
    parser.add_argument('--attribution',type=Path,default=Path('attribution-fictional.csv'))
    parser.add_argument('--output',type=Path,default=Path('results'))
    args=parser.parse_args()
    try:
        with args.calls.open(newline='') as handle: calls=list(csv.DictReader(handle))
        with args.attribution.open(newline='') as handle: attribution=list(csv.DictReader(handle))
        rows,counts=reconcile(calls,attribution)
        args.output.mkdir(mode=0o700)  # No overwrite of an earlier reconciliation.
        for name,selected in [('decisions.csv',rows),('candidate-billable.csv',[r for r in rows if r['decision']=='candidate_billable'])]:
            with (args.output/name).open('x',newline='') as handle:
                writer=csv.DictWriter(handle,fieldnames=['call_id','source','attribution_rows','decision','reason','currency','amount']);writer.writeheader();writer.writerows(selected)
        (args.output/'summary.json').write_text(json.dumps(counts,indent=2)+'\n')
        print(json.dumps(counts,indent=2))
    except (OSError,ValueError,KeyError) as error:
        raise SystemExit(f'Reconciliation stopped: {error}')


if __name__=='__main__': main()
