"""Accrual bank model: explicit inputs, funding feedback and bounded capital policy.

The remaining accounting policies require their own approval. This module never
infers a cash dividend schedule or fills missing parameters from a balance gap.
"""
from math import isfinite

EXTRA_KEYS={'loan_basis_to_book_factor','combined_ac_deduction_adjustment_rate','other_liabilities_to_deposits','annual_noncommon_equity_change','annual_minority_other_equity_change','annual_other_common_equity_rate'}

def finite_inputs(data,required):
    missing=required-set(data)
    if missing:raise ValueError('Missing inputs: '+','.join(sorted(missing)))
    for k in required:
        if isinstance(data[k],bool) or not isinstance(data[k],(int,float)) or not isfinite(data[k]):raise ValueError('Invalid input: '+k)

def income(*,opening,end,drivers,parameters,fraction,fee,loan_charge=None):
    finite_inputs(opening,{'assets','loans','deposits','investments','wholesale'})
    finite_inputs(end,{'assets','loans','deposits','investments','wholesale'})
    finite_inputs(drivers,{'x_aggregate_loan_yield','investment_yield','deposit_cost','normalized_credit_cost'})
    keys={'investment_target_debt_share','investment_average_basis_factor','other_earning_book_share','other_earning_average_basis_factor','other_earning_yield_spread','wholesale_average_basis_factor','wholesale_cost_spread_to_deposit','annual_market_income_to_assets','annual_other_business_income_to_assets','annual_other_business_cost_to_assets','annual_management_expense_to_assets','tax_surcharges_to_operating_income','minority_profit_share','annual_noncommon_allocation','nonloan_loss_to_assets','other_impairment_to_assets','effective_tax_rate','nonoperating_net_income'}
    finite_inputs(parameters,keys)
    if not 0<fraction<=1:raise ValueError('Invalid flow period')
    d=drivers;p=parameters;avg={k:(opening[k]+end[k])/2 for k in opening if k in end}
    if min(p['effective_tax_rate'],p['minority_profit_share'])<0 or max(p['effective_tax_rate'],p['minority_profit_share'])>1:raise ValueError('Invalid tax/minority fraction')
    r={}
    r['loan_interest']=avg['loans']*d['x_aggregate_loan_yield']*fraction
    r['investment_interest']=avg['investments']*p['investment_target_debt_share']*p['investment_average_basis_factor']*d['investment_yield']*fraction
    r['other_interest']=avg['assets']*p['other_earning_book_share']*p['other_earning_average_basis_factor']*(d['investment_yield']+p['other_earning_yield_spread'])*fraction
    r['deposit_interest']=avg['deposits']*d['deposit_cost']*fraction
    r['wholesale_interest']=avg['wholesale']*p['wholesale_average_basis_factor']*(d['deposit_cost']+p['wholesale_cost_spread_to_deposit'])*fraction
    r['net_interest_income']=r['loan_interest']+r['investment_interest']+r['other_interest']-r['deposit_interest']-r['wholesale_interest']
    r['net_fee_income']=fee
    r['market_related_income']=avg['assets']*p['annual_market_income_to_assets']*fraction
    r['other_business_income']=avg['assets']*p['annual_other_business_income_to_assets']*fraction
    r['operating_income']=r['net_interest_income']+fee+r['market_related_income']+r['other_business_income']
    r['management_expense']=avg['assets']*p['annual_management_expense_to_assets']*fraction
    r['other_business_cost']=avg['assets']*p['annual_other_business_cost_to_assets']*fraction
    r['surcharges']=r['operating_income']*p['tax_surcharges_to_operating_income']
    if r['surcharges']<0:raise ValueError('Negative revenue requires a separate surcharge basis')
    r['operating_expense']=r['management_expense']+r['other_business_cost']+r['surcharges']
    r['ppop']=r['operating_income']-r['operating_expense']
    r['loan_impairment']=avg['loans']*d['normalized_credit_cost']*fraction if loan_charge is None else loan_charge
    r['nonloan_impairment']=avg['assets']*p['nonloan_loss_to_assets']*fraction
    r['other_impairment']=avg['assets']*p['other_impairment_to_assets']*fraction
    r['pretax_profit']=r['ppop']-r['loan_impairment']-r['nonloan_impairment']-r['other_impairment']+p['nonoperating_net_income']*fraction
    r['income_tax']=max(0,r['pretax_profit'])*p['effective_tax_rate']
    r['net_profit']=r['pretax_profit']-r['income_tax']
    r['minority_profit']=r['net_profit']*p['minority_profit_share']
    r['parent_profit']=r['net_profit']-r['minority_profit']
    r['noncommon_allocation']=p['annual_noncommon_allocation']*fraction
    r['common_profit']=r['parent_profit']-r['noncommon_allocation']
    return r

