// calc-2026.jsx — Moteur de calcul RAYCAST 2026
// Source de vérité : LISTE_PRIX_RAYCOM_v4_FINALE_3.xlsx (via data-2026.jsx)
// Édition : 20 mai 2026
//
// PRIORITÉ ABSOLUE UTILISATEUR : "le calculateur solaire doit vraiment etre parfait
// et calculer comme il fauut et pas oublier les etages, pentes, bons increments"
//
// Architecture :
//   • Solaire = lookup table EXACT (24 paliers × 3 pentes) — pas de formule approximative
//   • Étages = +0,10 $/W par étage au-delà de 2 (× watts_total)
//   • Pente = mapping 4 buckets UI → 3 colonnes Excel
//   • Increments = snap au palier inférieur le plus proche + warning si extrapolation
//   • Toiture béton / Au sol / membrane = sur soumission (bloquant)
//   • 41+ panneaux = sur soumission (bloquant)
//
// Exposé via window.RAYCAST_CALC_2026 — coexiste avec window.RAYCAST_CALC (v25).

(function () {
  const TPS_RATE = 0.05;
  const TVQ_RATE = 0.09975;
  const TAXES_COMBINED = TPS_RATE + TVQ_RATE; // 0.14975

  // ============================================================================
  // FORMATTERS — format CA-FR (NBSP U+00A0, virgule décimale)
  // ============================================================================
  const NBSP = " ";
  const fmtCAD = (n, decimals = 2) => {
    if (n == null || isNaN(n) || !isFinite(n)) return "—";
    const sign = n < 0 ? "-" : "";
    const abs = Math.abs(n);
    const parts = abs.toFixed(decimals).split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, NBSP);
    return sign + parts.join(",") + NBSP + "$";
  };
  const fmtCAD0 = (n) => fmtCAD(n, 0);
  const fmtPct = (n, decimals = 1) => {
    if (n == null || isNaN(n) || !isFinite(n)) return "—";
    return n.toFixed(decimals).replace(".", ",") + NBSP + "%";
  };
  const fmtNum = (n, decimals = 0) => {
    if (n == null || isNaN(n) || !isFinite(n)) return "—";
    const parts = n.toFixed(decimals).split(".");
    parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, NBSP);
    return parts.join(",");
  };

  // ============================================================================
  // 🎯 CALCULATEUR SOLAIRE — PRIORITÉ 1
  // ============================================================================

  /**
   * Lookup du palier dans la table client (Excel "Solaire au watt").
   * Retourne le palier EXACT si trouvé, sinon le palier inférieur le plus proche.
   *
   * @param {number} panneaux - Nombre de panneaux désiré
   * @returns {object|null} { row, exact, panneaux_lookup, panneaux_requested, warning }
   */
  function lookupSolarPalier(panneaux) {
    const D = window.RAYCOM_DATA_2026;
    const table = D.solar_table_vendant;
    const requested = Math.max(0, Math.floor(panneaux));

    // 0 panneaux = pas de solaire
    if (requested === 0) {
      return { row: null, exact: true, panneaux_lookup: 0, panneaux_requested: 0, warning: null, blocking: false };
    }

    // > 80 panneaux : bloquant (hors table)
    if (requested > 80) {
      return {
        row: null, exact: false, panneaux_lookup: requested, panneaux_requested: requested,
        warning: `Configuration ${requested} panneaux > 80 → hors grille, soumission ad hoc obligatoire`,
        blocking: true
      };
    }
    // 41-80 panneaux : prix indicatifs (Excel a la grille jusqu'à 80) mais "sur soumission ad hoc" recommandé
    let oversizeWarning = null;
    if (requested > 40) {
      oversizeWarning = `Configuration ${requested} panneaux (>40) : prix indicatif de la grille, confirmer par soumission ad hoc commerciale (Hydro-Québec puissance max 40 kW résidentiel)`;
    }

    // Moins de 5 panneaux : pas dans la grille catalogue — minimum 5
    if (requested < 5) {
      return {
        row: table[0], exact: false, panneaux_lookup: table[0].panneaux, panneaux_requested: requested,
        warning: `Minimum 5 panneaux dans la grille catalogue — prix calculé pour 5 panneaux`,
        blocking: false
      };
    }

    // Cherche correspondance exacte
    const exact = table.find(r => r.panneaux === requested);
    if (exact) {
      return { row: exact, exact: true, panneaux_lookup: requested, panneaux_requested: requested, warning: oversizeWarning, blocking: false };
    }

    // Pas de match exact : prend le palier INFÉRIEUR le plus proche (conservateur)
    // Conformément à "lissage monotone +350$/panneau min entre paliers"
    let lower = table[0];
    let upper = null;
    for (const row of table) {
      if (row.panneaux <= requested) lower = row;
      if (row.panneaux > requested && !upper) upper = row;
    }
    // Interpolation linéaire entre les paliers (respectant le lissage monotone)
    const baseWarning = `Increment exact : choisir ${lower.panneaux} ou ${upper ? upper.panneaux : "—"} panneaux pour prix table direct`;
    const combinedWarning = oversizeWarning ? `${baseWarning} · ${oversizeWarning}` : baseWarning;
    return {
      row: lower,
      upper_row: upper,
      exact: false,
      panneaux_lookup: lower.panneaux,
      panneaux_requested: requested,
      warning: combinedWarning,
      blocking: false,
      interpolate: true
    };
  }

  /**
   * Détermine la colonne Excel selon la pente UI.
   * Mapping :
   *   plat / faible / standard → p1 (Pente 0-8/12)
   *   forte                    → p2 (Pente 9-10/12)
   *   tres_forte               → p3 (Pente 11-12/12)
   */
  function pitchKeyToExcelCol(pitch_key) {
    const D = window.RAYCOM_DATA_2026;
    return D.pitch_design_to_excel[pitch_key] || "p1";
  }

  /**
   * 🎯 CALCULATEUR SOLAIRE — fonction principale
   *
   * @param {object} cfg
   * @param {number} cfg.panneaux - Nombre de panneaux (4-40 dans grille, 41+ sur soumission)
   * @param {string} cfg.pitch_key - "plat" | "faible" | "standard" | "forte" | "tres_forte"
   * @param {number} cfg.floors - Nombre d'étages (1-4, défaut 2)
   * @param {string} cfg.roof_key - "bardeau" | "metal" | "tpo" | "epdm" | "beton" | "ground"
   *
   * @returns {object} {
   *   panneaux, kw, watts, pitch_col, floors,
   *   vendant_solaire_base, floors_extra, vendant_solaire_total,
   *   prix_par_watt, palier, blocking, warnings, errors
   * }
   */
  function calcSolaireAuWatt(cfg) {
    const D = window.RAYCOM_DATA_2026;
    const { panneaux = 0, pitch_key = "standard", floors = 2, roof_key = "bardeau" } = cfg;
    const errors = [];
    const warnings = [];

    // 1) Toiture béton / au sol / membrane lourde → BLOQUANT (sur soumission)
    const roofMeta = D.roof_types.find(r => r.key === roof_key);
    if (roofMeta && (roofMeta.requires_quote || roof_key === "beton")) {
      errors.push(`Toiture ${roofMeta.label} : ingénierie additionnelle requise — sur soumission ad hoc`);
      return {
        panneaux: 0, kw: 0, watts: 0, pitch_col: null, floors,
        vendant_solaire_base: 0, floors_extra: 0, vendant_solaire_total: 0,
        prix_par_watt: 0, palier: null, blocking: true, warnings, errors,
        roof_meta: roofMeta
      };
    }
    if (roofMeta && roofMeta.warning && !roofMeta.requires_quote) {
      warnings.push(roofMeta.warning);
    }

    // 2) Lookup palier
    const palier = lookupSolarPalier(panneaux);
    if (palier.blocking) {
      errors.push(palier.warning);
      return {
        panneaux, kw: 0, watts: 0, pitch_col: null, floors,
        vendant_solaire_base: 0, floors_extra: 0, vendant_solaire_total: 0,
        prix_par_watt: 0, palier, blocking: true, warnings, errors
      };
    }
    if (palier.warning) warnings.push(palier.warning);

    // 3) Si 0 panneau → tout à zéro (pas d'erreur)
    if (!palier.row) {
      return {
        panneaux: 0, kw: 0, watts: 0, pitch_col: null, floors,
        vendant_solaire_base: 0, floors_extra: 0, vendant_solaire_total: 0,
        prix_par_watt: 0, palier, blocking: false, warnings, errors
      };
    }

    // 4) Détermine la colonne Excel selon la pente
    const pitchCol = pitchKeyToExcelCol(pitch_key);

    // 5) Récupère le prix de base depuis la table (ou interpole si requested ≠ palier)
    let vendant_solaire_base;
    if (palier.exact || !palier.interpolate) {
      vendant_solaire_base = palier.row[pitchCol];
    } else {
      // Interpolation linéaire entre palier inférieur et supérieur, respectant lissage min
      const lower = palier.row;
      const upper = palier.upper_row;
      if (!upper) {
        vendant_solaire_base = lower[pitchCol];
      } else {
        const reqPanneaux = palier.panneaux_requested;
        const ratio = (reqPanneaux - lower.panneaux) / (upper.panneaux - lower.panneaux);
        const interpolated = lower[pitchCol] + (upper[pitchCol] - lower[pitchCol]) * ratio;
        // Force monotonie : +350$/panneau minimum entre lower et reqPanneaux
        const minBumpPerPanel = D.solar_rules.lissage_min_per_panel;
        const monotone_min = lower[pitchCol] + (reqPanneaux - lower.panneaux) * minBumpPerPanel;
        vendant_solaire_base = Math.max(interpolated, monotone_min);
      }
    }

    // 6) Calcul des watts totaux (panneau de référence : Thornova 500W)
    const reqPanneaux = palier.panneaux_requested || panneaux;
    const watts = reqPanneaux * D.panneau_ref.watts;
    const kw = watts / 1000;

    // 7) ÉTAGES — +0,10 $/W par étage au-delà de 2 (× watts_total)
    const floorsClamped = Math.max(1, Math.min(4, floors || 2));
    const extra_floors = Math.max(0, floorsClamped - 2);
    const floors_extra = extra_floors * D.solar_rules.floors_extra_per_w_per_floor * watts;

    if (extra_floors > 0) {
      warnings.push(`+${extra_floors} étage(s) au-delà du standard 2 → +${fmtCAD(floors_extra)} (${D.solar_rules.floors_extra_per_w_per_floor}$/W × ${watts}W × ${extra_floors})`);
    }

    // 8) Total vendant solaire
    const vendant_solaire_total = Math.round(vendant_solaire_base + floors_extra);
    const prix_par_watt = watts > 0 ? vendant_solaire_total / watts : 0;

    return {
      panneaux: reqPanneaux,
      kw,
      watts,
      pitch_col: pitchCol,
      pitch_key,
      floors: floorsClamped,
      extra_floors,
      vendant_solaire_base: Math.round(vendant_solaire_base),
      floors_extra: Math.round(floors_extra),
      vendant_solaire_total,
      prix_par_watt,
      palier,
      blocking: false,
      warnings,
      errors,
      roof_meta: roofMeta,
      panneau_ref: D.panneau_ref.name
    };
  }

  // ============================================================================
  // 📦 FORFAIT INSTALL — sélection du forfait selon config
  // ============================================================================

  /**
   * Sélectionne le forfait install 2026 (parmi les 21).
   *
   * @param {object} cfg
   * @param {string} cfg.brand - "fox_sig" | "tesla"
   * @param {number} cfg.onds - 1 | 2 | 3
   * @param {boolean} cfg.has_dim
   * @param {boolean} cfg.has_battery
   * @param {string} cfg.backup_mode - "none" | "partial" | "whole_home" (Tesla seulement)
   * @returns {object} forfait
   */
  function pickForfait2026(cfg) {
    const D = window.RAYCOM_DATA_2026;
    const { brand = "fox_sig", onds = 1, has_dim = false, has_battery = false, backup_mode = "none" } = cfg;
    const list = D.forfaits_install.filter(f => f.brand === brand);
    const ondsClamped = Math.max(1, Math.min(3, onds));

    if (brand === "tesla") {
      // Tesla : dim et batt toujours true (intégré PW3 + GW3)
      // Matching par onds + (full_backup | bu | base)
      if (backup_mode === "whole_home") {
        return list.find(f => f.onds === ondsClamped && f.full_backup === true)
            || list.find(f => f.onds === ondsClamped && f.bu === true)
            || list.find(f => f.onds === ondsClamped);
      }
      if (backup_mode === "partial") {
        return list.find(f => f.onds === ondsClamped && f.bu === true && !f.full_backup)
            || list.find(f => f.onds === ondsClamped);
      }
      return list.find(f => f.onds === ondsClamped && !f.bu) || list[0];
    }

    // Fox/Sig matching
    const wantDim = has_dim || has_battery; // DIM toujours requis avec batterie
    const wantBatt = has_battery;
    const wantBu = backup_mode === "partial";

    return list.find(f => f.onds === ondsClamped && f.dim === wantDim && f.batt === wantBatt && f.bu === wantBu)
        || list.find(f => f.onds === ondsClamped && f.dim === wantDim && f.batt === wantBatt)
        || list.find(f => f.onds === ondsClamped)
        || list[0];
  }

  // ============================================================================
  // 🔋 TOUT-EN-UN (BATTERIE SEULE — V3-E) — Tesla / Fox AIO / Fox H1 / Sigenergy
  // ============================================================================

  /**
   * Calcule le matériel batterie + accessoires pour le mode "tout-en-un" (sans solaire).
   * Couvre :
   *   • Tesla : 1-3 Powerwall 3 + Gateway 3 + DC expansions (max 3 par PW)
   *     - +1 495 $ install par PW supplémentaire (au-delà du forfait Duo/Trio)
   *     - +1 195 $ install par DC expansion
   *   • Fox AIO   : onduleur H1-AIO + N × EQ4000 (1-6 modules)
   *   • Fox H1    : onduleur H1 Hybrid + CM4000 + N × CS4000 (0-7) + Hub G2 + Inverter Kit
   *   • Sigenergy : SigenStor controller + N × Batterie 6 ou 9 kWh + LoadHub (si DIM)
   *
   * @param {object} cfg
   * @returns {object} { cost, sell, label, breakdown:[{label,qty,unit_sell,sell}], kwh, kw_inverter }
   */
  function calcBatteryOnly(cfg) {
    const D = window.RAYCOM_DATA_2026;
    const {
      brand = "fox_aio",
      // Tesla
      pw_count = 1,
      dc_expansions = 0,
      // Fox (AIO or H1) — utilise battery_bundle_id
      battery_bundle_id = null,
      // Sigenergy
      sig_battery_kwh = 9,
      sig_battery_qty = 2,
      sig_with_loadhub = true
    } = cfg;

    const breakdown = [];
    let cost = 0, sell = 0;
    let kwh = 0;
    let kw_inverter = 0;

    if (brand === "tesla") {
      const sku_pw = D.tesla_skus["MPW-1707000-11-M"];   // Powerwall 3
      const sku_gw = D.tesla_skus["MPW-1841000-01-E"];   // Gateway 3
      const sku_dc = D.tesla_skus["MPW-1807000-20-B"];   // DC Expansion
      const pwClamped = Math.max(1, Math.min(3, pw_count));
      const dcClamped = Math.max(0, Math.min(3, dc_expansions));

      // 1 Gateway 3 toujours
      cost += sku_gw.cost; sell += sku_gw.sell;
      breakdown.push({ code: "MPW-1841000-01-E", label: sku_gw.name, qty: 1, unit_sell: sku_gw.sell, sell: sku_gw.sell, cost: sku_gw.cost });

      // N × Powerwall 3
      cost += pwClamped * sku_pw.cost;
      sell += pwClamped * sku_pw.sell;
      kwh += pwClamped * sku_pw.kwh;
      kw_inverter += pwClamped * sku_pw.kw;
      breakdown.push({ code: "MPW-1707000-11-M", label: sku_pw.name, qty: pwClamped, unit_sell: sku_pw.sell, sell: pwClamped * sku_pw.sell, cost: pwClamped * sku_pw.cost });

      // N × DC Expansion par PW (au total : dcClamped × pwClamped)
      const dcTotal = dcClamped * pwClamped;
      if (dcTotal > 0) {
        cost += dcTotal * sku_dc.cost;
        sell += dcTotal * sku_dc.sell;
        kwh += dcTotal * sku_dc.kwh;
        breakdown.push({ code: "MPW-1807000-20-B", label: sku_dc.name, qty: dcTotal, unit_sell: sku_dc.sell, sell: dcTotal * sku_dc.sell, cost: dcTotal * sku_dc.cost });
      }

      // Install supplémentaire DC expansion : +1 195 $ vendant par DC (estim 30% marge → 836 $ coûtant)
      if (dcTotal > 0) {
        const inst_per_dc_sell = 1195;
        const inst_per_dc_cost = 836;
        cost += dcTotal * inst_per_dc_cost;
        sell += dcTotal * inst_per_dc_sell;
        breakdown.push({ code: null, kind: "labor", label: "Installation supplémentaire DC Expansion (1 195 $/expansion)", qty: dcTotal, unit_sell: inst_per_dc_sell, sell: dcTotal * inst_per_dc_sell, cost: dcTotal * inst_per_dc_cost });
      }

      return { cost, sell, label: `Tesla — ${pwClamped} Powerwall 3 + ${dcClamped} DC exp/PW`, breakdown, kwh, kw_inverter, brand };
    }

    if (brand === "fox_aio") {
      // Utilise un bundle AIO (id préfixé "aio-")
      const bundles = D.fox_ess.aio.bundles;
      const bundle = bundles.find(b => b.id === battery_bundle_id) || bundles[3]; // défaut AIO 11.4kW + 4×EQ4000
      cost += bundle.cost;
      sell += bundle.sell;
      kwh += bundle.kwh;
      const inv = D.fox_ess.aio.onduleurs[bundle.inv_sku];
      if (inv) kw_inverter = inv.kw;
      breakdown.push({ code: bundle.inv_sku || null, label: bundle.label, qty: 1, unit_sell: bundle.sell, sell: bundle.sell, cost: bundle.cost });

      // ProJoy obligatoire (universel — pas seulement solaire — sécurité Code canadien)
      // Pour batterie tout-en-un sans solaire : pas requis (norme s'applique uniquement aux PV strings)
      return { cost, sell, label: bundle.label, breakdown, kwh, kw_inverter, brand };
    }

    if (brand === "fox_h1") {
      const bundles = D.fox_ess.h1_hybrid.bundles;
      const bundle = bundles.find(b => b.id === battery_bundle_id) || bundles[3]; // défaut H1 11.4kW + 3 CS
      cost += bundle.cost;
      sell += bundle.sell;
      kwh += bundle.kwh;
      const inv = D.fox_ess.h1_hybrid.onduleurs[bundle.inv_sku];
      if (inv) kw_inverter = inv.kw;
      breakdown.push({ code: bundle.inv_sku || null, label: bundle.label, qty: 1, unit_sell: bundle.sell, sell: bundle.sell, cost: bundle.cost });
      return { cost, sell, label: bundle.label, breakdown, kwh, kw_inverter, brand };
    }

    if (brand === "sig") {
      // Contrôleur SigenStor
      const ctrl = D.sigenergy_skus["SIG-CTRL-115"];
      cost += ctrl.cost; sell += ctrl.sell;
      kw_inverter = ctrl.kw;
      breakdown.push({ code: "SIG-CTRL-115", label: ctrl.name, qty: 1, unit_sell: ctrl.sell, sell: ctrl.sell, cost: ctrl.cost });

      // Batteries 6 ou 9 kWh
      const battSku = sig_battery_kwh === 6 ? D.sigenergy_skus["SIG-BAT-6"] : D.sigenergy_skus["SIG-BAT-9"];
      const battQty = Math.max(1, Math.min(8, sig_battery_qty));
      cost += battQty * battSku.cost;
      sell += battQty * battSku.sell;
      kwh += battQty * battSku.kwh;
      breakdown.push({ code: sig_battery_kwh === 6 ? "SIG-BAT-6" : "SIG-BAT-9", label: battSku.name, qty: battQty, unit_sell: battSku.sell, sell: battQty * battSku.sell, cost: battQty * battSku.cost });

      // LoadHub (DIM) optionnel
      if (sig_with_loadhub) {
        const lh = D.sigenergy_skus["SIG-LOADHUB"];
        cost += lh.cost; sell += lh.sell;
        breakdown.push({ code: "SIG-LOADHUB", label: lh.name, qty: 1, unit_sell: lh.sell, sell: lh.sell, cost: lh.cost });
      }

      return { cost, sell, label: `Sigenergy SigenStor + ${battQty}× ${sig_battery_kwh} kWh`, breakdown, kwh, kw_inverter, brand };
    }

    return { cost: 0, sell: 0, label: "—", breakdown: [], kwh: 0, kw_inverter: 0, brand };
  }

  // ============================================================================
  // 🔌 BORNES EV — Calc forfait base + slider + modèle + DCC
  // ============================================================================

  /**
   * Calcule le prix d'une borne EV (1 borne).
   *
   * @param {object} cfg
   * @param {string} cfg.slider_key - "A_int_nmd" | "B_garage_pvc" | "C_garage_teck" | "D_ext_pvc" | "E_ext_teck" | "F_int_emt"
   * @param {number} cfg.cable_m - Longueur câble en mètres (1-50)
   * @param {string} cfg.modele_key - clé du modèle borne (ou null si "fournie_client")
   * @param {number} cfg.qty - Nombre de bornes (résid = 1, multilog = N)
   * @param {string|null} cfg.dcc_code - "OPT-DCC-12" | "OPT-DCC-10-40A" | etc., ou null
   * @returns {object}
   */
  function calcBorneEV(cfg) {
    const D = window.RAYCOM_DATA_2026;
    const { slider_key = "B_garage_pvc", cable_m = 5, modele_key = null, qty = 1, dcc_code = null, cables = null, dsdc = false } = cfg;

    // === V3 : Mode multi-cables combinables ===
    // Si cfg.cables = { A_int_nmd: 5, B_garage_pvc: 10, ... } avec au moins 1 entry > 0
    const cablesV3 = cables && typeof cables === "object" && Object.values(cables).some(m => m > 0);

    let forfait_cost, forfait_sell;
    let cables_breakdown = [];

    if (cablesV3) {
      // === V3 combiné : forfait base 495$ ajouté UNE fois + somme des cables sélectionnés ===
      const base_cost = D.borne_ev.forfait_base.cost_total;     // 278.33 $
      const base_sell = D.borne_ev.forfait_base.sell;           // 495 $
      let combined_cost = 0, combined_sell = 0;

      for (const [cableKey, m] of Object.entries(cables)) {
        if (!m || m <= 0) continue;
        const slider = D.borne_ev.sliders[cableKey];
        if (!slider) continue;
        // Snap au palier le plus proche supérieur (ou au max)
        const tier = slider.lengths.find(l => l.m === m)
                  || slider.lengths.find(l => l.m >= m)
                  || slider.lengths[slider.lengths.length - 1];
        // tier.cost_m et tier.sell_m sont les CUMULS pour m mètres (pas par mètre malgré le nom)
        combined_cost += tier.cost_m;
        // tier.total = base 495 + sell_m → on veut juste sell_m (sans base double-comptage)
        combined_sell += (tier.total - base_sell);
        cables_breakdown.push({ key: cableKey, label: slider.label, m: tier.m, cost: tier.cost_m, sell: (tier.total - base_sell) });
      }
      forfait_cost = base_cost + combined_cost;
      forfait_sell = base_sell + combined_sell;
    } else {
      // === Legacy : 1 seul slider + 1 longueur ===
      const slider = D.borne_ev.sliders[slider_key];
      if (!slider) return { cost: 0, sell: 0, qty: 0, breakdown: [], errors: ["Slider câble invalide"] };
      const lengths = slider.lengths;
      let chosen = lengths.find(l => l.m === cable_m) || lengths.find(l => l.m >= cable_m) || lengths[lengths.length - 1];
      forfait_cost = D.borne_ev.forfait_base.cost_total + (chosen.cost_m - 30);
      forfait_sell = chosen.total;
      cables_breakdown.push({ key: slider_key, label: slider.label, m: chosen.m, cost: chosen.cost_m, sell: (chosen.total - D.borne_ev.forfait_base.sell) });
    }

    // 2) Modèle borne
    let modele_cost = 0, modele_sell = 0, modele_label = "—";
    if (modele_key && D.borne_ev.modeles[modele_key]) {
      const m = D.borne_ev.modeles[modele_key];
      modele_cost = m.cost;
      modele_sell = m.sell;
      modele_label = `${m.brand} ${m.model}`;
    }

    // 3) DCC (optionnel)
    let dcc_cost = 0, dcc_sell = 0, dcc_label = null;
    if (dcc_code && D.borne_ev.dcc[dcc_code]) {
      const dcc = D.borne_ev.dcc[dcc_code];
      dcc_cost = dcc.cost;
      dcc_sell = dcc.sell;
      dcc_label = dcc.label;
    }

    // 4) Total (par borne × qty)
    const cost_per_unit = forfait_cost + modele_cost + dcc_cost;
    const sell_per_unit = forfait_sell + modele_sell + dcc_sell;
    const cost = cost_per_unit * qty;
    const sell = sell_per_unit * qty;

    // V3 : label adapté selon mode (combiné vs legacy)
    const forfaitLabel = cablesV3
      ? `Forfait base + ${cables_breakdown.length} type(s) de câblage combinés (${cables_breakdown.reduce((s, c) => s + c.m, 0)} m total)`
      : `Forfait base + slider ${slider_key} (${cables_breakdown[0]?.m || 0}m)`;

    return {
      cost, sell, qty,
      cost_per_unit, sell_per_unit,
      forfait_sell, forfait_cost,
      slider_label: cables_breakdown[0]?.label || "",
      cable_m: cables_breakdown[0]?.m || 0,
      cables_breakdown,
      modele_key: modele_key || null,
      modele_sku: (modele_key && D.borne_ev.modeles[modele_key]?.sku) || null, // SKU QBO réel ou null (Tesla → "[code à confirmer]")
      modele_label, modele_cost, modele_sell,
      forfait_base_code: null, // forfait main-d'œuvre borne (MO 2h + breaker 60A + connecteurs) — ligne service, pas de SKU produit
      dcc_code: dcc_code || null,           // OPT-DCC-* = code produit RVE
      dcc_label, dcc_cost, dcc_sell,
      breakdown: [
        { label: forfaitLabel, value: forfait_sell * qty },
        modele_sell > 0 ? { label: `Borne ${modele_label}`, value: modele_sell * qty } : null,
        dcc_sell > 0 ? { label: dcc_label, value: dcc_sell * qty } : null
      ].filter(Boolean),
      errors: []
    };
  }

  // ============================================================================
  // 🗺️ ZONE — détection par FSA + surcharge
  // ============================================================================

  /**
   * Détecte la zone depuis le code postal (FSA = 3 premiers caractères).
   * @param {string} postal - Code postal canadien (ex: "J7K 0H4")
   * @returns {object} { zone_key, blocked, label, sell_client, sell_partenaire }
   */
  function detectZone(postal) {
    const D = window.RAYCOM_DATA_2026;
    if (!postal) return { zone_key: "Z1", blocked: false, sell_client: 0, label: "Z1 — Rive-Nord immédiate (défaut)" };

    const cleaned = postal.toUpperCase().replace(/\s/g, "");
    // Validation format A1A1A1 (6 chars alphanumériques alternés)
    if (!/^[A-Z]\d[A-Z]\d[A-Z]\d$/.test(cleaned)) {
      return {
        zone_key: "INVALID", blocked: false,
        sell_client: 0, label: "Format code postal invalide",
        warning: `Format attendu : A1A 1A1 — reçu "${postal}"`
      };
    }
    const fsa = cleaned.substring(0, 3);
    const zoneKey = D.fsa_to_zone[fsa] || "Z7"; // Z7 = fallback FSA inconnus (régions très éloignées)

    // Z6 (régions éloignées) et Z7 (fallback) = HORS COUVERTURE STANDARD (sur soumission, non bloquant)
    if (zoneKey === "Z6" || zoneKey === "Z7") {
      const zoneMeta = D.zones.find(z => z.key === zoneKey);
      return {
        zone_key: zoneKey, blocked: false,
        sell_client: null, sell_partenaire: null,
        label: zoneMeta?.label || `${zoneKey} — hors couverture`,
        sub: zoneMeta?.sub,
        fsa,
        couverture: "sur_soumission",
        warning: `FSA ${fsa} (${zoneKey}) hors couverture standard — déplacement sur soumission individuelle.`
      };
    }

    const zoneMeta = D.zones.find(z => z.key === zoneKey);
    return {
      zone_key: zoneKey, blocked: false,
      sell_client: zoneMeta.sell_client || 0,
      sell_partenaire: zoneMeta.sell_partenaire || 0,
      label: zoneMeta.label, sub: zoneMeta.sub,
      fsa,
      couverture: zoneMeta.baked_in ? "inclus" : "standard"
    };
  }

  // ============================================================================
  // 🌞 PRODUCTION SOLAIRE
  // ============================================================================

  function calcProduction(state) {
    const D = window.RAYCOM_DATA_2026;
    const { solar, client } = state;
    if (!solar || !solar.kw || solar.kw <= 0) return 0;

    // Lookup irradiation : priorité FSA (NRCan), fallback région, fallback moyenne provinciale
    let kwh_per_kw = 1311; // moyenne provinciale QC NRCan (fallback ultime)
    if (client?.address_postal && D.irradiation_by_fsa) {
      const fsa = client.address_postal.toUpperCase().replace(/\s/g, "").substr(0, 3);
      if (D.irradiation_by_fsa[fsa]) {
        kwh_per_kw = D.irradiation_by_fsa[fsa];
      }
    }
    if (kwh_per_kw === 1311) {
      const region = D.regions.find(r => r.key === client?.region_id);
      if (region) kwh_per_kw = region.kwh_per_kw;
    }

    const orientationFactor = { "S": 1.00, "SE": 0.96, "SO": 0.96, "E": 0.86, "O": 0.86, "NE": 0.76, "NO": 0.76, "N": 0.68 }[client?.orientation] || 0.96;
    const roof = D.roof_types.find(r => r.key === solar.roof_type);
    const pitch = D.pitch_brackets_ui.find(p => p.key === solar.pitch_key);
    // Optimal = pente standard (15-30°). Plus plat ou plus pentu = perte.
    const slopeFactor = (() => {
      if (roof?.flat) return 0.92;
      if (pitch?.key === "standard") return 1.00;
      if (pitch?.key === "faible") return 0.97;
      if (pitch?.key === "forte") return 0.95;
      if (pitch?.key === "tres_forte") return 0.93;
      if (pitch?.key === "plat") return 0.92;
      return 0.95;
    })();

    return Math.round(solar.kw * kwh_per_kw * orientationFactor * slopeFactor);
  }

  // ============================================================================
  // 💰 ÉCONOMIES + SUBVENTIONS
  // ============================================================================

  function calcSavings(state, production_kwh) {
    const D = window.RAYCOM_DATA_2026;
    const { client, solar, borne } = state;
    const tariff = D.hq_tariffs.find(t => t.id === client?.hq_tariff) || D.hq_tariffs[0];
    const base = (production_kwh * tariff.rate) / 100;
    let peak_bonus = 0;
    if (client?.hq_tariff === "FlexD" && solar?.battery_kwh > 0) {
      const bill = client.bill_input_mode === "dollars"
        ? (client.annual_hq_bill || 0)
        : ((client.annual_hq_kwh || 0) * tariff.rate / 100);
      peak_bonus = bill * Math.min(0.15, solar.battery_kwh / 100);
    }
    // V2X — éligible si borne sigenergy_v2x OU flag solar.with_v2x (directive #13)
    let v2x = 0;
    const v2x_eligible = (borne?.modele_key === "sigenergy_v2x") || !!solar?.with_v2x;
    if (v2x_eligible) {
      const ev_kwh = ((client?.ev_km_per_year || 0) * (client?.ev_kwh_per_100km || 0)) / 100;
      v2x = ev_kwh * 0.15;
    }
    return { base, peak_bonus, v2x, v2x_eligible, total: base + peak_bonus + v2x };
  }

  // ============================================================================
  // 💰 V2X — Revenu annuel V2G/V2X (directive #4 + #13)
  // ============================================================================
  /**
   * Calcule le revenu V2X annuel.
   * @param {object} state - state.borne.modele_key === "sigenergy_v2x" OU state.solar.with_v2x
   * @returns {object} { eligible, ev_kwh, revenue }
   */
  function calcV2X(state) {
    const eligible = (state?.borne?.modele_key === "sigenergy_v2x") || !!state?.solar?.with_v2x;
    if (!eligible) return { eligible: false, ev_kwh: 0, revenue: 0 };
    const ev_kwh = ((state.client?.ev_km_per_year || 0) * (state.client?.ev_kwh_per_100km || 0)) / 100;
    const revenue = ev_kwh * 0.15; // 0,15 $/kWh injection bidirectionnelle (estim HQ V2X)
    return { eligible, ev_kwh, revenue };
  }

  /**
   * 💰 LogisVert (Hydro-Québec) — Subvention solaire résidentielle
   *
   * NOUVELLE FORMULE 2026 (directive JF #1, 20 mai 2026) :
   *   HT_solaire_admissible = solar_sell + onduleur_sell + install_solaire_sell
   *                         + cablage_pv_sell + rsd_sell
   *   logisvert = min(kW × 1000$, 40% × HT_solaire_admissible)
   *
   * EXCLUSIONS (non admissibles) :
   *   • Batterie (Powerwall, EQ4000, CM4000/CS4000) — exclusion explicite HQ
   *   • Borne VÉ (couverte par Roulez Vert)
   *   • Mise à niveau panneau électrique
   *   • Câblage AC borne / 240V
   *   • Permis municipal
   *   • Services internes vendeur
   *
   * @param {object} state
   * @param {number|object} ht_admissible_or_calc - Soit un nombre (HT solaire admissible),
   *                                                 soit l'objet calc d'où on extrait le HT admissible
   * @returns {number} montant LogisVert ($)
   */
  function calcLogisVert(state, ht_admissible_or_calc) {
    const D = window.RAYCOM_DATA_2026;
    if (!state?.solar || state.solar.kw <= 0) return 0;

    // Détermine HT_solaire_admissible
    let ht_admissible = 0;
    if (typeof ht_admissible_or_calc === "number") {
      // Mode legacy : argument passé directement (utilisé par les tests originaux)
      ht_admissible = ht_admissible_or_calc;
    } else if (ht_admissible_or_calc && typeof ht_admissible_or_calc === "object") {
      // Mode 2026 : extraire du calc — solar + install + onduleur(=battery_sell SI bundle inclut onduleur)
      // + câblage PV (options dont code OPT-PV-*-METRE)
      const calc = ht_admissible_or_calc;
      ht_admissible = (calc.solar_sell || 0) + (calc.install_sell || 0);
      // Câblage PV (admissible LogisVert)
      const opts = state.options_electriques || [];
      const cablage_pv = opts
        .filter(o => o.code && /^OPT-PV-.+-METRE$/.test(o.code))
        .reduce((s, o) => s + (o.sell || 0) * (o.qty || 1), 0);
      ht_admissible += cablage_pv;
      // RSD ProJoy obligatoire (admissible)
      const rsd = opts.filter(o => o.code === "OPT-PROJOY-RSD" || o.code === "FRK-5101081")
        .reduce((s, o) => s + (o.sell || 0) * (o.qty || 1), 0);
      ht_admissible += rsd;
    }

    const isIles = state.client?.address_postal &&
      D.subventions.logisvert.iles_madeleine_fsa.some(fsa => state.client.address_postal.toUpperCase().startsWith(fsa));
    const ratePerKw = isIles
      ? D.subventions.logisvert.montant_iles_madeleine_par_kw
      : D.subventions.logisvert.montant_par_kw;

    // Plafonné à 40 kW résid (au-delà = commercial → autre subvention)
    const kw_effectif = Math.min(state.solar.kw, D.subventions.logisvert.puissance_max_kw);
    const by_kw = kw_effectif * ratePerKw;
    const cap_pct = ht_admissible * D.subventions.logisvert.plafond_pct_cout_admissible;

    return Math.round(Math.min(by_kw, cap_pct));
  }

  function calcRoulezVert(state) {
    const D = window.RAYCOM_DATA_2026;
    if (!state.borne || !state.borne.qty || state.borne.qty === 0) return 0;

    const qty = state.borne.qty;
    const building = state.building_type;

    if (building === "residentiel_unifam") {
      return Math.min(1, qty) * D.subventions.roulez_vert_resid.montant_max;
    }
    if (building === "multilog") {
      const rv = Math.min(qty * D.subventions.roulez_vert_zev_multilog.roulez_vert_max_par_borne, qty * 5000);
      const zev = Math.min(D.subventions.roulez_vert_zev_multilog.zev_max_par_projet, qty * D.subventions.roulez_vert_zev_multilog.zev_par_borne);
      return rv + zev;
    }
    return 0;
  }

  // ============================================================================
  // 💼 SUBVENTIONS COMMERCIALES (directive JF #6, 20 mai 2026)
  // ============================================================================
  /**
   * Calcule les subventions commerciales cumulables pour projets > résidentiel/multilog.
   * Sources : Solutions efficaces HQ (1000$/kW max 1MW) + CII fédéral 30% technologies propres
   *           + PIVEZ + RV milieu travail (bornes).
   *
   * @param {object} state - state.building_type ∈ com_petit | com_moyen | com_grand
   * @param {object} calc - { solar_sell, install_sell, battery_sell, borne_sell, ht }
   * @returns {object} { solutions_efficaces, cii_federal, pivez, rv_milieu_travail, total, cumul_logisvert }
   */
  function calcCommercialSubventions(state, calc) {
    const D = window.RAYCOM_DATA_2026;
    const subv = D.subventions?.commercial || {};
    const isCommercial = ["com_petit", "com_moyen", "com_grand"].includes(state?.building_type);
    if (!isCommercial) {
      return { solutions_efficaces: 0, cii_federal: 0, pivez: 0, rv_milieu_travail: 0, total: 0, cumul_logisvert: 0 };
    }

    const kw = state.solar?.kw || 0;
    // HT admissible commercial = solaire + install + onduleur (= battery_sell uniquement si bundle contient onduleur)
    const ht_admissible = (calc?.solar_sell || 0) + (calc?.install_sell || 0);

    // Solutions efficaces HQ : 1000$/kW, max 1 MW, plafond 40% HT admissible
    const max_kw = subv.solutions_efficaces_max_kw || 1000;
    const kw_eligible = Math.min(kw, max_kw);
    const se_by_kw = kw_eligible * (subv.solutions_efficaces_par_kw || 1000);
    const se_cap_pct = ht_admissible * 0.40;
    const solutions_efficaces = Math.round(Math.min(se_by_kw, se_cap_pct));

    // CII fédéral 30% technologies propres (cumulable avec LogisVert/Solutions efficaces)
    const cii_federal = Math.round(ht_admissible * (subv.cii_federal_pct || 0.30));

    // PIVEZ (programme installation VÉ en milieu de travail) — par borne commerciale
    // RV milieu travail — par borne commerciale
    const borne_qty = state.borne?.qty || 0;
    const pivez = isCommercial ? borne_qty * (subv.pivez_par_borne || 0) : 0;
    const rv_milieu_travail = isCommercial ? borne_qty * (subv.rv_milieu_travail_par_borne || 0) : 0;

    const total = solutions_efficaces + cii_federal + pivez + rv_milieu_travail;
    return { solutions_efficaces, cii_federal, pivez, rv_milieu_travail, total, cumul_logisvert: 0 };
  }

  // ============================================================================
  // 💼 COMMISSION VENDEUR — RETIRÉE (V7, directive JF 29 mai 2026)
  // ============================================================================

  // ============================================================================
  // 💳 PAYMENT SCHEDULE (directive JF #8 adapté, stocké dans state JSONB)
  // ============================================================================
  /**
   * Pré-génère le calendrier de paiements selon le scénario de la soumission.
   * 3 scénarios standards Raycom 2026 :
   *   A. Borne seule       → 1 paiement Interac/CC jour install
   *   B. Solaire financé   → dépôt 1000$ + balance FinanceIT à la mise en service
   *   C. Solaire comptant  → dépôt 1000$ + acompte 50% HT post-DA HQ + balance fin
   *
   * @param {object} state
   * @param {object} calc - { ht, ttc, fin }
   * @returns {Array<object>} installments [{ sequence, trigger_event, amount_cad, amount_pct, label, status }]
   */
  function buildPaymentSchedule(state, calc) {
    if (!calc || !calc.ttc) return [];
    const ttc = calc.ttc;
    const ht = calc.ht || 0;
    const project_type = state?.project_type || "solaire_batterie";
    const planId = state?.client?.financing_plan_id || "comptant";
    const isFinanced = planId === "fit_240m" || planId === "fit_promo_0_3";

    // Scénario A : Borne seule (project_type === "borne")
    if (project_type === "borne") {
      return [{
        sequence: 1,
        trigger_event: "on_install_day",
        amount_cad: Math.round(ttc * 100) / 100,
        amount_pct: 100,
        label: "Paiement complet · jour d'installation",
        method_options: ["interac", "cc", "transfer"],
        status: "pending"
      }];
    }

    // Scénario B : Solaire financé (FinanceIT)
    if (isFinanced) {
      const deposit = 1000;
      const balance = Math.max(0, ttc - deposit);
      return [
        {
          sequence: 1, trigger_event: "on_acceptance",
          amount_cad: deposit, amount_pct: Math.round((deposit / ttc) * 1000) / 10,
          label: "Dépôt initial · à l'acceptation de la soumission",
          method_options: ["interac", "cc", "transfer"],
          status: "pending"
        },
        {
          sequence: 2, trigger_event: "on_commissioning",
          amount_cad: Math.round(balance * 100) / 100, amount_pct: Math.round((balance / ttc) * 1000) / 10,
          label: "Balance via FinanceIT · à la mise en service",
          method_options: ["financit"],
          status: "pending"
        }
      ];
    }

    // Scénario C : Solaire comptant (ou CC) — dépôt + 50% post-DA + balance
    const deposit = 1000;
    const acompte_pct = 0.50;
    const acompte = Math.round((ht * acompte_pct - deposit) * 100) / 100;
    const taxes = (calc.tps || 0) + (calc.tvq || 0);
    const balance = Math.round((ht * (1 - acompte_pct) + taxes) * 100) / 100;
    return [
      {
        sequence: 1, trigger_event: "on_acceptance",
        amount_cad: deposit, amount_pct: Math.round((deposit / ttc) * 1000) / 10,
        label: "Dépôt initial · à l'acceptation",
        method_options: ["interac", "cc", "transfer"],
        status: "pending"
      },
      {
        sequence: 2, trigger_event: "on_hq_approval",
        amount_cad: acompte, amount_pct: Math.round((acompte / ttc) * 1000) / 10,
        label: "Acompte 50% du HT · à l'autorisation HQ (post-DA)",
        method_options: ["interac", "transfer"],
        status: "pending"
      },
      {
        sequence: 3, trigger_event: "on_commissioning",
        amount_cad: balance, amount_pct: Math.round((balance / ttc) * 1000) / 10,
        label: "Balance + taxes · à la mise en service",
        method_options: ["interac", "transfer"],
        status: "pending"
      }
    ];
  }

  // ============================================================================
  // 💳 FINANCEMENT
  // ============================================================================

  function calcFinanceit(state, ttc) {
    const D = window.RAYCOM_DATA_2026;
    const planId = state.client?.financing_plan_id || "comptant";
    const plan = D.financement.plans.find(p => p.id === planId);
    if (!plan || plan.id === "comptant") {
      return { plan, monthly: 0, biweekly: 0, weekly: 0, total_interest: 0, fee: 0 };
    }
    if (plan.id === "fit_promo_0_3") {
      // FinanceIT 0 $ / 0 % pendant 3 mois, puis conversion au plan standard (fit_240m)
      if (ttc < (plan.min_amount || 1000)) {
        return { plan, monthly: 0, biweekly: 0, weekly: 0, total_interest: 0, fee: 0,
                 warning: `Montant minimum ${fmtCAD(plan.min_amount)} pour FinanceIT` };
      }
      const after = D.financement.plans.find(p => p.id === (plan.then_plan_id || "fit_240m")) || {};
      const P = ttc;
      const r = ((after.apr || 0) / 100) / 12;
      const n = after.months || 240;
      let monthly = r === 0 ? P / n : P * (r / (1 - Math.pow(1 + r, -n)));
      if (!isFinite(monthly)) monthly = 0;
      return {
        plan, monthly,
        biweekly: (monthly * 12) / 26,
        weekly: (monthly * 12) / 52,
        total_interest: monthly * n - P,
        fee: 0,
        promo_free_months: plan.months || 3,
        after_plan: after
      };
    }
    if (plan.id === "fit_240m") {
      if (ttc < (plan.min_amount || 1000)) {
        return { plan, monthly: 0, biweekly: 0, weekly: 0, total_interest: 0, fee: 0,
                 warning: `Montant minimum ${fmtCAD(plan.min_amount)} pour FinanceIT` };
      }
      const P = ttc; // pas de premier 1000$ retiré dans v2026
      const r = (plan.apr / 100) / 12;
      const n = plan.months;
      let monthly;
      if (r === 0) monthly = P / n;
      else monthly = P * (r / (1 - Math.pow(1 + r, -n)));
      if (!isFinite(monthly)) monthly = 0;
      return {
        plan, monthly,
        biweekly: (monthly * 12) / 26,
        weekly: (monthly * 12) / 52,
        total_interest: monthly * n - P,
        fee: ttc * (plan.fee_pct || 0)
      };
    }
    return { plan, monthly: 0, biweekly: 0, weekly: 0, total_interest: 0, fee: 0 };
  }

  // ============================================================================
  // 🎯 FULL CALC 2026 — Master function
  // ============================================================================

  /**
   * Calculateur maître 2026.
   * @param {object} state - État complet de la soumission
   * @returns {object} calc - Résultat complet avec breakdown
   */
  function fullCalc2026(state) {
    const D = window.RAYCOM_DATA_2026;
    if (!state) return zeroCalc();

    const allWarnings = [];
    const allErrors = [];

    // ─── 1) ZONE
    const zone = detectZone(state.client?.address_postal);
    if (zone.error) allErrors.push(zone.error);
    if (zone.warning) allWarnings.push(zone.warning);

    // V3-E : Détermine si on est en mode tout-en-un (batterie sans solaire)
    const isBatteryOnly = state.project_type === "batterie";
    const isBorne = state.project_type === "borne";
    const hasSolar = state.solar && state.solar.panneaux > 0 && !isBorne && !isBatteryOnly;

    // ─── 2) SOLAIRE (priorité 1 utilisateur — calc parfait)
    let solarCalc = null;
    let solar_cost = 0; // approximé via marge inverse depuis sell (18% solaire)
    let solar_sell = 0;
    if (hasSolar) {
      solarCalc = calcSolaireAuWatt({
        panneaux: state.solar.panneaux,
        pitch_key: state.solar.pitch_key,
        floors: state.solar.floors,
        roof_key: state.solar.roof_type
      });
      if (solarCalc.errors.length) allErrors.push(...solarCalc.errors);
      if (solarCalc.warnings.length) allWarnings.push(...solarCalc.warnings);
      solar_sell = solarCalc.vendant_solaire_total;
      // Coûtant solaire : à 18% marge, cost = sell × 0.82
      solar_cost = solar_sell * (1 - D.solar_rules.margin_solar_pct);
      // Injecte kw dérivé dans state pour les calculs downstream (LogisVert, Production, etc.)
      // sans muter le state original
      state = { ...state, solar: { ...state.solar, kw: solarCalc.kw } };
    }

    // ─── 3) FORFAIT INSTALL — solaire OU batterie tout-en-un
    let forfait = null;
    let install_cost = 0, install_sell = 0;
    const needsForfait = hasSolar || isBatteryOnly;
    if (needsForfait) {
      // V3-E : Pour tout-en-un, le nombre d'onduleurs Tesla = pw_count
      const tesla_onds = state.solar?.battery_brand === "tesla" && isBatteryOnly
        ? Math.max(1, Math.min(3, state.solar.pw_count || 1))
        : state.solar?.onds || 1;
      forfait = pickForfait2026({
        brand: state.solar?.battery_brand === "tesla" ? "tesla" : "fox_sig",
        onds: tesla_onds,
        has_dim: !!state.solar?.with_dim || (!!state.solar?.battery_brand && state.solar.battery_brand !== "none"),
        has_battery: !!state.solar?.battery_brand && state.solar.battery_brand !== "none",
        backup_mode: state.solar?.backup_mode || "none"
      });
      if (forfait) {
        install_cost = forfait.cost;
        install_sell = forfait.sell;
      }
    }

    // ─── 4) ONDULEUR / BATTERIE
    let battery_cost = 0, battery_sell = 0, battery_label = null;
    let batteryOnlyCalc = null;
    if (state.solar?.battery_brand && state.solar.battery_brand !== "none") {
      // Batterie (tout-en-un OU couplée au solaire) — calcul exhaustif via calcBatteryOnly.
      // L'onduleur hybride (PW3 / Fox H1 / SigenStor) est inclus ici ; la table solaire « au watt »
      // n'inclut PAS d'onduleur (confirmé JF) → aucun double-comptage onduleur.
      batteryOnlyCalc = calcBatteryOnly({
        brand: state.solar.battery_brand,
        pw_count: state.solar.pw_count || 1,
        dc_expansions: state.solar.dc_expansions || 0,
        battery_bundle_id: state.solar.battery_bundle_id,
        sig_battery_kwh: state.solar.sig_battery_kwh || 9,
        sig_battery_qty: state.solar.sig_battery_qty || 2,
        sig_with_loadhub: !!state.solar.with_dim
      });
      battery_cost = batteryOnlyCalc.cost;
      battery_sell = batteryOnlyCalc.sell;
      battery_label = batteryOnlyCalc.label;
    } else if (state.solar?.battery_bundle_id) {
      // Mode solaire+batterie classique — bundle Fox
      const id = state.solar.battery_bundle_id;
      const foxAio = D.fox_ess.aio.bundles.find(b => b.id === id);
      const foxH1 = D.fox_ess.h1_hybrid.bundles.find(b => b.id === id);
      const bundle = foxAio || foxH1;
      if (bundle) {
        battery_cost = bundle.cost;
        battery_sell = bundle.sell;
        battery_label = bundle.label;
      }
    }

    // ─── 5) BORNES EV
    let borneCalc = null;
    if (state.borne && state.borne.qty > 0) {
      borneCalc = calcBorneEV({
        // V3 : mode multi-cables combinables (priorité si cables.* > 0)
        cables: state.borne.cables || null,
        // Legacy fallback (1 type unique)
        slider_key: state.borne.slider_key || "B_garage_pvc",
        cable_m: state.borne.cable_m || 5,
        modele_key: state.borne.client_supplied ? "fournie_client" : (state.borne.modele_key || "tesla_gen3_nacs"),
        qty: state.borne.qty,
        dcc_code: state.borne.dcc_code || null,
        dsdc: state.borne.dsdc || false
      });
    }
    const borne_cost = borneCalc?.cost || 0;
    const borne_sell = borneCalc?.sell || 0;

    // ─── 6) OPTIONS ÉLECTRIQUES
    const options = state.options_electriques || [];
    const options_cost = options.reduce((s, o) => s + (o.cost || 0) * (o.qty || 1), 0);
    const options_sell = options.reduce((s, o) => s + (o.sell || 0) * (o.qty || 1), 0);

    // ─── 7) SERVICES
    const services = state.services || [];
    const services_cost = services.reduce((s, sv) => s + (sv.cost || 0) * (sv.qty || 1), 0);
    const services_sell = services.reduce((s, sv) => s + (sv.sell || 0) * (sv.qty || 1), 0);

    // ─── 8) ZONE EXTRA
    const zone_cost = zone.sell_partenaire || 0; // approximation coûtant
    const zone_sell = zone.sell_client || 0;

    // ─── 9) TOTAUX HT
    const cost_total = solar_cost + install_cost + battery_cost + borne_cost + options_cost + services_cost + zone_cost;
    const ht_solaire_pur = solar_sell; // pour LogisVert (batterie exclue)
    const ht = solar_sell + install_sell + battery_sell + borne_sell + options_sell + services_sell + zone_sell;
    const tps = ht * TPS_RATE;
    const tvq = ht * TVQ_RATE;
    const ttc = ht + tps + tvq;

    // ─── 10) PRODUCTION + ÉCONOMIES + SUBVENTIONS
    const production = state.solar ? calcProduction(state) : 0;
    const savings = state.solar ? calcSavings(state, production) : { base: 0, peak_bonus: 0, v2x: 0, total: 0 };

    // LogisVert (nouvelle formule : HT_admissible = solar + install + onduleur + cablage_pv + rsd)
    // (Directive JF #1, 20 mai 2026)
    const partialCalcForLogisVert = {
      solar_sell, install_sell,
      onduleur_sell: 0  // pour l'instant inclus dans battery_sell — à raffiner si on sépare
    };
    const logisvert = calcLogisVert(state, partialCalcForLogisVert);

    // Subventions commerciales (directive JF #6)
    const commercial_subv = calcCommercialSubventions(state, { solar_sell, install_sell, battery_sell, borne_sell, ht });

    const rve = calcRoulezVert(state);
    const total_grant = logisvert + rve + (commercial_subv.total || 0);
    const net_cost = Math.max(0, ttc - total_grant);

    // V2X — revenu annuel (directive JF #4 + #13)
    const v2x = calcV2X(state);

    const payback = savings.total > 0 ? net_cost / savings.total : Infinity;
    const savings_25 = savings.total * 25 * 0.90;

    // ─── 11) FINANCEMENT
    const fin = calcFinanceit(state, ttc);

    // ─── 11b) PAYMENT SCHEDULE (directive JF #8 — stocké dans state JSONB)
    const payment_schedule = buildPaymentSchedule(state, { ht, ttc, tps, tvq, fin });

    // ─── 12) MARGE
    const marge_brute = ht - cost_total;
    const marge_pct = ht > 0 ? (marge_brute / ht) * 100 : 0;
    const marge_ajustee = marge_brute - fin.fee;
    const marge_aj_pct = ht > 0 ? (marge_ajustee / ht) * 100 : 0;

    // ─── 12b) Commission vendeur retirée (V7)

    // ─── 13) KPIs
    const tariff = D.hq_tariffs.find(t => t.id === state.client?.hq_tariff) || D.hq_tariffs[0];
    const annual_kwh = state.client?.bill_input_mode === "dollars"
      ? ((state.client.annual_hq_bill || 0) * 100 / tariff.rate)
      : (state.client?.annual_hq_kwh || 0);
    const coverage_pct = annual_kwh > 0 ? Math.min(150, (production / annual_kwh) * 100) : 0;
    const battery_kwh_total = (batteryOnlyCalc && batteryOnlyCalc.kwh) || state.solar?.battery_kwh || 0;
    const daily_consumption = annual_kwh / 365;
    const autonomy_days = (daily_consumption > 0 && battery_kwh_total > 0) ? battery_kwh_total / (daily_consumption * 0.2) : 0;
    const home_value = ttc * 0.55;

    // Garde anti-devis « batterie à 0 $ » : une batterie sélectionnée DOIT être chiffrée
    // (bloque l'envoi/PDF via `blocking` si le matériel batterie n'a pas été calculé).
    if (state.solar?.battery_brand && state.solar.battery_brand !== "none" && battery_sell <= 0) {
      allErrors.push("Batterie sélectionnée mais non chiffrée — choisissez la capacité/le modèle de batterie avant de poursuivre.");
    }

    return {
      version: D.meta.version,
      // Inputs résolu
      solarCalc, forfait, borneCalc, batteryOnlyCalc, zone,
      // Cost breakdown
      solar_cost, solar_sell, install_cost, install_sell,
      battery_cost, battery_sell, battery_label,
      borne_cost, borne_sell,
      options_cost, options_sell,
      services_cost, services_sell,
      zone_cost, zone_sell,
      cost_total, ht_solaire_pur, ht, tps, tvq, ttc,
      // Subventions (résid + commercial)
      logisvert, rve, commercial_subv, total_grant, net_cost,
      // V2X (directive #13)
      v2x,
      // Production + économies
      production, savings, payback, savings_25,
      annual_kwh, coverage_pct, autonomy_days, home_value,
      // Financement
      fin,
      // Marge
      marge_brute, marge_pct, marge_ajustee, marge_aj_pct,
      // Commission vendeur retirée (V7)
      // Payment schedule (directive #8)
      payment_schedule,
      // Warnings + erreurs
      warnings: allWarnings,
      errors: allErrors,
      blocking: allErrors.length > 0
    };
  }

  function zeroCalc() {
    return {
      solarCalc: null, forfait: null, borneCalc: null, zone: null,
      solar_cost: 0, solar_sell: 0, install_cost: 0, install_sell: 0,
      battery_cost: 0, battery_sell: 0, battery_label: null,
      borne_cost: 0, borne_sell: 0, options_cost: 0, options_sell: 0,
      services_cost: 0, services_sell: 0, zone_cost: 0, zone_sell: 0,
      cost_total: 0, ht_solaire_pur: 0, ht: 0, tps: 0, tvq: 0, ttc: 0,
      logisvert: 0, rve: 0, total_grant: 0, net_cost: 0,
      production: 0, savings: { base: 0, peak_bonus: 0, v2x: 0, total: 0 },
      payback: Infinity, savings_25: 0,
      annual_kwh: 0, coverage_pct: 0, autonomy_days: 0, home_value: 0,
      fin: { plan: null, monthly: 0, biweekly: 0, weekly: 0, total_interest: 0, fee: 0 },
      marge_brute: 0, marge_pct: 0, marge_ajustee: 0, marge_aj_pct: 0,
      warnings: [], errors: [], blocking: false
    };
  }

  // ============================================================================
  // EXPORTS
  // ============================================================================
  window.RAYCAST_CALC_2026 = {
    // Formatters
    fmtCAD, fmtCAD0, fmtPct, fmtNum, NBSP,
    // Solaire
    calcSolaireAuWatt, lookupSolarPalier, pitchKeyToExcelCol,
    // Forfait
    pickForfait2026,
    // V3-E : Batterie tout-en-un
    calcBatteryOnly,
    // Borne
    calcBorneEV,
    // Zone
    detectZone,
    // Production / subventions / financement
    calcProduction, calcSavings, calcLogisVert, calcRoulezVert, calcFinanceit,
    // Phase 2 — nouvelles fonctions (directives JF #1, #2, #4, #6, #8, #13)
    calcCommercialSubventions, calcV2X, buildPaymentSchedule,
    // Master
    fullCalc2026, zeroCalc,
    // Constants
    TPS_RATE, TVQ_RATE, TAXES_COMBINED
  };
})();
