"""Past-only hourly features with explicit availability; no forecast fitted."""
from datetime import datetime,timedelta,timezone
import math,json
def stamp(s):
    t=datetime.fromisoformat(s)
    if t.tzinfo is None or t.utcoffset()!=timedelta(0):raise ValueError('explicit UTC timestamps required')
    return t
def features(records):
    if not records:raise ValueError('nonempty records')
    rows=sorted(records,key=lambda r:stamp(r['event_time']));times=[stamp(r['event_time']) for r in rows]
    if len(set(times))!=len(times):raise ValueError('duplicate time')
    if any(b-a!=timedelta(hours=1) for a,b in zip(times,times[1:])):raise ValueError('regular one-hour grid required')
    for r,t in zip(rows,times):
        if type(r['value']) not in (int,float) or not math.isfinite(r['value']):raise ValueError('finite numeric values')
        if stamp(r['available_at'])<t:raise ValueError('this observation contract forbids availability before event')
    out=[]
    for i,decision in enumerate(times):
        def past(k):
            j=i-k
            return rows[j]['value'] if j>=0 and stamp(rows[j]['available_at'])<=decision else None
        window=[past(k) for k in (1,2,3)]
        out.append({'decision_time':decision.isoformat(),'lag1':past(1),'lag2':past(2),'rolling3_past_only':None if any(v is None for v in window) else sum(window)/3})
    return out
def sample():
    start=datetime(2026,11,1,tzinfo=timezone.utc)
    return [{'event_time':(start+timedelta(hours=i)).isoformat(),'available_at':(start+timedelta(hours=i,minutes=10)).isoformat(),'value':v} for i,v in enumerate([10,12,14,13,16,18,17,20])]
if __name__=='__main__':
    records=sample();base=features(records)
    assert features(records[::-1])==base
    assert base[0]['lag1'] is None and base[2]['rolling3_past_only'] is None
    assert base[3]['lag1']==14 and base[3]['lag2']==12 and base[3]['rolling3_past_only']==12
    for cut in range(len(records)):
        changed=[dict(r,value=r['value']+10000 if i>=cut else r['value']) for i,r in enumerate(records)]
        assert features(changed)[:cut+1]==base[:cut+1]
    late=[r.copy() for r in records];late[2]['available_at']=records[4]['available_at']
    assert features(late)[3]['lag1'] is None and features(late)[3]['rolling3_past_only'] is None
    bad=[records+[records[0]],records[:2]+records[3:], [{**records[0],'event_time':'2026-11-01T00:00:00'}], [{**records[0],'value':float('nan')}]]
    for case in bad:
        try:features(case)
        except ValueError:pass
        else:raise AssertionError('invalid case accepted')
    print(json.dumps({'features':base,'future_mutation_cutoffs_tested':8,'late_arrival_excluded':True,'invalid_cases_rejected':4,'model_trained':False,'pandas_run':False},indent=2))
