"""Exact source inequalities and explicitly scoped mathematical bounds."""
from dataclasses import dataclass
from fractions import Fraction as F
from math import exp,expm1,log,log1p
from population import Action,validate_configuration


def mv(A,x):return [sum((a*b for a,b in zip(row,x)),F(0)) for row in A]


def q_valid(q,size):
    if len(q)!=size or not all(0<x<=1 for x in q):raise ValueError('Supersolution needs 0<q<=1 in every source state.')


@dataclass(frozen=True)
class ExposureCertificate:
    q:tuple
    c:F
    def verify(self,source,continuation=None):
        q_valid(self.q,source.size)
        if self.c<0:raise ValueError('Nonnegative exposure coefficient required.')
        baseline=source.phi(self.q);Rq=source.eraser_direction(self.q)
        slack=[self.c*(1-x)-r for x,r in zip(self.q,Rq)]
        if max(baseline)>0 or min(slack)<0:raise ValueError('Exposure inequalities failed for this source.')
        off=(continuation or source).phi(self.q)
        if max(off)>0:raise ValueError('Declared continuation does not satisfy the supersolution condition.')
        return dict(baseline_residual=baseline,control_slack=slack,continuation_residual=off)

    def survival_floor(self,z,budget,source):
        self.verify(source);validate_configuration(z,source.size)
        if budget<0:raise ValueError('Pathwise budget must be nonnegative.')
        alpha=exp(-float(self.c)*float(budget))
        return -expm1(sum(n*log1p(-alpha*float(1-q)) for q,n in zip(self.q,z)))

    def necessary_exposure(self,founders,risk,index):
        if founders<1 or int(founders)!=founders or not 0<risk<1:raise ValueError('Positive founder count and risk in (0,1) required.')
        eta=float(1-self.q[index]);required_single=-expm1(log1p(-risk)/founders)
        if eta<=required_single:return 0.
        if self.c==0:return float('inf')
        return log(eta/required_single)/float(self.c)


@dataclass(frozen=True)
class AmplitudeCertificate:
    q:tuple
    maximum:F
    def verify(self,source):
        q_valid(self.q,source.size)
        if self.maximum<0:raise ValueError('Nonnegative amplitude required.')
        low=source.phi(self.q);high=source.phi(self.q,Action(self.maximum))
        if max(low)>0 or max(high)>0:raise ValueError('Uniform amplitude certificate rejected.')
        return dict(baseline_residual=low,maximum_action_residual=high)
    def survival_floor_exact(self,z,source):
        self.verify(source);validate_configuration(z,source.size);product=F(1)
        for q,n in zip(self.q,z):product*=q**int(n)
        return 1-product


@dataclass(frozen=True)
class WeightedDrift:
    weights:tuple
    gamma:F
    action:Action
    def verify(self,source):
        if len(self.weights)!=source.size or min(self.weights)<=0 or self.gamma<=0:raise ValueError('Positive weights and decay rate required.')
        slack=[-self.gamma*w-a for w,a in zip(self.weights,mv(source.mean_matrix(self.action),self.weights))]
        if min(slack)<0:raise ValueError('Weighted drift fails.')
        return slack
    def sufficient(self,source,z,risk):
        self.verify(source);validate_configuration(z,source.size)
        if not 0<risk<1:raise ValueError('Risk must lie in (0,1).')
        C=sum(n*w for n,w in zip(z,self.weights))/min(self.weights)
        time=max(0,log(float(C)/risk)/float(self.gamma)) if C else 0.
        return dict(duration=time,eraser_exposure=float(self.action.eraser)*time,added_death_exposure=float(self.action.protected_death)*time,
            scope='Guaranteed finite-time risk under this constant action, contingent on admitted amplitude and duration. Floating logarithms display the exact formula; they are not rounded admission tests.')


def collatz_bounds(source,action,weights):
    if len(weights)!=source.size or min(weights)<=0:raise ValueError('Positive weight required.')
    ratios=[a/w for a,w in zip(mv(source.mean_matrix(action),weights),weights)]
    return min(ratios),max(ratios)


def m_matrix_test(source,action):
    A=source.mean_matrix(action);M=[[-x for x in row] for row in A];det=F(1);minors=[]
    if any(M[i][j]>0 for i in range(source.size) for j in range(source.size) if i!=j):raise ValueError('Not a Z-matrix.')
    for i in range(source.size):
        if M[i][i]==0:return dict(subcritical=False,leading_minors=minors+[F(0)])
        det*=M[i][i];minors.append(det)
        if det<=0:return dict(subcritical=False,leading_minors=minors)
        for k in range(i+1,source.size):
            factor=M[k][i]/M[i][i]
            for j in range(i,source.size):M[k][j]-=factor*M[i][j]
    return dict(subcritical=True,leading_minors=minors)


def finite_time_floor(founders,time,death_ceiling):
    if founders<0 or int(founders)!=founders or time<0 or death_ceiling<=0:raise ValueError('Invalid finite-time bound inputs.')
    if founders==0:return 0.
    if time==0:return 1.
    return -expm1(founders*log(-expm1(-death_ceiling*time)))


def concentration_lower_bound(required_exposure,horizon,maximum_rate,half_saturation,elimination=None,volume=None,initial=0.):
    """Conditional necessary amount only, under the specified calibrated law."""
    if required_exposure<0 or horizon<=0 or maximum_rate<=0 or half_saturation<=0:raise ValueError('Positive law and horizon required.')
    if required_exposure>=maximum_rate*horizon:return dict(status='no_finite_AUC_can_meet_necessary_exposure')
    auc=half_saturation*horizon*required_exposure/(maximum_rate*horizon-required_exposure)
    result=dict(status='necessary_only',target_concentration_AUC_lower=auc)
    if elimination is not None and volume is not None:
        if elimination<=0 or volume<=0 or initial<0:raise ValueError('Positive clearance and volume required.')
        result['administered_amount_lower']=max(0,volume*(elimination*auc-initial))
    return result


def population_barrier_generator_identity(source,q,z,action):
    """Exact verification at a configuration; no founder independence assumption."""
    q_valid(q,source.size);validate_configuration(z,source.size)
    V=F(1)
    for x,n in zip(q,z):V*=x**n
    Q=source.generator(action);direct=F(0)
    for i,n in enumerate(z):
        for j in range(source.size):
            if i!=j:direct+=V*n*Q[i][j]*(q[j]/q[i]-1)
        direct+=V*n*(source.death[i]+action.protected_death*source.protected[i])*(1/q[i]-1)
        direct+=V*n*source.birth[i]*sum(p*(q[j]*q[k]/q[i]-1) for j,k,p in source.pairs[i])
    factored=V*sum(n*f/x for n,f,x in zip(z,source.phi(q,action),q))
    if direct!=factored:raise AssertionError('Population generator mismatch.')
    return dict(direct=direct,factored=factored)
