"""Literal per-core currents and monotone response bands; all inputs are rational."""
from dataclasses import dataclass
from fractions import Fraction as F


@dataclass(frozen=True)
class ActivityBox:
    lower: F
    upper: F

    def __post_init__(self):
        object.__setattr__(self,'lower',F(self.lower));object.__setattr__(self,'upper',F(self.upper))
        if not 0<self.lower<=self.upper<=1:raise ValueError('boxes require 0 < lower <= upper <= 1')


@dataclass(frozen=True)
class Core:
    tail: str
    head: str
    a: F = F(1)
    b: F = F(1)
    ratio_lower: F | None = None
    ratio_upper: F | None = None
    lower_weight: F = F(1)
    upper_weight: F = F(1)

    def __post_init__(self):
        for name in ['a','b','lower_weight','upper_weight']:object.__setattr__(self,name,F(getattr(self,name)))
        if self.tail==self.head or min(self.a,self.b,self.lower_weight,self.upper_weight)<=0:raise ValueError('distinct endpoints and positive factors/demand weights required')
        lo=self.a/self.b if self.ratio_lower is None else F(self.ratio_lower)
        hi=self.a/self.b if self.ratio_upper is None else F(self.ratio_upper)
        if not 0<lo<=hi:raise ValueError('invalid ratio interval')
        object.__setattr__(self,'ratio_lower',lo);object.__setattr__(self,'ratio_upper',hi)

    def lower(self,x):
        r=self.ratio_upper;return (r*x+2*x*x)/(r+2)

    def upper(self,x):
        r=self.ratio_lower;return (r*x+x*x)/(r+1)

    def currents(self,x,y):
        p=self.a*(x-y);q=self.b*(y-x*x)
        return dict(p=p,q=q,tail_production=2*q-p,head_production=p-q)

    def margins(self,x,y):return (y-self.lower(x))/self.lower_weight,(self.upper(x)-y)/self.upper_weight

    def uncertain_factor_activity_minima(self,x,y,radius,activity_error):
        x,y,r,e=map(F,[x,y,radius,activity_error])
        if not 0<=r<1 or e<0 or not 0<x-e<=x+e<=1 or not 0<y-e<=y+e<=1:raise ValueError('invalid uncertainty box')
        if x-e<=y+e or y-e<=(x+e)**2:raise ValueError('the monotone-corner formula requires both currents positive throughout the box')
        lo=2*self.b*(1-r)*(y-e-(x+e)**2)-self.a*(1+r)*(x-y+2*e)
        hi=self.a*(1-r)*(x-y-2*e)-self.b*(1+r)*(y+e-(x-e)**2)
        return lo,hi


@dataclass
class ActivityProblem:
    boxes: dict
    cores: tuple

    def __post_init__(self):
        if any(not isinstance(v,ActivityBox) for v in self.boxes.values()):raise ValueError('ActivityBox values required')
        pairs=set()
        for e in self.cores:
            if e.tail not in self.boxes or e.head not in self.boxes:raise ValueError('every core endpoint needs a box')
            pair=frozenset((e.tail,e.head))
            if pair in pairs:raise ValueError('the paper assumes a simple oriented source graph')
            pairs.add(pair)

    def check_rational(self,state,margin=F(0),strict=False):
        if set(state)!=set(self.boxes):return False
        if any(not b.lower<=F(state[v])<=b.upper for v,b in self.boxes.items()):return False
        for e in self.cores:
            margins=e.margins(F(state[e.tail]),F(state[e.head]))
            if any(m<=margin if strict else m<margin for m in margins):return False
        return True

    def total_accounts(self,state):
        food=F(0);production={v:F(0) for v in self.boxes};rows=[]
        for e in self.cores:
            r=e.currents(F(state[e.tail]),F(state[e.head]));food+=r['q'];production[e.tail]+=r['tail_production'];production[e.head]+=r['head_production'];rows.append(dict(edge=[e.tail,e.head],**r))
        assert sum(production.values())==food
        return dict(cores=rows,food=food,species_production=production,total_internal_production=sum(production.values()))


def windmill(modules,a=F(2),b=F(1),shortcut_a=F(1),box=None):
    if type(modules) is not int or modules<1:raise ValueError('positive integer module count required')
    boxes={'A':ActivityBox(F(1,10),F(9,10))};cores=[]
    for i in range(modules):
        B,C=f'B{i}',f'C{i}';boxes[B]=ActivityBox(F(1,100),F(9,10));boxes[C]=ActivityBox(F(1,1000),F(9,10))
        cores.extend([Core('A',B,a,b),Core(B,C,a,b),Core('A',C,shortcut_a,b)])
    return ActivityProblem(boxes,tuple(cores))
