export type GoalInputs = {wish:string;homeName?:string;carName?:string;cardName?:string;goalName:string;cashflowWish:string;cashflowTarget:number;cashflowCurrent:number;age:number|null;targetAge:number;target:number;invested:number;returnRate:number;homeYears:number;carYears:number;cardYears:number;cash:number;reserve:number;start:string};
export const emptyFinancialGoals:GoalInputs={wish:'',homeName:'',carName:'',cardName:'',goalName:'',cashflowWish:'',cashflowTarget:0,cashflowCurrent:0,age:null,targetAge:0,target:0,invested:0,returnRate:0,homeYears:0,carYears:0,cardYears:0,cash:0,reserve:0,start:''};
export function payment(balance:number,annualRate:number,months:number){if(balance<=0)return 0;if(months<=0)return Infinity;const r=annualRate/12;return r===0?balance/months:balance*r/(1-Math.pow(1+r,-months));}
export function savingPayment(target:number,opening:number,annualRate:number,months:number){if(months<=0)return Math.max(0,target-opening);const r=annualRate/12;const growth=Math.pow(1+r,months);return Math.max(0,r===0?(target-opening)/months:(target-opening*growth)*r/(growth-1));}
export type PlannerInput={income:number;expenses:number;funds:number;home:number;homeRate:number;homePay:number;car:number;carRate:number;carPay:number;cards:number;cardsRate:number;cardsPay:number;goals:GoalInputs;actuals?:Record<string,{income?:number}>};
export function debtNames(g:GoalInputs){return [g.homeName?.trim()||'Debt 1',g.carName?.trim()||'Debt 2',g.cardName?.trim()||'Debt 3'];}
export const strategies=[{id:'cashflow',name:'Free cash flow first',description:'Pay debt 1 on its target schedule, clear debt 2, then debt 3, then save.'},{id:'interest',name:'Reduce interest first',description:'Pay debt 1 on its target schedule; direct extra money to the highest-rate debt, then save.'},{id:'parallel',name:'Save alongside debt',description:'Pay debt 1 on its target schedule; fund the savings target each month, then clear the highest-rate debt.'}] as const;
export type Strategy=typeof strategies[number]['id'];
export function plannerErrors(p:PlannerInput){const g=p.goals,names=debtNames(g);const errors:string[]=[];if(p.income<=0)errors.push('Add monthly net income in Income & budget.');if(p.home>0&&(!(g.homeYears>0)||g.homeYears>60))errors.push(`Set a payoff period for ${names[0]} between 1 and 60 years.`);if(g.target>0&&(g.age===null||g.targetAge<=g.age||g.targetAge-g.age>60))errors.push('Enter your current age and a later target age, up to 60 years away.');if(p.car>0&&(!(g.carYears>0)||g.carYears>60))errors.push(`Set a payoff period for ${names[1]} between 1 and 60 years.`);if(p.car>0&&p.carPay<=0)errors.push(`Enter the monthly payment for ${names[1]}.`);if(p.cards>0&&(!(g.cardYears>0)||g.cardYears>60))errors.push(`Set a payoff period for ${names[2]} between 1 and 60 years.`);if(p.cards>0&&p.cardsPay<=0)errors.push(`Enter the monthly payment for ${names[2]}.`);if(!p.home&&!p.car&&!p.cards&&!g.target)errors.push('Add a debt balance or a savings target to build a plan.');return errors;}
export function simulate(p:PlannerInput,strategy:Strategy,settleCar=false,extraIncome=0,detail=true){const g=p.goals;const start=g.start||openingMonth();const homeMonths=Math.max(1,Math.round(g.homeYears*12));const carMonths=Math.max(1,Math.round(g.carYears*12));const cardMonths=Math.max(1,Math.round(g.cardYears*12));const savingMonths=g.target>0?Math.max(1,Math.round((g.targetAge-(g.age??g.targetAge))*12)):0;const horizon=Math.min(720,Math.max(p.home>0?homeMonths:0,savingMonths,p.car>0?carMonths:0,p.cards>0?cardMonths:0,1));const homePay=Math.max(p.homePay,payment(p.home,p.homeRate,homeMonths));const monthlySaving=savingPayment(g.target,g.invested,g.returnRate,savingMonths);let cash=g.cash,investment=g.invested,balances=[p.car,p.cards,p.home];let lump=0;if(settleCar&&p.car>0&&cash-g.reserve>=p.car){lump=p.car;cash-=p.car;balances[0]=0;}const payoff:(number|null)[]=balances.map(b=>b===0?0:null);const rates=[p.carRate,p.cardsRate,p.homeRate],payments=[p.carPay,p.cardsPay,homePay];const envelope=p.income-p.expenses-p.funds+p.homePay+p.carPay+p.cardsPay+extraIncome;let interest=0,shortfall=0,savingsAtTarget=g.target===0?investment:0,homeAtTarget=p.home,carAtTarget=balances[0],cardsAtTarget=balances[1],firstAllocation={home:0,car:0,cards:0,reserve:0,saving:0,extra:0};const rows:{month:number;debts:number;home:number;investment:number;cash:number;contribution:number;interest:number;shortfall:number;homePaid:number;carPaid:number;cardsPaid:number;reservePaid:number;income:number}[]=[];
for(let month=1;month<=horizon;month++){let thisInterest=0;balances=balances.map((b,i)=>{const add=b*rates[i]/12;thisInterest+=add;return b+add});interest+=thisInterest;const due=balances.map((b,i)=>Math.min(b,payments[i]));const required=due.reduce((s,v)=>s+v,0);const monthIncome=p.actuals?.[planMonthKey(start,month)]?.income??p.income;const monthEnvelope=envelope+monthIncome-p.income;const gap=Math.max(0,required-monthEnvelope);shortfall=Math.max(shortfall,gap);let remaining=Math.max(0,monthEnvelope);const paid=[0,0,0];for(const i of [2,0,1]){const n=Math.min(due[i],remaining);balances[i]=Math.max(0,balances[i]-n);remaining-=n;paid[i]+=n;}const reservePaid=Math.min(remaining,Math.max(0,g.reserve-cash));cash+=reservePaid;remaining-=reservePaid;let contribution=0,extra=0;
if(strategy==='parallel'){const n=Math.min(remaining,monthlySaving);contribution+=n;remaining-=n;}
const order=strategy==='cashflow'?[0,1]:[0,1,2].sort((i,j)=>rates[j]-rates[i]);for(const i of order){const n=Math.min(remaining,balances[i]);balances[i]-=n;remaining-=n;paid[i]+=n;extra+=n;}
contribution+=remaining;investment=investment*(1+g.returnRate/12)+contribution;
for(let i=0;i<3;i++)if(balances[i]<0.005){balances[i]=0;if(payoff[i]===null)payoff[i]=month;}
if(month===homeMonths)homeAtTarget=balances[2];if(month===carMonths)carAtTarget=balances[0];if(month===cardMonths)cardsAtTarget=balances[1];if(month===savingMonths)savingsAtTarget=investment;
if(month===1)firstAllocation={home:paid[2],car:paid[0],cards:paid[1],reserve:reservePaid,saving:contribution,extra};
if(detail)rows.push({month,debts:balances.reduce((s,b)=>s+b,0),home:balances[2],investment,cash,contribution,interest:thisInterest,shortfall:gap,homePaid:paid[2],carPaid:paid[0],cardsPaid:paid[1],reservePaid,income:monthIncome+extraIncome});}
const homeMet=p.home===0||homeAtTarget<0.01;const carMet=p.car===0||carAtTarget<0.01;const cardsMet=p.cards===0||cardsAtTarget<0.01;const savingsMet=g.target===0||savingsAtTarget+0.01>=g.target;return {strategy,settleCar,lump,homePay,monthlySaving,envelope,firstAllocation,payoff,rows,interest,shortfall,savingsAtTarget,homeAtTarget,carAtTarget,cardsAtTarget,homeMet,carMet,cardsMet,savingsMet,feasible:homeMet&&carMet&&cardsMet&&savingsMet&&shortfall<0.01,reserveMet:cash+0.01>=g.reserve,horizon};}
export function comparePlans(p:PlannerInput){const cashOption=p.car>0&&p.goals.cash-p.goals.reserve>=p.car;const plans=strategies.flatMap(s=>[false,...(cashOption?[true]:[])].map(settle=>{const current=simulate(p,s.id,settle);let low=0,high=1;while(!simulate(p,s.id,settle,high,false).feasible&&high<1e8)high*=2;for(let i=0;i<30;i++){const mid=(low+high)/2;if(simulate(p,s.id,settle,mid,false).feasible)high=mid;else low=mid;}const additional=current.feasible?0:Math.ceil(high);return {...current,additional,requiredPlan:additional>0?simulate(p,s.id,settle,additional):current};}));return plans.sort((a,b)=>a.additional-b.additional||a.requiredPlan.interest-b.requiredPlan.interest);}
export function validFinancialGoals(g:unknown):g is GoalInputs{if(!g||typeof g!=='object')return false;const x=g as GoalInputs;return ['homeName','carName','cardName'].every(k=>x[k as keyof GoalInputs]===undefined||(typeof x[k as keyof GoalInputs]==='string'&&String(x[k as keyof GoalInputs]).length<=120))&&typeof x.goalName==='string'&&x.goalName.length<=120&&typeof x.cashflowWish==='string'&&x.cashflowWish.length<=2000&&[x.cashflowTarget,x.cashflowCurrent].every(v=>Number.isFinite(v)&&v>=0)&&typeof x.wish==='string'&&x.wish.length<=2000&&(x.age===null||Number.isFinite(x.age)&&x.age>=0&&x.age<=120)&&['targetAge','target','invested','returnRate','homeYears','carYears','cardYears','cash','reserve'].every(k=>Number.isFinite(x[k as keyof GoalInputs])&&Number(x[k as keyof GoalInputs])>=0)&&x.targetAge<=120&&x.homeYears<=60&&x.carYears<=60&&x.cardYears<=60&&x.returnRate<=0.5&&typeof x.start==='string'&&(x.start===''||/^\d{4}-\d{2}$/.test(x.start));}

