"""Replay the final bank operating model, terminal cash and valuations from CSV.

Python standard library only. Run from any directory; no archived input is read.
The default is read-only verification. --tail also replays 9,600 terminal years.
"""
from pathlib import Path
from datetime import date
import argparse, csv, math, hashlib, re, sys
from collections import Counter
sys.dont_write_bytecode=True
from bank_alm_engine_v9 import solve
from bank_terminal_engine_v2 import solve as terminal_solve

ROOT=Path(__file__).resolve().parents[1]
NOW=date(2026,9,2)
COUNTS=Counter()
def read(rel):
    with (ROOT/rel).open(encoding='utf-8-sig',newline='') as f:return list(csv.DictReader(f))
def group(row,prefix):return {k[len(prefix)+2:]:float(v) for k,v in row.items() if k.startswith(prefix+'__') and v!=''}
def close(a,b,label,rtol=3e-10,atol=1e-6):
    a,b=float(a),float(b)
    assert math.isfinite(a) and math.isfinite(b) and math.isclose(a,b,rel_tol=rtol,abs_tol=atol),(label,a,b)
    COUNTS['numeric_cells']+=1
def yf(a,b):return (b-a).days/365
def xirr(flows):
    def npv(r):return sum(v/(1+r)**yf(NOW,d) for d,v in flows)
    lo,hi=-.99,5.
    assert npv(lo)>0 and npv(hi)<0
    for _ in range(150):
        mid=(lo+hi)/2
        if npv(mid)>0:lo=mid
        else:hi=mid
    return (lo+hi)/2
