"""Clock-invariant capacity inference with incomplete, calibrated observations."""
from dataclasses import dataclass
from fractions import Fraction as F
from math import ceil, floor, inf, isfinite
import mpmath as mp
from source import rational


def hoeffding_radius(n,delta):
    delta=rational(delta)
    if type(n)!=int or n<1 or not 0<delta<1:raise ValueError('Positive n and 0<delta<1 required.')
    mp.iv.dps=40
    x=mp.iv.mpf(delta.numerator)/delta.denominator
    upper=(mp.iv.sqrt(mp.iv.ln(2/x)/(2*n)))._mpi_[1]
    sign,mantissa,exponent,_=upper
    return F((-1)**sign*mantissa)*F(2)**exponent


@dataclass(frozen=True)
class DurationInterval:
    lower: F
    upper: object  # None means +infinity; never a missing observation to discard.

    def __post_init__(self):
        object.__setattr__(self,'lower',rational(self.lower))
        if self.upper is not None:object.__setattr__(self,'upper',rational(self.upper))
        if self.lower<0 or self.upper is not None and self.upper<self.lower:raise ValueError('Invalid duration interval.')

    @staticmethod
    def from_crossings(first,second):
        # Crossing brackets may share an endpoint; no independence is asserted.
        if first.lower> (second.upper if second.upper is not None else inf):raise ValueError('Incompatible crossing order.')
        lo=F(0) if first.upper is None else max(F(0),second.lower-first.upper)
        hi=None if second.upper is None else second.upper-first.lower
        return DurationInterval(lo,hi)


def compare_intervals(U,V,ratio=F(1)):
    ratio=rational(ratio)
    if ratio<=0:raise ValueError('Positive ratio required.')
    if U.upper is not None and V.lower>ratio*U.upper:return 'positive'
    if V.upper is not None and V.upper<=ratio*U.lower:return 'negative'
    return 'unknown'


@dataclass(frozen=True)
class CensoredComparison:
    positive: int
    negative: int
    unknown: int

    def __post_init__(self):
        if any(type(v)!=int or v<0 for v in [self.positive,self.negative,self.unknown]) or self.total<1:
            raise ValueError('Nonnegative category counts and positive total required.')

    @property
    def total(self):return self.positive+self.negative+self.unknown

    def confidence(self,delta=F(1,20),mean_error=F(0)):
        error=rational(mean_error)
        if not 0<=error<=1:raise ValueError('Interval-error allowance must lie in [0,1].')
        e=hoeffding_radius(self.total,delta)+error
        return max(F(0),F(self.positive,self.total)-e),min(F(1),1-F(self.negative,self.total)+e)


@dataclass(frozen=True)
class PairedCapacity:
    a: F = F(1,100)
    early: int = 2
    late: int = 4

    def __post_init__(self):
        object.__setattr__(self,'a',rational(self.a))
        if self.a<=0 or type(self.early)!=int or type(self.late)!=int or not 0<=self.early<self.late:
            raise ValueError('Require a>0 and ordered integer states.')

    def probability(self,R):
        R=rational(R)
        if R<=self.late:raise ValueError('Both held states must have positive exit rates.')
        i,j=self.early,self.late
        qi=(self.a+i)*(1-F(i)/R);qj=(self.a+j)*(1-F(j)/R)
        return qi/(qi+qj)

    def capacity_set(self,pinterval,a_interval=None):
        lo,hi=map(rational,pinterval)
        aL,aU=(self.a,self.a) if a_interval is None else tuple(map(rational,a_interval))
        if not 0<=lo<=hi<=1 or not 0<aL<=aU:raise ValueError('Invalid parameter interval.')
        i,j=self.early,self.late
        kL=(1-hi)/hi*(aL+i)/(aL+j) if hi else inf
        kU=(1-lo)/lo*(aU+i)/(aU+j) if lo else inf
        if kL>=1 or kU<=0:return dict(status='model incompatibility',capacity=None,depletion=None)
        kL=max(F(0),kL)
        lower=(j-i*kL)/(1-kL);upper=(j-i*kU)/(1-kU) if kU<1 else None
        return dict(status='finite interval' if upper is not None else ('unresolved' if kL==0 else 'lower bound'),
                    capacity=[lower,upper],lower_open=kL==0,upper_open=upper is None,
                    depletion=[F(0) if upper is None else 1/upper,1/lower],
                    depletion_zero_is_limit=upper is None)