export const financialPurpose='This is not financial advice. Sovereign is a personal financial goal planner to help you see what you have, identify concerns, and explore practical ways to work towards your financial goals.';
export function makePlannerInput(p:any):PlannerInput{const a=p.assumptions;return {income:monthlyIncome(p),expenses:p.budget.reduce((s:number,b:any)=>s+b.amount,0),funds:p.funds.reduce((s:number,f:any)=>s+f.amount,0),home:a[17],homeRate:a[18],homePay:a[19],car:a[20],carRate:a[22],carPay:a[21],cards:a[23],cardsRate:a[25],cardsPay:a[24],actuals:p.actuals,goals:{...emptyFinancialGoals,...p.financialGoals,cash:a[8],reserve:a[9]}};}
export function openingMonth(){const parts=new Intl.DateTimeFormat('en-CA',{timeZone:'Africa/Johannesburg',year:'numeric',month:'2-digit'}).formatToParts(new Date());return parts.find(p=>p.type==='year')!.value+'-'+parts.find(p=>p.type==='month')!.value;}
export function planMonthLabel(start:string,n:number|null){if(n===null)return 'Not reached in this projection';if(n===0)return 'Already clear / settled at start';const [y,m]=start.split('-').map(Number);return new Date(Date.UTC(y,m-1+n,1)).toLocaleDateString('en-ZA',{timeZone:'UTC',month:'short',year:'numeric'});}

