// Configurator.jsx — "Build your plan" wizard.
//
// State: { sets, perSet: { sourceId: bool, ... } shared, pollMin }
// Live price = computePrice(state).
// Two preset functions return ready-made config objects.
// "Continue" routes to the existing #trial / quote modal with the
//   config pre-filled in the Notes field. (Stage 2 will swap this
//   for the real signup → Paddle subscription flow.)
//
// All copy uses translations from t.config (added to i18n.jsx).
// The `t` argument is the active locale's translation root.

const { useState, useMemo, useEffect } = React;

// --- Pricing model — must match the server-side validator (stage 2) ---
//
// Visible hierarchy (€/source/month at 30-min baseline):
//   Basic                      €2.00   cheapest
//   Proxy-backed (WH, Polovni) €4.50
//   Heavy premium              €11.00
//   On-request                 €15.00  most expensive
//
// Basic is now BELOW proxy-backed (was €6.50, above proxy). This fixes
// the "basic is more expensive than premium" inversion.
//
// Polling speed scales every source's rate by a single multiplier curve
// (no per-tier curve any more — same multiplier for everyone, matches the
// underlying scraper cost model better).

const SOURCE_RATE = {
  // Basic — direct HTTP or ScrapingBee classic (~1 credit/req), near-zero marginal cost
  3: 2.00, 4: 2.00, 7: 2.00, 11: 2.00, 14: 2.00,
  // Phase 2/2b/2c/3a basic-tier EU expansion (16-27)
  16: 2.00, 17: 2.00, 18: 2.00, 19: 2.00, 20: 2.00,
  21: 2.00, 22: 2.00, 23: 2.00, 25: 2.00, 26: 2.00, 27: 2.00,
  // Proxy-backed — Webshare residential bandwidth
  1: 4.50, 6: 4.50,
  // Heavy premium — ScrapingBee credits (~25-75 credits/req)
  2: 11.00, 5: 11.00, 9: 11.00, 12: 11.00, 13: 11.00,
  15: 11.00, // Hasznaltauto.hu — ScrapingBee premium (HU residential)
  24: 11.00, // AutoTrack.nl — ScrapingBee premium (NL residential)
  // On-request — ScrapingBee stealth tier (~75 credits/req)
  8: 15.00, 10: 15.00,
};

// Single poll multiplier curve, applied uniformly to all source rates.
const POLL_MULT = {
  60: 0.7,
  30: 1.0,
  15: 1.6,
  10: 2.4,
  5:  4.5,
  1: 12.0,
};

const POLL_OPTIONS = [60, 30, 15, 10, 5, 1].map(v => ({ v }));

const BASE_FEE       = 5.00;
const EXTRA_SET_FEE  = 3.00;
const PRICE_FLOOR    = 5.00;

function pollMultiplier(min) {
  return POLL_MULT[min] || 1.0;
}

// Lookup a source's tier from SOURCE_META (defined below).
function tierOf(sourceId) {
  const m = (window.__cfgSourceMeta || []).find(s => s.id === sourceId);
  return (m && m.tier) || 'basic';
}

// Per-source contribution at the active speed, before per-set multiplier.
function sourceRate(sourceId, pollMin) {
  return (SOURCE_RATE[sourceId] || 0) * pollMultiplier(pollMin);
}

// --- Presets — real bundled packages with flat marketing prices ---
//
// Each preset has a `flatPrice` that the configurator displays when the
// current config matches the preset *exactly*. Touch any field (sets,
// poll, sources) and the config falls back to the cost-plus formula
// below, becoming a "Custom plan".
//
// The flat prices are deliberately below the cost-plus total to give
// preset buyers a discount over piecemeal customization. Pro at €99
// vs cost-plus €127 = a €28/mo bundling discount; Starter at €9 vs
// cost-plus €24.50 = a €15.50/mo entry-tier discount.
//
// If you change a preset, ALSO update the matching pricing card copy
// in web/i18n.jsx ("From €9/mo" / "From €99/mo") so the card and the
// configurator stay aligned.

