"""Harbor Helpdesk, Module 02: read-only standard-library worked example.
Usage: python3 worked_analysis.py tickets.csv
Fixed teaching scope/cutoff, no network/storage writes. Not a production pipeline.
"""
import csv
import datetime as dt
import json
import statistics
import sys

UTC = dt.timezone.utc
SNAPSHOT = dt.datetime(2026, 6, 22, tzinfo=UTC)
START = dt.datetime(2026, 6, 1, tzinfo=UTC)
SPLIT = dt.datetime(2026, 6, 8, tzinfo=UTC)
END = dt.datetime(2026, 6, 15, tzinfo=UTC)
BIN_LABELS = ['[0,1)', '[1,2)', '[2,4)', '[4,8)', '[8,infinity)']
REQUIRED = ['ticket_id', 'created_at_utc', 'channel', 'queue', 'priority',
            'first_response_at_utc', 'first_resolved_at_utc',
            'status_at_snapshot', 'satisfaction_1_to5']

def timestamp(value):
    result = dt.datetime.fromisoformat(value.replace('Z', '+00:00'))
    if result.tzinfo is None or result.utcoffset() != dt.timedelta(0):
        raise ValueError('Timestamps must explicitly use UTC.')
    return result

def distribution(values):
    ordered = sorted(values)
    n = len(ordered)
    return {'observed': n, 'sum_hours': sum(ordered),
            'mean_hours': statistics.mean(ordered) if n else None,
            'median_hours': statistics.median(ordered) if n else None,
            'minimum_hours': min(ordered) if n else None,
            'maximum_hours': max(ordered) if n else None,
            'sorted_hours': ordered,
            'bin_counts': [sum(0 <= v < 1 for v in ordered),
                           sum(1 <= v < 2 for v in ordered),
                           sum(2 <= v < 4 for v in ordered),
                           sum(4 <= v < 8 for v in ordered),
                           sum(v >= 8 for v in ordered)]}

def summarize(rows):
    durations = [r['response_hours'] for r in rows if r['response_hours'] is not None]
    ratings = [r['rating'] for r in rows if r['rating'] is not None]
    mature = [r for r in rows if r['mature_72h']]
    timely = sum(v <= 4 for v in durations)
    resolved = sum(r['resolution_hours'] is not None and r['resolution_hours'] <= 72 for r in mature)
    return {'eligible': len(rows), 'observed_responses': len(durations), 'timely': timely,
            'timely_rate': timely / len(durations) if durations else None,
            'response_coverage': len(durations) / len(rows) if rows else None,
            'mature_for_72h': len(mature), 'resolved_within_72h': resolved,
            'resolution_rate': resolved / len(mature) if mature else None,
            'rating_observed': len(ratings), 'rating_mean': statistics.mean(ratings) if ratings else None,
            'rating_coverage': len(ratings) / len(rows) if rows else None,
            'response_distribution': distribution(durations)}

def analyze(path):
    parsed = []
    seen = set()
    with open(path, newline='', encoding='utf-8-sig') as handle:
        reader = csv.DictReader(handle)
        if reader.fieldnames != REQUIRED:
            raise ValueError('Unexpected columns: use the documented source schema.')
        for source in reader:
            key = source['ticket_id']
            if not key or key in seen:
                raise ValueError('Missing or duplicate ticket key; investigate, do not drop silently.')
            seen.add(key)
            if source['channel'] not in ['email', 'chat'] or source['queue'] not in ['general', 'billing'] or source['priority'] not in ['high', 'normal'] or source['status_at_snapshot'] not in ['resolved', 'open']:
                raise ValueError('Unsupported category.')
            created = timestamp(source['created_at_utc'])
            if not START <= created < END:
                raise ValueError('Out-of-scope creation timestamp; review exclusions explicitly.')
            events = [timestamp(source[f]) if source[f] else None for f in ['first_response_at_utc', 'first_resolved_at_utc']]
            for event in events:
                if event is not None and not created <= event <= SNAPSHOT:
                    raise ValueError('Invalid event chronology or snapshot coverage.')
            if events[0] is not None and events[1] is not None and events[1] < events[0]:
                raise ValueError('Resolution precedes response; investigate the record.')
            if (source['status_at_snapshot'] == 'resolved') != (events[1] is not None):
                raise ValueError('Status and resolution conflict under this simplified case dictionary.')
            rating = int(source['satisfaction_1_to5']) if source['satisfaction_1_to5'] else None
            if rating is not None and not 1 <= rating <= 5:
                raise ValueError('Rating outside 1–5.')
            parsed.append({'ticket_id': key, 'cohort': 'week_1' if created < SPLIT else 'week_2',
                           'channel': source['channel'], 'priority': source['priority'],
                           'response_hours': (events[0] - created).total_seconds()/3600 if events[0] is not None else None,
                           'resolution_hours': (events[1] - created).total_seconds()/3600 if events[1] is not None else None,
                           'mature_72h': SNAPSHOT-created >= dt.timedelta(hours=72), 'rating': rating})
    cohorts = []
    for name in ['week_1', 'week_2']:
        group = [r for r in parsed if r['cohort'] == name]
        result = summarize(group)
        result['cohort'] = name
        result['channels'] = {c: summarize([r for r in group if r['channel'] == c]) for c in ['email', 'chat']}
        result['priorities'] = {p: summarize([r for r in group if r['priority'] == p]) for p in ['high', 'normal']}
        cohorts.append(result)
    def weighted(result, weights):
        if any(weights[c] is None or (weights[c] > 0 and result['channels'][c]['timely_rate'] is None) for c in weights):
            return None
        return sum(weights[c] * result['channels'][c]['timely_rate'] for c in weights if weights[c] > 0)
    reference = cohorts[0]
    w1_weights = {c: reference['channels'][c]['eligible']/reference['eligible'] if reference['eligible'] else None for c in ['email', 'chat']}
    # Weights are ticket shares; when response coverage differs, investigate before interpreting standardization.
    return {'case': 'Harbor Helpdesk — fictional teaching data', 'version': '2026-10-15.2',
            'snapshot_utc': SNAPSHOT.isoformat(), 'source_count': len(parsed),
            'bin_labels_hours': BIN_LABELS, 'cohorts': cohorts,
            'combined': summarize(parsed),
            'combined_channels': {c: summarize([r for r in parsed if r['channel'] == c]) for c in ['email', 'chat']},
            'fixed_week1_ticket_weights': w1_weights,
            'fixed_week1_weighted_rates': [weighted(r, w1_weights) for r in cohorts],
            'equal_channel_weighted_rates': [weighted(r, {'email': .5, 'chat': .5}) for r in cohorts],
            'derived_tickets': parsed,
            'interpretation': 'Descriptive summaries, not causal effects. Missing observations stay unavailable. Fixed ticket weights require coverage review if coverage differs.'}

if __name__ == '__main__':
    if len(sys.argv) != 2:
        raise SystemExit('Usage: python3 worked_analysis.py tickets.csv')
    print(json.dumps(analyze(sys.argv[1]), indent=2, ensure_ascii=False, allow_nan=False))
