/* Local-only LTC accounts. Quantities use integer litoshis; cost is pooled globally. */
(function(root,factory){const api=factory(typeof module==='object'&&module.exports?require('./tools-core.js'):root.LwTools,typeof module==='object'&&module.exports?require('./portfolio-fx.js'):root.LwPortfolioFx);if(typeof module==='object'&&module.exports)module.exports=api;else root.LwPortfolioLedger=api;})(typeof globalThis!=='undefined'?globalThis:this,(T,P)=>{
'use strict';
const SCALE=1e8,MAX=84000000,DEFAULT={id:'main',name:'Main account'};
function units(value,label='LTC amount',zero=false){const n=T.number(value,label,MAX),u=Math.round(n*SCALE);if((zero?u<0:u<1)||!Number.isSafeInteger(u)||Math.abs(n-u/SCALE)>Number.EPSILON*Math.max(1,n)*2)throw Error(label+' must have at most 8 decimals'+(zero?'.':' and be positive.'));return u;}
function account(row){if(!row||typeof row.id!=='string'||!/^[a-zA-Z0-9_-]{1,100}$/.test(row.id)||typeof row.name!=='string'||!row.name.trim()||row.name.trim().length>80)throw Error('An account needs a name of 1–80 characters and a valid identifier.');return {id:row.id,name:row.name.trim()};}
function transaction(row,index=0){
 if(!row||typeof row!=='object'||Array.isArray(row))throw Error('Invalid transaction.');
 const accountId=row.accountId??'main';if(typeof accountId!=='string')throw Error('Choose an account.');let tx;
 if(row.type==='transfer'){
  const base=T.transaction({...row,type:'buy',price:0,fee:0},index);
  if(typeof row.toAccountId!=='string'||row.toAccountId===accountId)throw Error('A transfer needs two different accounts.');
  tx={...base,type:'transfer',accountId,toAccountId:row.toAccountId,networkFee:units(row.networkFee??0,'Network fee',true)/SCALE,currency:'USD',fx:{USD:1},fxDate:null,fxSource:'USD only',valuationPrice:null};
 }else{
  tx={...P.transaction(row,index),accountId,toAccountId:null,networkFee:0,valuationPrice:null};
  if(row.valuationPrice!==undefined&&row.valuationPrice!==null&&row.valuationPrice!==''){tx.valuationPrice=T.number(row.valuationPrice,'pre-trade valuation price');if(tx.valuationPrice<=0)throw Error('A pre-trade valuation must be positive.');}
 }
 if(!tx.id||tx.id.length>150)throw Error('Invalid transaction identifier.');return tx;
}
function normalize(value){
 if(!value||![2,3,4].includes(value.version)||!Array.isArray(value.transactions))throw Error('Unsupported portfolio version.');
 const accounts=(value.version===4?value.accounts:[DEFAULT]);if(!Array.isArray(accounts)||!accounts.length||accounts.length>100)throw Error('Keep between 1 and 100 accounts.');
 const normalized=accounts.map(account),ids=new Set(normalized.map(a=>a.id));if(ids.size!==normalized.length)throw Error('Duplicate account identifiers.');
 if(new Set(normalized.map(a=>a.name.toLowerCase())).size!==normalized.length)throw Error('Use a different name for each account.');
 if(value.transactions.length>5000)throw Error('Use a maximum of 5,000 transactions.');
 const transactions=value.transactions.map((r,i)=>transaction(value.version<4?{...r,accountId:'main'}:r,i));
 if(new Set(transactions.map(t=>t.id)).size!==transactions.length)throw Error('Duplicate transaction identifiers.');
 for(const t of transactions)if(!ids.has(t.accountId)||(t.type==='transfer'&&!ids.has(t.toAccountId)))throw Error('A transaction refers to an unknown account.');
 const checks=value.version===4?(value.reconciliations??[]):[];if(!Array.isArray(checks)||checks.length>100)throw Error('Invalid balance checks.');
 const reconciliations=checks.map(c=>{if(!c||!ids.has(c.accountId))throw Error('A balance check refers to an unknown account.');if(T.date(c.date)>new Date().toISOString().slice(0,10))throw Error('Balance checks cannot be in the future.');return {accountId:c.accountId,date:c.date,amount:units(c.amount,'Observed LTC balance',true)/SCALE};});
 if(new Set(reconciliations.map(c=>c.accountId)).size!==reconciliations.length)throw Error('Keep one latest balance check per account.');
 return {version:4,accounts:normalized,transactions,reconciliations};
}
function ordered(rows){return rows.map((r,i)=>({...r,order:i})).sort((a,b)=>a.date.localeCompare(b.date)||a.order-b.order);}
function convert(t,target){if(t.type==='transfer')return t;const rate=t.fx[target]/t.fx[t.currency];if(!Number.isFinite(rate)||rate<=0)throw Error('Historical '+target+' FX is missing for '+t.date+'.');return {...t,price:t.price*rate,fee:t.fee*rate,valuationPrice:t.valuationPrice===null?null:t.valuationPrice*rate};}
function ledger(input,target='USD',currentPrice=null,end=null){
 const state=normalize(input);if(!P.currencies.includes(target))throw Error('Invalid reporting currency.');if(end)T.date(end);
 const balances=new Map(state.accounts.map(a=>[a.id,0]));let qty=0,basis=0,realized=0,fees=0,networkUnits=0,networkBasis=0,purchases=0,proceeds=0;const history=[];
 for(const original of ordered(state.transactions)){
  if(end&&original.date>end)continue;const row=convert(original,target),u=units(row.amount),before=qty;let cash=0,released=0,gain=null,expense=0;
  if(row.type==='buy'){const cost=row.amount*row.price+row.fee;qty+=u;balances.set(row.accountId,balances.get(row.accountId)+u);basis+=cost;purchases+=cost;fees+=row.fee;cash=-cost;}
  else if(row.type==='sell'){
   if(u>balances.get(row.accountId))throw Error('Sale on '+row.date+' exceeds the LTC available in its account.');
   const gross=row.amount*row.price;if(row.fee>gross)throw Error('A sale fee cannot exceed its proceeds.');
   released=basis*u/qty;cash=gross-row.fee;gain=cash-released;realized+=gain;proceeds+=cash;fees+=row.fee;basis-=released;qty-=u;balances.set(row.accountId,balances.get(row.accountId)-u);
  }else{
   const feeUnits=units(row.networkFee,'Network fee',true),debit=u+feeUnits;
   if(!Number.isSafeInteger(debit)||debit>balances.get(row.accountId))throw Error('Transfer on '+row.date+' exceeds the source balance including the network fee.');
   expense=qty?basis*feeUnits/qty:0;networkBasis+=expense;networkUnits+=feeUnits;basis-=expense;qty-=feeUnits;balances.set(row.accountId,balances.get(row.accountId)-debit);balances.set(row.toAccountId,balances.get(row.toAccountId)+u);
  }
  if(!Number.isSafeInteger(qty)||qty<0||qty>MAX*SCALE)throw Error('LTC balance is outside the supported range.');if(!qty)basis=0;
  history.push({...row,beforeAmount:before/SCALE,balance:qty/SCALE,netCash:cash,realized:gain,costBasis:released,networkExpense:expense,accountBalance:balances.get(row.accountId)/SCALE});
 }
 const amount=qty/SCALE,known=Number.isFinite(currentPrice)&&currentPrice>0,value=qty===0?0:known?amount*currentPrice:null,unrealized=value===null?null:value-basis,total=value===null?null:realized+unrealized-networkBasis;
 return {amount,basis,average:qty?basis/amount:null,realized,unrealized,value,total,fees,networkLtc:networkUnits/SCALE,networkBasis,purchases,proceeds,history,accounts:state.accounts.map(a=>({...a,amount:balances.get(a.id)/SCALE,value:known?balances.get(a.id)/SCALE*currentPrice:null}))};
}
function fingerprint(t){return JSON.stringify([t.type,t.date,t.amount,t.accountId,t.toAccountId,t.networkFee,t.currency,t.price,t.fee,t.notes]);}
function merge(existing,incoming){
 const a=normalize(existing),b=normalize(incoming),accounts=a.accounts.slice();
 for(const incomingAccount of b.accounts){const found=accounts.find(x=>x.id===incomingAccount.id);if(found&&found.name!==incomingAccount.name&&a.transactions.length===0&&a.accounts.length===1&&found.id==='main'&&found.name===DEFAULT.name)accounts[accounts.indexOf(found)]=incomingAccount;else if(!found)accounts.push(incomingAccount);}
 const count=new Map(),byId=new Map(a.transactions.map(t=>[t.id,t]));for(const t of a.transactions)count.set(fingerprint(t),(count.get(fingerprint(t))||0)+1);
 const added=[];let duplicates=0;
 for(const t of b.transactions){const key=fingerprint(t),same=byId.get(t.id);if(same&&fingerprint(same)!==key)throw Error('An imported transaction identifier conflicts with an existing record.');const n=count.get(key)||0;if(n){count.set(key,n-1);duplicates++;}else{if(same)throw Error('Duplicate imported transaction identifier.');added.push(t);}}
 const checks=a.reconciliations.slice();for(const c of b.reconciliations)if(!checks.some(x=>x.accountId===c.accountId))checks.push(c);
 const state=normalize({...a,accounts,transactions:a.transactions.concat(added),reconciliations:checks});ledger(state);return {state,added,duplicates};
}
function reconciliation(input){const state=normalize(input);return state.reconciliations.map(c=>{const r=ledger(state,'USD',null,c.date).accounts.find(a=>a.id===c.accountId),difference=(units(c.amount,'Balance',true)-units(r.amount,'Balance',true))/SCALE;return {...c,name:r.name,expected:r.amount,difference};});}
function taxRows(input){const state=normalize(input);ledger(state);if(state.transactions.some(t=>t.type==='transfer'&&t.networkFee>0))throw Error('This portfolio contains network-fee disposals. The FIFO tool cannot classify their tax treatment; import and review a separately prepared trade list instead. Nothing was imported.');return P.usdRows(state.transactions.filter(t=>t.type!=='transfer'));}
return {SCALE,DEFAULT,units,account,transaction,normalize,ordered,convert,ledger,merge,reconciliation,taxRows};
});