const PRESETS = [
  {
    id:       'starter',
    name:     'Starter',
    sets:     1,
    pollMin:  30,
    sources:  [3, 7, 11], // OLX.ba + AutoScout24 + Kleinanzeigen
    flatPrice: 9.00,
  },
  {
    id:       'pro',
    name:     'Pro',
    sets:     5,
    pollMin:  30,
    sources:  [2, 1, 7],  // Mobile.de + Willhaben + AutoScout24
    flatPrice: 99.00,
  },
];

const PRESET_STARTER = PRESETS[0];
const PRESET_PRO     = PRESETS[1];

// matchesPreset — exact-match check. Order-independent on sources.
function matchesPreset(cfg, preset) {
  if (!cfg || !preset) return false;
  if (cfg.sets !== preset.sets) return false;
  if (cfg.pollMin !== preset.pollMin) return false;
  if (cfg.sources.length !== preset.sources.length) return false;
  for (const s of cfg.sources) {
    if (!preset.sources.includes(s)) return false;
  }
  return true;
}

// activePreset — returns the matching preset object or null.
function activePreset(cfg) {
  return PRESETS.find(p => matchesPreset(cfg, p)) || null;
}

// inPresetEnvelope — true when cfg "extends" a preset: same set count,
// contains all of the preset's sources (extras allowed), poll can be
// anything. Within an envelope, the preset's flat price stays as the
// anchor and additions show as deltas (extras, speed upgrade) rather
// than the user dropping into full cost-plus pricing.
function inPresetEnvelope(cfg, preset) {
  if (cfg.sets !== preset.sets) return false;
  for (const s of preset.sources) {
    if (!cfg.sources.includes(s)) return false;
  }
  return true;
}

// activeEnvelope — first preset whose envelope cfg sits in (and isn't
// already an exact match for). Returns null if cfg is fully custom.
function activeEnvelope(cfg) {
  if (activePreset(cfg)) return null; // exact match handled separately
  for (const p of PRESETS) {
    if (inPresetEnvelope(cfg, p)) return p;
  }
  return null;
}

