"""Explicit deposit acceptance, bounded runoff and dividend reserve model.

New research policies authorized by the user's delegated adjustment request.
No changes to the frozen v4/v8 engine or historical values.
"""
from bank_aggregate_engine import income


def solve(*, opening, targets, drivers, parameters, extras, liquidity, policy,
          fraction, fee, h1_common=0.0):
    o, d, p, x, l = opening, drivers, parameters, extras, liquidity
    trace = []; tol = max(1e-6,1e-12*opening["assets"])
    core_open = dict(o, assets=o['assets']-o['liquidity'])
    min_deposits = min(targets['deposits'], o['deposits']*(1-policy['deposit_runoff_limit']*fraction))
    def root(fn, lo, hi):
        a,b=fn(lo),fn(hi)
        if abs(a)<tol:return lo
        if abs(b)<tol:return hi
        if a*b>0:return None
        for _ in range(80):
            mid=(lo+hi)/2; value=fn(mid)
            if abs(value)<tol:return mid
            if value*a>0:lo=mid;a=value
            else:hi=mid
        raise ArithmeticError('Bisection did not converge')

    def candidate(scale,payout,runoff):
        end={k:o[k]+min(0,targets[k]-o[k])+scale*max(0,targets[k]-o[k])-runoff*o[k]*fraction for k in ['loans','investments']}
        avgloan=(o['loans']+end['loans'])/2
        charge=avgloan*d['normalized_credit_cost']*fraction
        adj=avgloan*x['combined_ac_deduction_adjustment_rate']*fraction
        allowance=o['allowance']+charge+adj
        if 'terminal_allowance_to_loans' in policy:
            allowance=end['loans']*policy['terminal_allowance_to_loans']
            adj=allowance-o['allowance']-charge
        loan_net=end['loans']*x['loan_basis_to_book_factor']-allowance
        core=(loan_net+end['investments'])/(1-p['other_assets_share'])
        detail=dict(scale=scale,payout=payout,runoff=runoff,core_assets=core)
        def fail(status,**kw):
            trace.append(dict(detail,status=status,**kw)); return None
        if min(allowance,loan_net,end['investments'])<0:return fail('NEGATIVE_ASSET_OR_ALLOWANCE')
        if core>targets['assets']+tol:return fail('CORE_ASSET_CEILING')
        noncommon=o['noncommon_equity']+x['annual_noncommon_equity_change']*fraction
        oci=o['common_equity']*p['oci_amplitude_on_equity']*fraction
        other_common=o['common_equity']*x['annual_other_common_equity_rate']*fraction
        floor=l['wholesale_retained_floor']
        def evaluate(dep,funding,liquid):
            ee=dict(end,assets=core,deposits=dep,wholesale=funding)
            inc=income(opening=core_open,end=ee,drivers=d,parameters=p,fraction=fraction,fee=fee,loan_charge=charge)
            avgl=(o['liquidity']+liquid)/2
            li=avgl*l['liquidity_yield']*fraction;cost=avgl*l['liquidity_cost_rate']*fraction;loss=avgl*l['liquidity_loss_rate']*fraction
            surcharge=li*p['tax_surcharges_to_operating_income']
            inc.update(liquidity_interest=li,liquidity_cost=cost,liquidity_loss=loss)
            inc['net_interest_income']+=li;inc['operating_income']+=li;inc['operating_expense']+=cost+surcharge;inc['ppop']+=li-cost-surcharge;inc['surcharges']+=surcharge;inc['nonloan_impairment']+=loss
            inc['pretax_profit']+=li-cost-surcharge-loss
            inc['income_tax']=max(0,inc['pretax_profit'])*p['effective_tax_rate'];inc['net_profit']=inc['pretax_profit']-inc['income_tax'];inc['minority_profit']=inc['net_profit']*p['minority_profit_share'];inc['parent_profit']=inc['net_profit']-inc['minority_profit'];inc['common_profit']=inc['parent_profit']-inc['noncommon_allocation']
            # Full FY2026 earnings fund FY2026 distributions. H1 earnings are
            # already in opening equity, hence only H2 profit is added here.
            dividend=max(0,inc['common_profit']+h1_common)*payout
            cash=o['dividend_reserve']+dividend*policy['same_year_payment_share']
            reserve=dividend*(1-policy['same_year_payment_share'])
            common=o['common_equity']+inc['common_profit']-dividend+oci+other_common+p['planned_new_common_equity']
            minority=o['minority_equity']+inc['minority_profit']*(1-p['minority_dividend_payout'])+x['annual_minority_other_equity_change']*fraction
            otherliab=dep*x['other_liabilities_to_deposits']
            residual=core+liquid-dep-funding-otherliab-reserve-common-noncommon-minority
            return dict(residual=residual,income=inc,common_equity=common,minority_equity=minority,recognized_dividend=dividend,cash_dividend=cash,dividend_reserve=reserve,other_liabilities=otherliab)
        dep=targets['deposits']
        def caps(dep):return dep*p['wholesale_to_deposit_cap'],max(0,min(dep*l['liquidity_cap_to_deposits'],targets['assets']-core))
        cap,lc=caps(dep)
        # Reduce deposit intake only if an explicit surplus cannot be invested
        # inside the liquidity/asset cap. Interest expense is recalculated.
        if evaluate(dep,floor,lc)['residual']<-tol:
            def excess(dd):return evaluate(dd,floor,caps(dd)[1])['residual']
            if excess(min_deposits)<-tol:return fail('SURPLUS_BEYOND_DEPOSIT_RUNOFF_LIMIT')
            dep=root(excess,min_deposits,dep)
            cap,lc=caps(dep)
        if floor>cap+tol:return fail('RETAINED_DEBT_EXCEEDS_CAP')
        r0=evaluate(dep,floor,0)['residual']
        if abs(r0)<tol:funding,liquid=floor,0.
        elif r0>0:
            funding=root(lambda w:evaluate(dep,w,0)['residual'],floor,cap)
            if funding is None:return fail('FUNDING_CAP')
            liquid=0.
        else:
            liquid=root(lambda a:evaluate(dep,floor,a)['residual'],0,lc)
            if liquid is None:return fail('LIQUIDITY_CAP')
            funding=floor
        out=evaluate(dep,funding,liquid)
        assets=core+liquid;rwa=core*d['rwa_density']+liquid*l['liquidity_risk_weight'];cet1=out['common_equity']+core*p['regulatory_scope_adjustment_to_assets'];ratio=cet1/rwa
        if ratio+1e-10<p['research_operating_cet1_target']:return fail('CAPITAL_TARGET',cet1_ratio=ratio)
        if min(out['common_equity'],out['minority_equity'],noncommon)<0:return fail('NEGATIVE_EQUITY')
        trace.append(dict(detail,status='FEASIBLE',cet1_ratio=ratio,deposits=dep,wholesale=funding,liquidity=liquid,total_assets=assets))
        ee=dict(end,assets=assets,deposits=dep,wholesale=funding,liquidity=liquid,allowance=allowance,common_equity=out['common_equity'],minority_equity=out['minority_equity'],noncommon_equity=noncommon,dividend_reserve=out['dividend_reserve'])
        return dict(**out,end=ee,core_assets=core,loan_net=loan_net,allowance_charge=charge,combined_deduction_adjustment=adj,other_assets=core*p['other_assets_share'],noncommon_equity=noncommon,oci=oci,other_common=other_common,rwa=rwa,cet1=cet1,cet1_ratio=ratio,scale=scale,payout=payout,runoff=runoff,deposit_target=targets['deposits'],deposit_not_accepted=targets['deposits']-dep,asset_ceiling=targets['assets'],feasible=True,status='COMPLETE_CONDITIONAL_ALM',trace=trace)
    # First retain existing principal: explore permitted growth and payouts.
    # Only after exhausting that set allow explicitly bounded natural runoff.
    for runoff in [0]+[j/100 for j in range(1,round(policy['asset_runoff_limit']*100)+1)]:
        for payout_step in range(11):
            payout=d['dividend_payout_ratio']*(1-payout_step/10)
            for growth_step in (range(101) if runoff==0 else [100]):
                out=candidate(1-growth_step/100,payout,runoff)
                if out:return out
    return dict(feasible=False,status='NO_FEASIBLE_POLICY_POINT',trace=trace)
