(()=>{'use strict';
const C=LwCalculator,T=LwTools,P=LwPortfolioFx,L=LwPortfolioLedger,R=LwPortfolioPerformance,IO=LwPortfolioIO,$=id=>document.getElementById(id),KEY='lw_portfolio_v2',UNDO=KEY+'_undo';
let lastChart=null,chartKey='',fxBatchBusy=false,accountRenderKey=null;const accountValueCells=new Map();
let accounts=[L.DEFAULT],reconciliations=[],performanceResult=null,records=[],recoveryRaw=null,raw=null,locked=false,market={rows:[],offline:false},currency='USD',page=0,pending=null,importToken=0,fxToken=0,fxSource='USD only';
const snapshot=()=>({version:4,accounts,transactions:records,reconciliations});
const today=new Date().toISOString().slice(0,10),id=()=>crypto.randomUUID(),fmt=(n,d=2)=>n===null?'—':C.format(Object.is(n,-0)?0:n,d),say=(key,value)=>{$(key).textContent=value;};
let backupToken=0;
$('txDate').value=today;$('txDate').max=today;
function read(){
 try{raw=localStorage.getItem(KEY);let data;
 if(raw!==null)data=L.normalize(JSON.parse(raw));else{recoveryRaw=localStorage.getItem('lw_portfolio');const legacy=JSON.parse(recoveryRaw||'[]');if(!Array.isArray(legacy))throw Error('Invalid legacy holdings.');data=L.normalize({version:2,transactions:legacy});}
 L.ledger(data);({accounts,reconciliations}=data);records=data.transactions;$('undoChange').disabled=!localStorage.getItem(UNDO);
 }catch(e){locked=true;say('pfStatus','Saved data could not be safely loaded. Changes are disabled; existing browser data has not been overwritten. '+e.message);for(const key of ['saveTransaction','confirmImport','saveAccount','saveBalance'])$(key).disabled=true;}
}
function commit(next,{undo=false,accounts:nextAccounts=accounts,reconciliations:nextChecks=reconciliations}={}){
 if(locked)throw Error('Storage is locked. Export your original browser data before recovery.');
 const data=L.normalize({version:4,accounts:nextAccounts,transactions:next,reconciliations:nextChecks});L.ledger(data);
 const current=localStorage.getItem(KEY);if(current!==raw)throw Error('The portfolio changed in another tab. Reload before saving.');
 const text=JSON.stringify(data);
 if(current&&JSON.parse(current).version<4&&!localStorage.getItem(KEY+'_before_v4'))localStorage.setItem(KEY+'_before_v4',current);
 if(!current&&!localStorage.getItem(KEY+'_legacy_before_v4')){const legacy=localStorage.getItem('lw_portfolio');if(legacy)localStorage.setItem(KEY+'_legacy_before_v4',legacy);}
 if(!undo)localStorage.setItem(UNDO,JSON.stringify(snapshot()));
 localStorage.setItem(KEY,text);raw=text;records=data.transactions;accounts=data.accounts;reconciliations=data.reconciliations;
 $('undoChange').disabled=undo;if(undo)localStorage.removeItem(UNDO);
 renderAccountControls();renderHistory();renderSummary();renderPerformance();say('pfStatus','Saved locally. Export a JSON backup to keep a separate copy.');backupStatus();
}
function chart(parts){
 const compact=window.innerWidth<=600,key=JSON.stringify([parts,currency,compact]);lastChart=parts;if(key===chartKey)return;chartKey=key;
 const wrap=$('returnChart');wrap.replaceChildren();if(!parts){say('returnChartNote','A fresh valuation and complete historical FX are needed for the attribution chart.');return;}
 say('returnChartNote','LTC price effect + FX effect − trading fees − network fees at current value = total P&L. This arithmetic breakdown values spent LTC at the current quote.');
 const NS='http://www.w3.org/2000/svg',svg=document.createElementNS(NS,'svg');svg.setAttribute('viewBox',compact?'0 0 360 400':'0 0 720 310');svg.setAttribute('role','img');svg.setAttribute('aria-labelledby','returnChartTitle');
 const title=document.createElementNS(NS,'title');title.id='returnChartTitle';title.textContent='Return components in '+currency+': LTC '+fmt(parts.ltc)+', FX '+fmt(parts.fx)+', fees '+fmt(parts.fees)+', network '+fmt(parts.network)+', total '+fmt(parts.total);svg.append(title);
 const values=[['LTC price',parts.ltc],['Currency effect',parts.fx],['Trading fees',parts.fees],['Network fees',parts.network],['Total P&L',parts.total]],max=Math.max(1,...values.map(v=>Math.abs(v[1])));
 const make=(tag,attrs,text)=>{const el=document.createElementNS(NS,tag);for(const[k,v]of Object.entries(attrs))el.setAttribute(k,v);if(text!==undefined)el.textContent=text;svg.append(el);return el;};
 const center=compact?180:365;
 make('line',{x1:center,y1:compact?40:8,x2:center,y2:compact?392:302,stroke:'currentColor','stroke-opacity':.4});
 values.forEach(([label,value],i)=>{const y=(compact?14:18)+i*(compact?78:58),w=Math.abs(value)/max*(compact?155:155),display=Math.abs(value)>=1e9?value.toExponential(2):fmt(value);
  make('text',{x:8,y:y+(compact?12:22),fill:'currentColor','font-size':compact?13:15},label);
  make('rect',{x:value<0?center-w:center,y:compact?y+25:y,width:Math.max(w,.5),height:compact?23:32,rx:4,fill:value<0?'#d05b5b':'#268c78'});
  make('text',{x:compact?352:540,y:y+(compact?12:22),fill:'currentColor','font-size':compact?12:14,'text-anchor':compact?'end':'start'},(value>0?'+':'')+display+' '+currency);
 });wrap.append(svg);
}
function renderSummary(){
 if(locked)return;const usd=C.quote(market.rows,'USD'),quote=C.quote(market.rows,currency),basic=L.ledger(snapshot());
 say('quoteStatus',quote.usable?'Source '+new Date(quote.time).toISOString().replace('T',' ').replace(/\.\d+Z$/,' UTC')+(quote.delayed?' · older FX input':'')+(market.offline?' · connection issue':''):'Valuation unavailable · '+quote.error);
 say('sumLtc',fmt(basic.amount,8));let result;
 try{result=L.ledger(snapshot(),currency,quote.usable?quote.rate:null);if(quote.usable&&usd.usable){const gross=basic.history.reduce((sum,r)=>sum+(r.type==='sell'?1:r.type==='buy'?-1:0)*r.amount*r.price,0),ltc=((basic.amount+basic.networkLtc)*usd.rate+gross)*quote.rate/usd.rate,fees=-result.fees,network=-basic.networkLtc*quote.rate;result.attribution={ltc,fees,network,fx:currency==='USD'?0:result.total-ltc-fees-network,total:result.total};}say('fxSummaryNote',records.some(r=>r.fxSource==='ECB reference')?'Historical returns include daily ECB reference FX. They may differ from your executed exchange rates.':'Historical returns use your recorded transaction FX. Older USD records remain valid in USD.');}
 catch(e){result={value:quote.usable?basic.amount*quote.rate:null};say('fxSummaryNote',e.message+' Original records are unchanged; USD accounting remains available.');}
 for(const [el,key]of [['sumValue','value'],['sumBasis','basis'],['sumAverage','average'],['sumRealized','realized'],['sumUnrealized','unrealized'],['sumTotal','total'],['sumFees','fees']]){const n=result[key];say(el,Number.isFinite(n)?fmt(n)+' '+currency:'—');$(el).classList.toggle('tool-negative',Number.isFinite(n)&&n<0);}
 chart(result.attribution);say('networkFeeSummary','Network fees: '+fmt(basic.networkLtc,8)+' LTC. Cost basis removed: '+(Number.isFinite(result.networkBasis)?fmt(result.networkBasis)+' '+currency:'historical FX needed')+'. This expense is included in total P&L.');renderAccounts(basic,quote);$('fillLegacyFx').disabled=locked||fxBatchBusy||!records.some(r=>r.type!=='transfer'&&r.currency==='USD'&&Object.keys(r.fx).length===1);
}
function button(label,fn){const b=document.createElement('button');b.type='button';b.textContent=label;b.addEventListener('click',fn);return b;}
function renderHistory(){
 const originals=new Map(records.map(r=>[r.id,r])),rows=L.ledger(snapshot()).history.slice().reverse();page=Math.min(page,Math.max(0,Math.ceil(rows.length/25)-1));const body=$('historyBody');body.replaceChildren();$('portfolioStart').hidden=rows.length>0;
 for(const usd of rows.slice(page*25,page*25+25)){const tx=originals.get(usd.id),gross=tx.amount*tx.price,cash=tx.type==='transfer'?0:tx.type==='buy'?-(gross+tx.fee):gross-tx.fee,tr=document.createElement('tr');
 for(const value of [tx.date,tx.type,accountName(tx.accountId)+(tx.toAccountId?' → '+accountName(tx.toAccountId):''),fmt(tx.amount,8),tx.type==='transfer'?'LTC':tx.currency,tx.type==='transfer'?'—':fmt(tx.price),tx.type==='transfer'?fmt(tx.networkFee,8)+' LTC':fmt(tx.fee),fmt(cash),fmt(usd.realized),tx.type==='transfer'?'—':tx.fxSource+(tx.fxDate?' · '+tx.fxDate:''),tx.notes]){const td=document.createElement('td');td.textContent=value;tr.append(td);}
 const actions=document.createElement('td');actions.append(button('Edit',()=>edit(tx)),button('Delete',()=>{try{commit(records.filter(r=>r.id!==tx.id));}catch(e){say('pfStatus',e.message);}}));tr.append(actions);body.append(tr);}
 if(!rows.length){const tr=document.createElement('tr'),td=document.createElement('td');td.colSpan=12;td.textContent='No transactions yet. Add a purchase to start.';tr.append(td);body.append(tr);}
 say('pageLabel','Page '+(page+1)+' of '+Math.max(1,Math.ceil(rows.length/25))+' · '+rows.length+' transactions');$('prevPage').disabled=page===0;$('nextPage').disabled=(page+1)*25>=rows.length;
}
function currencyLabels(){const c=$('txCurrency').value;say('txPriceLabel','Price per LTC ('+c+')');say('txFeeLabel','Total trading fee ('+c+')');say('valuationPriceLabel','Pre-trade LTC valuation ('+c+')');}
function setFx(rates,date,source){for(const c of ['EUR','GBP','PLN'])$('fx'+c).value=rates[c]??'';$('fxDate').value=date||'';fxSource=source;say('fxStatus',source+(date?' · reference date '+date:''));}
function edit(tx){fxToken++;$('editId').value=tx.id;for(const[input,key]of [['txType','type'],['txDate','date'],['txAmount','amount'],['txPrice','price'],['txFee','fee'],['txNotes','notes'],['txCurrency','currency'],['txAccount','accountId'],['txToAccount','toAccountId'],['txNetworkFee','networkFee'],['txValuationPrice','valuationPrice']])$(input).value=tx[key]??'';transactionMode();setFx(tx.fx,tx.fxDate,tx.fxSource);currencyLabels();say('formTitle','Edit transaction');$('cancelEdit').hidden=false;$('txType').focus();}
function reset(){fxToken++;$('transactionForm').reset();$('editId').value='';$('txDate').value=today;setFx({},'','USD only');renderAccountControls();transactionMode();currencyLabels();$('cancelEdit').hidden=true;say('formTitle','Add transaction');say('formError','');}
$('cancelEdit').addEventListener('click',reset);$('txCurrency').addEventListener('change',currencyLabels);
$('txDate').addEventListener('change',()=>{fxToken++;setFx({},'','USD only');say('fxStatus','Date changed. Load reference FX or enter documented rates for this date.');});
for(const key of ['fxEUR','fxGBP','fxPLN','fxDate'])$(key).addEventListener('input',()=>{fxToken++;fxSource='Manual';if(!$('fxDate').value)$('fxDate').value=$('txDate').value;say('fxStatus','Manual FX · check the rate date.');});
async function getFx(date){const r=await fetch('/api/historical-fx.php?date='+encodeURIComponent(date),{signal:AbortSignal.timeout(16000)}),v=await r.json();if(!r.ok)throw Error(v.error||'Historical FX unavailable.');P.transaction({id:'fx-check',type:'buy',date,amount:1,price:1,fee:0,currency:'USD',fx:v.rates,fxDate:v.date,fxSource:v.source});return v;}
$('loadFx').addEventListener('click',async()=>{const token=++fxToken,date=$('txDate').value;$('loadFx').disabled=true;say('fxStatus','Loading daily ECB reference rates…');try{const v=await getFx(date);if(token===fxToken&&date===$('txDate').value)setFx(v.rates,v.date,v.source);}catch(e){if(token===fxToken)say('fxStatus',e.message);}finally{$('loadFx').disabled=false;}});
$('transactionForm').addEventListener('submit',e=>{e.preventDefault();try{
 const existing=$('editId').value,fx={USD:1};for(const c of ['EUR','GBP','PLN'])if($('fx'+c).value!=='')fx[c]=$('fx'+c).value;
 const entry=L.transaction({accountId:$('txAccount').value,toAccountId:$('txToAccount').value,networkFee:$('txNetworkFee').value||0,valuationPrice:$('txValuationPrice').value||null,id:existing||id(),type:$('txType').value,date:$('txDate').value,amount:$('txAmount').value,price:$('txPrice').value,fee:$('txFee').value,notes:$('txNotes').value,currency:$('txCurrency').value,fx,fxDate:$('fxDate').value||null,fxSource:Object.keys(fx).length===1?'USD only':fxSource});
 commit(existing?records.map(r=>r.id===existing?entry:r):records.concat(entry));reset();
 }catch(err){say('formError',err.message);}});
$('displayCurrency').addEventListener('change',()=>{currency=$('displayCurrency').value;renderSummary();$('performancePrice').value='';say('performancePriceLabel','Ending LTC price ('+currency+')');renderPerformance();});
$('prevPage').addEventListener('click',()=>{page--;renderHistory();});$('nextPage').addEventListener('click',()=>{page++;renderHistory();});
$('undoChange').addEventListener('click',()=>{try{const previous=L.normalize(JSON.parse(localStorage.getItem(UNDO)));commit(previous.transactions,{undo:true,accounts:previous.accounts,reconciliations:previous.reconciliations});reset();}catch(e){say('pfStatus',e.message);}});
function download(text,name,type){const url=URL.createObjectURL(new Blob([text],{type})),a=document.createElement('a');a.href=url;a.download=name;a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);}
$('backupJson').addEventListener('click',()=>{if(locked){if((raw??recoveryRaw)!==null)download(raw??recoveryRaw,'litecoin-portfolio-recovery-'+today+'.json','application/json');else say('pfStatus','No readable portfolio data is available.');return;}markBackup(snapshot());download(IO.backup(snapshot()),'litecoin-portfolio-'+today+'.json','application/json');});
$('exportCsv').addEventListener('click',()=>{if(!locked)download(IO.csv(snapshot()),'litecoin-transactions-'+today+'.csv','text/csv');});
function cancelImport(){pending=null;importToken++;$('importPreview').hidden=true;$('portfolioImport').value='';}
$('cancelImport').addEventListener('click',cancelImport);
function previewImport(incoming){const result=L.merge(snapshot(),incoming),added=result.added;pending={type:'import',data:result.state,base:raw};say('importSummary',added.length+' transactions to add; '+result.duplicates+' existing transactions skipped. Accounts and saved balance checks are included. Review the first 20:');say('importRows',added.slice(0,20).map(r=>r.date+' | '+r.type+' | '+r.amount+' LTC | account '+r.accountId+(r.type==='transfer'?' → '+r.toAccountId+' | network fee '+r.networkFee+' LTC':' | '+r.price+' '+r.currency+' | fee '+r.fee+' | FX '+(r.fxDate||'USD only'))).join('\n'));$('confirmImport').textContent='Add transactions';$('importPreview').hidden=false;}
$('portfolioImport').addEventListener('change',async()=>{const file=$('portfolioImport').files[0],token=++importToken;pending=null;$('importPreview').hidden=true;if(!file)return;try{if(file.size>2000000)throw Error('Maximum import size is 2 MB.');const incoming=IO.parse(await file.text());if(token!==importToken)return;previewImport(incoming);}catch(e){say('pfStatus','Import not applied: '+e.message+' For another CSV format, use the column mapper above.');}});
document.addEventListener('lw-portfolio-import',e=>{try{pending=null;$('importPreview').hidden=true;importToken++;previewImport(e.detail);}catch(error){say('pfStatus','Mapped import not applied: '+error.message);}});
$('fillLegacyFx').addEventListener('click',async()=>{if(locked||fxBatchBusy)return;fxBatchBusy=true;const token=++importToken,base=raw,dates=[...new Set(records.filter(r=>r.type!=='transfer'&&r.currency==='USD'&&Object.keys(r.fx).length===1).map(r=>r.date))].slice(0,30),found=new Map();pending=null;$('importPreview').hidden=true;$('fillLegacyFx').disabled=true;
 try{for(const date of dates){say('pfStatus','Loading reference FX '+(found.size+1)+'/'+dates.length+' · '+date);const v=await getFx(date);if(token!==importToken)return;found.set(date,v);}
 if(raw!==base)throw Error('Portfolio changed during lookup. Try again.');const next=records.map(r=>{const v=found.get(r.date);return v&&r.type!=='transfer'&&r.currency==='USD'&&Object.keys(r.fx).length===1?L.transaction({...r,fx:v.rates,fxDate:v.date,fxSource:v.source}):r;});L.ledger({...snapshot(),transactions:next});pending={type:'fx',rows:next,base};say('importSummary','Review reference FX for '+dates.length+' transaction dates. Original USD prices, amounts and fees stay unchanged. Up to 30 dates are processed per batch.');say('importRows',[...found.entries()].map(([date,v])=>date+' → ECB '+v.date+' | 1 USD = '+fmt(v.rates.EUR,6)+' EUR / '+fmt(v.rates.GBP,6)+' GBP / '+fmt(v.rates.PLN,6)+' PLN').join('\n'));$('confirmImport').textContent='Apply reference FX';$('importPreview').hidden=false;say('pfStatus','Reference FX ready for review. Nothing has been saved yet.');
 }catch(e){say('pfStatus','Reference FX not applied: '+e.message);}finally{fxBatchBusy=false;renderSummary();}});
$('confirmImport').addEventListener('click',()=>{try{if(!pending||locked)return;if(pending.type==='fx'){if(raw!==pending.base)throw Error('Portfolio changed after the preview. Load the rates again.');commit(pending.rows);}else{if(raw!==pending.base)throw Error('Portfolio changed after the preview. Import again.');commit(pending.data.transactions,{accounts:pending.data.accounts,reconciliations:pending.data.reconciliations});}cancelImport();}catch(e){say('pfStatus','Changes not applied: '+e.message);}});
function accountName(key){return accounts.find(a=>a.id===key)?.name||key;}
function renderAccountControls(){
 for(const key of ['txAccount','txToAccount','balanceAccount']){const select=$(key),old=select.value;select.replaceChildren();for(const a of accounts){const option=document.createElement('option');option.value=a.id;option.textContent=a.name;select.append(option);}select.value=accounts.some(a=>a.id===old)?old:accounts[0].id;}
 if($('txToAccount').value===$('txAccount').value)$('txToAccount').value=accounts.find(a=>a.id!==$('txAccount').value)?.id||'';
 balanceExpected();
}
function transactionMode(){const transfer=$('txType').value==='transfer';for(const key of ['tradeCurrency','tradePrice','tradeFee','tradeValuation','tradeFx'])$(key).hidden=transfer;for(const key of ['txCurrency','txPrice','txFee','txValuationPrice','fxEUR','fxGBP','fxPLN','fxDate','loadFx'])$(key).disabled=transfer;$('transferDestination').hidden=!transfer;$('transferFee').hidden=!transfer;$('transferHint').hidden=!transfer;$('txToAccount').disabled=!transfer;$('txNetworkFee').disabled=!transfer;say('txAmountLabel',transfer?'LTC received by destination':'LTC amount');say('txAccountLabel',transfer?'Source account':'Account');}
$('txType').addEventListener('change',transactionMode);$('txAccount').addEventListener('change',()=>{if($('txToAccount').value===$('txAccount').value)$('txToAccount').value=accounts.find(a=>a.id!==$('txAccount').value)?.id||'';});
function renderAccounts(basic,quote){
 const key=raw+'|'+currency;if(key===accountRenderKey){for(const a of basic.accounts){const cell=accountValueCells.get(a.id),text=quote.usable?fmt(a.amount*quote.rate)+' '+currency:'—';if(cell&&cell.textContent!==text)cell.textContent=text;}return;}accountRenderKey=key;accountValueCells.clear();
 const checks=new Map(L.reconciliation(snapshot()).map(c=>[c.accountId,c])),body=$('accountsBody');body.replaceChildren();
 for(const a of basic.accounts){const tr=document.createElement('tr'),c=checks.get(a.id);for(const value of [a.name,fmt(a.amount,8),quote.usable?fmt(a.amount*quote.rate)+' '+currency:'—',c?c.date+' · '+(c.difference===0?'matched':(c.difference>0?'+':'')+fmt(c.difference,8)+' LTC'):'Not checked']){const td=document.createElement('td');td.textContent=value;tr.append(td);if(tr.children.length===3)accountValueCells.set(a.id,td);}const actions=document.createElement('td');actions.append(button('Rename',()=>{$('accountControls').open=true;$('editAccountId').value=a.id;$('accountName').value=a.name;say('accountFormTitle','Rename account');$('cancelAccount').hidden=false;$('accountName').focus();}));const remove=button('Remove',()=>{try{commit(records,{accounts:accounts.filter(x=>x.id!==a.id),reconciliations:reconciliations.filter(x=>x.accountId!==a.id)});}catch(e){say('accountError',e.message);}});remove.disabled=accounts.length===1||records.some(t=>t.accountId===a.id||t.toAccountId===a.id);remove.title=remove.disabled?'Accounts used in transaction history must be kept.':'';actions.append(remove);tr.append(actions);body.append(tr);}
 const checksBody=$('balanceChecks');checksBody.replaceChildren();for(const c of checks.values()){const p=document.createElement('p');p.textContent=c.name+' · '+c.date+': recorded '+fmt(c.expected,8)+' LTC, observed '+fmt(c.amount,8)+' LTC, difference '+(c.difference>0?'+':'')+fmt(c.difference,8)+' LTC. ';p.append(button('Remove check',()=>{try{commit(records,{reconciliations:reconciliations.filter(x=>x.accountId!==c.accountId)});}catch(e){say('accountError',e.message);}}));checksBody.append(p);}balanceExpected();
}
function cancelAccount(){$('editAccountId').value='';$('accountName').value='';$('cancelAccount').hidden=true;say('accountFormTitle','Add an account');}
$('cancelAccount').addEventListener('click',cancelAccount);
$('accountForm').addEventListener('submit',e=>{e.preventDefault();try{const edit=$('editAccountId').value,a=L.account({id:edit||id(),name:$('accountName').value});commit(records,{accounts:edit?accounts.map(x=>x.id===edit?a:x):accounts.concat(a)});cancelAccount();say('accountError','');}catch(err){say('accountError',err.message);}});
$('balanceDate').value=today;$('balanceDate').max=today;
function balanceExpected(){if(locked)return;try{const a=L.ledger(snapshot(),'USD',null,$('balanceDate').value).accounts.find(a=>a.id===$('balanceAccount').value);say('balanceExpected',a?'Recorded balance at the end of this UTC date: '+fmt(a.amount,8)+' LTC.':'');}catch(e){say('balanceExpected',e.message);}}
$('balanceDate').addEventListener('change',balanceExpected);$('balanceAccount').addEventListener('change',balanceExpected);
$('balanceForm').addEventListener('submit',e=>{e.preventDefault();try{const check={accountId:$('balanceAccount').value,date:$('balanceDate').value,amount:$('balanceAmount').value};commit(records,{reconciliations:reconciliations.filter(c=>c.accountId!==check.accountId).concat(check)});say('accountError','');}catch(err){say('accountError',err.message);}});
$('performanceDate').value=today;$('performanceDate').max=today;
function renderPerformance(){
 performanceResult=null;$('exportPerformance').disabled=true;for(const key of ['performanceProfit','performanceXirr','performanceTwr'])say(key,'—');$('performanceMissing').replaceChildren();say('xirrStatus','');say('twrStatus','');
 if(locked)return;
 if(!$('performancePrice').value){say('performanceStatus','Enter an ending LTC valuation or use the current quote. Historical results need a price for the selected end date.');return;}
 try{const r=R.calculate(snapshot(),{currency,end:$('performanceDate').value,price:$('performancePrice').value});if(r.error)throw Error(r.error);performanceResult=r;
 say('performanceStatus','LTC position only · '+r.start+' to '+r.end+' (UTC). Buys are contributions and net sale proceeds are withdrawals. Transfers between your accounts are internal.');say('performanceProfit',fmt(r.profit)+' '+currency);
 say('performanceXirr',r.xirr.error?'Unavailable':fmt(r.xirr.rate*100)+'%');say('xirrStatus',r.xirr.error||'Annualized, actual days / 365. Observed cash-flow span: '+r.xirr.days+' days. Annualization can amplify short-period results.');
 say('performanceTwr',r.twr.error?'Incomplete':fmt(r.twr.rate*100)+'%');say('twrStatus',r.twr.error||'Cumulative return over this period, including recorded trading and network costs.');
 for(const missing of (r.twr.missing||[])){const li=document.createElement('li');li.append(button(missing.date+' · '+missing.type+' · add valuation',()=>edit(records.find(t=>t.id===missing.id))));$('performanceMissing').append(li);}$('exportPerformance').disabled=false;
 }catch(e){say('performanceStatus',e.message);}
}
$('performanceForm').addEventListener('submit',e=>{e.preventDefault();renderPerformance();});
for(const key of ['performanceDate','performancePrice'])$(key).addEventListener('input',()=>{say('performanceSource','Manual valuation: check the price, currency and date.');renderPerformance();});
$('useCurrentValuation').addEventListener('click',()=>{const q=C.quote(market.rows,currency);if(!q.usable){say('performanceStatus',q.error);return;}$('performanceDate').value=today;$('performancePrice').value=q.rate;say('performanceSource','Market quote snapshot: '+new Date(q.time).toISOString()+'. The saved input will not follow subsequent price changes.');renderPerformance();});
$('exportPerformance').addEventListener('click',()=>{if(!performanceResult)return;const r=performanceResult,rows=[['Date','Investor cash flow '+r.currency,'Entry'],...r.flows.map(f=>[f.date,f.value,f.id==='terminal-valuation'?'Terminal valuation (not a transaction)':'Transaction '+f.id])];download(rows.map(a=>a.map(IO.cell).join(',')).join('\r\n'),'litecoin-performance-'+r.end+'.csv','text/csv');});

window.addEventListener('storage',e=>{if(e.key===KEY||e.key===null)say('pfStatus','Portfolio changed in another tab. Reload to review it before making changes.');});
window.addEventListener('resize',()=>chart(lastChart));
read();if(!locked){renderAccountControls();transactionMode();renderHistory();renderSummary();renderPerformance();}LwMarket.subscribe(s=>{market=s;renderSummary();});
async function digest(state){const bytes=await crypto.subtle.digest('SHA-256',new TextEncoder().encode(JSON.stringify(L.normalize(state))));return Array.from(new Uint8Array(bytes),v=>v.toString(16).padStart(2,'0')).join('');}
async function markBackup(state){try{localStorage.setItem(KEY+'_backup_meta',JSON.stringify({at:Date.now(),digest:await digest(state)}));backupStatus();}catch{say('portfolioBackupStatus','Backup download requested. Its age could not be saved in browser storage.');}}
async function backupStatus(){const t=++backupToken;try{const meta=JSON.parse(localStorage.getItem(KEY+'_backup_meta')||'null');if(!meta){say('portfolioBackupStatus','No complete backup download recorded in this browser. Start with an account, enter or import trades, check its balance, then keep a JSON backup.');return;}const hash=await digest(snapshot());if(t!==backupToken)return;say('portfolioBackupStatus','Last complete backup download requested: '+new Date(meta.at).toISOString()+' · '+Math.max(0,Math.floor((Date.now()-meta.at)/86400000))+' days ago. '+(hash===meta.digest?'Current records match that export.':'Records have changed since that export. Create a new backup.')+' Check that the downloaded file was actually saved.');}catch{say('portfolioBackupStatus','Backup history unavailable. Keep a separate JSON copy.');}}
$('backupEncrypted').addEventListener('click',async()=>{if(locked){say('encryptedStatus','Saved data are locked; use the recovery JSON export first.');return;}const state=snapshot(),password=$('portfolioPassword').value;$('portfolioPassword').value='';$('backupEncrypted').disabled=true;try{const encrypted=await LwPortfolioCrypto.encrypt(IO.backup(state),password);download(encrypted,'litecoin-portfolio-encrypted-'+today+'.json','application/json');await markBackup(state);say('encryptedStatus','Encrypted file download requested. Check the file was saved and keep your password separately.');}catch(e){say('encryptedStatus',e.message);}finally{$('backupEncrypted').disabled=false;}});
$('encryptedImport').addEventListener('change',async()=>{const file=$('encryptedImport').files[0],token=++importToken,password=$('portfolioPassword').value;$('portfolioPassword').value='';pending=null;$('importPreview').hidden=true;if(!file)return;try{if(file.size>3000000)throw Error('Maximum encrypted file is 3 MB.');const text=await LwPortfolioCrypto.decrypt(await file.text(),password);if(token!==importToken)return;previewImport(IO.parse(text));say('encryptedStatus','Decrypted locally. Review the import before adding records.');}catch(e){say('encryptedStatus',e.message);}finally{$('encryptedImport').value='';}});

$('portfolioExample').addEventListener('click',()=>{const input=[{id:'example-1',date:'2025-01-01',type:'buy',amount:1,price:50,fee:1},{id:'example-2',date:'2025-02-01',type:'buy',amount:.5,price:60,fee:1},{id:'example-3',date:'2025-03-01',type:'sell',amount:.25,price:75,fee:.25}],r=LwTools.ledger(input,75),body=$('portfolioExampleRows');body.replaceChildren();for(const v of r.history){const tr=document.createElement('tr');for(const value of [v.date,v.type,v.amount,v.price,v.fee,v.balance]){const td=document.createElement('td');td.textContent=String(value);tr.append(td);}body.append(tr);}$('portfolioExampleSummary').textContent='Remaining '+r.amount.toFixed(8)+' LTC · cost basis $'+r.basis.toFixed(2)+' · ending value $'+r.value.toFixed(2)+' · realized $'+r.realized.toFixed(2)+' · unrealized $'+r.unrealized.toFixed(2)+'. This uses weighted average acquisition cost, not a tax convention.';$('portfolioExampleResult').hidden=false;});
backupStatus();

})();
