"""Reference solution for Customer support analysis project: measure unanswered tickets, not just fast replies.
Original synthetic teaching data only. Python 3.10+, standard library.
Run python solve.py [path-to-project-inputs]. No network or credentials.
"""
from pathlib import Path
from datetime import datetime, date
import csv, json, math, statistics, sys

def read(root, name):
    with (Path(root)/name).open(newline='',encoding='utf-8') as f:
        reader=csv.DictReader(f)
        if not reader.fieldnames or len(reader.fieldnames)!=len(set(reader.fieldnames)):
            raise ValueError(f'{name}: missing or duplicate headers')
        rows=list(reader)
        if any(None in row or any(v is None for v in row.values()) for row in rows):
            raise ValueError(f'{name}: inconsistent row length')
        return rows

def rules(root): return json.loads((Path(root)/'rules.json').read_text())

def number(value):
    x=float(value)
    if not math.isfinite(x): raise ValueError('Non-finite input')
    return x

def nonnegative(value):
    x=number(value)
    if x<0:raise ValueError('Negative quantity or amount')
    return x

def flag(value):
    if value not in ('true','false'):raise ValueError('Boolean must be true or false')
    return value=='true'

def timestamp(value):
    d=datetime.fromisoformat(value.replace('Z','+00:00'))
    if d.tzinfo is None:raise ValueError('Timestamp requires an explicit time zone')
    return d

def unique(rows, keys, ignore=()):
    by={};duplicates=0
    for row in rows:
        key=tuple(row[k] for k in keys)
        if any(not k for k in key):raise ValueError('Missing record identity')
        if key in by:
            prior={k:v for k,v in by[key].items() if k not in ignore}
            new={k:v for k,v in row.items() if k not in ignore}
            if prior!=new:raise ValueError(f'Conflicting records at {key}')
            duplicates+=1
        else:by[key]=row
    return list(by.values()),duplicates

def rounded(value):
    if isinstance(value,float): return round(value,8)
    if isinstance(value,dict):return {k:rounded(v) for k,v in value.items()}
    if isinstance(value,list):return [rounded(v) for v in value]
    return value

def month_index(s):
    d=datetime.strptime(s,'%Y-%m')
    if d.strftime('%Y-%m')!=s:raise ValueError('Use YYYY-MM')
    return d.year*12+d.month

def support_response(root):
    r=rules(root);asof=timestamp(r['as_of']);target=nonnegative(r['target_elapsed_minutes'])
    tickets,_=unique(read(root,'tickets.csv'),['ticket_id']);events,retries=unique(read(root,'events.csv'),['event_id'])
    by={x['ticket_id']:[] for x in tickets};future=0
    for e in events:
        if e['ticket_id'] not in by:raise ValueError('Orphan event')
        if e['actor'] not in ('agent','bot') or e['visibility'] not in ('public','private'):raise ValueError('Unknown event actor or visibility')
        at=timestamp(e['at'])
        if at>asof:future+=1;continue
        if e['actor']=='agent' and e['visibility']=='public':by[e['ticket_id']].append(at)
    delays=[];states={'met':0,'breached':0,'pending':0};team={};unanswered=0
    for t in tickets:
        created=timestamp(t['created_at'])
        if created>asof:raise ValueError('Future-created ticket in cohort')
        if any(at<created for at in by[t['ticket_id']]):raise ValueError('Reply before creation')
        if by[t['ticket_id']]:
            delay=(min(by[t['ticket_id']])-created).total_seconds()/60;delays.append(delay)
            state='met' if delay<=target else 'breached'
        else:
            unanswered+=1;state='breached' if (asof-created).total_seconds()/60>target else 'pending'
        states[state]+=1;d=team.setdefault(t['team'],{'met':0,'breached':0,'pending':0});d[state]+=1
    evaluated=states['met']+states['breached']
    return {'tickets':len(tickets),'response_observed':len(delays),'unanswered':unanswered,'retry_events_removed':retries,'future_events_excluded':future,'response_median_minutes':statistics.median(delays) if delays else None,'response_p90_minutes':sorted(delays)[math.ceil(.9*len(delays))-1] if delays else None,'met':states['met'],'breached':states['breached'],'pending':states['pending'],'evaluated':evaluated,'met_share_evaluated':states['met']/evaluated if evaluated else None,'met_share_answered_only':states['met']/len(delays) if delays else None,'by_team':team}

def main():
    directory=Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).resolve().parent
    try:
        result=support_response(directory)
        print(json.dumps(rounded(result),indent=2,allow_nan=False))
    except (OSError,ValueError,KeyError,TypeError,ZeroDivisionError) as exc:
        print(f"Input could not be analyzed: {exc}",file=sys.stderr);return 2
    return 0
if __name__=='__main__':raise SystemExit(main())