// priceBreakdown — single source of truth for what the user pays AND
// what the breakdown footer renders. Returns:
//   {
//     mode:  'starter-bundle' | 'pro-bundle'
//          | 'starter-plan'   | 'pro-plan'
//          | 'custom',
//     lines: [{ key, label, amount }, ...],
//     total: number,
//     preset: PresetObj | null,   // when on a preset/envelope
//   }
//
// The UI renders `lines` plus a "Total per month" row. Whatever lives
// in here is what the user sees — no double-counting because the lines
// ARE the price.
function priceBreakdown(cfg) {
  const exact = activePreset(cfg);
  const envelope = activeEnvelope(cfg);
  const mult = pollMultiplier(cfg.pollMin);

  // 1. Exact preset match → single bundle line at the flat price.
  if (exact) {
    const lines = [{
      key:    'bundle',
      label:  `${exact.name} package (${exact.sets} set${exact.sets===1?'':'s'}, ${exact.sources.length} sources, ${exact.pollMin} min)`,
      amount: exact.flatPrice,
    }];
    return {
      mode:   exact.id === 'starter' ? 'starter-bundle' : 'pro-bundle',
      lines,
      total:  exact.flatPrice,
      preset: exact,
    };
  }

  // 2. Preset envelope → flat anchor + deltas for extras and speed.
  if (envelope) {
    const lines = [{
      key:    'bundle',
      label:  `${envelope.name} package (${envelope.sets} set${envelope.sets===1?'':'s'}, ${envelope.sources.length} included source${envelope.sources.length===1?'':'s'}, 30 min)`,
      amount: envelope.flatPrice,
    }];

    // Speed upgrade: the cost of polling the included sources faster
    // than the preset's baseline 30-min poll. Scales by set count too,
    // so a 5-set Pro envelope at 5-min is correctly more expensive than
    // a 1-set Starter envelope at 5-min.
    const includedRateAt30 = envelope.sources.reduce(
      (acc, s) => acc + (SOURCE_RATE[s] || 0), 0,
    );
    const speedDelta = includedRateAt30 * envelope.sets * (mult - 1.0);
    if (speedDelta > 0.005) {
      lines.push({
        key:    'speed',
        label:  `Speed upgrade (every ${cfg.pollMin} min)`,
        amount: speedDelta,
      });
    }

    // Extras: every source NOT in the preset's included list.
    const includedSet = new Set(envelope.sources);
    const extras = cfg.sources.filter(s => !includedSet.has(s));
    if (extras.length > 0) {
      const extrasCost = extras.reduce(
        (acc, s) => acc + sourceRate(s, cfg.pollMin) * envelope.sets, 0,
      );
      lines.push({
        key:    'extras',
        label:  envelope.sets === 1
          ? `${extras.length} extra source${extras.length===1?'':'s'}`
          : `${extras.length} extra source${extras.length===1?'':'s'} × ${envelope.sets} sets`,
        amount: extrasCost,
      });
    }

    const total = Math.max(lines.reduce((a, l) => a + l.amount, 0), PRICE_FLOOR);
    return {
      mode:   envelope.id === 'starter' ? 'starter-plan' : 'pro-plan',
      lines,
      total,
      preset: envelope,
    };
  }

  // 3. Fully custom — cost-plus.
  const lines = [
    { key: 'base', label: 'Base fee', amount: BASE_FEE },
  ];
  if (cfg.sets > 1) {
    lines.push({
      key:    'extra-sets',
      label:  `${cfg.sets - 1} extra set${cfg.sets-1===1?'':'s'}`,
      amount: (cfg.sets - 1) * EXTRA_SET_FEE,
    });
  }
  if (cfg.sources.length > 0) {
    const perSet = cfg.sources.reduce((a, s) => a + sourceRate(s, cfg.pollMin), 0);
    lines.push({
      key:    'sources',
      label:  `${cfg.sources.length} source${cfg.sources.length===1?'':'s'} × ${cfg.sets} set${cfg.sets===1?'':'s'} @ ${cfg.pollMin} min`,
      amount: perSet * cfg.sets,
    });
  }
  const total = Math.max(lines.reduce((a, l) => a + l.amount, 0), PRICE_FLOOR);
  return { mode: 'custom', lines, total, preset: null };
}

// computePrice — convenience wrapper used by quote-handoff code etc.
function computePrice(cfg) {
  return priceBreakdown(cfg).total;
}

// --- Source ordering & metadata (mirrors webapp/src/constants/sources.ts) ---

// Customer-facing source grouping — four visible tiers matching the
// pricing hierarchy (cheapest → most expensive). Tier names mirror the
// SourceTier TypeScript type in webapp/src/constants/sources.ts:
//   basic   — direct HTTP, broad coverage   (€2/mo per slot @ 30 min)
//   proxy   — Webshare residential proxy    (€4.50/mo per slot)
//   premium — ScrapingBee single-page       (€11/mo per slot)
//   custom  — ScrapingBee stealth, on req.  (€15/mo per slot)
//
// SOURCE_META is rendered top-to-bottom in this order so the source
// grid visually ascends in price.
const SOURCE_META = [
  // Basic — direct HTTP or ScrapingBee classic (~1 credit/req)
  { id: 7,  name: 'AutoScout24',       tier: 'basic' },
  { id: 11, name: 'Kleinanzeigen',     tier: 'basic' },
  { id: 3,  name: 'OLX.ba',            tier: 'basic' },
  { id: 14, name: 'Gebrauchtwagen.at', tier: 'basic' },
  { id: 4,  name: 'Ricardo.ch',        tier: 'basic' },
  { id: 16, name: 'Otomoto.pl',        tier: 'basic' },
  { id: 17, name: 'Automoto.it',       tier: 'basic' },
  { id: 18, name: 'Leboncoin.fr',      tier: 'basic' },
  { id: 19, name: 'Coches.net',        tier: 'basic' },
  { id: 20, name: 'Marktplaats.nl',    tier: 'basic' },
  { id: 21, name: '2dehands.be',       tier: 'basic' },
  { id: 22, name: 'Sauto.cz',          tier: 'basic' },
  { id: 23, name: 'Autovit.ro',        tier: 'basic' },
  { id: 25, name: 'motor.es',          tier: 'basic' },
  { id: 26, name: 'Gratka.pl',         tier: 'basic' },
  { id: 27, name: 'Blocket.se',        tier: 'basic' },
  // Proxy — Webshare residential proxy
  { id: 1,  name: 'Willhaben',         tier: 'proxy' },
  { id: 6,  name: 'Polovniautomobili', tier: 'proxy' },
  // Premium — ScrapingBee premium proxy (~25-75 credits/req)
  { id: 2,  name: 'Mobile.de',         tier: 'premium' },
  { id: 5,  name: 'Tutti.ch',          tier: 'premium' },
  { id: 9,  name: 'Avto.net',          tier: 'premium' },
  { id: 12, name: 'AutoScout24.ch',    tier: 'premium' },
  { id: 13, name: 'Anibis.ch',         tier: 'premium' },
  { id: 15, name: 'Hasznaltauto.hu',   tier: 'premium' },
  { id: 24, name: 'AutoTrack.nl',      tier: 'premium' },
  // On-request — ScrapingBee stealth tier (premium customer ask only)
  { id: 8,  name: 'Subito.it',         tier: 'custom' },
  { id: 10, name: 'Njuskalo.hr',       tier: 'custom' },
];

