"""Offline docking logic exercise. No hardware commands, power control, or networking.
The five-second timeout is a teaching value, not a physical robot requirement.
A safe real implementation needs independent hardware interlocks and a support system.
"""
from dataclasses import dataclass
import math

class InterlockError(RuntimeError):
    pass

STATES = ['approach', 'aligned', 'supported', 'drive_disabled',
          'contacts_confirmed', 'charging', 'charge_complete',
          'contacts_isolated', 'drive_ready', 'undock']

@dataclass
class DockingMock:
    state: str = 'approach'
    last_time: float = 0.0
    fault: str | None = None

    def reject(self, reason):
        # Latch the fault; never infer that disabling torque is safe for a biped.
        self.fault = reason
        self.state = 'fault'
        raise InterlockError(reason)

    def advance(self, event, now, *, tag_fresh=False, aligned=False,
                support=False, drive_disabled=False, contacts=False,
                charger_confirmed=False, charge_complete=False,
                isolated=False, drive_ready=False):
        if self.fault:
            raise InterlockError('Latched fault: ' + self.fault)
        if not math.isfinite(now) or now < self.last_time or now-self.last_time > 5:
            self.reject('Invalid, backward or timed-out event timestamp')
        current = STATES.index(self.state)
        if current == len(STATES)-1 or event != STATES[current+1]:
            self.reject('Out-of-order state transition')
        if self.state in STATES[2:8] and not support:
            self.reject('Mechanical support is not confirmed')
        if event == 'aligned' and not (tag_fresh and aligned):
            self.reject('Fresh target and alignment required')
        if event == 'supported' and not support:
            self.reject('Mechanical support required before disabling drive')
        if event == 'drive_disabled' and not (support and drive_disabled):
            self.reject('Support and actual drive-disabled confirmation required')
        if event == 'contacts_confirmed' and not (support and drive_disabled and contacts):
            self.reject('Drive-disabled and contact confirmation required')
        if event == 'charging' and not (support and drive_disabled and contacts and charger_confirmed):
            self.reject('Contact alone does not establish charging')
        if event == 'charge_complete' and not charge_complete:
            self.reject('Charger/pack completion confirmation required')
        if event == 'contacts_isolated' and not isolated:
            self.reject('Charge path isolation must be confirmed')
        if event == 'drive_ready' and not (isolated and drive_ready):
            self.reject('Isolated charge path and verified drive readiness required')
        if event == 'undock' and not (isolated and drive_ready):
            self.reject('Undock interlocks not satisfied')
        self.state, self.last_time = event, now
        return self.state

# These booleans stand for already-verified measurements, NOT real sensor code.
VALID = dict(tag_fresh=True, aligned=True, support=True, drive_disabled=True,
             contacts=True, charger_confirmed=True, charge_complete=True,
             isolated=True, drive_ready=True)
def reach(state):
    dock=DockingMock()
    for i,event in enumerate(STATES[1:STATES.index(state)+1],1):
        dock.advance(event,i*.1,**VALID)
    return dock

def rejected(dock,event,now,**changes):
    data={**VALID,**changes}
    try: dock.advance(event,now,**data)
    except InterlockError: assert dock.state=='fault'
    else: raise AssertionError('Expected a blocked transition')
    try: dock.advance(event,now+.1,**VALID)
    except InterlockError: pass
    else: raise AssertionError('Fault should remain latched')

if __name__=='__main__':
    dock=DockingMock()
    print('OFFLINE DEMO — no motor or charger interface')
    print(dock.state)
    for i,event in enumerate(STATES[1:],1):
        print(dock.advance(event,i*.1,**VALID))
    assert dock.state=='undock'
    rejected(DockingMock(),'aligned',.1,tag_fresh=False)
    rejected(reach('aligned'),'supported',.2,support=False)
    rejected(reach('supported'),'drive_disabled',.3,drive_disabled=False)
    rejected(reach('drive_disabled'),'contacts_confirmed',.4,contacts=False)
    rejected(reach('contacts_confirmed'),'charging',.5,charger_confirmed=False)
    rejected(reach('charging'),'charge_complete',.6,support=False)
    rejected(reach('charge_complete'),'contacts_isolated',.7,isolated=False)
    rejected(reach('contacts_isolated'),'drive_ready',.8,drive_ready=False)
    rejected(reach('charging'),'undock',.6)
    rejected(DockingMock(),'aligned',6)
    rejected(DockingMock(),'aligned',float('nan'))
    rejected(reach('aligned'),'supported',0)
    print('PASS: nominal cycle and 12 fault cases; faults stay latched.')
    print('Physical charging, contact hardware and support behavior are not validated by this mock.')