def verify(tail=False):
    comparisons=read('10_cross_bank_report/bank_quality_growth_comparison.csv')
    annual=read('10_cross_bank_report/annual_fair_prices_2026_2036.csv')
    returns=read('10_cross_bank_report/return_cases.csv')
    assert len(comparisons)==8 and len(annual)==264 and len(returns)==48
    for bank in comparisons:
        folder=bank['folder'];eid=bank['entity_id'];b=Path(folder)
        inputs=read(b/'07_model/model_inputs_final.csv')
        forecast=read(b/'07_model/alm_accrual_forecast_gate5_v9.csv')
        tailrows=read(b/'08_valuation_validation/terminal_cash_extension_gate6_v2.csv')
        policies=read(b/'08_valuation_validation/terminal_policy_inputs.csv')
        ddm=read(b/'08_valuation_validation/valuation_ddm.csv')
        report=read(b/'09_report/report_metrics.csv')
        assert len(inputs)==len(forecast)==33 and len(tailrows)==1200
        COUNTS['preserved_terminal_rows']+=len(tailrows)
        for r in inputs:
            case=r['scenario'];year=int(r['year']);f=next(x for x in forecast if x['scenario']==case and int(x['year'])==year)
            out=solve(opening=group(r,'opening'),targets=group(r,'targets'),drivers=group(r,'drivers'),parameters=group(r,'parameters'),extras=group(r,'extras'),liquidity=group(r,'liquidity_parameters'),policy=group(r,'policy'),fraction=float(r['period_fraction']),fee=float(r['fee']),h1_common=float(r['h1_common']))
            assert out['feasible'],(eid,case,year,out['status'])
            for key,val in out.items():
                if isinstance(val,dict):
                    for k,v in val.items():
                        if key+'_'+k in f:close(v,f[key+'_'+k],(eid,case,year,key,k))
                elif isinstance(val,(float,int)) and not isinstance(val,bool) and key in f:close(val,f[key],(eid,case,year,key))
            for k,h1 in group(r,'h1_actuals').items():close(out['income'][k]+h1,f['FY_'+k],(eid,case,year,'FY',k))
            COUNTS['operating_rows']+=1
        for case in ['bear','base','bull']:
            last=next(r for r in inputs if r['scenario']==case and r['year']=='2036')
            f=next(r for r in forecast if r['scenario']==case and r['year']=='2036')
            shares=float(last['shares_mn'])
            if tail:
                for policy in [p for p in policies if p['scenario']==case]:
                    state={k[4:]:float(v) for k,v in f.items() if k.startswith('end_')};fee=float(f['FY_net_fee_income'])
                    for year in range(2037,2237):
                        d=group(last,'drivers');p=group(last,'policy');g=float(policy['g']);p['terminal_allowance_to_loans']=float(policy['allowance_to_loans'])
                        for k in ['total_asset_growth','loan_growth','investment_asset_growth','deposit_growth','fee_income_growth']:d[k]=g
                        d['dividend_payout_ratio']=float(policy['payout_2036'])+(float(policy['target_payout_2041'])-float(policy['payout_2036']))*min(1,(year-2036)/5)
                        targets={k:state[k]*(1+g) for k in ['assets','loans','investments','deposits']};fee*=1+g
                        out=terminal_solve(opening=state,targets=targets,drivers=d,parameters=group(last,'parameters'),extras=group(last,'extras'),liquidity=group(last,'liquidity_parameters'),policy=p,fraction=1.,fee=fee)
                        assert out['feasible'],(eid,case,year,'tail')
                        saved=next(r for r in tailrows if r['scenario']==case and r['mode']==policy['mode'] and int(r['year'])==year)
                        for k in ['cash_dividend','recognized_dividend','cet1_ratio','rwa']:close(out[k],saved[k],(eid,case,year,k),rtol=1e-8,atol=1e-4)
                        for k in ['assets','loans','allowance']:close(out['end'][k],saved[k],(eid,case,year,k),rtol=1e-8,atol=1e-4)
                        close(out['income']['common_profit'],saved['common_profit'],(eid,case,year,'profit'),rtol=1e-8,atol=1e-4)
                        state=out['end'].copy();COUNTS['replayed_terminal_rows']+=1
            flows=[(date.fromisoformat(r['payment_date']),float(r['amount'])/shares) for r in ddm if r['scenario']==case and not r['terminal_value']]
            flows += [(date(int(r['year']),6,30),float(r['cash_dividend'])/shares) for r in tailrows if r['scenario']==case and r['mode']=='constant_payout']
            assert 200<=len(flows)<=210 and len({d for d,v in flows})==len(flows)
            rm=next(r for r in report if r['scenario']==case);ke=float(rm['ke'])
            def value(t):return sum(v/(1+ke)**yf(t,d) for d,v in flows if d>t)
            close(value(NOW),rm['fair_value'],(eid,case,'current_value'));COUNTS['current_values']+=1
            previous=NOW;pv=value(NOW)
            for year in range(2026,2037):
                t=date(year,12,31);v=value(t);cash=sum(c for d,c in flows if d<=t)
                record=next(r for r in annual if r['entity_id']==eid and r['scenario']==case and r['valuation_date']==t.isoformat())
                close(v,record['fair_price'],(eid,case,year,'annual_price'))
                close(cash,record['cash_since_20260902'],(eid,case,year,'cash'))
                expected=pv*(1+ke)**yf(previous,t)-sum(c*(1+ke)**yf(d,t) for d,c in flows if previous<d<=t)
                close(v,expected,(eid,case,year,'rollforward'));previous,pv=t,v;COUNTS['annual_prices']+=1
                if year in [2031,2036]:
                    ret=next(r for r in returns if r['entity_id']==eid and r['scenario']==case and int(r['year'])==year)
                    close(v,ret['price'],(eid,case,year,'exit'));close(cash,ret['cumulative_cash'],(eid,case,year,'return_cash'))
                    ir=xirr([(NOW,-float(rm['current_price']))]+[(d,c) for d,c in flows if d<=t]+[(t,v)])
                    close(ir,ret['xirr'],(eid,case,year,'XIRR'));COUNTS['return_cases']+=1
        scores=read(b/'08_valuation_validation/quality_assumptions_gate6_v3.csv')
        close(sum(float(r['weighted_score']) for r in scores),bank['quality_score'],(eid,'quality'))
        base=sorted([r for r in forecast if r['scenario']=='base'],key=lambda x:int(x['year']))
        close((float(base[-1]['FY_common_profit'])/float(base[0]['FY_common_profit']))**.1-1,bank['profit_cagr'],(eid,'growth'))
        print(bank['name']+': operating model, cash valuation, annual prices'+(', terminal replay' if tail else '')+' verified',flush=True)
    print('VERIFIED '+', '.join(f'{k}={v}' for k,v in sorted(COUNTS.items())),flush=True)
    return dict(COUNTS)
if __name__=='__main__':
    parser=argparse.ArgumentParser();parser.add_argument('--tail',action='store_true');args=parser.parse_args();verify(args.tail)