def aggregate(*,opening,targets,drivers,parameters,extras,fraction,fee,extra_approval_id,required_extra_approval_id):
    if not required_extra_approval_id or extra_approval_id!=required_extra_approval_id:raise PermissionError('Remaining accounting assumptions are not approved')
    finite_inputs(extras,EXTRA_KEYS)
    finite_inputs(opening,{'assets','loans','deposits','investments','wholesale','allowance','common_equity','noncommon_equity','minority_equity'})
    finite_inputs(targets,{'assets','loans','investments','deposits'})
    finite_inputs(drivers,{'rwa_density','dividend_payout_ratio'})
    finite_inputs(parameters,{'other_assets_share','oci_amplitude_on_equity','regulatory_scope_adjustment_to_assets','research_operating_cet1_target','wholesale_to_deposit_cap','planned_new_common_equity','minority_dividend_payout'})
    if not 0<=parameters['other_assets_share']<1:raise ValueError('Invalid other asset share')
    if not 0<=drivers['dividend_payout_ratio']<=1:raise ValueError('Invalid payout ceiling')
    trace=[];p=parameters;x=extras
    def candidate(scale,payout):
        # Scaling removes proposed positive growth; planned contractions remain.
        end={k:opening[k]+min(0,targets[k]-opening[k])+scale*max(0,targets[k]-opening[k]) for k in ['loans','investments']}
        end['deposits']=targets['deposits']
        avgloan=(opening['loans']+end['loans'])/2
        charge=avgloan*drivers['normalized_credit_cost']*fraction
        adjustment=avgloan*x['combined_ac_deduction_adjustment_rate']*fraction
        allowance=opening['allowance']+charge+adjustment
        if allowance<0:return dict(feasible=False,status='NEGATIVE_ALLOWANCE',scale=scale)
        loanbook=end['loans']*x['loan_basis_to_book_factor'];loannet=loanbook-allowance
        if loannet<0:return dict(feasible=False,status='NEGATIVE_LOAN_NET',scale=scale)
        end['assets']=(loannet+end['investments'])/(1-p['other_assets_share'])
        otherliab=end['deposits']*x['other_liabilities_to_deposits']
        noncommon=opening['noncommon_equity']+x['annual_noncommon_equity_change']*fraction
        oci=opening['common_equity']*p['oci_amplitude_on_equity']*fraction
        othercommon=opening['common_equity']*x['annual_other_common_equity_rate']*fraction
        # Explicit funding root: operating results are recalculated at each trial.
        funding=opening['wholesale'];iterations=0
        for iterations in range(1,201):
            end['wholesale']=funding
            inc=income(opening=opening,end=end,drivers=drivers,parameters=p,fraction=fraction,fee=fee,loan_charge=charge)
            dividend=max(0,inc['common_profit'])*payout
            common=opening['common_equity']+inc['common_profit']-dividend+oci+othercommon+p['planned_new_common_equity']
            minority=opening['minority_equity']+inc['minority_profit']*(1-p['minority_dividend_payout'])+x['annual_minority_other_equity_change']*fraction
            required=end['assets']-end['deposits']-otherliab-common-minority-noncommon
            if required<0:return dict(feasible=False,status='EXPLICIT_SURPLUS_ALLOCATION_REQUIRED',scale=scale)
            if abs(required-funding)<1e-13*max(1,end['assets']):break
            funding=required
        else:return dict(feasible=False,status='FUNDING_NOT_CONVERGED',scale=scale)
        cet1=common+end['assets']*p['regulatory_scope_adjustment_to_assets'];rwa=end['assets']*drivers['rwa_density']
        ratio=cet1/rwa if rwa>0 else float('-inf')
        flags=[]
        if end['assets']>targets['assets']+1e-6:flags.append('ASSET_CEILING')
        if funding>end['deposits']*p['wholesale_to_deposit_cap']+1e-6:flags.append('FUNDING_CAP')
        if ratio+1e-10<p['research_operating_cet1_target']:flags.append('CAPITAL_TARGET')
        if min(common,minority,noncommon)<0:flags.append('NEGATIVE_EQUITY_COMPONENT')
        trace.append(dict(scale=scale,payout=payout,funding=funding,assets=end['assets'],loans=end['loans'],investments=end['investments'],common_profit=inc['common_profit'],cet1_ratio=ratio,status=';'.join(flags) or 'FEASIBLE'))
        return dict(feasible=not flags,status=';'.join(flags) or 'FEASIBLE',end=end.copy(),income=inc,allowance=allowance,allowance_charge=charge,combined_deduction_adjustment=adjustment,loan_book=loanbook,loan_net=loannet,other_assets=end['assets']*p['other_assets_share'],other_liabilities=otherliab,common_equity=common,minority_equity=minority,noncommon_equity=noncommon,oci=oci,other_common=othercommon,recognized_dividend=dividend,cash_dividend=None,cet1=cet1,rwa=rwa,cet1_ratio=ratio,scale=scale,payout=payout,balance_residual=end['assets']-end['deposits']-funding-otherliab-common-minority-noncommon,iterations=iterations)
    # Search the permitted growth range before reducing the payout rate.
    # A bounded grid avoids assuming global monotonicity of feasibility.
    trials=[(1-j/100,drivers['dividend_payout_ratio']) for j in range(101)]
    trials += [(0,drivers['dividend_payout_ratio']*(1-j/10)) for j in range(1,11)]
    for scale,payout in trials:
            out=candidate(scale,payout)
            if out['feasible']:
                out.update(status='AGGREGATE_ACCRUAL_ONLY',growth_search_resolution=.01,trace=trace,cash_calendar_released=False,valuation_ready=False)
                return out
    return dict(feasible=False,status='NO_FEASIBLE_GRID_POINT',trace=trace,cash_calendar_released=False,valuation_ready=False)
