/* global */
// ============================================================================
// EX-ANTE FINANCIAL CALCULATION ENGINE
// ============================================================================
// Pure functions only — no React, no database. The data layer reads rows
// from the database, hands them to this engine, and renders the results.
// Mirrors the Excel reference model (Modele_Evaluation_Ex_Ante_Guide_DP_2019).
//
// Convention: all amounts are in the dossier's native currency (FCFA / EUR /
// USD / etc.). Years are 1-indexed in output arrays (index 0 = Year 1 unless
// stated otherwise — CashFlow uses index 0 = Year 0 for the construction
// year; see computeCashFlow).
//
// Exposed via window.exanteEngine.
// ============================================================================

(function () {

  // ---------- low-level financial primitives ---------------------------------

  // Net Present Value. cashflows[0] is at t=0 (no discount).
  function npv(rate, cashflows) {
    if (!cashflows || !cashflows.length) return 0;
    let v = 0;
    for (let t = 0; t < cashflows.length; t++) {
      v += (cashflows[t] || 0) / Math.pow(1 + rate, t);
    }
    return v;
  }

  // Internal Rate of Return — Newton-Raphson with bisection fallback.
  // Returns null if no convergence (e.g. all positive cashflows).
  function irr(cashflows, guess) {
    if (!cashflows || cashflows.length < 2) return null;
    // Skip degenerate cases (all positive or all negative -> no sign change)
    const positives = cashflows.some((c) => c > 0);
    const negatives = cashflows.some((c) => c < 0);
    if (!positives || !negatives) return null;

    let r = (guess != null) ? guess : 0.1;
    for (let iter = 0; iter < 100; iter++) {
      let v = 0, dv = 0;
      for (let t = 0; t < cashflows.length; t++) {
        const f = Math.pow(1 + r, t);
        v += (cashflows[t] || 0) / f;
        if (t > 0) dv -= t * (cashflows[t] || 0) / (f * (1 + r));
      }
      if (Math.abs(v) < 1e-7) return r;
      if (dv === 0) break;
      const next = r - v / dv;
      if (!isFinite(next) || next <= -0.999) break;
      if (Math.abs(next - r) < 1e-9) return next;
      r = next;
    }
    // Fallback: bisection between -0.95 and 5
    let lo = -0.95, hi = 5;
    const f = (rr) => {
      let v = 0;
      for (let t = 0; t < cashflows.length; t++) v += (cashflows[t] || 0) / Math.pow(1 + rr, t);
      return v;
    };
    const fl = f(lo), fh = f(hi);
    if (fl * fh > 0) return null;
    for (let iter = 0; iter < 100; iter++) {
      const mid = (lo + hi) / 2;
      const fm = f(mid);
      if (Math.abs(fm) < 1e-7) return mid;
      if (fl * fm < 0) hi = mid; else lo = mid;
    }
    return (lo + hi) / 2;
  }

  // ---------- helpers --------------------------------------------------------

  // Pick the right scenario column (low/mid/high) given the active label.
  function pickScenario(scen, base) {
    if (!scen) return 0;
    if (scen === "Bas")  return base.low  || 0;
    if (scen === "Haut") return base.high || 0;
    return base.mid || 0;
  }

  function safe(v, fallback) { return (v == null || isNaN(v)) ? (fallback || 0) : Number(v); }

  // ---------- amortization ---------------------------------------------------

  // Straight-line amortization. Returns annual depreciation given an asset.
  function lineAmortization(line) {
    const totalCost = safe(line.quantity, 1) * safe(line.unit_cost, 0);
    const years = safe(line.amort_years, 0);
    if (!years) return 0;
    const rate = safe(line.amort_rate, 1 / years);
    return totalCost * rate;
  }

  // Aggregate annual depreciation across all CAPEX lines.
  function totalAnnualDepreciation(capexLines) {
    return (capexLines || []).reduce((s, l) => s + lineAmortization(l), 0);
  }

  // ---------- CAPEX summary --------------------------------------------------

  function summarizeCapex(capexLines, contingencyRate) {
    const subtotal = (capexLines || []).reduce(
      (s, l) => s + safe(l.quantity, 1) * safe(l.unit_cost, 0), 0
    );
    const contingencies = subtotal * safe(contingencyRate, 0.1);
    const total = subtotal + contingencies;
    return { subtotal, contingencies, total };
  }

  // Bucket CAPEX by category (for the amortization breakdown).
  function capexByCategory(capexLines) {
    const buckets = {};
    (capexLines || []).forEach((l) => {
      const cat = l.category || "other";
      if (!buckets[cat]) buckets[cat] = { count: 0, total: 0, depreciation: 0 };
      buckets[cat].count++;
      buckets[cat].total += safe(l.quantity, 1) * safe(l.unit_cost, 0);
      buckets[cat].depreciation += lineAmortization(l);
    });
    return buckets;
  }

  // ---------- OPEX projection (line × year matrix) ---------------------------

  // Project a single OPEX line forward `years` years.
  // The Excel uses (1 + inflation)^t, but we honour the per-line category so
  // personnel inflation can differ from energy inflation when scenarios are
  // configured. Falls back to inputs.inflation_annual.
  function projectOpexLine(line, years, inputs, scen) {
    const inflation = pickInflation(line.inflation_category, inputs, scen);
    const y1 = safe(line.year1_amount, 0);
    const out = new Array(years);
    for (let t = 0; t < years; t++) out[t] = y1 * Math.pow(1 + inflation, t);
    return out;
  }

  function pickInflation(category, inputs, scen) {
    // Scenario-specific rates if present, otherwise fall back to global inflation.
    const sActive = inputs && inputs.scenario;
    if (scen && sActive) {
      const k = sActive === "Bas" ? "_low" : sActive === "Haut" ? "_high" : "_mid";
      if (category === "personnel" && scen["personnel_inflation" + k] != null) return Number(scen["personnel_inflation" + k]);
      if (category === "energy"    && scen["energy_inflation" + k]    != null) return Number(scen["energy_inflation" + k]);
      if ((category === "other" || category === "fixed") && scen["other_inflation" + k] != null) return Number(scen["other_inflation" + k]);
    }
    return safe(inputs && inputs.inflation_annual, 0.03);
  }

  // Total OPEX per year (sum across all lines).
  function totalOpexByYear(opexLines, years, inputs, scen) {
    const tot = new Array(years).fill(0);
    (opexLines || []).forEach((l) => {
      const series = projectOpexLine(l, years, inputs, scen);
      for (let t = 0; t < years; t++) tot[t] += series[t];
    });
    return tot;
  }

  // ---------- Revenue projection ---------------------------------------------

  // Volume_t = volume_y1 × (1 + growth)^t
  // Tariff_t = tariff_y1 × (1 + tariff_growth)^t
  // Revenue_t = Volume_t × Tariff_t   (or, if is_fixed, just the inflated y1_tariff)
  function projectRevenueLine(line, years, inputs, scen) {
    const sActive = inputs && inputs.scenario;
    const k = sActive === "Bas" ? "_low" : sActive === "Haut" ? "_high" : "_mid";
    const volumeGrowth = scen && scen["volume_growth" + k] != null
      ? Number(scen["volume_growth" + k])
      : safe(inputs && inputs.inflation_annual, 0.03);
    const tariffGrowth = scen && scen["tariff_growth" + k] != null
      ? Number(scen["tariff_growth" + k])
      : safe(inputs && inputs.inflation_annual, 0.02);

    const v1 = safe(line.year1_volume, 0);
    const p1 = safe(line.year1_tariff, 0);
    const out = new Array(years);
    if (line.is_fixed) {
      for (let t = 0; t < years; t++) out[t] = p1 * Math.pow(1 + tariffGrowth, t);
    } else {
      for (let t = 0; t < years; t++) {
        const vt = v1 * Math.pow(1 + volumeGrowth, t);
        const pt = p1 * Math.pow(1 + tariffGrowth, t);
        out[t] = vt * pt;
      }
    }
    return out;
  }

  function totalRevenueByYear(revenueLines, years, inputs, scen) {
    const tot = new Array(years).fill(0);
    (revenueLines || []).forEach((l) => {
      const series = projectRevenueLine(l, years, inputs, scen);
      for (let t = 0; t < years; t++) tot[t] += series[t];
    });
    return tot;
  }

  // ---------- BFR / Working capital ------------------------------------------

  // Excel formula:
  //   Créances clients = Revenu × Jcréances / 365
  //   Stocks          = OPEX × Jstocks / 365
  //   Dettes fourn.   = OPEX × Jfourn / 365
  //   BFR             = Créances + Stocks − Dettes fournisseurs
  function computeBfr(revenueByYear, opexByYear, inputs) {
    const dRecv = safe(inputs && inputs.receivables_days, 30);
    const dInv  = safe(inputs && inputs.inventory_days,   15);
    const dPay  = safe(inputs && inputs.payables_days,    45);
    const out = revenueByYear.map((r, t) => {
      const opx = opexByYear[t] || 0;
      const receivables = r   * dRecv / 365;
      const inventory   = opx * dInv  / 365;
      const payables    = opx * dPay  / 365;
      return {
        receivables, inventory, payables,
        bfr: receivables + inventory - payables,
      };
    });
    // Variation of BFR (year-over-year)
    const dBfr = new Array(out.length);
    for (let t = 0; t < out.length; t++) {
      dBfr[t] = t === 0 ? out[t].bfr : out[t].bfr - out[t-1].bfr;
    }
    return { perYear: out, deltas: dBfr };
  }

  // ---------- Financing & Debt schedule --------------------------------------

  // Build a per-loan straight-line amortization schedule with grace period.
  // years = full horizon. Returns:
  //   { opening, drawdown, interest, principal, debtService, closing, dscr }
  // dscr is filled in by computeIndicators because it needs revenue/opex.
  function debtScheduleFor(loan, horizon) {
    const principal = safe(loan.amount, 0);
    const rate      = safe(loan.interest_rate, 0);
    const maturity  = Math.max(1, safe(loan.maturity_years, 1));
    const grace     = safe(loan.grace_years, 0);
    const draw      = safe(loan.drawdown_year, 0);
    // Years where principal is being repaid: from year (draw+grace) to (draw+maturity).
    const opening   = new Array(horizon).fill(0);
    const drawdown  = new Array(horizon).fill(0);
    const interest  = new Array(horizon).fill(0);
    const principalP= new Array(horizon).fill(0);
    const closing   = new Array(horizon).fill(0);
    // Drawdown happens at draw year (all at once for simplicity)
    if (draw < horizon) drawdown[draw] = principal;
    const repayYears = Math.max(1, maturity - grace);
    const annualPrincipal = principal / repayYears;
    let balance = 0;
    for (let t = 0; t < horizon; t++) {
      opening[t]  = balance;
      balance    += drawdown[t];
      interest[t] = balance * rate;
      // Principal repayment after grace period and until maturity
      if (t >= draw + grace && t < draw + maturity) {
        principalP[t] = Math.min(annualPrincipal, balance);
      }
      balance    -= principalP[t];
      closing[t]  = balance;
    }
    const debtService = interest.map((i, t) => i + principalP[t]);
    return { opening, drawdown, interest, principal: principalP, closing, debtService };
  }

  // Aggregate debt across all loans.
  function aggregateDebtSchedule(loans, horizon) {
    const out = {
      opening: new Array(horizon).fill(0),
      drawdown: new Array(horizon).fill(0),
      interest: new Array(horizon).fill(0),
      principal: new Array(horizon).fill(0),
      closing: new Array(horizon).fill(0),
      debtService: new Array(horizon).fill(0),
    };
    (loans || []).forEach((l) => {
      const s = debtScheduleFor(l, horizon);
      for (let t = 0; t < horizon; t++) {
        out.opening[t]     += s.opening[t];
        out.drawdown[t]    += s.drawdown[t];
        out.interest[t]    += s.interest[t];
        out.principal[t]   += s.principal[t];
        out.closing[t]     += s.closing[t];
        out.debtService[t] += s.debtService[t];
      }
    });
    return out;
  }

  // From the financing-sources rows, isolate the debt and synthesize a
  // single (or many) loan(s). Returns a loans[] array compatible with
  // aggregateDebtSchedule.
  function buildLoansFromFinancing(financing, inputs) {
    return (financing || [])
      .filter((f) => f.kind === "debt" && safe(f.amount, 0) > 0)
      .map((f) => ({
        amount: f.amount,
        interest_rate: f.interest_rate != null ? f.interest_rate : safe(inputs && inputs.debt_interest_rate, 0.04),
        maturity_years: f.maturity_years || safe(inputs && inputs.debt_maturity_years, 15),
        grace_years:    f.grace_years    != null ? f.grace_years    : safe(inputs && inputs.debt_grace_years, 3),
        drawdown_year:  f.drawdown_year || 0,
      }));
  }

  function totalGrants(financing) {
    return (financing || []).filter((f) => f.kind === "grant").reduce((s, f) => s + safe(f.amount, 0), 0);
  }
  function totalEquity(financing) {
    return (financing || []).filter((f) => f.kind === "equity").reduce((s, f) => s + safe(f.amount, 0), 0);
  }
  function totalDebt(financing) {
    return (financing || []).filter((f) => f.kind === "debt").reduce((s, f) => s + safe(f.amount, 0), 0);
  }

  // ---------- PL — Profit & Loss ---------------------------------------------

  // PL is per-year: Revenue, OPEX, EBITDA, Amortization, EBIT, Interest, EBT,
  // Tax, Net Income.
  function computePL(revenueByYear, opexByYear, capexTotal, amortYears, debtSchedule, inputs) {
    const years = revenueByYear.length;
    const taxRate = safe(inputs && inputs.tax_rate_effective, 0.3);
    const amort = capexTotal / Math.max(1, safe(amortYears, 25));
    const out = [];
    for (let t = 0; t < years; t++) {
      const revenue = revenueByYear[t] || 0;
      const opex    = opexByYear[t]    || 0;
      const ebitda  = revenue - opex;
      const ebit    = ebitda - amort;
      const interest = (debtSchedule && debtSchedule.interest[t]) || 0;
      const ebt     = ebit - interest;
      const tax     = Math.max(0, ebt) * taxRate;
      const netIncome = ebt - tax;
      out.push({
        revenue, opex, ebitda, amortization: amort, ebit, interest,
        ebt, tax, netIncome,
        ebitdaMargin: revenue > 0 ? ebitda / revenue : 0,
      });
    }
    return out;
  }

  // ---------- CashFlow -------------------------------------------------------

  // Returns an array where index 0 = Year 0 (construction) and the rest are
  // operating years.
  // FCFE = FCF + grants + debt drawdown − principal − interest
  function computeCashFlow(opts) {
    const {
      capexTotal, grants, debt,
      revenueByYear, opexByYear,
      bfr,
      pl,
    } = opts;
    const operatingYears = revenueByYear.length;
    const totalYears     = operatingYears + 1; // includes year 0
    const inflows  = new Array(totalYears).fill(0);
    const outflows = new Array(totalYears).fill(0);
    const fcfProject  = new Array(totalYears).fill(0);
    const fcfEquity   = new Array(totalYears).fill(0);

    // Year 0 — construction
    outflows[0]  = capexTotal;
    inflows[0]   = grants + (debt.drawdown[0] || 0);
    fcfProject[0]= -capexTotal;
    fcfEquity[0] = -capexTotal + grants + (debt.drawdown[0] || 0);

    // Operating years
    for (let i = 0; i < operatingYears; i++) {
      const t = i + 1;
      const rev = revenueByYear[i];
      const opx = opexByYear[i];
      const ebit = pl[i].ebit;
      const tax  = pl[i].tax;
      const dBfr = bfr.deltas[i];
      const interest  = debt.interest[i] || 0;
      const principal = debt.principal[i] || 0;
      const draw      = debt.drawdown[i+1] || 0; // typically 0 if drawn at Y0
      // FCF projet (non-leveraged): EBIT × (1−taux) + amort − ΔBFR − CAPEX − refurbishments
      // Simplified: revenue − opex − tax − ΔBFR
      const fcfP = rev - opx - tax - dBfr;
      const fcfE = fcfP - interest - principal + draw;
      inflows[t]  = rev + draw;
      outflows[t] = opx + tax + dBfr + interest + principal;
      fcfProject[t] = fcfP;
      fcfEquity[t]  = fcfE;
    }
    // Cumulative
    const cumProject = new Array(totalYears);
    const cumEquity  = new Array(totalYears);
    cumProject[0] = fcfProject[0];
    cumEquity[0]  = fcfEquity[0];
    for (let t = 1; t < totalYears; t++) {
      cumProject[t] = cumProject[t-1] + fcfProject[t];
      cumEquity[t]  = cumEquity[t-1]  + fcfEquity[t];
    }
    return { inflows, outflows, fcfProject, fcfEquity, cumProject, cumEquity };
  }

  // ---------- DSCR -----------------------------------------------------------

  // DSCR_t = (Revenue_t − OPEX_t) / debt_service_t   (proxy for CFADS / debt service)
  function computeDscrSeries(revenueByYear, opexByYear, debtSchedule) {
    return revenueByYear.map((r, t) => {
      const opx = opexByYear[t] || 0;
      const ds  = (debtSchedule && debtSchedule.debtService[t]) || 0;
      const cfads = r - opx;
      if (!ds || ds < 1e-3) return 999; // no debt → infinite DSCR
      return cfads / ds;
    });
  }

  // ---------- Top-level: compute everything from raw rows --------------------

  function computeAll(opts) {
    const {
      inputs, scen,
      capexLines, opexLines, revenueLines, financingSources,
    } = opts;
    const horizon = Math.max(1, safe(inputs && inputs.horizon_years, 25));
    const operatingYears = horizon; // for Phase 2 we treat horizon as fully operating
    // CAPEX
    const capex = summarizeCapex(capexLines, inputs && inputs.contingencies_capex);
    const amortYears = safe(inputs && inputs.amort_infrastructure_years, 25);
    // OPEX / Revenue projections
    const opexByYear    = totalOpexByYear(opexLines, operatingYears, inputs, scen);
    const revenueByYear = totalRevenueByYear(revenueLines, operatingYears, inputs, scen);
    // BFR
    const bfr = computeBfr(revenueByYear, opexByYear, inputs);
    // Financing breakdown
    const grants = totalGrants(financingSources);
    const equity = totalEquity(financingSources);
    const debtTotal = totalDebt(financingSources);
    const loans = buildLoansFromFinancing(financingSources, inputs);
    const debt = aggregateDebtSchedule(loans, operatingYears + 1);
    // PL (operating years only)
    const pl = computePL(revenueByYear, opexByYear, capex.total, amortYears, {
      interest: debt.interest.slice(1), // PL excludes Y0
    }, inputs);
    // Cash flow (includes Y0)
    const cf = computeCashFlow({
      capexTotal: capex.total, grants, debt,
      revenueByYear, opexByYear, bfr, pl,
    });
    // DSCR (operating years)
    const dscrSeries = computeDscrSeries(revenueByYear, opexByYear, {
      debtService: debt.debtService.slice(1),
    });
    // Indicators
    const discountFin = safe(inputs && inputs.discount_rate_financial, 0.09);
    const npvProject  = npv(discountFin, cf.fcfProject);
    const irrProject  = irr(cf.fcfProject);
    const npvEquity   = npv(discountFin, cf.fcfEquity);
    const irrEquity   = irr(cf.fcfEquity);
    const dscrMin     = dscrSeries.length ? Math.min.apply(null, dscrSeries) : null;
    const ebitdaMarginAvg = pl.length
      ? pl.reduce((s, r) => s + r.ebitdaMargin, 0) / pl.length
      : 0;

    return {
      horizon, operatingYears,
      capex,
      financing: { grants, equity, debt: debtTotal, total: grants + equity + debtTotal, loans },
      opexByYear,
      revenueByYear,
      bfr,
      debt,
      pl,
      cashFlow: cf,
      dscrSeries,
      indicators: {
        npvProject, irrProject,
        npvEquity,  irrEquity,
        dscrMin,
        ebitdaMarginAvg,
        // For convenience: "is project bankable?" — DP guide thresholds
        viable: npvProject >= 0 && irrProject != null && irrProject > discountFin && (dscrMin == null || dscrMin >= 1.2),
      },
    };
  }

  // ============================================================================
  // PHASE 3 — ECONOMIC ANALYSIS
  // ============================================================================

  // ---------- Public Finance impact ------------------------------------------
  // Per-year flows:
  //   + Recettes publiques = Revenue × public_revenue_rate
  //   + Subvention CAPEX (Y0 only, from financing 'grant' total)
  //   − Support O&M public (sum of OPEX rows flagged is_public_support, inflated)
  //   ± Other transfers from exante_public_transfers
  //
  // Returns { perYear:[{revenues, subsidy, omSupport, otherIn, otherOut, net}],
  //           cumulative:[…], npv }
  function computePublicFinance(opts) {
    const {
      inputs, scen, revenueByYear, opexLines, financingSources,
      publicTransfers, // array of exante_public_transfers rows
    } = opts;
    const years = revenueByYear.length;
    const rate  = safe(inputs && inputs.public_revenue_rate, 0.05);
    const taxRate = safe(inputs && inputs.tax_rate_effective, 0.3);
    const subsidy = totalGrants(financingSources);

    // Project public-support OPEX rows year-by-year (year 0 = construction => 0)
    const publicOpexLines = (opexLines || []).filter((l) => l.is_public_support);
    const omSupportByYear = totalOpexByYear(publicOpexLines, years, inputs, scen);

    // Other transfers: split into inflow/outflow per year
    const otherIn  = new Array(years).fill(0);
    const otherOut = new Array(years).fill(0);
    (publicTransfers || []).forEach((t) => {
      const start = Math.max(1, safe(t.start_year, 1));    // year >=1 means operating year
      const end   = t.end_year != null ? safe(t.end_year, years) : years;
      const amt   = safe(t.amount_per_year, 0);
      for (let y = start; y <= Math.min(end, years); y++) {
        const idx = y - 1; // operating year index
        if (idx < 0 || idx >= years) continue;
        if (t.direction === "inflow") otherIn[idx] += amt;
        else otherOut[idx] += amt;
      }
    });

    const perYear = [];
    for (let t = 0; t < years; t++) {
      const revenues = revenueByYear[t] * rate;
      // Subsidy is only at Y0 (construction). The "operating year 0" in this
      // array corresponds to Year 1 of operation. Subsidy is NOT counted here
      // — it is added separately when computing the Year 0 line in the UI.
      const omSupport = omSupportByYear[t];
      perYear.push({
        year: t + 1,
        revenues, subsidy: 0,
        omSupport, otherIn: otherIn[t], otherOut: otherOut[t],
        net: revenues + otherIn[t] - omSupport - otherOut[t],
      });
    }
    // Cumulative
    let cum = -subsidy; // starts at construction-year outflow (subsidy disbursed)
    const cumulative = perYear.map((r) => { cum += r.net; return cum; });
    // NPV at economic discount rate
    const discountEco = safe(inputs && inputs.discount_rate_economic, 0.09);
    // Build a Y0..Yn array where Y0 is the subsidy outflow
    const flows = [-subsidy, ...perYear.map((r) => r.net)];
    const npvNet = npv(discountEco, flows);

    return {
      perYear, cumulative, subsidy, npvNet,
      totalRevenues: perYear.reduce((s, r) => s + r.revenues, 0),
      totalOmSupport: perYear.reduce((s, r) => s + r.omSupport, 0),
      totalOtherIn:   perYear.reduce((s, r) => s + r.otherIn, 0),
      totalOtherOut:  perYear.reduce((s, r) => s + r.otherOut, 0),
      cumulativeFinal: cumulative.length ? cumulative[cumulative.length - 1] : -subsidy,
    };
  }

  // ---------- MPR — Méthode des Prix de Référence -----------------------------

  // Apply a per-year flat-amount series from a list of transfer/externality rows.
  function aggregateYearly(rows, years, defaultGrowth) {
    const out = new Array(years).fill(0);
    (rows || []).forEach((r) => {
      const start = Math.max(1, safe(r.start_year, 1));
      const end   = r.end_year != null ? safe(r.end_year, years) : years;
      const base  = safe(r.amount_year1 != null ? r.amount_year1 : r.amount_per_year, 0);
      const g     = safe(r.growth_rate, defaultGrowth || 0);
      for (let y = start; y <= Math.min(end, years); y++) {
        const idx = y - 1;
        if (idx < 0 || idx >= years) continue;
        out[idx] += base * Math.pow(1 + g, idx); // grow from Y1
      }
    });
    return out;
  }

  function computeMpr(opts) {
    const {
      inputs, capexLines, opexByYear, revenueByYear,
      debtSchedule,
      mprTransfers,         // Phase 1
      externalities,        // Phase 2
      conversionFactors,    // Phase 3 (dict by input_kind)
    } = opts;
    const years = revenueByYear.length;
    const capexTotal = (capexLines || []).reduce(
      (s, l) => s + safe(l.quantity, 1) * safe(l.unit_cost, 0), 0
    ) * (1 + safe(inputs && inputs.contingencies_capex, 0.1));

    // ---- PHASE 1: eliminate transfers ----
    // For each category, build a per-year deduction.
    const transfersBy = {
      capex_tax: new Array(years).fill(0),
      opex_tax:  new Array(years).fill(0),
      customs:   new Array(years).fill(0),
      subsidy:   new Array(years).fill(0),
      interest:  debtSchedule && debtSchedule.interest ? debtSchedule.interest.slice(1, years + 1) : new Array(years).fill(0),
      other:     new Array(years).fill(0),
    };
    (mprTransfers || []).forEach((t) => {
      const start = Math.max(1, safe(t.start_year, 1));
      const end   = t.end_year != null ? safe(t.end_year, years) : years;
      const amt   = safe(t.amount_per_year, 0);
      for (let y = start; y <= Math.min(end, years); y++) {
        const idx = y - 1;
        if (idx < 0 || idx >= years) continue;
        if (transfersBy[t.category] != null) transfersBy[t.category][idx] += amt;
      }
    });
    // Capex tax + customs are one-shot at Y0 if their start_year=0; we treat
    // them as a year-0 deduction here:
    const capexTaxY0 = (mprTransfers || [])
      .filter((t) => (t.category === "capex_tax" || t.category === "customs") && safe(t.start_year, 1) === 0)
      .reduce((s, t) => s + safe(t.amount_per_year, 0), 0);

    // Real CAPEX = CAPEX − one-shot taxes/customs at Y0
    const realCapexY0 = capexTotal - capexTaxY0;
    // Real OPEX per year = OPEX − opex_tax − customs (recurrent) − other
    const realOpexByYear = opexByYear.map((opx, t) =>
      opx - transfersBy.opex_tax[t] - transfersBy.customs[t] - transfersBy.other[t]
    );
    // Real Revenue per year = Revenue − subsidies in revenue
    const realRevenueByYear = revenueByYear.map((r, t) => r - transfersBy.subsidy[t]);

    // ---- PHASE 2: externalities ----
    const positives = (externalities || []).filter((e) => e.kind === "positive");
    const negatives = (externalities || []).filter((e) => e.kind === "negative");
    const extPosByYear = aggregateYearly(positives, years, 0);
    const extNegByYear = aggregateYearly(negatives, years, 0);
    const extNetByYear = extPosByYear.map((p, t) => p - extNegByYear[t]);

    // ---- PHASE 3: conversion factors ----
    const cf = {};
    (conversionFactors || []).forEach((row) => { cf[row.input_kind] = safe(row.factor, 1); });
    const fCapex = cf.capex_local || cf.capex_imported || 1;
    const fOpex  = cf.opex_personnel || cf.opex_materials || 1;
    const fRev   = cf.revenue || 1;

    const adjCapexY0 = realCapexY0 * fCapex;
    const adjOpexByYear = realOpexByYear.map((v) => v * fOpex);
    const adjRevenueByYear = realRevenueByYear.map((v) => v * fRev);

    // ---- Net economic benefit per year ----
    // Economic flow Y0 = -CAPEX (adjusted)
    // Economic flow Yt = adjRevenue_t - adjOpex_t + extNet_t
    const economicFlows = new Array(years + 1);
    economicFlows[0] = -adjCapexY0;
    for (let t = 0; t < years; t++) {
      economicFlows[t + 1] = adjRevenueByYear[t] - adjOpexByYear[t] + extNetByYear[t];
    }

    // Indicators
    const discountEco = safe(inputs && inputs.discount_rate_economic, 0.09);
    const enpv = npv(discountEco, economicFlows);
    const eirr = irr(economicFlows);

    // B/C ratio = NPV(benefits) / NPV(costs)
    //   benefits = adjRevenue + ext+
    //   costs    = adjCapex(Y0) + adjOpex + ext-
    const benefitsByYear = adjRevenueByYear.map((r, t) => r + extPosByYear[t]);
    const costsByYear    = adjOpexByYear.map((o, t) => o + extNegByYear[t]);
    // Add CAPEX at Y0 to costs
    const benefitsFlow = [0, ...benefitsByYear];
    const costsFlow    = [adjCapexY0, ...costsByYear];
    const npvBenefits = npv(discountEco, benefitsFlow);
    const npvCosts    = npv(discountEco, costsFlow);
    const bcRatio = npvCosts > 0 ? npvBenefits / npvCosts : null;

    return {
      capexTotal, realCapexY0, adjCapexY0,
      transfersBy, capexTaxY0,
      realOpexByYear, realRevenueByYear,
      adjOpexByYear, adjRevenueByYear,
      extPosByYear, extNegByYear, extNetByYear,
      economicFlows,
      indicators: {
        enpv, eirr, bcRatio,
        npvBenefits, npvCosts,
        viable: enpv >= 0 && (bcRatio == null || bcRatio >= 1) && (eirr == null || eirr > discountEco),
      },
    };
  }

  // ---------- Sensitivity ----------------------------------------------------
  // Re-run computeAll with a shocked input parameter. Returns the new
  // indicators only (lightweight).
  function sensitivityShock(baseOpts, variable, deltaPct) {
    // Clone inputs deeply enough for our purpose
    const inp = { ...baseOpts.inputs };
    let capexLines = baseOpts.capexLines || [];
    let opexLines  = baseOpts.opexLines  || [];
    let revenueLines = baseOpts.revenueLines || [];
    const k = 1 + deltaPct;
    if (variable === "capex") {
      capexLines = capexLines.map((l) => ({ ...l, unit_cost: safe(l.unit_cost, 0) * k }));
    } else if (variable === "volumes") {
      revenueLines = revenueLines.map((l) => ({ ...l, year1_volume: safe(l.year1_volume, 0) * k }));
    } else if (variable === "tariffs") {
      revenueLines = revenueLines.map((l) => ({ ...l, year1_tariff: safe(l.year1_tariff, 0) * k }));
    } else if (variable === "opex") {
      opexLines = opexLines.map((l) => ({ ...l, year1_amount: safe(l.year1_amount, 0) * k }));
    } else if (variable === "discount") {
      inp.discount_rate_financial = safe(inp.discount_rate_financial, 0.09) * k;
      inp.discount_rate_economic  = safe(inp.discount_rate_economic, 0.09) * k;
    }
    const computed = computeAll({
      ...baseOpts, inputs: inp,
      capexLines, opexLines, revenueLines,
    });
    return computed.indicators;
  }

  function buildSensitivityMatrix(baseOpts, variables, deltas) {
    return variables.map((v) => ({
      variable: v,
      rows: deltas.map((d) => ({
        delta: d,
        indicators: sensitivityShock(baseOpts, v, d),
      })),
    }));
  }

  // Economic sensitivity: re-run computeAll AND computeMpr with shocked inputs
  // to capture both financial and economic indicators in one pass.
  function economicSensitivityShock(baseOpts, mprBaseOpts, variable, deltaPct) {
    const inp = { ...baseOpts.inputs };
    let capexLines    = baseOpts.capexLines    || [];
    let opexLines     = baseOpts.opexLines     || [];
    let revenueLines  = baseOpts.revenueLines  || [];
    const k = 1 + deltaPct;
    if (variable === "capex") {
      capexLines = capexLines.map((l) => ({ ...l, unit_cost: safe(l.unit_cost, 0) * k }));
    } else if (variable === "volumes") {
      revenueLines = revenueLines.map((l) => ({ ...l, year1_volume: safe(l.year1_volume, 0) * k }));
    } else if (variable === "tariffs") {
      revenueLines = revenueLines.map((l) => ({ ...l, year1_tariff: safe(l.year1_tariff, 0) * k }));
    } else if (variable === "opex") {
      opexLines = opexLines.map((l) => ({ ...l, year1_amount: safe(l.year1_amount, 0) * k }));
    } else if (variable === "discount") {
      inp.discount_rate_financial = safe(inp.discount_rate_financial, 0.09) * k;
      inp.discount_rate_economic  = safe(inp.discount_rate_economic, 0.09)  * k;
    }
    // Compute financial side with the new lines
    const fin = computeAll({ ...baseOpts, inputs: inp, capexLines, opexLines, revenueLines });
    // Compute MPR with the corresponding shocked series and the new debt schedule
    const eco = computeMpr({
      ...mprBaseOpts,
      inputs: inp,
      capexLines,
      opexByYear: fin.opexByYear,
      revenueByYear: fin.revenueByYear,
      debtSchedule: fin.debt,
    });
    return { fin: fin.indicators, eco: eco.indicators };
  }

  // ============================================================================
  // PHASE 4 — QUALITY GRID & MULTI-CRITERIA SCORING
  // ============================================================================

  // Quality grid: each item is scored 0-3. Score % = avg(notes) / 3.
  // Decision thresholds (standard ex-ante quality grid):
  //   ≥ 75% : CONSISTENT ✓
  //   60-74%: AVEC OBSERVATIONS
  //   < 60% : REJET ✗
  function computeQualityGrid(items) {
    const scored = (items || []).filter((i) => i.note != null);
    const total = scored.length;
    const sum = scored.reduce((s, i) => s + Number(i.note), 0);
    const avg = total > 0 ? sum / total : 0;
    const pct = avg / 3;
    // Per-dimension breakdown
    const byDim = {};
    scored.forEach((i) => {
      if (!byDim[i.dimension]) byDim[i.dimension] = { count: 0, sum: 0 };
      byDim[i.dimension].count++;
      byDim[i.dimension].sum += Number(i.note);
    });
    Object.keys(byDim).forEach((k) => {
      byDim[k].avg = byDim[k].count > 0 ? byDim[k].sum / byDim[k].count : 0;
      byDim[k].pct = byDim[k].avg / 3;
    });
    const totalItems = (items || []).length;
    const completion = totalItems > 0 ? total / totalItems : 0;
    return {
      totalItems, scoredItems: total, completion,
      avg, pct,
      byDim,
      decision: pct >= 0.75 ? "consistent" : pct >= 0.6 ? "with_observations" : "reject",
    };
  }

  // Multi-criteria: each row has evaluation 0-3, weight 1-3.
  // Per-dimension score: avg of (evaluation × weight) for that dimension.
  // Global score: weighted average of dimension averages.
  // Decision threshold mirrors quality grid (≥75 consistent, ≥60 with obs, <60 reject).
  function computeMulticriteria(items) {
    const scored = (items || []).filter((i) => i.evaluation != null);
    const byDim = {};
    scored.forEach((i) => {
      if (!byDim[i.dimension]) byDim[i.dimension] = { count: 0, weightedSum: 0, weightSum: 0, sumScore: 0 };
      const w = Number(i.weight) || 1;
      const ev = Number(i.evaluation) || 0;
      byDim[i.dimension].count++;
      byDim[i.dimension].weightedSum += ev * w;
      byDim[i.dimension].weightSum   += w;
      byDim[i.dimension].sumScore    += ev * w; // for the "score pondéré" table
    });
    Object.keys(byDim).forEach((k) => {
      const d = byDim[k];
      d.weightedAvg = d.weightSum > 0 ? d.weightedSum / d.weightSum : 0;
      d.pct = d.weightedAvg / 3;
    });
    // Global score = average of dimension weighted averages (each dimension
    // weighted equally — the per-dimension weights have already been applied
    // inside each dimension).
    const dimsScored = Object.values(byDim);
    const globalAvg = dimsScored.length > 0
      ? dimsScored.reduce((s, d) => s + d.weightedAvg, 0) / dimsScored.length
      : 0;
    const globalPct = globalAvg / 3;
    const totalItems = (items || []).length;
    const completion = totalItems > 0 ? scored.length / totalItems : 0;
    return {
      totalItems, scoredItems: scored.length, completion,
      byDim, globalAvg, globalPct,
      decision: globalPct >= 0.75 ? "consistent" : globalPct >= 0.6 ? "with_observations" : "reject",
    };
  }

  // ============================================================================
  // PHASE 7 — STAKEHOLDER ACCOUNTS (Table 22)
  // ============================================================================

  // Compute per-stakeholder revenue, cost, margin, tax and net IMPACT
  // (= with-project net − without-project net). Returns one entry per actor
  // plus a `total` aggregate so totals are easy to render.
  function computeStakeholderAccounts(opts) {
    const { stakeholders, inputs, scen, revenueLines, opexLines, exanteComputed } = opts;
    const years = (exanteComputed && exanteComputed.revenueByYear && exanteComputed.revenueByYear.length) || safe(inputs && inputs.horizon_years, 25);
    const inflation = safe(inputs && inputs.inflation_annual, 0.03);

    // Pre-aggregate revenue per "basis" key — sum of relevant revenue lines per year.
    const byBasis = { all: new Array(years).fill(0), c1: new Array(years).fill(0), c2: new Array(years).fill(0), c3: new Array(years).fill(0), c4: new Array(years).fill(0) };
    (revenueLines || []).forEach((l) => {
      const series = projectRevenueLine(l, years, inputs, scen);
      const code = (l.component_code || "").toLowerCase();
      for (let t = 0; t < years; t++) {
        byBasis.all[t] += series[t];
        if (code === "c1") byBasis.c1[t] += series[t];
        else if (code === "c2") byBasis.c2[t] += series[t];
        else if (code === "c3") byBasis.c3[t] += series[t];
        else if (code === "c4") byBasis.c4[t] += series[t];
      }
    });

    // Public-O&M support per year (used by stakeholders flagged absorbs_public_om)
    const publicOpexLines = (opexLines || []).filter((l) => l.is_public_support);
    const publicOmByYear = totalOpexByYear(publicOpexLines, years, inputs, scen);

    const rows = (stakeholders || []).map((s) => {
      // WITH-project series
      let withRevenue, withCost;
      if (s.revenue_basis === "manual") {
        const r1 = safe(s.manual_revenue_y1, 0);
        withRevenue = new Array(years);
        for (let t = 0; t < years; t++) withRevenue[t] = r1 * Math.pow(1 + inflation, t);
      } else {
        const basis = byBasis[s.revenue_basis || "all"] || byBasis.all;
        const pct = safe(s.revenue_pct, 0) / 100;
        withRevenue = basis.map((v) => v * pct);
      }
      if (s.manual_cost_y1 != null && s.manual_cost_y1 !== "") {
        const c1 = safe(s.manual_cost_y1, 0);
        withCost = new Array(years);
        for (let t = 0; t < years; t++) withCost[t] = c1 * Math.pow(1 + inflation, t);
      } else {
        const costPct = safe(s.cost_pct_of_revenue, 0) / 100;
        withCost = withRevenue.map((v) => v * costPct);
      }
      if (s.absorbs_public_om) {
        withCost = withCost.map((v, t) => v + publicOmByYear[t]);
      }
      const withNet  = withRevenue.map((r, t) => r - withCost[t]);
      const taxPct = safe(s.tax_pct, 0) / 100;
      const taxes   = withNet.map((n) => Math.max(0, n) * taxPct);
      const withAfterTax = withNet.map((n, t) => n - taxes[t]);

      // WITHOUT-project baseline (inflated)
      const b1Rev = safe(s.baseline_revenue_y1, 0);
      const b1Cost = safe(s.baseline_cost_y1, 0);
      const baselineRev  = new Array(years);
      const baselineCost = new Array(years);
      for (let t = 0; t < years; t++) {
        baselineRev[t]  = b1Rev  * Math.pow(1 + inflation, t);
        baselineCost[t] = b1Cost * Math.pow(1 + inflation, t);
      }
      const baselineNet = baselineRev.map((r, t) => r - baselineCost[t]);

      // Impact = with − without
      const impact = withAfterTax.map((v, t) => v - baselineNet[t]);

      // Aggregates
      const cumWithRevenue   = withRevenue.reduce((s, v) => s + v, 0);
      const cumWithCost      = withCost.reduce((s, v) => s + v, 0);
      const cumWithTax       = taxes.reduce((s, v) => s + v, 0);
      const cumWithNet       = withAfterTax.reduce((s, v) => s + v, 0);
      const cumBaselineNet   = baselineNet.reduce((s, v) => s + v, 0);
      const cumImpact        = impact.reduce((s, v) => s + v, 0);

      // Per-beneficiary annual avg
      const benefCount = Math.max(1, safe(s.beneficiary_count, 1));
      const perBeneficiaryPerYear = cumWithNet / years / benefCount;
      const perBeneficiaryImpactPerYear = cumImpact / years / benefCount;

      return {
        id: s.id, name: s.name,
        beneficiary_count: safe(s.beneficiary_count, 0),
        baseline_beneficiary_count: safe(s.baseline_beneficiary_count, 0),
        revenue_basis: s.revenue_basis,
        revenue_pct: safe(s.revenue_pct, 0),
        cost_pct_of_revenue: safe(s.cost_pct_of_revenue, 0),
        tax_pct: safe(s.tax_pct, 0),
        absorbs_public_om: !!s.absorbs_public_om,
        baseline_revenue_y1: safe(s.baseline_revenue_y1, 0),
        baseline_cost_y1: safe(s.baseline_cost_y1, 0),
        // Per-year series
        withRevenue, withCost, withNet, taxes, withAfterTax,
        baselineRev, baselineCost, baselineNet,
        impact,
        // Aggregates
        cumWithRevenue, cumWithCost, cumWithTax, cumWithNet,
        cumBaselineNet, cumImpact,
        perBeneficiaryPerYear,
        perBeneficiaryImpactPerYear,
      };
    });

    // Total row
    const sumArr = (key) => rows.reduce((s, r) => s + r[key], 0);
    const total = {
      cumWithRevenue: sumArr("cumWithRevenue"),
      cumWithCost:    sumArr("cumWithCost"),
      cumWithTax:     sumArr("cumWithTax"),
      cumWithNet:     sumArr("cumWithNet"),
      cumBaselineNet: sumArr("cumBaselineNet"),
      cumImpact:      sumArr("cumImpact"),
    };
    return { rows, total, years };
  }

  // ---------- Final decision -------------------------------------------------
  // Combines all three viability checks (financial, economic, multicriteria)
  // into a single GO/CONDITIONAL/NO-GO decision per the standard rule:
  //   GO          : financial AND economic AND multicriteria ≥ 75
  //   CONDITIONAL : at least one passes AND multicriteria ≥ 60
  //   NO-GO       : everything else
  function computeFinalDecision({ financial, economic, quality, multicriteria }) {
    const finOk  = financial && financial.viable === true;
    const ecoOk  = economic && economic.viable === true;
    const qualOk = quality && quality.pct >= 0.6;
    const mcOk   = multicriteria && multicriteria.globalPct >= 0.6;
    const mcGreat = multicriteria && multicriteria.globalPct >= 0.75;
    let decision = "no_go";
    let reasons = [];
    if (finOk && ecoOk && qualOk && mcGreat) {
      decision = "go";
      reasons.push("Tous les seuils franchis (financier, économique, qualité, multicritère ≥ 75%)");
    } else if ((finOk || ecoOk) && mcOk) {
      decision = "conditional";
      if (!finOk) reasons.push("Viabilité financière à consolider");
      if (!ecoOk) reasons.push("Viabilité économique à consolider");
      if (!qualOk) reasons.push("Grille de contrôle qualité < 60%");
      if (!mcGreat) reasons.push("Score multicritère 60-74% (avec observations)");
    } else {
      decision = "no_go";
      if (!finOk) reasons.push("Viabilité financière non démontrée");
      if (!ecoOk) reasons.push("Viabilité économique non démontrée");
      if (!qualOk) reasons.push("Grille de contrôle qualité insuffisante (< 60%)");
      if (!mcOk) reasons.push("Score multicritère < 60% (rejet)");
    }
    return { decision, reasons, finOk, ecoOk, qualOk, mcOk, mcGreat };
  }

  // ---------- Expose --------------------------------------------------------
  window.exanteEngine = {
    npv, irr,
    pickScenario, safe,
    lineAmortization, totalAnnualDepreciation,
    summarizeCapex, capexByCategory,
    projectOpexLine, totalOpexByYear,
    projectRevenueLine, totalRevenueByYear,
    computeBfr,
    debtScheduleFor, aggregateDebtSchedule, buildLoansFromFinancing,
    totalGrants, totalEquity, totalDebt,
    computePL, computeCashFlow, computeDscrSeries,
    computeAll,
    // Phase 3
    computePublicFinance,
    computeMpr,
    aggregateYearly,
    sensitivityShock, buildSensitivityMatrix, economicSensitivityShock,
    // Phase 4
    computeQualityGrid, computeMulticriteria, computeFinalDecision,
    // Phase 7
    computeStakeholderAccounts,
  };

})();