export function planMonthKey(start:string,n:number){const [year,month]=start.split('-').map(Number);return new Date(Date.UTC(year,month-1+n,1)).toISOString().slice(0,7);}
export function financialPlanState(p:any,scenario={index:0,funded:false}){const input=makePlannerInput(p),errors=plannerErrors(input),start=input.goals.start||openingMonth();if(Math.abs((p.budget[0]?.amount||0)-input.homePay-input.carPay-input.cardsPay)>1)errors.push('Debt repayment budget and monthly loan payments must match. Review Financial goals.');const plans=errors.length?[]:comparePlans(input);const choice=plans[Math.min(scenario.index,plans.length-1)];const active=choice&&(scenario.funded?choice.requiredPlan:choice);return {input,errors,start,plans,choice,active,funded:scenario.funded,period:active?planMonthLabel(start,1)+' – '+planMonthLabel(start,active.horizon):'No goal dates entered'};}

export type IncomeSource={id:string;name:string;amount:number};
export function additionalIncomeSources(p:any):IncomeSource[]{if(Array.isArray(p.additionalIncome))return p.additionalIncome;return p.incomeOther&&(p.incomeOther.amount>0||p.incomeOther.name)?[{id:'previous-other-income',name:p.incomeOther.name||'',amount:p.incomeOther.amount||0}]:[];}
export function validAdditionalIncome(v:any):v is IncomeSource[]{return Array.isArray(v)&&v.length<=50&&v.every(x=>x&&typeof x.id==='string'&&x.id.length>0&&x.id.length<=80&&typeof x.name==='string'&&x.name.length<=120&&typeof x.amount==='number'&&Number.isFinite(x.amount)&&x.amount>=0)&&new Set(v.map(x=>x.id)).size===v.length;}
export function monthlyIncome(p:any){return Number(p.assumptions[5]||0)+additionalIncomeSources(p).reduce((sum,x)=>sum+x.amount,0);}
