"""Reproduce a frozen seven-calendar-day Litecoin observer report. No network or writes.
Usage: python analyze.py.txt [package_directory]
Transactions come from ONE as-of snapshot. Daily cohort snapshots are not added.
"""
from pathlib import Path
from datetime import datetime,timezone
from fractions import Fraction
from collections import Counter,defaultdict
import csv,json,hashlib,re,sys
STATES=['confirmed','pending','left_mempool','censored_gap','recheck_required','censored_reorg']
def utc(epoch):return datetime.fromtimestamp(epoch,timezone.utc).isoformat().replace('+00:00','Z')
def quantile(values,q):
    if not values:return None
    values=sorted(values);position=Fraction(len(values)-1)*q;lower=position.numerator//position.denominator
    return float(Fraction(values[lower])+(values[min(lower+1,len(values)-1)]-values[lower])*(position-lower))
def classification(row,snapshot):
    try:
        first=int(row['first_seen']);confirmed=int(row['confirmed_at']);closed=int(row['closed_at']);height=int(row['block_height'])
        valid=row['state']=='confirmed' and first<=confirmed<=snapshot and closed==confirmed and height>0 and re.fullmatch('[0-9a-f]{64}',row['block_hash']) and re.fullmatch('[0-9a-f]{64}',row['txid']) and int(row['txid'][:8],16)%16==0
    except (ValueError,KeyError,TypeError):valid=False
    return confirmed-first if valid else None
def deadlines(rows,snapshot,start,end):
    enrolled=[r for r in rows if start<=int(r['first_seen'])<end]
    aged=[r for r in enrolled if int(r['first_seen'])<=snapshot-600]
    breakdown=Counter()
    for row in aged:
        if classification(row,snapshot) is None:breakdown[row['state'] if row['state']!='confirmed' else 'invalid_provenance']+=1
    thresholds=[]
    for threshold in [60,300,600]:
        bins=Counter({'matched_by_threshold':0,'matched_later':0,'unresolved':0})
        for row in aged:
            duration=classification(row,snapshot)
            bins['unresolved' if duration is None else 'matched_by_threshold' if duration<=threshold else 'matched_later']+=1
        assert sum(bins.values())==len(aged)
        thresholds.append({'seconds':threshold,**dict(bins),'denominator':len(aged)})
    young=Counter(r['state'] for r in enrolled if int(r['first_seen'])>snapshot-600)
    return {'window_start':start,'window_end_exclusive':end,'snapshot_at':snapshot,'maturity_seconds':600,'denominator':len(aged),'enrolled_total':len(enrolled),'too_young':len(enrolled)-len(aged),'too_young_counts':dict(sorted(young.items())),'unresolved_breakdown':dict(sorted(breakdown.items())),'thresholds':thresholds,'meaning':'Observed records only. Unresolved/censored records are never inferred to be known-late or failed. This is not a network-wide completion probability or merchant policy.'}
