// Etape detail v3 — constellation layout (bubbles)
// Left: title, description, chips, big counter
// Right: bubble cluster — entreprises as floating circles with initials + name

const { useMemo, useEffect, useRef, useLayoutEffect, useState } = React;

// Compute initials from a company name (max 2 letters, uppercase)
function initialsOf(name) {
  if (!name) return '?';
  const cleaned = name
    .replace(/\([^)]*\)/g, '')
    .replace(/[\u2018\u2019']/g, '')
    .trim();
  const words = cleaned.split(/\s+/).filter(w => /^[A-Za-zÀ-ÿ]/.test(w));
  if (words.length === 0) return cleaned.slice(0, 2).toUpperCase();
  if (words.length === 1) return words[0].slice(0, 2).toUpperCase();
  // skip generic prefix words
  const SKIP = new Set(['les', 'le', 'la', 'des', 'de', 'du', 'l', 'd']);
  const filtered = words.filter(w => !SKIP.has(w.toLowerCase()));
  const pick = filtered.length >= 2 ? filtered : words;
  return (pick[0][0] + (pick[1]?.[0] || pick[0][1] || '')).toUpperCase();
}

// Deterministic pseudo-random for stable layout on rerender
function seededRng(seed) {
  let s = seed >>> 0;
  return () => {
    s = (s * 1664525 + 1013904223) >>> 0;
    return s / 0xffffffff;
  };
}

// Pack circles with non-overlapping placement, biased toward the center.
// Strategy: candidate positions are sampled from a Gaussian-like distribution
// centred on the stage, so bubbles cluster in the middle while still filling
// the available area when there are many items.
// Returns { bubbles: [{x,y,r,item,idx}], contentHeight }.
function packBubbles(items, width, height, seed = 1) {
  if (!items.length || !width || !height) return { bubbles: [], contentHeight: height };
  const rng = seededRng(seed);

  const count = items.length;
  let R_MIN, R_MAX;
  if (count <= 8)       { R_MIN = 70;  R_MAX = 105; }
  else if (count <= 15) { R_MIN = 58;  R_MAX = 88; }
  else if (count <= 30) { R_MIN = 48;  R_MAX = 72; }
  else if (count <= 60) { R_MIN = 40;  R_MAX = 58; }
  else                  { R_MIN = 34;  R_MAX = 50; }

  const margin = 6;
  const avgR = (R_MIN + R_MAX) / 2;
  const requiredArea = count * Math.PI * avgR * avgR / 0.62;
  const stageH = Math.max(height, Math.ceil(requiredArea / width));

  // Box-Muller transform: two uniform → one standard-normal sample
  function gaussianRng() {
    const u1 = Math.max(1e-9, rng());
    const u2 = rng();
    return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  }

  // Sample a candidate position biased toward (cx, cy) with given spread (sigma)
  function sampleCentered(cx, cy, sigmaX, sigmaY, r) {
    const x = cx + gaussianRng() * sigmaX;
    const y = cy + gaussianRng() * sigmaY;
    // Clamp so bubble stays within bounds
    return {
      x: Math.max(r + margin, Math.min(width  - r - margin, x)),
      y: Math.max(r + margin, Math.min(stageH - r - margin, y)),
    };
  }

  const bubbles = items.map((item, i) => {
    const r = R_MIN + rng() * (R_MAX - R_MIN);
    return { item, r, x: 0, y: 0, idx: i };
  });
  bubbles.sort((a, b) => b.r - a.r);

  const cx = width  / 2;
  const cy = stageH / 2;
  // Spread: roughly 28 % of each dimension — tight enough to look clustered,
  // loose enough to avoid excessive overlaps.
  const sigmaX = width  * 0.28;
  const sigmaY = stageH * 0.28;

  const placed = [];

  for (const b of bubbles) {
    let bestValid = null, bestValidScore = -Infinity;
    let bestInvalid = null, bestInvalidScore = -Infinity;

    const TRIES = 400;
    for (let t = 0; t < TRIES; t++) {
      const pos = t < TRIES * 0.75
        ? sampleCentered(cx, cy, sigmaX, sigmaY, b.r)
        : {
            x: b.r + margin + rng() * (width  - 2 * (b.r + margin)),
            y: b.r + margin + rng() * (stageH - 2 * (b.r + margin)),
          };

      let minSlack = Infinity;
      let ok = true;
      for (const p of placed) {
        const dx = p.x - pos.x, dy = p.y - pos.y;
        const d = Math.sqrt(dx * dx + dy * dy);
        const slack = d - (p.r + b.r + 4);
        if (slack < minSlack) minSlack = slack;
        if (slack < 0) ok = false;
      }

      if (ok) {
        const distToCenter = Math.sqrt((pos.x - cx) ** 2 + (pos.y - cy) ** 2);
        const score = -distToCenter;
        if (score > bestValidScore) { bestValidScore = score; bestValid = pos; }
        if (t < TRIES * 0.75) break;
      } else {
        if (minSlack > bestInvalidScore) { bestInvalidScore = minSlack; bestInvalid = pos; }
      }
    }
    const chosen = bestValid ?? bestInvalid;
    if (chosen) {
      b.x = chosen.x; b.y = chosen.y;
      placed.push(b);
    }
  }

  placed.sort((a, b) => a.idx - b.idx);
  return { bubbles: placed, contentHeight: stageH };
}

const AllCompaniesPanel = window.AllCompaniesPanel;

function BubbleCluster({ items, onPick }) {
  const wrapRef = useRef(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  const [revealCount, setRevealCount] = useState(0);

  useLayoutEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver((entries) => {
      const e = entries[0];
      if (!e) return;
      const { width, height } = e.contentRect;
      setSize({ w: width, h: height });
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  // Stable seed by item-set signature so layout reshuffles when filter changes
  const seed = useMemo(() => {
    let h = 0;
    items.forEach((e, i) => {
      const s = e.nom + i;
      for (let k = 0; k < s.length; k++) h = ((h << 5) - h + s.charCodeAt(k)) | 0;
    });
    return Math.abs(h) || 1;
  }, [items]);

  const { bubbles, contentHeight } = useMemo(
    () => packBubbles(items, size.w, size.h, seed),
    [items, size.w, size.h, seed]
  );

  // Stagger reveal
  useEffect(() => {
    setRevealCount(0);
    const total = items.length;
    const dur = total > 60 ? 12 : total > 30 ? 18 : 28;
    let n = 0;
    const id = setInterval(() => {
      n = Math.min(total, n + Math.max(2, Math.floor(total / 24)));
      setRevealCount(n);
      if (n >= total) clearInterval(id);
    }, dur);
    return () => clearInterval(id);
  }, [items]);

  // When contentHeight > viewport h, the wrapper is scrollable;
  // we set the inner canvas to that height.
  return (
    <div className="bubbles-scroll" ref={wrapRef}>
      <div
        className="bubbles"
        style={{ height: `${Math.max(size.h, contentHeight)}px` }}
      >
        {/* Decorative drift halo */}
        <div className="bubbles__halo" aria-hidden="true" />
        {bubbles.map((b, i) => {
          const visible = i < revealCount;
          const initials = initialsOf(b.item.nom); // unused, kept for stable keys
          const nameSize = Math.max(11, Math.round(b.r * 0.22));
          const floatDelay = (i * 0.31) % 4;
          return (
            <button
              key={`${b.item.nom}_${b.item._service}_${i}`}
              className="bubble"
              onClick={() => onPick(b.item)}
              title={b.item.nom}
              style={{
                left: `${b.x - b.r}px`,
                top: `${b.y - b.r}px`,
                width: `${b.r * 2}px`,
                height: `${b.r * 2}px`,
                opacity: visible ? 1 : 0,
                transform: visible ? 'scale(1)' : 'scale(0.4)',
                transition: 'opacity 540ms cubic-bezier(0.34,1.56,0.64,1), transform 540ms cubic-bezier(0.34,1.56,0.64,1)',
                animationDelay: `${floatDelay}s`,
              }}
            >
              <span className="bubble__name" style={{ fontSize: `${nameSize}px` }}>
                {b.item.nom}
              </span>
            </button>
          );
        })}
        {bubbles.length === 0 && items.length === 0 && (
          <div className="bubbles__empty">
            <div className="bubbles__empty-title">Aucune entreprise</div>
          </div>
        )}
      </div>
    </div>
  );
}

function StageDetail({ etape, onBack, onPickEntreprise, transversalNav, mergedNav, activeFilter: activeFilterFromRoute, onFilterChange, isTv }) {
  const filters = useMemo(() => window.SVAUtils.buildFilters(etape), [etape]);
  const allEnts = useMemo(() => window.SVAUtils.allEntreprisesInEtape(etape), [etape]);

  const activeFilter = activeFilterFromRoute ?? (filters[0]?.id || '__all__');
  const tabsRef = useRef(null);
  const [tabInd, setTabInd] = useState({ left: 0, width: 0 });
  const [showAllPanel, setShowAllPanel] = useState(false);

  const visibleEnts = useMemo(() => {
    if (activeFilter === '__all__') return allEnts;
    const f = filters.find(x => x.id === activeFilter);
    return f ? f.entreprises : allEnts;
  }, [activeFilter, filters, allEnts]);

  // Animated count
  const totalUnique = new Set(allEnts.map(e => e.nom)).size;
  const visibleUnique = new Set(visibleEnts.map(e => e.nom)).size;
  const [animCount, setAnimCount] = useState(visibleUnique);
  useEffect(() => {
    const start = performance.now();
    const from = animCount;
    const to = visibleUnique;
    let raf = 0;
    const tick = (t) => {
      const p = Math.min(1, (t - start) / 700);
      const e = 1 - Math.pow(1 - p, 3);
      setAnimCount(Math.round(from + (to - from) * e));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [visibleUnique]);

  const stageRef = useRef(null);

  const scrollToStage = () => {
    if (window.innerWidth <= 1023 && stageRef.current) {
      stageRef.current.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }
  };

  const activeNav = transversalNav || mergedNav;

  useEffect(() => {
    if (!activeNav || !tabsRef.current) return;
    const btns = tabsRef.current.querySelectorAll('button');
    const active = btns[activeNav.active];
    if (!active) return;
    setTabInd({ left: active.offsetLeft, width: active.offsetWidth });
  }, [activeNav?.active]);

  const num = etape.nom.split(/\.|\(/)[0].trim();
  const looksLikeNum = /^\d+[a-z]?$/i.test(num);
  const name = mergedNav
    ? mergedNav.mergedName
    : looksLikeNum
      ? etape.nom.replace(/^\d+[a-z]?\.\s*/i, '')
      : etape.nom;

  // Description: build from the etape's own description if available, else summary of services
  const description = useMemo(() => {
    if (etape.description) return etape.description;
    // build subtle summary: list service names truncated
    const names = filters.map(f => f.label).filter(Boolean);
    if (names.length === 0) return null;
    return names.slice(0, 4).join(' · ') + (names.length > 4 ? '…' : '');
  }, [etape, filters]);

  return (
    <div className="etape" key={etape.nom}>
      <div className="etape-bg">
        <div className="etape-bg__blob" />
        <div className="etape-bg__blob b2" />
      </div>

      <div className="etape-grid">
        {/* LEFT — title, intro, chips, counter */}
        <aside className="etape-side">
          <button className={`btn-back${isTv ? ' btn-back--tv' : ''}`} onClick={onBack}>
            <span aria-hidden="true">←</span> Retour au circuit
          </button>

          <div className="etape-side__head">
            <h1 className="etape-side__title">{name}</h1>
            {description && <p className="etape-side__desc">{description}</p>}
          </div>

          {mergedNav && etape.analyse && (
            <div className="etape-analyse">
              <div className="etape-analyse__heading">Analyse</div>
              <div className="etape-analyse__rule" />
              <div className="etape-analyse__body">
                <div className={`etape-analyse__icon etape-analyse__icon--${etape.analyseType === 'positive' ? 'pos' : 'neg'}`} aria-hidden="true">
                  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
                    {etape.analyseType === 'positive'
                      ? <><line x1="8" y1="3" x2="8" y2="13" /><line x1="3" y1="8" x2="13" y2="8" /></>
                      : <line x1="3" y1="8" x2="13" y2="8" />
                    }
                  </svg>
                </div>
                <p className="etape-analyse__text">{etape.analyse}</p>
              </div>
            </div>
          )}

          {activeNav && (
            <div className="trans-tabs" ref={tabsRef}>
              <div className="trans-tabs__ind" style={{ left: tabInd.left, width: tabInd.width }} />
              <button
                className={activeNav.active === 0 ? 'active' : ''}
                onClick={() => activeNav.onPick(0)}
              >Pour alumineries</button>
              <button
                className={activeNav.active === 1 ? 'active' : ''}
                onClick={() => activeNav.onPick(1)}
              >Général</button>
            </div>
          )}

          {!mergedNav && etape.analyse && (
            <div className="etape-analyse">
              <div className="etape-analyse__heading">Analyse</div>
              <div className="etape-analyse__rule" />
              <div className="etape-analyse__body">
                <div className={`etape-analyse__icon etape-analyse__icon--${etape.analyseType === 'positive' ? 'pos' : 'neg'}`} aria-hidden="true">
                  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round">
                    {etape.analyseType === 'positive'
                      ? <><line x1="8" y1="3" x2="8" y2="13" /><line x1="3" y1="8" x2="13" y2="8" /></>
                      : <line x1="3" y1="8" x2="13" y2="8" />
                    }
                  </svg>
                </div>
                <p className="etape-analyse__text">{etape.analyse}</p>
              </div>
            </div>
          )}

          {allEnts.length > 0 && (
            <div>
              <div className="etape-chips">
                <button className="chip chip--all" onClick={() => setShowAllPanel(true)}>
                  <span className="chip__lbl">Voir toutes les entreprises</span>
                  <span className="chip__count">{totalUnique}</span>
                </button>
              </div>
              <div className="etape-chips">
                {filters.map(f => (
                  <button
                    key={f.id}
                    className={`chip ${activeFilter === f.id ? 'active' : ''}`}
                    onClick={() => { onFilterChange(f.id); scrollToStage(); }}
                    title={f.label}
                  >
                    <span className="chip__lbl">{f.label}</span>
                    <span className="chip__count">{f.entreprises.length}</span>
                  </button>
                ))}
              </div>
            </div>
          )}

        </aside>

        {/* RIGHT — bubble constellation */}
        <div className="etape-stage" ref={stageRef}>
          <BubbleCluster
            items={visibleEnts}
            onPick={onPickEntreprise}
          />
        </div>
      </div>

      {showAllPanel && (
        <AllCompaniesPanel
          entreprises={allEnts}
          onClose={() => setShowAllPanel(false)}
          onPick={onPickEntreprise}
        />
      )}
    </div>
  );
}

window.StageDetail = StageDetail;
