#!/usr/bin/env python3
"""Local formal-offer cohort calculator. Standard library only; no network or writes."""
import argparse
import csv
import datetime as dt
import json
import re
import sys
from collections import Counter

FIELDS = ['consideration_id', 'revision', 'first_issued_on', 'recorded_on', 'status', 'outcome_on']
STATUSES = {'pending', 'accepted', 'declined', 'company_withdrawn'}


def date(value):
    if not re.fullmatch(r'\d{4}-\d{2}-\d{2}', value):
        raise ValueError('dates must be YYYY-MM-DD')
    return dt.date.fromisoformat(value)


def calculate(path, start, end, as_of):
    start, end, as_of = map(date, (start, end, as_of))
    if not start <= end <= as_of:
        raise ValueError('require start <= end <= as-of')
    groups = {}
    with open(path, newline='', encoding='utf-8-sig') as stream:
        reader = csv.DictReader(stream)
        if reader.fieldnames != FIELDS:
            raise ValueError('use the exact six-column template header')
        for line, row in enumerate(reader, 2):
            if None in row or any(v is None for v in row.values()):
                raise ValueError(f'line {line}: extra or missing columns')
            ident = row['consideration_id']
            if not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', ident):
                raise ValueError(f'line {line}: use a nonempty opaque ID')
            if not re.fullmatch(r'[1-9][0-9]*', row['revision']):
                raise ValueError(f'line {line}: revision must be a positive integer')
            issued, recorded = date(row['first_issued_on']), date(row['recorded_on'])
            status = row['status']
            if status not in STATUSES or recorded < issued:
                raise ValueError(f'line {line}: invalid status or chronology')
            outcome = date(row['outcome_on']) if row['outcome_on'] else None
            if (status == 'pending') != (outcome is None):
                raise ValueError(f'line {line}: pending has no outcome date; terminal status requires one')
            if outcome and not issued <= outcome <= recorded:
                raise ValueError(f'line {line}: outcome must fall between issue and record dates')
            groups.setdefault(ident, []).append((int(row['revision']), issued, recorded, status, outcome))
    counts = Counter({key: 0 for key in sorted(STATUSES)})
    for ident, history in groups.items():
        history.sort()
        if [r[0] for r in history] != list(range(1, len(history) + 1)):
            raise ValueError(f'{ident}: duplicate or missing revision; start at 1')
        if len({r[1] for r in history}) != 1:
            raise ValueError(f'{ident}: first issue date changed across revisions')
        previous = None
        for current in history:
            if previous and (current[2] < previous[2] or
                             (previous[3] != 'pending' and current[3:] != previous[3:])):
                raise ValueError(f'{ident}: reversed chronology or changed terminal outcome')
            previous = current
        issued = history[0][1]
        eligible = [r for r in history if r[2] <= as_of]
        if start <= issued <= end:
            if not eligible:
                raise ValueError(f'{ident}: issued in cohort but no snapshot at cutoff; reconstruct history')
            counts[eligible[-1][3]] += 1
    total = sum(counts.values())
    resolved = counts['accepted'] + counts['declined']
    ratio = lambda denominator: round(100 * counts['accepted'] / denominator, 2) if denominator else None
    return {'method': 'formal_offer_start_cohort_v1', 'start': str(start), 'end': str(end),
            'as_of': str(as_of), 'considerations': total, 'counts': dict(counts),
            'candidate_resolved_denominator': resolved, 'candidate_resolved_acceptance_pct': ratio(resolved),
            'all_issued_denominator': total, 'accepted_share_all_issued_pct': ratio(total),
            'note': 'null means undefined, not zero. Pending and company withdrawals are separate. Not a benchmark or hire rate.'}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('csv_path')
    for flag in ('start', 'end', 'as-of'):
        parser.add_argument('--' + flag, required=True)
    args = parser.parse_args()
    try:
        result = calculate(args.csv_path, args.start, args.end, args.as_of)
    except (ValueError, OSError, csv.Error) as exc:
        print('Input rejected: ' + str(exc), file=sys.stderr)
        return 2
    print(json.dumps(result, indent=2, sort_keys=True))
    return 0


if __name__ == '__main__':
    sys.exit(main())