def analyze(directory):
    directory=Path(directory);m=json.loads((directory/'snapshot.json').read_text(encoding='utf-8'))
    assert m['release']=='confirmation-week-20261014-v1' and m['protocol']=='lw-confirmations-2026-09-27-v2'
    start=m['cohort_start'];end=m['cohort_end_exclusive'];snapshot=m['outcomes_snapshot_at']
    assert end-start==7*86400 and start%86400==0 and end%86400==0 and snapshot>=end
    for name,digest in m['inputs'].items():
        assert Path(name).name==name and hashlib.sha256((directory/name).read_bytes()).hexdigest()==digest,name+' input checksum'
    with (directory/'cohort.csv').open(encoding='utf-8',newline='') as f:rows=list(csv.DictReader(f))
    ids=set();counts=Counter({s:0 for s in STATES});daily_enrollment={utc(start+i*86400)[:10]:Counter({s:0 for s in STATES}) for i in range(7)};durations=[];daily_durations=defaultdict(list);block_hashes=set()
    for row in rows:
        txid=row['txid'];assert re.fullmatch('[0-9a-f]{64}',txid) and txid not in ids and int(txid[:8],16)%16==0;ids.add(txid)
        first=int(row['first_seen']);assert start<=first<end and row['state'] in STATES
        closed=int(row['closed_at']) if row['closed_at'] else None
        assert closed is None or first<=closed<=snapshot
        day=utc(first)[:10];counts[row['state']]+=1;daily_enrollment[day][row['state']]+=1
        duration=classification(row,snapshot)
        if row['state']=='confirmed':
            assert duration is not None,'Invalid confirmed provenance'
            durations.append(duration);daily_durations[day].append(duration);block_hashes.add(row['block_hash'])
    daily_coverage={utc(start+i*86400)[:10]:{'polls':0,'covered_seconds':0,'baseline_exclusions':0,'states':Counter()} for i in range(7)}
    seen_polls=set();last_at=None
    with (directory/'polls.csv').open(encoding='utf-8',newline='') as f:
        for row in csv.DictReader(f):
            at=int(row['at']);covered=int(row['covered_seconds']);excluded=int(row['baseline_excluded'])
            assert start<=at<end and at not in seen_polls and (last_at is None or at>last_at)
            seen_polls.add(at);last_at=at;assert 0<=covered<=30 and excluded>=0
            assert not covered or row['state']=='collecting'
            day=utc(at)[:10];d=daily_coverage[day];d['polls']+=1;d['covered_seconds']+=covered;d['baseline_exclusions']+=excluded;d['states'][row['state']]+=1
    manifests=json.loads((directory/'source-manifests.json').read_text(encoding='utf-8'))
    assert len(manifests)==7 and {x['coverage_day'] for x in manifests}==set(daily_coverage),'Expected seven distinct UTC coverage days'
    for source in manifests:
        day=source['coverage_day'];assert day in daily_coverage
        day_start=int(datetime.fromisoformat(day).replace(tzinfo=timezone.utc).timestamp())
        assert source['coverage_start']==day_start and source['coverage_end']==day_start+86400
        assert source['protocol']==m['protocol'] and source['snapshot_at']>=source['coverage_end']
        assert source['covered_seconds']==daily_coverage[day]['covered_seconds']
        assert source['baseline_exclusions']==daily_coverage[day]['baseline_exclusions']
        assert source['poll_states']==dict(daily_coverage[day]['states'])
    assert len(manifests)==7
    hist=[sum(lo<=v<=hi for v in durations) for lo,hi in [(0,60),(61,150),(151,300),(301,600),(601,10**15)]]
    assert sum(hist)==counts['confirmed']
    daily=[]
    for day,c in daily_enrollment.items():
        coverage=daily_coverage[day]
        daily.append({'day':day,'enrolled':sum(c.values()),'counts':dict(c),'matched_median_seconds':quantile(daily_durations[day],Fraction(1,2)),'matched_p90_seconds':quantile(daily_durations[day],Fraction(9,10)),**{k:v for k,v in coverage.items() if k!='states'},'poll_states':dict(coverage['states'])})
    covered=sum(x['covered_seconds'] for x in daily)
    reorg_hashes={r['block_hash'] for r in rows if r['state']=='censored_reorg' and re.fullmatch('[0-9a-f]{64}',r['block_hash'])}
    return {'schema':1,'release':m['release'],'protocol':m['protocol'],'cohort_start':start,'cohort_end_exclusive':end,'outcomes_snapshot_at':snapshot,'scheduled_seconds':end-start,'covered_seconds':covered,'coverage_percent':float(Fraction(covered,end-start)*100),'total':len(rows),'counts':dict(counts),'confirmed_blocks':len(block_hashes),'reorg_censored_original_block_hashes':len(reorg_hashes),'matched_histogram':hist,'matched_median_seconds':quantile(durations,Fraction(1,2)),'matched_p90_seconds':quantile(durations,Fraction(9,10)),'matched_max_seconds':max(durations) if durations else None,'daily':daily,'deadlines':deadlines(rows,snapshot,start,end),'scope':'One sampled node observer; first mempool sighting to observed inclusion. Matched-only timings omit unresolved outcomes. No fee rates, network guarantees or payment-service credit times.'}
if __name__=='__main__':print(json.dumps(analyze(Path(sys.argv[1]) if len(sys.argv)>1 else Path(__file__).resolve().parent),indent=2,sort_keys=True))
