"""Exact two-pool histories: every transfer has available material and a ledger."""
from dataclasses import dataclass,asdict
from fractions import Fraction as F


def nonnegative(*values):
    if any(F(v)<0 for v in values):raise ValueError('Nonnegative material amounts required.')


@dataclass(frozen=True)
class Window:
    mobile:F
    reserve:F
    fresh:F=F(0)
    release:F=F(0)
    uptake:F=F(0)
    collection:F=F(0)
    mobile_loss:F=F(0)
    reserve_loss:F=F(0)
    def __post_init__(self):
        nonnegative(*asdict(self).values())
        if self.release>self.reserve+self.fresh:raise ValueError('Release precedes uptake: insufficient available reserve.')
        if self.release+self.reserve_loss>self.reserve+self.fresh+self.uptake:raise ValueError('Reserve loss exceeds available inventory.')
        if self.uptake+self.collection+self.mobile_loss>self.mobile+self.release:raise ValueError('Mobile demand exceeds available inventory.')
    @property
    def final_mobile(self):return self.mobile+self.release-self.uptake-self.collection-self.mobile_loss
    @property
    def final_reserve(self):return self.reserve+self.fresh+self.uptake-self.release-self.reserve_loss
    def replay(self):
        p,r=self.mobile,self.reserve;rows=[dict(event='initial',mobile=p,reserve=r,collected=F(0),lost=F(0),fresh=F(0))]
        Q=D=A=F(0)
        for event,dp,dr,q,d,f in [('fresh',0,self.fresh,0,0,self.fresh),('release',self.release,-self.release,0,0,0),
            ('uptake',-self.uptake,self.uptake,0,0,0),('collection',-self.collection,0,self.collection,0,0),
            ('mobile-loss',-self.mobile_loss,0,0,self.mobile_loss,0),('reserve-loss',0,-self.reserve_loss,0,self.reserve_loss,0)]:
            p+=dp;r+=dr;Q+=q;D+=d;A+=f
            if p<0 or r<0 or p+r+Q+D!=self.mobile+self.reserve+A:raise ArithmeticError('Material replay failed.')
            rows.append(dict(event=event,mobile=p,reserve=r,collected=Q,lost=D,fresh=A))
        return rows


@dataclass(frozen=True)
class Recovery:
    mobile_retention:F=F(1,20)
    reserve_retention:F=F(9,10)
    mobile_input:F=F(0)
    reserve_input:F=F(0)
    def __post_init__(self):
        nonnegative(*asdict(self).values())
        if not self.mobile_retention<=self.reserve_retention<=1:raise ValueError('Require 0 <= mobile <= reserve retention <= 1.')


@dataclass(frozen=True)
class History:
    first:Window
    second:Window
    recovery:Recovery
    def __post_init__(self):
        w,v,r=self.first,self.second,self.recovery
        if v.mobile>r.mobile_retention*w.final_mobile+r.mobile_input or v.reserve>r.reserve_retention*w.final_reserve+r.reserve_input:
            raise ValueError('Second-window state exceeds the calibrated recovery map.')
    @property
    def fresh(self):return self.first.fresh+self.second.fresh
    def ledger(self):
        rows=[]
        for i,w in enumerate([self.first,self.second],1):rows.extend(dict(window=i,**r) for r in w.replay())
        return rows
    def extra_unobserved_formation(self,amount):
        # Arbitrary fresh input can be lost without changing either collection.
        nonnegative(amount);v=asdict(self.second);v['fresh']+=amount;v['reserve_loss']+=amount
        return History(self.first,Window(**v),self.recovery)


def attaining_history(q1,q2,B,e,s,J,H,initial_reserve=F(0)):
    q1,q2,B,e,s,J,H,R0=map(F,(q1,q2,B,e,s,J,H,initial_reserve))
    nonnegative(q1,q2,B,J,H,R0)
    if R0>B:raise ValueError('Initial reserve exceeds total stock.')
    A=max(F(0),q1-B);x=max(F(0),B-q1);r=min(J,x);p=x-r
    first=Window(B-R0,R0,A,max(F(0),R0+A-r),max(F(0),r-R0-A),q1)
    carry=e*p+s*r;fresh=max(F(0),q2-H-carry);release=max(F(0),q2-e*p)
    second=Window(e*p,s*r+H,fresh,release,F(0),q2,e*p+release-q2,s*r+H+fresh-release)
    return History(first,second,Recovery(e,s,F(0),H))


def amount_error(volume,concentration,volume_error,concentration_error):
    volume,concentration,volume_error,concentration_error=map(F,(volume,concentration,volume_error,concentration_error))
    nonnegative(volume_error,concentration_error)
    return abs(volume)*concentration_error+abs(concentration)*volume_error+volume_error*concentration_error
