#!/usr/bin/env python3
"""Fixed-horizon outbound recruiting funnel. Python 3, standard library only.

Runs locally, reads CSV, writes aggregate CSV to stdout. No network calls.
See recruitment-funnel-method.md for assumptions and field definitions.
"""
import argparse
import csv
from datetime import date, timedelta
import re
import sys

STAGES = ('contacted', 'replied', 'interested', 'booked', 'held', 'offered', 'accepted')
FIELDS = ('person_id', 'role_id', *STAGES)


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


def calculate(rows, role, start, end, as_of, days):
    if days < 1 or start > end or end > as_of:
        raise ValueError('require positive follow-up days and start <= end <= as-of')
    records = {}
    duplicates = 0
    for line, raw in enumerate(rows, 2):
        if any(field not in raw for field in FIELDS):
            raise ValueError('missing required CSV column')
        if None in raw or any(raw[field] is None for field in FIELDS):
            raise ValueError(f'row {line}: wrong number of CSV cells')
        row = {field: raw[field].strip() for field in FIELDS}
        if not row['person_id'] or not row['role_id']:
            raise ValueError(f'row {line}: person_id and role_id are required')
        key = (row['person_id'], row['role_id'])
        if key in records:
            if records[key] != row:
                raise ValueError(f'row {line}: conflicting duplicate person-role pair')
            duplicates += 1
            continue
        dates = [parse_date(row[stage]) if row[stage] else None for stage in STAGES]
        if dates[0] is None:
            raise ValueError(f'row {line}: contacted date is required')
        for index, value in enumerate(dates):
            if value is None:
                continue
            if value > as_of:
                raise ValueError(f'row {line}: an event is after the as-of date')
            if index and (dates[index - 1] is None or value < dates[index - 1]):
                raise ValueError(f'row {line}: missing predecessor or reversed stage dates')
        records[key] = row

    counts = [0] * len(STAGES)
    in_window = immature = 0
    for row in records.values():
        entered = parse_date(row['contacted'])
        if row['role_id'] != role or not start <= entered <= end:
            continue
        in_window += 1
        deadline = entered + timedelta(days=days)
        if deadline > as_of:
            immature += 1
            continue
        for index, stage in enumerate(STAGES):
            if row[stage] and parse_date(row[stage]) <= deadline:
                counts[index] += 1
    rates = []
    for index in range(1, len(STAGES)):
        denominator, numerator = counts[index - 1:index + 1]
        rates.append((f'{STAGES[index - 1]}_to_{STAGES[index]}', numerator, denominator))
    rates.append(('contacted_to_accepted', counts[-1], counts[0]))
    return {'contacts_in_window': in_window, 'immature_contacts': immature,
            'identical_duplicate_rows_ignored': duplicates, 'counts': counts, 'rates': rates}


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('csv_path')
    parser.add_argument('--role', required=True)
    parser.add_argument('--start', required=True, type=parse_date)
    parser.add_argument('--end', required=True, type=parse_date)
    parser.add_argument('--as-of', required=True, type=parse_date)
    parser.add_argument('--days', type=int, default=14)
    args = parser.parse_args()
    try:
        with open(args.csv_path, newline='', encoding='utf-8-sig') as handle:
            reader = csv.DictReader(handle)
            if not reader.fieldnames or not set(FIELDS) <= set(reader.fieldnames):
                raise ValueError('CSV requires all field-dictionary headers, even when it has no rows')
            result = calculate(reader, args.role, args.start, args.end, args.as_of, args.days)
    except (OSError, ValueError) as exc:
        parser.exit(2, f'Cannot calculate: {exc}\n')
    output = csv.writer(sys.stdout, lineterminator='\n')
    output.writerow(('kind', 'metric', 'numerator', 'denominator', 'percent'))
    for metric in ('contacts_in_window', 'immature_contacts', 'identical_duplicate_rows_ignored'):
        output.writerow(('audit', metric, result[metric], '', ''))
    for stage, count in zip(STAGES, result['counts']):
        output.writerow(('count', stage, count, '', ''))
    for metric, numerator, denominator in result['rates']:
        percent = f'{100 * numerator / denominator:.1f}' if denominator else 'unknown'
        output.writerow(('rate', metric, numerator, denominator, percent))


if __name__ == '__main__':
    main()
