import csv,json,datetime
from pathlib import Path
from zoneinfo import ZoneInfo
p=Path(__file__).parent;tz=ZoneInfo('Asia/Shanghai');cut=datetime.datetime.fromisoformat('2026-10-17T00:00:00+08:00');date=lambda s:datetime.datetime.fromisoformat(s).astimezone(tz).date()
users=list(csv.DictReader((p/'signups.csv').open(encoding='utf-8-sig')));events=list(csv.DictReader((p/'visits.csv').open(encoding='utf-8-sig')));days={}
for r in events:
 t=datetime.datetime.fromisoformat(r['visited_at_utc']).astimezone(tz)
 if t<cut:days.setdefault(r['user_id'],set()).add(t.date())
result=[];cohorts={};excluded=[]
for u in users:
 d=date(u['registered_at_utc']);day7=d+datetime.timedelta(days=7);ok=day7+datetime.timedelta(days=1)<=cut.date();ret=ok and day7 in days.get(u['user_id'],set());week=d-datetime.timedelta(days=d.weekday());row=[u['user_id'],str(d),str(week),str(day7),int(ok),int(ret)];result.append(row)
 if ok:
  a=cohorts.setdefault(str(week),[0,0]);a[0]+=1;a[1]+=int(ret)
 else:excluded.append([u['user_id'],str(d),str(day7),'D7 incomplete'])
def out(name,h,rows):
 with(p/name).open('w',encoding='utf-8-sig',newline='')as f:w=csv.writer(f);w.writerow(h);w.writerows(rows)
out('user-day7-results.csv',['user_id','registered_date','cohort_week','day7','eligible','retained'],result)
out('excluded-users.csv',['user_id','registered_date','day7','reason'],excluded)
rows=[[k,*v,round(v[1]/v[0],8)]for k,v in sorted(cohorts.items())];out('retention-cohorts.csv',['cohort_week','eligible','retained','retention'],rows)
assert len(users)==40 and len({u['user_id']for u in users})==40
by={r[0]:r for r in result};assert by['U035'][4]==0 and by['U040'][4]==0
assert sum(r[4]for r in result)==34 and len(excluded)==6
print(json.dumps({'users':40,'events':len(events),'eligible':34,'excluded':6,'cohorts':rows},ensure_ascii=False))