@dataclass(frozen=True)
class BlockCapacity:
    """P(V>rU) from a finite race; disjoint consecutive state blocks."""
    start: int = 1
    middle: int = 3
    end: int = 5
    a: F = F(1,100)
    ratio: F = F(1)

    def __post_init__(self):
        object.__setattr__(self,'a',rational(self.a));object.__setattr__(self,'ratio',rational(self.ratio))
        if any(type(x)!=int for x in [self.start,self.middle,self.end]) or not 0<=self.start<self.middle<self.end or self.end-self.start>50 or self.a<=0 or self.ratio<=0:
            raise ValueError('Invalid blocks, ratio or exact-state budget.')

    def probability(self,R=None):
        # None is the R -> infinity limit. Exact backward race recursion avoids
        # explicitly inverting a Kronecker-sum matrix.
        if R is not None:
            R=rational(R)
            if R<=self.end-1:raise ValueError('Positive held-state rates required.')
        q=lambda z:(self.a+z)*(1-F(z)/R if R is not None else 1)
        u=[q(z) for z in range(self.start,self.middle)]
        v=[self.ratio*q(z) for z in range(self.middle,self.end)]
        P=[[F(0)]*(len(v)+1) for _ in range(len(u)+1)]
        for j in range(len(v)):P[-1][j]=F(1)
        for i in reversed(range(len(u))):
            for j in reversed(range(len(v))):P[i][j]=(u[i]*P[i+1][j]+v[j]*P[i][j+1])/(u[i]+v[j])
        return P[0][0]

    def _root_bracket(self,p,iterations=60):
        lo=F(self.end-1);hi=F(self.end)
        for _ in range(100):
            if self.probability(hi)<=p:break
            hi*=2
        else:raise ArithmeticError('Capacity root exceeds search budget.')
        for _ in range(iterations):
            mid=(lo+hi)/2
            if self.probability(mid)>p:lo=mid
            else:hi=mid
        return lo,hi

    def capacity_set(self,pinterval):
        lo,hi=map(rational,pinterval);pinf=self.probability()
        if not 0<=lo<=hi<=1:raise ValueError('Invalid probability interval.')
        if hi<=pinf or lo>=1:return dict(status='model incompatibility',capacity=None)
        lower=F(self.end-1) if hi==1 else self._root_bracket(hi)[0]
        upper=None if lo<=pinf else self._root_bracket(lo)[1]
        return dict(status='finite outer interval' if upper is not None else 'lower bound',capacity=[lower,upper],
                    lower_open=hi==1,rounding='exact bisection outer endpoints; theorem supplies strict monotonicity')


@dataclass(frozen=True)
class SignalCalibration:
    baseline: tuple = (F(95),F(105))
    gain: tuple = (F(9,10000),F(11,10000))
    error: F = F(5)

    def state_interval(self,y,capacity=None):
        bL,bU=map(rational,self.baseline);kL,kU=map(rational,self.gain);e=rational(self.error);y=rational(y)
        if bL>bU or not 0<kL<=kU or e<0:raise ValueError('Invalid calibration contract.')
        lower=max(0,ceil((y-bU-e)/kU));upper=floor((y-bL+e)/kL)
        if capacity is not None:
            if type(capacity)!=int or capacity<0:raise ValueError('Integer state cap required.')
            upper=min(upper,capacity)
        return None if upper<lower else (lower,upper)


def crossing_bracket(times,states,threshold):
    """Certain below/above observations for a monotone, latched count trajectory."""
    if len(times)!=len(states) or not times:raise ValueError('Matching observations required.')
    times=list(map(rational,times))
    if times[0]<0 or any(a>=b for a,b in zip(times,times[1:])):raise ValueError('Increasing nonnegative times required.')
    for s in states:
        if s is not None and (len(s)!=2 or not 0<=s[0]<=s[1]):raise ValueError('Invalid state interval.')
    if any(s is None for s in states):return dict(status='model incompatibility',interval=None)
    running_lower=0
    for s in states:
        running_lower=max(running_lower,s[0])
        if running_lower>s[1]:return dict(status='model incompatibility',interval=None)
    below=[t for t,s in zip(times,states) if s[1]<threshold]
    above=[t for t,s in zip(times,states) if s[0]>=threshold]
    if below and above and max(below)>=min(above):return dict(status='model incompatibility',interval=None)
    if above and above[0]==0:return dict(status='initially above',interval=DurationInterval(0,0))
    return dict(status='bracketed' if above else 'right censored',interval=DurationInterval(max(below,default=F(0)),min(above) if above else None))
