"""Bounded property-carrying split-rest model. Never determines complete legality or authorizes driving."""
from dataclasses import dataclass
from datetime import datetime,timedelta,timezone
from itertools import combinations
from pathlib import Path
import argparse,json
HOUR=3600
@dataclass(frozen=True)
class Interval:
 start:datetime
 end:datetime
 status:str
 @property
 def seconds(self):return (self.end-self.start).total_seconds()
def instant(value):
 if not isinstance(value,str):raise ValueError('ISO date-time strings required')
 dt=datetime.fromisoformat(value.replace('Z','+00:00'))
 if dt.tzinfo is None or dt.utcoffset() is None:raise ValueError('Explicit date-time offsets required')
 return dt.astimezone(timezone.utc)
def timeline(records):
 if not isinstance(records,list) or not records:raise ValueError('Nonempty interval list required')
 if len(records)>10000:raise ValueError('Reference timeline limited to 10000 intervals')
 result=[]
 for raw in records:
  if not isinstance(raw,dict) or raw.get('status') not in {'ON','DRIVING','OFF','SLEEPER'}:raise ValueError('Unsupported or unknown status')
  row=Interval(instant(raw.get('start')),instant(raw.get('end')),raw['status'])
  if row.seconds<=0:raise ValueError('Intervals must have positive duration')
  if result and row.start!=result[-1].end:raise ValueError('Timeline gaps and overlaps are unsupported')
  result.append(row)
 return result
def rest_runs(rows):
 runs=[];i=0
 while i<len(rows):
  if rows[i].status not in {'OFF','SLEEPER'}:i+=1;continue
  start=i;j=i+1
  while j<len(rows) and rows[j].status in {'OFF','SLEEPER'}:j+=1
  group=rows[start:j];seconds=(group[-1].end-group[0].start).total_seconds()
  runs.append({'start':group[0].start,'end':group[-1].end,'seconds':seconds,'pure_sleeper':all(r.status=='SLEEPER' for r in group),'mixed_status':len({r.status for r in group})>1})
  i=j
 return runs
def overlap_seconds(row,start,end):return max(0,(min(row.end,end)-max(row.start,start)).total_seconds())
def driving(rows,start,end):return sum(overlap_seconds(r,start,end) for r in rows if r.status=='DRIVING')
def evaluate(records):
 rows=timeline(records);rests=rest_runs(rows);start=rows[0].start;now=rows[-1].end;results=[]
 for first,second in combinations(rests,2):
  if first['seconds']<2*HOUR or second['seconds']<2*HOUR:continue
  if max(first['seconds'],second['seconds'])>=10*HOUR:continue
  if first['seconds']+second['seconds']<10*HOUR:continue
  if not ((first['pure_sleeper'] and first['seconds']>=7*HOUR) or (second['pure_sleeper'] and second['seconds']>=7*HOUR)):continue
  # A full rest elsewhere changes the clock basis and is deliberately not modeled.
  if any(r['seconds']>=10*HOUR and r['start']<now for r in rests):continue
  anchor=first['end'];drive=driving(rows,anchor,now);window=(now-anchor).total_seconds()-second['seconds']
  before_drive=driving(rows,start,second['start']);before_window=(second['start']-start).total_seconds()-first['seconds']
  # Check modeled limits whenever driving occurs, including before the pair completes.
  prior_limit=False
  for r in rows:
   if r.status!='DRIVING' or r.start>=second['start']:continue
   t=min(r.end,second['start']);excluded=overlap_seconds(Interval(first['start'],first['end'],'SLEEPER'),start,t)
   if driving(rows,start,t)>11*HOUR or (t-start).total_seconds()-excluded>14*HOUR:prior_limit=True
  post_limit=False
  for r in rows:
   if r.status!='DRIVING' or r.end<=anchor:continue
   t=r.end;excluded=overlap_seconds(Interval(second['start'],second['end'],'OFF'),anchor,t)
   if driving(rows,anchor,t)>11*HOUR or (t-anchor).total_seconds()-excluded>14*HOUR:post_limit=True
  results.append({'first_rest_hours':first['seconds']/HOUR,'second_rest_hours':second['seconds']/HOUR,'long_period_pure_sleeper':True,'clock_anchor':anchor.isoformat(),'window14_used_hours':window/HOUR,'driving11_used_hours':drive/HOUR,'remaining14_hours':max(0,14-window/HOUR),'remaining11_hours':max(0,11-drive/HOUR),'before_second_driving_hours':before_drive/HOUR,'before_second_window_hours':before_window/HOUR,'modeled_limit_exceeded':prior_limit or post_limit,'modeled_window_exhausted':window>=14*HOUR,'rest_pattern_eligible_in_model':True})
 ranked=sorted(results,key=lambda r:(not r['modeled_limit_exceeded'],min(r['remaining14_hours'],r['remaining11_hours'])),reverse=True)
 return {'candidate_pairs':ranked,'best_modeled_pair':ranked[0] if ranked else None,'driving_authorized':False,'complete_compliance_determination':False,'unmodeled':['Cycle limits','Applicable 30-minute-break conditions','History before supplied timeline and initial reset validity','Jurisdictional and special-operation exceptions','ELD edit/annotation and identity validity','Full-reset transitions and mixed-status long-period interpretations'],'scope':'Chronological complete supplied intervals; ordinary bounded split-rest arithmetic only','source':'https://www.ecfr.gov/current/title-49/subtitle-B/chapter-III/subchapter-B/part-395/subpart-A/section-395.1'}
def demo(long=7,short=3,reverse=False):
 specs=[('ON',.5),('DRIVING',5),('OFF' if reverse else 'SLEEPER',short if reverse else long),('DRIVING',4),('SLEEPER' if reverse else 'OFF',long if reverse else short),('DRIVING',1)]
 now=datetime(2026,10,20,tzinfo=timezone.utc);records=[]
 for status,hours in specs:
  end=now+timedelta(hours=hours);records.append({'start':now.isoformat(),'end':end.isoformat(),'status':status});now=end
 return records
def main():
 ap=argparse.ArgumentParser();ap.add_argument('--input');ap.add_argument('--output');args=ap.parse_args()
 records=json.loads(Path(args.input).read_text(encoding='utf-8')) if args.input else demo()
 result=evaluate(records)
 if args.output:
  p=Path(args.output);p.parent.mkdir(parents=True,exist_ok=True);p.write_text(json.dumps(result,indent=2),encoding='utf-8')
 print(json.dumps(result,indent=2))
if __name__=='__main__':main()