// Expose for tierOf() lookup before component mounts.
window.__cfgSourceMeta = SOURCE_META;

// Single source of truth for "how many marketplaces" in landing copy.
// Derived from SOURCE_META so adding a source updates every mention.
// Landing.jsx substitutes the {{count}} token in i18n strings with this.
window.AUTOVIZ_SOURCE_COUNT = SOURCE_META.length;

const TIER_COPY = {
  basic:   { dot: '#22c55e', label: 'Basic' },
  proxy:   { dot: '#3b82f6', label: 'Standard' },
  premium: { dot: '#7C6CF5', label: 'Premium' },
  custom:  { dot: '#ec4899', label: 'On request' },
};

function fmtPrice(n) {
  // Always show 2 decimals, comma thousands.
  return '€' + n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}

// --- Component ---

function Configurator({ initialPreset = null, onClose, t }) {
  // Configurator copy lives under t.pricing.config (nested inside the
  // pricing block in i18n.jsx) so this stays grouped with related strings.
  const c = (t && t.pricing && t.pricing.config) || {};
  const [cfg, setCfg] = useState(() => {
    if (initialPreset === 'starter') return { ...PRESET_STARTER };
    if (initialPreset === 'pro')     return { ...PRESET_PRO };
    return { sets: 1, pollMin: 30, sources: [3] };
  });

  // Modal-mount flags. We mount the SignupModal and SourceRequestModal
  // on the configurator level so they share the page lock and don't
  // require lifting state up to <Landing>.
  const [signupOpen,        setSignupOpen]        = useState(false);
  const [sourceRequestOpen, setSourceRequestOpen] = useState(false);

  // Re-apply preset when prop changes (so clicking "Pro" while configurator
  // is open updates instead of doing nothing).
  useEffect(() => {
    if (initialPreset === 'starter') setCfg({ ...PRESET_STARTER });
    else if (initialPreset === 'pro') setCfg({ ...PRESET_PRO });
  }, [initialPreset]);

  const breakdown = useMemo(() => priceBreakdown(cfg), [cfg]);
  const price     = breakdown.total;
  // onPreset is true whenever cfg matches a preset exactly OR sits in
  // its envelope (i.e. anchored to Starter/Pro pricing rather than the
  // fully custom cost-plus formula). The Configurator uses this to mark
  // included sources, choose the price-tag badge, and render the right
  // breakdown shape.
  const onPreset  = breakdown.preset;
  const isBundle  = breakdown.mode === 'starter-bundle' || breakdown.mode === 'pro-bundle';

  function toggleSource(id) {
    setCfg(s => ({
      ...s,
      sources: s.sources.includes(id) ? s.sources.filter(x => x !== id) : [...s.sources, id],
    }));
  }

  function setSets(n) {
    const v = Math.max(1, Math.min(50, n));
    setCfg(s => ({ ...s, sets: v }));
  }

  function setPoll(min) {
    setCfg(s => ({ ...s, pollMin: min }));
  }

  // Continue → open SignupModal with the active config. The modal
  // handles the signup → Paddle handoff itself. If at least one of the
  // selected sources is on the "custom / on request" tier (subito,
  // njuskalo), we can't self-serve checkout yet — fall back to the
  // existing quote modal so the user reaches sales rather than hitting
  // a 503 on the Paddle endpoint.
  function onContinue() {
    if (cfg.sources.length === 0) return;

    const hasCustomTier = cfg.sources.some(id => {
      const m = SOURCE_META.find(s => s.id === id);
      return m && m.tier === 'custom';
    });

    if (hasCustomTier && typeof window.AutoVizQuote === 'function') {
      // Sales path: this is a quote, not self-serve.
      const sourceNames = cfg.sources
        .map(id => (SOURCE_META.find(s => s.id === id) || {}).name)
        .filter(Boolean).join(', ');
      const summary =
        `On-request plan via configurator:
- Sets: ${cfg.sets}
- Sources: ${sourceNames}
- Poll: every ${cfg.pollMin} min
- Computed monthly: ${fmtPrice(price)}
- Includes on-request sources (Subito/Njuskalo) — needs sales setup`;
      window.AutoVizQuote({ notes: summary, profiles: String(cfg.sets), speed: cfg.pollMin + 'm' });
      return;
    }

    setSignupOpen(true);
  }

  // Active checkout slug for the signup → checkout call:
  //   - exact preset match    → 'starter' / 'pro' (catalog price)
  //   - envelope OR fully custom → 'custom' (non-catalog inline price,
  //                                          computed server-side from cfg)
  const checkoutSlug = isBundle ? onPreset.id : 'custom';
  const checkoutPlanName = isBundle
    ? onPreset.name
    : (onPreset ? `${onPreset.name} (customized)` : 'Custom plan');

  // Flag is loaded from a sibling JSX module (flags.jsx). Pull it into a
  // capitalized local so JSX treats it as a component.
  const Flag = (window.SourceFlag) || (() => null);

  return (
    <div className="cfg" id="configurator">
      <div className="cfg__head">
        <div>
          <span className="section__kicker">{c.kicker || 'Build your plan'}</span>
          <h2 className="section__title">
            {c.title1 || 'Pay '}<em>{c.title2 || 'only for what you use.'}</em>
          </h2>
          <p className="section__subtitle">
            {c.subtitle || 'Pick how many searches, which marketplaces, and how fast you want alerts. Price updates live.'}
          </p>
        </div>
        <div className="cfg__price">
          {/* Price-tag pill — three states:
                • exact preset match  → "STARTER BUNDLE" / "PRO BUNDLE"
                • inside an envelope  → "STARTER PLAN"   / "PRO PLAN"
                • fully custom        → "CUSTOM PLAN"
              Bundle and envelope both keep the preset's flat-price anchor,
              the only difference is whether extras are present. */}
          <div className="cfg__price-tag">
            {onPreset
              ? (isBundle
                  ? (c.bundleTag || '{name} bundle').replace('{name}', onPreset.name).toUpperCase()
                  : (c.planTag   || '{name} plan'  ).replace('{name}', onPreset.name).toUpperCase())
              : (c.customTag || 'Custom plan').toUpperCase()}
          </div>
          <div className="cfg__price-amount">{fmtPrice(price)}</div>
          <div className="cfg__price-unit">{c.perMonth || '/ month'}</div>
          <div className="cfg__price-trial">
            {(c.trialBadge || '5 days free, then {p}/month').replace('{p}', fmtPrice(price))}
          </div>
        </div>
      </div>

      <div className="cfg__grid">
        {/* Sets */}
        <div className="cfg__section">
          <div className="cfg__section-head">
            <span className="cfg__section-num">01</span>
            <h3 className="cfg__section-title">{c.setsTitle || 'Search sets'}</h3>
            <p className="cfg__section-desc">
              {c.setsDesc || 'Each set is an independent search with its own filters. More sets = more parallel searches.'}
            </p>
          </div>
          <div className="cfg__stepper">
            <button className="cfg__step-btn" onClick={() => setSets(cfg.sets - 1)} disabled={cfg.sets <= 1} aria-label="Decrease">−</button>
            <div className="cfg__step-val">{cfg.sets}</div>
            <button className="cfg__step-btn" onClick={() => setSets(cfg.sets + 1)} disabled={cfg.sets >= 50} aria-label="Increase">+</button>
            <span className="cfg__step-label">{cfg.sets === 1 ? (c.set || 'set') : (c.sets || 'sets')}</span>
          </div>
        </div>

        {/* Poll */}
        <div className="cfg__section">
          <div className="cfg__section-head">
            <span className="cfg__section-num">02</span>
            <h3 className="cfg__section-title">{c.pollTitle || 'Alert speed'}</h3>
            <p className="cfg__section-desc">
              {c.pollDesc || 'Choose how quickly you want to hear about new listings. Faster alerts cost a bit more.'}
            </p>
          </div>
          <div className="cfg__poll">
            {POLL_OPTIONS.map(o => {
              const badge =
                o.v === 1  ? (c.fastest    || 'Fastest')    :
                o.v === 5  ? (c.faster     || 'Faster')     :
                o.v === 30 ? (c.bestValue  || 'Best value') :
                null;
              return (
                <button
                  key={o.v}
                  className={`cfg__poll-btn ${cfg.pollMin === o.v ? 'is-active' : ''}`}
                  onClick={() => setPoll(o.v)}
                >
                  <span className="cfg__poll-num">{o.v}</span>
                  <span className="cfg__poll-unit">{o.v === 1 ? (c.min || 'min') : (c.mins || 'min')}</span>
                  {badge && <span className="cfg__poll-badge">{badge}</span>}
                </button>
              );
            })}
          </div>
        </div>

        {/* Sources */}
        <div className="cfg__section cfg__section--full">
          <div className="cfg__section-head">
            <span className="cfg__section-num">03</span>
            <h3 className="cfg__section-title">{c.sourcesTitle || 'Listing sources'}</h3>
            <p className="cfg__section-desc">
              {c.sourcesDesc || 'Choose the marketplaces you want monitored. Pricing depends on market coverage and alert speed.'}
            </p>
          </div>
          <div className="cfg__sources">
            {SOURCE_META.map(s => {
              const enabled  = cfg.sources.includes(s.id);
              const tier     = TIER_COPY[s.tier] || TIER_COPY.basic;
              // When on a preset (bundle or envelope), the preset's own
              // sources are "Included" — they're inside the flat price.
              // Sources the user added on top still show their delta.
              const inPreset = !!(onPreset && onPreset.sources.includes(s.id));
              // Cost for adding/keeping this source as an extra. In
              // envelope mode this is multiplied by the preset's set
              // count (e.g. Pro = 5 sets); otherwise by cfg.sets.
              const setCount = onPreset ? onPreset.sets : cfg.sets;
              const itemCost = sourceRate(s.id, cfg.pollMin) * setCount;
              return (
                <button
                  key={s.id}
                  className={`cfg__src ${enabled ? 'is-on' : ''}`}
                  onClick={() => toggleSource(s.id)}
                  aria-pressed={enabled}
                >
                  <div className="cfg__src-flag"><Flag sourceId={s.id} size={18} /></div>
                  <div className="cfg__src-body">
                    <div className="cfg__src-name">{s.name}</div>
                    <div className="cfg__src-meta">
                      <span className="cfg__src-tier" style={{color: tier.dot}}>● {tier.label}</span>
                    </div>
                  </div>
                  <div className="cfg__src-price">
                    {inPreset
                      ? (c.included || 'Included')
                      : (enabled ? '+' + fmtPrice(itemCost) : fmtPrice(itemCost))}
                  </div>
                </button>
              );
            })}
            {/* "Add missing source" CTA — opens SourceRequestModal that
                hits POST /api/v1/source-request. Same admin pipeline as
                quote requests, just a different DB table. */}
            <button
              type="button"
              className="cfg__src cfg__src--add"
              onClick={() => setSourceRequestOpen(true)}
              aria-label="Add a marketplace we don't support yet"
            >
              <div className="cfg__src-flag cfg__src-flag--ghost">+</div>
              <div className="cfg__src-body">
                <div className="cfg__src-name">{c.addSourceTitle || 'Add a marketplace'}</div>
                <div className="cfg__src-meta">
                  <span className="cfg__src-tier" style={{color: '#9ca3af'}}>
                    {c.addSourceMeta || 'Request a source we don’t monitor yet'}
                  </span>
                </div>
              </div>
              <div className="cfg__src-price cfg__src-price--ghost">
                {c.addSourceCta || 'Tell us'}
              </div>
            </button>
          </div>
        </div>
      </div>

      <div className="cfg__footer">
        <div className="cfg__breakdown">
          {/* Lines come directly from priceBreakdown(cfg) — what's shown
              is exactly what's summed. No risk of breakdown drifting
              away from the displayed total. */}
          {breakdown.lines.map(line => (
            <div key={line.key} className="cfg__breakdown-row">
              <span>{line.label}</span>
              <span>{fmtPrice(line.amount)}</span>
            </div>
          ))}
          {/* On a preset bundle (exact match), show an explicit
              "Included sources" courtesy line so the user sees why the
              source rows are marked "Included" — even though it
              doesn't add to the total. */}
          {isBundle && (
            <div className="cfg__breakdown-row cfg__breakdown-row--muted">
              <span>{c.includedSourcesLine || 'Included sources'}</span>
              <span>{c.included || 'Included'}</span>
            </div>
          )}
          <div className="cfg__breakdown-row cfg__breakdown-row--total">
            <span>{c.totalMonthly || 'Total per month'}</span>
            <span>{fmtPrice(price)}</span>
          </div>
        </div>

        <div className="cfg__cta">
          <p className="cfg__trial-copy">
            {(c.trialCopy || 'Free for 5 days. Then {p}/month auto-charged. Cancel anytime in Settings — no charge if you cancel before the trial ends.')
              .replace('{p}', fmtPrice(price))}
          </p>
          <button
            className="btn btn--primary cfg__continue"
            onClick={onContinue}
            disabled={cfg.sources.length === 0}
            data-umami-event="configurator-continue"
          >
            {c.continue || 'Start 5-day free trial'}
          </button>
          {cfg.sources.length === 0 && (
            <p className="cfg__warn">{c.pickSource || 'Pick at least one source to continue.'}</p>
          )}
        </div>
      </div>

      {/* Signup + source-request modals. Mounted inline so they share
          the configurator's React tree (no portal needed for now). */}
      {(() => {
        const Signup = window.SignupModal;
        const SourceReq = window.SourceRequestModal;
        return (
          <>
            {Signup && (
              <Signup
                open={signupOpen}
                onClose={() => setSignupOpen(false)}
                planConfig={{
                  preset_id:   onPreset ? onPreset.id : null,
                  sets:        cfg.sets,
                  poll_min:    cfg.pollMin,
                  sources:     cfg.sources,
                  price_cents: Math.round(price * 100),
                  currency:    'EUR',
                  mode:        breakdown.mode,
                }}
                planName={checkoutPlanName}
                planSlug={checkoutSlug}
                monthlyPrice={price}
              />
            )}
            {SourceReq && (
              <SourceReq
                open={sourceRequestOpen}
                onClose={() => setSourceRequestOpen(false)}
                submittedFrom="landing"
              />
            )}
          </>
        );
      })()}
    </div>
  );
}

Object.assign(window, { Configurator, computePrice, PRESET_STARTER, PRESET_PRO });
