"""Finite-sample intervals from independent founder records and calibration.

No demographic rate or sister kernel is supplied to this inference layer.
"""
from dataclasses import dataclass
from fractions import Fraction as Q
import numpy as np
from certkit import I,kl_interval,hoeffding_interval,evaluate,kl_lower


@dataclass(frozen=True)
class FounderCounts:
    deadline: tuple                 # j00,j01,j10,j11
    exits: tuple                    # all demographic exits with initial marks 0,1

    def __post_init__(self):
        if len(self.deadline)!=4 or len(self.exits)!=2 or any(not isinstance(v,(int,np.integer)) or v<0 for v in self.deadline+self.exits) or self.total==0:raise ValueError('Six nonnegative counts with a positive total required.')

    @property
    def total(self):return sum(self.deadline)+sum(self.exits)

    @classmethod
    def from_full(cls,counts):
        c=np.asarray(counts)
        if c.ndim!=2 or c.shape[0]!=2 or c.shape[1] not in [7,12] or np.any(c<0) or np.any(c!=c.astype(int)):raise ValueError('Expected 2x7 immediate or 2x12 delayed counts.')
        return cls(tuple(int(x) for x in c[:,:2].ravel()),tuple(int(x) for x in c[:,2:].sum(axis=1)))

    def features(self):return self.deadline+(self.deadline[2]+self.deadline[3]+self.exits[1],)


@dataclass(frozen=True)
class CalibrationCounts:
    successes: tuple
    samples_per_state: int

    def __post_init__(self):
        if self.samples_per_state<1 or len(self.successes)!=2 or any(int(k)!=k or not 0<=k<=self.samples_per_state for k in self.successes):raise ValueError('Two valid labelled calibration counts required.')


@dataclass(frozen=True)
class InductionInference:
    deadline_rate: Q = Q(1)
    scheme: str = 'kl'
    evaluator: str = 'cancelled'
    feature_bias: Q = Q(0)            # e.g. justified finite movie-cap discrepancy

    def __post_init__(self):
        if self.deadline_rate<=0 or self.scheme not in ['kl','hoeffding'] or self.evaluator not in ['matrix','cancelled'] or not 0<=self.feature_bias<=1:raise ValueError('Invalid inference configuration.')

    def feature(self,k,n,L,bias=Q(0)):
        method=kl_interval if self.scheme=='kl' else hoeffding_interval
        s=method(Q(int(k),int(n)),int(n),L)
        if self.scheme=='kl':
            # Independently check the actual exclusion inequality, not the
            # weaker accidentally scaled check in the original source helper.
            for p in (s.a,s.b):
                if 0<p<1:assert n*kl_lower(Q(int(k),int(n)),p)>=L
        return I(max(Q(0),s.a-bias),min(Q(1),s.b+bias))

    def arm(self,counts,calibration):
        f=[self.feature(k,counts.total,Q(25,4),self.feature_bias) for k in counts.features()]
        cal=[self.feature(k,calibration.samples_per_state,Q(6)) for k in calibration.successes]
        try:r=evaluate([[f[0],f[1]],[f[2],f[3]]],f[4],cal[0],cal[1],I(self.deadline_rate),self.evaluator)
        except ValueError as e:return dict(status='model_conflict',reason=str(e))
        if r is None:return dict(status='unresolved',lower='0',upper='infinity',reason='Marker contrast or determinant enclosure reaches zero.')
        return dict(status='resolved',lower=str(r.a),upper=str(r.b),display=r.pair())

    def contrast(self,control,treated,calibration):
        u=self.arm(control,calibration);t=self.arm(treated,calibration)
        if any(x['status']=='model_conflict' for x in [u,t]):return dict(status='model_conflict',control=u,treated=t)
        if any(x['status']!='resolved' for x in [u,t]):return dict(status='unresolved',lower='-infinity',upper='infinity',control=u,treated=t)
        r=I(t['lower'],t['upper'])-I(u['lower'],u['upper'])
        return dict(status='resolved',lower=str(r.a),upper=str(r.b),display=r.pair(),width=str(r.width()),control=u,treated=t,
            coverage='At least 0.95 under independent founder/calibration units and the stated constant-rate calibrated-channel model; twelve overlapping features use one union bound.')
