/* CA Automator — Accounting Automation engines (JS mirror of
   server/acct_engines.py). Kept numerically identical to the Python engine —
   every change here must be made there too, and vice versa. */
(function (global) {
  'use strict';

  const R2 = x => Math.round((Number(x) || 0) * 100) / 100;
  const N = (v, dflt) => (v === null || v === undefined || v === '') ? (dflt || 0) : (isFinite(Number(v)) ? Number(v) : (dflt || 0));
  const safeDiv = (a, b) => b ? (a / b) : 0;

  function ymd(s) {
    if (!s) return null;
    const parts = String(s).split('-');
    if (parts.length !== 3) return null;
    const y = parseInt(parts[0], 10), m = parseInt(parts[1], 10), d = parseInt(parts[2], 10);
    if (!isFinite(y) || !isFinite(m) || !isFinite(d)) return null;
    return [y, m, d];
  }

  function jdn(y, m, q) {
    const a = Math.floor((14 - m) / 12);
    const yy = y + 4800 - a;
    const mm = m + 12 * a - 3;
    return q + Math.floor((153 * mm + 2) / 5) + 365 * yy + Math.floor(yy / 4) - Math.floor(yy / 100) + Math.floor(yy / 400) - 32045;
  }

  function daysBetween(d1, d2) {
    const p1 = ymd(d1), p2 = ymd(d2);
    if (!p1 || !p2) return 0;
    return jdn(p2[0], p2[1], p2[2]) - jdn(p1[0], p1[1], p1[2]);
  }

  function monthIndex(y, m) { return y * 12 + (m - 1); }

  const SUSPENSE_RX = /suspense|clearing account|unidentified|un-?reconciled|difference account/i;

  function tbDiagnostics(rows, headNature, materiality, movementThresholdPct) {
    headNature = headNature || {};
    materiality = N(materiality);
    const threshold = N(movementThresholdPct, 20);
    const outRows = [];
    const suspense = [], unusualBalance = [], unclassified = [], largeMovements = [];
    let totalDr = 0, totalCr = 0;

    (rows || []).forEach(r => {
      const name = r.name || '', group = r.group || '';
      const dr = N(r.dr), cr = N(r.cr);
      const net = R2(dr - cr);
      const head = r.head || '';
      totalDr += dr; totalCr += cr;
      const entry = { name, group, head, dr: R2(dr), cr: R2(cr), net };

      if (SUSPENSE_RX.test(name + ' ' + group)) {
        suspense.push(Object.assign({}, entry, { reason: 'Account name suggests a suspense/clearing balance' }));
      }
      if (!head && Math.abs(net) > materiality) unclassified.push(entry);

      if (head && Object.prototype.hasOwnProperty.call(headNature, head)) {
        const expectedCredit = headNature[head] === 'credit';
        const actualCredit = net < 0;
        if (net !== 0 && expectedCredit !== actualCredit && Math.abs(net) > materiality) {
          unusualBalance.push(Object.assign({}, entry, {
            expected_side: expectedCredit ? 'credit' : 'debit', actual_side: actualCredit ? 'credit' : 'debit' }));
        }
      }
      const prev = r.prev;
      if (prev !== null && prev !== undefined && prev !== '') {
        const prevV = N(prev);
        const variance = R2(net - prevV);
        const variancePct = prevV ? R2(safeDiv(variance, Math.abs(prevV)) * 100) : null;
        if (variancePct !== null && Math.abs(variancePct) > threshold && Math.abs(variance) > materiality) {
          largeMovements.push(Object.assign({}, entry, { prev: R2(prevV), variance, variance_pct: variancePct }));
        }
      }
      outRows.push(entry);
    });

    totalDr = R2(totalDr); totalCr = R2(totalCr);
    return {
      rows: outRows, total_dr: totalDr, total_cr: totalCr,
      tie_out_diff: R2(totalDr - totalCr), balanced: Math.abs(totalDr - totalCr) < 0.01,
      suspense_candidates: suspense, unusual_balances: unusualBalance,
      unclassified, large_movements: largeMovements,
    };
  }

  // Citations genuinely differ per country (Nepal: NFRS/NAS, a verbatim
  // local adoption of IFRS/IAS under local numbering; India: Ind AS, its
  // own IFRS-converged standards, same numbering convention; UAE: IFRS/IAS
  // directly) — mirrors server/acct_engines.py's _STANDARDS exactly.
  const STANDARDS = {
    NP: { gaap: 'Nepal Financial Reporting Standards (NFRS) / NFRS for SMEs as applicable',
          leases: 'NFRS 16', borrowing: 'NAS 23', deferredTax: 'NAS 12', fx: 'NAS 21',
          taxAct: 'the Income Tax Act, 2058' },
    IN: { gaap: 'the Indian Accounting Standards (Ind AS) notified under the Companies (Indian Accounting Standards) Rules, 2015, or the Accounting Standards (AS) where Ind AS does not apply',
          leases: 'Ind AS 116', borrowing: 'Ind AS 23', deferredTax: 'Ind AS 12', fx: 'Ind AS 21',
          taxAct: 'the Income-tax Act, 1961' },
    AE: { gaap: 'International Financial Reporting Standards (IFRS) / IFRS for SMEs as applicable',
          leases: 'IFRS 16', borrowing: 'IAS 23', deferredTax: 'IAS 12', fx: 'IAS 21',
          taxAct: 'Federal Decree-Law No. 47 of 2022 on the Taxation of Corporations and Businesses' },
  };

  function policyLibrary(countryCode) {
    const s = STANDARDS[countryCode] || STANDARDS.NP;
    return [
      { key: 'basis', title: 'Basis of preparation', heads: null,
        text: `These financial statements have been prepared on a going concern basis under the historical cost convention, in accordance with ${s.gaap}.` },
      { key: 'estimates', title: 'Use of estimates', heads: null,
        text: 'The preparation of financial statements requires management to make estimates and assumptions that affect the reported amounts of assets, liabilities, income and expenses. Actual results may differ from these estimates.' },
      { key: 'cash', title: 'Cash and cash equivalents', heads: ['cash'],
        text: 'Cash and cash equivalents comprise cash in hand, balances with banks and short-term deposits with an original maturity of three months or less.' },
      { key: 'revenue', title: 'Revenue recognition', heads: ['revenue', 'other_income'],
        text: 'Revenue is recognised to the extent it is probable that economic benefits will flow to the entity and the amount can be reliably measured, net of trade discounts and value added tax.' },
      { key: 'ppe', title: 'Property, plant and equipment', heads: ['ppe', 'acc_dep', 'cwip'],
        text: 'Property, plant and equipment are stated at cost less accumulated depreciation and impairment, if any. Depreciation is provided on a systematic basis over the estimated useful life of each class of asset.' },
      { key: 'intangible', title: 'Intangible assets', heads: ['intangible'],
        text: 'Intangible assets are stated at cost less accumulated amortisation and impairment, if any, and are amortised over their estimated useful economic life.' },
      { key: 'inventory', title: 'Inventories', heads: ['inventories', 'opening_stock', 'closing_stock', 'purchases'],
        text: 'Inventories are valued at the lower of cost and net realisable value. Cost is determined on a first-in-first-out / weighted average basis as applicable to each class of inventory.' },
      { key: 'receivables', title: 'Trade receivables and impairment', heads: ['trade_rec'],
        text: "Trade receivables are stated at their transaction value less an allowance for expected credit losses, assessed based on the age of the receivable and specific knowledge of the counterparty's ability to pay." },
      { key: 'borrowing', title: 'Borrowings and finance costs', heads: ['lt_borrow', 'st_borrow', 'finance'],
        text: `Borrowings are initially recognised at the amount of proceeds received, net of transaction costs, and subsequently measured at amortised cost. Finance costs are recognised in profit or loss in the period incurred, except to the extent capitalised against a qualifying asset under ${s.borrowing}.` },
      { key: 'leases', title: 'Leases', heads: ['lt_prov', 'other_ncl'],
        text: `At inception of a contract, the entity assesses whether the contract is, or contains, a lease under ${s.leases}. Right-of-use assets and lease liabilities are recognised for leases other than short-term and low-value leases, which are expensed on a straight-line basis over the lease term.` },
      { key: 'provisions', title: 'Provisions and contingent liabilities', heads: ['lt_prov', 'st_prov'],
        text: 'Provisions are recognised when the entity has a present obligation as a result of a past event, it is probable that an outflow of resources will be required to settle the obligation, and a reliable estimate can be made of the amount. Contingent liabilities are disclosed but not recognised.' },
      { key: 'tax', title: 'Taxation', heads: ['current_tax', 'deferred_tax', 'dtl', 'dta', 'tax_prov'],
        text: `Income tax expense comprises current and deferred tax. Current tax is the expected tax payable on taxable income for the year, computed under ${s.taxAct}. Deferred tax is recognised on temporary differences between the carrying amounts of assets/liabilities and their tax base, under ${s.deferredTax}.` },
      { key: 'fx', title: 'Foreign currency transactions', heads: ['nc_inv'],
        text: `Transactions in foreign currencies are recorded at the exchange rate prevailing on the transaction date. Monetary assets and liabilities denominated in foreign currency are translated at the closing rate at the reporting date, with exchange differences recognised in profit or loss, under ${s.fx}.` },
      { key: 'equity', title: 'Share capital and reserves', heads: ['share_capital', 'reserves'],
        text: 'Ordinary shares are classified as equity. Reserves represent amounts set aside from profit or arising on specific transactions, as disclosed in the Statement of Changes in Equity.' },
    ];
  }

  function suggestPolicies(headsPresent, countryCode) {
    const present = new Set(headsPresent || []);
    return policyLibrary(countryCode || 'NP').filter(p => p.heads === null || p.heads.some(h => present.has(h)))
      .map(p => ({ key: p.key, title: p.title, text: p.text }));
  }

  const CF_SECTIONS = ['operating_pat_start', 'operating_adjustment', 'working_capital', 'operating_other', 'investing', 'financing'];

  function cashFlowStatement(lines, openingCash, closingCashActual) {
    const bySection = {}; const totals = {};
    CF_SECTIONS.forEach(s => { bySection[s] = []; totals[s] = 0; });
    (lines || []).forEach(ln => {
      const section = CF_SECTIONS.indexOf(ln.section) !== -1 ? ln.section : 'operating_adjustment';
      const amount = N(ln.amount);
      bySection[section].push({ label: ln.label || '', amount: R2(amount), note: ln.note || '' });
      totals[section] += amount;
    });
    const netOperating = totals.operating_pat_start + totals.operating_adjustment + totals.working_capital + totals.operating_other;
    const netInvesting = totals.investing;
    const netFinancing = totals.financing;
    const netChange = netOperating + netInvesting + netFinancing;
    const opening = N(openingCash);
    const computedClosing = R2(opening + netChange);
    const roundedTotals = {};
    CF_SECTIONS.forEach(s => { roundedTotals[s] = R2(totals[s]); });
    const result = {
      by_section: bySection, totals: roundedTotals,
      net_operating: R2(netOperating), net_investing: R2(netInvesting), net_financing: R2(netFinancing),
      net_change: R2(netChange), opening_cash: R2(opening), computed_closing_cash: computedClosing,
    };
    if (closingCashActual !== null && closingCashActual !== undefined && closingCashActual !== '') {
      const actual = R2(N(closingCashActual));
      result.closing_cash_actual = actual;
      result.reconciliation_diff = R2(computedClosing - actual);
      result.ties_out = Math.abs(computedClosing - actual) < 0.01;
    }
    return result;
  }

  function equityMovement(components) {
    const out = [];
    const totals = { opening_balance: 0, profit_for_year: 0, oci: 0, dividend: 0, share_issued: 0,
      share_buyback: 0, transfer_in: 0, transfer_out: 0, other_movement: 0, closing_balance: 0 };
    (components || []).forEach(c => {
      const opening = N(c.opening_balance), profit = N(c.profit_for_year), oci = N(c.oci), dividend = N(c.dividend);
      const issued = N(c.share_issued), buyback = N(c.share_buyback), tin = N(c.transfer_in), tout = N(c.transfer_out);
      const other = N(c.other_movement);
      const closing = opening + profit + oci - dividend + issued - buyback + tin - tout + other;
      const row = { name: c.name || '', opening_balance: R2(opening), profit_for_year: R2(profit), oci: R2(oci),
        dividend: R2(dividend), share_issued: R2(issued), share_buyback: R2(buyback), transfer_in: R2(tin),
        transfer_out: R2(tout), other_movement: R2(other), closing_balance: R2(closing) };
      out.push(row);
      Object.keys(totals).forEach(k => { totals[k] += row[k]; });
    });
    const roundedTotals = {};
    Object.keys(totals).forEach(k => { roundedTotals[k] = R2(totals[k]); });
    return { components: out, totals: roundedTotals };
  }

  function provisionMovement(items) {
    const out = [];
    let totalOpening = 0, totalCharge = 0, totalClosing = 0;
    (items || []).forEach(it => {
      const opening = N(it.opening_balance), charge = N(it.charge_for_year), utilized = N(it.utilized), reversed = N(it.reversed);
      const closing = opening + charge - utilized - reversed;
      out.push({ item: it.item || '', category: it.category || 'provision', opening_balance: R2(opening),
        charge_for_year: R2(charge), utilized: R2(utilized), reversed: R2(reversed), closing_balance: R2(closing) });
      totalOpening += opening; totalCharge += charge; totalClosing += closing;
    });
    return { rows: out, total_opening: R2(totalOpening), total_charge: R2(totalCharge), total_closing: R2(totalClosing) };
  }

  function prepaymentAmortization(items, asOf) {
    const asOfYmd = ymd(asOf);
    const out = [];
    (items || []).forEach(it => {
      const total = N(it.total_amount);
      const s = ymd(it.start_date), e = ymd(it.end_date);
      if (!s || !e || !asOfYmd) {
        out.push({ item: it.item || '', total_amount: R2(total), total_months: 0, monthly_amount: 0,
          elapsed_months: 0, amortized_to_date: 0, balance_carried_forward: R2(total) });
        return;
      }
      let totalMonths = monthIndex(e[0], e[1]) - monthIndex(s[0], s[1]) + 1;
      totalMonths = Math.max(1, totalMonths);
      const monthly = R2(safeDiv(total, totalMonths));
      const cutoff = monthIndex(asOfYmd[0], asOfYmd[1]) < monthIndex(e[0], e[1]) ? asOfYmd : [e[0], e[1], e[2]];
      let elapsed = monthIndex(cutoff[0], cutoff[1]) - monthIndex(s[0], s[1]) + 1;
      elapsed = Math.max(0, Math.min(elapsed, totalMonths));
      const amortized = R2(Math.min(monthly * elapsed, total));
      out.push({ item: it.item || '', total_amount: R2(total), total_months: totalMonths, monthly_amount: monthly,
        elapsed_months: elapsed, amortized_to_date: amortized, balance_carried_forward: R2(total - amortized) });
    });
    const totalAmount = R2(out.reduce((s, r) => s + r.total_amount, 0));
    const totalAmortized = R2(out.reduce((s, r) => s + r.amortized_to_date, 0));
    return { rows: out, total_amount: totalAmount, total_amortized: totalAmortized,
      total_balance_carried_forward: R2(totalAmount - totalAmortized) };
  }

  function fxRevaluation(items) {
    const out = [];
    let totalPl = 0;
    (items || []).forEach(it => {
      const fa = N(it.foreign_amount), bookedRate = N(it.booked_rate), closingRate = N(it.closing_rate);
      const classification = it.classification || 'asset';
      const bookedNpr = R2(fa * bookedRate), revaluedNpr = R2(fa * closingRate);
      const rawMovement = R2(revaluedNpr - bookedNpr);
      const plImpact = classification === 'asset' ? rawMovement : R2(-rawMovement);
      out.push({ item: it.item || '', currency: it.currency || '', foreign_amount: R2(fa), booked_rate: R2(bookedRate),
        closing_rate: R2(closingRate), classification, booked_npr: bookedNpr, revalued_npr: revaluedNpr, gain_loss: plImpact });
      totalPl += plImpact;
    });
    return { rows: out, total_gain_loss: R2(totalPl) };
  }

  function interestAccrual(items) {
    const out = [];
    let total = 0;
    (items || []).forEach(it => {
      const principal = N(it.principal), rate = N(it.rate_pct);
      const days = Math.max(0, daysBetween(it.from_date, it.to_date));
      const basis = N(it.day_count_basis, 365) || 365;
      const compounding = it.compounding || 'simple';
      let accrued;
      if (compounding === 'annual') {
        const years = days / basis;
        accrued = principal ? principal * (Math.pow(1 + rate / 100, years) - 1) : 0;
      } else {
        accrued = principal * (rate / 100) * (days / basis);
      }
      accrued = R2(accrued);
      out.push({ item: it.item || '', principal: R2(principal), rate_pct: rate, days, day_count_basis: basis,
        compounding, accrued_interest: accrued });
      total += accrued;
    });
    return { rows: out, total_accrued_interest: R2(total) };
  }

  function borrowingCostCapitalization(specificBorrowings, generalBorrowings, capitalizableExpenditureGeneral, periodDays, incomeFromTempInvestment) {
    periodDays = N(periodDays, 365) || 365;
    let specificCost = 0;
    const specificRows = (specificBorrowings || []).map(b => {
      const principal = N(b.principal), rate = N(b.rate_pct);
      const cost = R2(principal * (rate / 100) * (periodDays / 365));
      specificCost += cost;
      return { principal: R2(principal), rate_pct: rate, cost };
    });
    specificCost = R2(specificCost - N(incomeFromTempInvestment));

    const genPrincipalTotal = (generalBorrowings || []).reduce((s, b) => s + N(b.principal), 0);
    let genCostTotal = 0;
    const generalRows = (generalBorrowings || []).map(b => {
      const principal = N(b.principal), rate = N(b.rate_pct);
      const cost = R2(principal * (rate / 100) * (periodDays / 365));
      genCostTotal += cost;
      return { principal: R2(principal), rate_pct: rate, cost };
    });
    const weightedAvgRate = R2(safeDiv((generalBorrowings || []).reduce((s, b) => s + N(b.principal) * N(b.rate_pct), 0), genPrincipalTotal));
    genCostTotal = R2(genCostTotal);

    capitalizableExpenditureGeneral = N(capitalizableExpenditureGeneral);
    const generalCapitalizedUncapped = R2(capitalizableExpenditureGeneral * (weightedAvgRate / 100) * (periodDays / 365));
    const generalCapitalized = (generalBorrowings && generalBorrowings.length) ? R2(Math.min(generalCapitalizedUncapped, genCostTotal)) : 0;

    const totalCapitalized = R2(Math.max(0, specificCost) + generalCapitalized);
    return {
      specific_borrowings: specificRows, specific_cost: specificCost,
      general_borrowings: generalRows, general_borrowing_cost_incurred: genCostTotal,
      weighted_avg_capitalization_rate_pct: weightedAvgRate,
      capitalizable_expenditure_general: R2(capitalizableExpenditureGeneral),
      general_capitalized_uncapped: generalCapitalizedUncapped, general_capitalized: generalCapitalized,
      total_capitalized: totalCapitalized,
    };
  }

  function checklistSummary(items) {
    const byArea = {};
    (items || []).forEach(c => {
      const area = c.area || '';
      const g = byArea[area] || (byArea[area] = { total: 0, done: 0 });
      g.total++;
      if (c.status === 'done') g.done++;
    });
    const out = {};
    Object.keys(byArea).forEach(area => {
      const g = byArea[area];
      out[area] = { total: g.total, done: g.done, completion_pct: R2(safeDiv(g.done, g.total) * 100) };
    });
    return out;
  }

  global.AcctEngines = {
    R2, tbDiagnostics, suggestPolicies, cashFlowStatement, equityMovement, provisionMovement,
    prepaymentAmortization, fxRevaluation, interestAccrual, borrowingCostCapitalization, checklistSummary,
  };
})(window);
