<svelte:options customElement={{ tag: "release-timeline", shadow: "none" }} />

<script>
  import { onMount, onDestroy, tick } from "svelte";
  import { fetchTimeline, connectSSE, formatElapsed, timeAgo } from "./lib/api.js";
  import { envColors, envLaneColor, envBadgeClasses, statusDotColor } from "./lib/colors.js";
  import { pipelineSummary, deployStageLabel, waitStageLabel, STATUS_CONFIG } from "./lib/status.js";

  // Props from attributes
  export let org = "";
  export let project = "";

  // Reactive state
  let timeline = [];
  let lanes = [];
  let initialLoading = true;  // only true until first successful load
  let error = null;
  let disconnectSSE = null;
  let now = Date.now();
  let timerInterval = null;

  // DOM refs for swim lane positioning
  let timelineEl = null;
  let laneBarData = {};

  const BAR_WIDTH = 20;
  const BAR_GAP = 4;
  const DOT_SIZE = 12;
  const IN_FLIGHT = new Set(["QUEUED", "RUNNING", "ASSIGNED"]);
  const DEPLOYED = new Set(["SUCCEEDED"]);

  // ── Data fetching ────────────────────────────────────────────────

  // Debounce re-fetches: multiple SSE events within 300ms only trigger one fetch
  let refetchTimer = null;

  function scheduleRefetch() {
    if (refetchTimer) return; // already scheduled
    refetchTimer = setTimeout(() => {
      refetchTimer = null;
      refreshData();
    }, 300);
  }

  async function loadData() {
    try {
      error = null;
      const data = await fetchTimeline(org, project);
      applyTimelineData(data.timeline, data.lanes);
      initialLoading = false;
      scheduleComputeLaneBars();
    } catch (e) {
      error = e.message;
      initialLoading = false;
    }
  }

  // Background refresh: merge new data without loading state
  async function refreshData() {
    try {
      const data = await fetchTimeline(org, project);
      applyTimelineData(data.timeline, data.lanes);
      scheduleComputeLaneBars();
    } catch (e) {
      // Silently ignore refresh failures — we still have the old data
      console.warn("[release-timeline] refresh failed:", e);
    }
  }

  // Merge new timeline data, preserving object identity where possible
  // to minimize DOM thrash. Uses slug as the stable key.
  function applyTimelineData(newTimeline, newLanes) {
    // Build a map of existing releases by slug for fast lookup
    const existingBySlug = new Map();
    for (const item of timeline) {
      if (item.kind === "release" && item.release) {
        existingBySlug.set(item.release.slug, item);
      }
    }

    // Merge: reuse existing objects when data hasn't changed
    const merged = newTimeline.map(newItem => {
      if (newItem.kind !== "release" || !newItem.release) return newItem;
      const existing = existingBySlug.get(newItem.release.slug);
      if (!existing) return newItem;
      // Shallow-compare key fields; if same, keep the old reference
      const oldR = existing.release;
      const newR = newItem.release;
      if (oldR.dest_envs === newR.dest_envs &&
          oldR.has_pipeline === newR.has_pipeline &&
          pipelineStagesEqual(oldR.pipeline_stages, newR.pipeline_stages) &&
          destinationsEqual(oldR.destinations, newR.destinations)) {
        return existing; // same reference = no DOM update
      }
      return newItem;
    });

    timeline = merged;
    lanes = newLanes;
  }

  function pipelineStagesEqual(a, b) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (a[i].status !== b[i].status || a[i].started_at !== b[i].started_at || a[i].completed_at !== b[i].completed_at) return false;
    }
    return true;
  }

  function destinationsEqual(a, b) {
    if (a.length !== b.length) return false;
    for (let i = 0; i < a.length; i++) {
      if (a[i].status !== b[i].status || a[i].completed_at !== b[i].completed_at) return false;
    }
    return true;
  }

  // ── SSE event handling ───────────────────────────────────────────

  function handleEvent(type, data) {
    if (type === "destination" && data.action === "status_changed") {
      handleDestinationUpdate(data);
    } else if (type === "release") {
      if (data.action === "created") {
        scheduleRefetch();
      } else if (data.action === "status_changed" || data.action === "updated") {
        handleReleaseUpdate(data);
      }
    } else if (type === "artifact" && (data.action === "created" || data.action === "updated")) {
      scheduleRefetch();
    } else if (type === "pipeline") {
      handlePipelineUpdate(data);
    }
  }

  function handleDestinationUpdate(data) {
    const status = data.metadata?.status;
    const destName = data.metadata?.destination_name || data.resource_id;
    const env = data.metadata?.environment;
    if (!status || !destName) return;

    let changed = false;
    timeline = timeline.map(item => {
      if (item.kind !== "release" || !item.release) return item;
      const r = item.release;

      // Check if this release has a matching destination
      const destIdx = r.destinations.findIndex(d => d.name === destName);
      if (destIdx === -1) return item; // no match, keep same reference

      changed = true;
      const newDests = r.destinations.map(d =>
        d.name === destName ? { ...d, status, ...(["SUCCEEDED","FAILED","TIMED_OUT","CANCELLED"].includes(status) ? { completed_at: new Date().toISOString() } : {}) } : d
      );
      const newEnvStatuses = newDests.map(d => `${d.environment}:${d.status || "PENDING"}`).join(",");

      const newStages = env ? r.pipeline_stages.map(s =>
        s.stage_type === "deploy" && s.environment === env ? { ...s, status: status === "ASSIGNED" ? "RUNNING" : status } : s
      ) : r.pipeline_stages;

      return {
        ...item,
        release: { ...r, destinations: newDests, dest_envs: newEnvStatuses, pipeline_stages: newStages }
      };
    });
    if (changed) scheduleComputeLaneBars();
  }

  function handleReleaseUpdate(data) {
    const status = data.metadata?.status;
    const env = data.metadata?.environment;
    if (status && env) {
      handleDestinationUpdate(data);
    } else {
      scheduleRefetch();
    }
  }

  function handlePipelineUpdate(data) {
    const stageStatus = data.metadata?.status;
    const stageEnv = data.metadata?.environment;
    const stageType = data.metadata?.stage_type;
    if (!stageStatus) {
      if (data.action === "created" || data.action === "updated") scheduleRefetch();
      return;
    }

    let changed = false;
    timeline = timeline.map(item => {
      if (item.kind !== "release" || !item.release) return item;
      const r = item.release;
      let stageChanged = false;
      const newStages = r.pipeline_stages.map(s => {
        if (stageEnv && s.stage_type === "deploy" && s.environment === stageEnv) {
          stageChanged = true;
          return { ...s, status: stageStatus, ...(s.started_at ? {} : { started_at: new Date().toISOString() }) };
        }
        if (stageType === "wait" && s.stage_type === "wait") {
          stageChanged = true;
          return { ...s, status: stageStatus };
        }
        return s;
      });
      if (!stageChanged) return item; // keep same reference
      changed = true;
      return { ...item, release: { ...r, pipeline_stages: newStages } };
    });
    if (changed) scheduleComputeLaneBars();
  }

  // ── Swim lane bar computation ────────────────────────────────────

  function parseEnvs(raw) {
    if (!raw) return [];
    return raw.split(",").map(s => s.trim()).filter(Boolean).map(entry => {
      const colon = entry.indexOf(":");
      if (colon === -1) return { env: entry, status: "SUCCEEDED" };
      return { env: entry.slice(0, colon), status: entry.slice(colon + 1) };
    });
  }

  // Debounce lane bar computation to one per frame
  let laneBarRaf = null;
  function scheduleComputeLaneBars() {
    if (laneBarRaf) return;
    laneBarRaf = requestAnimationFrame(() => {
      laneBarRaf = null;
      tick().then(computeLaneBars);
    });
  }

  function computeLaneBars() {
    if (!timelineEl) return;
    const timelineRect = timelineEl.getBoundingClientRect();
    if (timelineRect.height === 0) return;
    const timelineH = timelineRect.height;

    const cards = Array.from(timelineEl.querySelectorAll("[data-release]"));
    const newBarData = {};

    for (const lane of lanes) {
      const env = lane.name;
      let deployedCard = null, flightCard = null;
      let deployedIdx = -1, flightIdx = -1;

      for (let i = 0; i < cards.length; i++) {
        const entries = parseEnvs(cards[i].dataset.envs);
        for (const entry of entries) {
          if (entry.env !== env) continue;
          if (DEPLOYED.has(entry.status) && !deployedCard) { deployedCard = cards[i]; deployedIdx = i; }
          if (IN_FLIGHT.has(entry.status) && !flightCard) { flightCard = cards[i]; flightIdx = i; }
        }
      }

      const deployedTop = deployedCard ? deployedCard.getBoundingClientRect().top - timelineRect.top : null;
      const flightTop = flightCard ? flightCard.getBoundingClientRect().top - timelineRect.top : null;

      let solidH = 0;
      if (deployedTop !== null && flightTop !== null) {
        solidH = timelineH - Math.max(deployedTop, flightTop);
      } else if (deployedTop !== null) {
        solidH = timelineH - deployedTop;
      }

      const hasHatch = !!flightCard;
      let hatchTop = 0, hatchH = 0, isForward = false;
      if (flightCard) {
        isForward = deployedIdx === -1 || flightIdx < deployedIdx;
        const anchorY = deployedTop !== null ? deployedTop : timelineH;
        const topY = Math.min(anchorY, flightTop);
        const bottomY = Math.max(anchorY, flightTop);
        hatchTop = topY;
        hatchH = Math.max(bottomY - topY, 4);
      }

      const dots = [];
      for (const card of cards) {
        const entries = parseEnvs(card.dataset.envs);
        if (!entries.find(e => e.env === env)) continue;
        const avatar = card.querySelector("[data-avatar]");
        const anchor = avatar || card;
        const r = anchor.getBoundingClientRect();
        dots.push(r.top + r.height / 2 - timelineRect.top);
      }

      newBarData[env] = { solidH, hasHatch, hatchTop, hatchH, isForward, dots, color: envColors(env) };
    }

    laneBarData = newBarData;
  }

  // ── Hatch pattern SVG ────────────────────────────────────────────

  // Cache hatch pattern data URIs to avoid re-encoding on every render
  const hatchCache = new Map();
  function hatchPattern(color, bgColor) {
    const key = `${color}|${bgColor}`;
    let cached = hatchCache.get(key);
    if (cached) return cached;
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"><rect width="8" height="8" fill="${bgColor}"/><path d="M-2,2 l4,-4 M0,8 l8,-8 M6,10 l4,-4" stroke="${color}" stroke-width="1.5" opacity="0.6"/></svg>`;
    cached = `url("data:image/svg+xml,${encodeURIComponent(svg)}")`;
    hatchCache.set(key, cached);
    return cached;
  }

  // ── Lifecycle ────────────────────────────────────────────────────

  onMount(() => {
    loadData();
    // Update "time ago" labels every 10 seconds instead of every 1 second
    // — 1s resolution adds no value for "3m ago" style labels
    timerInterval = setInterval(() => { now = Date.now(); }, 10000);
  });

  onDestroy(() => {
    if (disconnectSSE) disconnectSSE();
    if (timerInterval) clearInterval(timerInterval);
    if (refetchTimer) clearTimeout(refetchTimer);
    if (laneBarRaf) cancelAnimationFrame(laneBarRaf);
  });

  // Connect SSE after first data load
  $: if (!initialLoading && !error && org && !disconnectSSE) {
    disconnectSSE = connectSSE(org, project, handleEvent);
  }

  // Recompute lane bars on window resize (debounced via rAF)
  function handleResize() { scheduleComputeLaneBars(); }

  // ── Helpers for template ─────────────────────────────────────────

  function elapsedStr(startedAt, completedAt, status) {
    if (!startedAt) return "";
    const start = new Date(startedAt).getTime();
    if (isNaN(start)) return "";
    if (completedAt && status !== "RUNNING" && status !== "QUEUED") {
      const end = new Date(completedAt).getTime();
      if (!isNaN(end)) return formatElapsed(Math.floor((end - start) / 1000));
    }
    return formatElapsed(Math.floor((now - start) / 1000));
  }

  // Unique key for each timeline item (used in keyed {#each})
  function itemKey(item) {
    if (item.kind === "release" && item.release) return `r:${item.release.slug}`;
    if (item.kind === "hidden") return `h:${item.count}:${(item.releases || [])[0]?.slug || ""}`;
    return `u:${Math.random()}`;
  }

  // Which deploy stages to show as badges on the summary line,
  // filtered to match the current pipeline state.
  function summaryShowsStage(summary, stageStatus) {
    if (!summary) return false;
    switch (summary.label) {
      case "Pipeline complete":  return stageStatus === "SUCCEEDED";
      case "Pipeline failed":    return stageStatus === "FAILED" || stageStatus === "RUNNING" || stageStatus === "ASSIGNED";
      case "Deploying to":       return stageStatus === "RUNNING" || stageStatus === "ASSIGNED";
      case "Queued":             return stageStatus === "QUEUED";
      case "Waiting for time window": return stageStatus === "RUNNING" || stageStatus === "ASSIGNED";
      default:                   return stageStatus !== "PENDING" && stageStatus !== "SUCCEEDED";
    }
  }

  $: laneCount = lanes.length;
  $: gutterWidth = laneCount * (BAR_WIDTH + BAR_GAP) + 8;
</script>

<svelte:window on:resize={handleResize} />

{#if initialLoading}
  <div class="max-w-5xl mx-auto p-12 text-center text-gray-400">
    <span class="w-5 h-5 inline-block border-2 border-gray-300 border-t-gray-600 rounded-full animate-spin"></span>
    <p class="mt-2 text-sm">Loading releases...</p>
  </div>
{:else if error}
  <div class="max-w-5xl mx-auto p-6 border border-red-200 rounded-lg text-center">
    <p class="text-red-600">{error}</p>
    <button class="mt-2 text-sm text-gray-500 hover:text-gray-900 underline" on:click={loadData}>Retry</button>
  </div>
{:else if timeline.length === 0}
  <div class="max-w-5xl mx-auto p-6 border border-gray-200 rounded-lg text-center">
    <p class="text-gray-600">No releases yet.</p>
    <p class="text-sm text-gray-400 mt-2">Create a release with <code class="bg-gray-100 px-1 rounded">forest release create</code></p>
  </div>
{:else}
  <div class="max-w-5xl mx-auto grid" style="grid-template-columns: {gutterWidth}px 1fr; grid-template-rows: 1fr auto;">
    <!-- Swim lane gutter -->
    <div class="flex" style="grid-row: 1;">
      {#each lanes as lane (lane.name)}
        {@const bar = laneBarData[lane.name]}
        {@const [barColor, lightColor] = bar?.color || [lane.color, "#e5e7eb"]}
        <div style="width: {BAR_WIDTH}px; margin-right: {BAR_GAP}px; position: relative;">
          {#if bar}
            {#if bar.hasHatch}
              <div class="lane-bar lane-pulse" style="position: absolute; left: 0; width: 100%; top: {bar.hatchTop}px; height: {bar.hatchH + (bar.solidH > 0 ? BAR_WIDTH / 2 : 0)}px; background-image: {bar.isForward ? hatchPattern(barColor, lightColor) : hatchPattern('#f59e0b', '#fef3c7')}; background-size: 8px 8px; background-repeat: repeat; border-radius: 9999px; z-index: 0;"></div>
            {/if}
            {#if bar.solidH > 0}
              <div class="lane-bar" style="position: absolute; bottom: 0; left: 0; width: 100%; height: {bar.solidH + (bar.hasHatch ? BAR_WIDTH / 2 : 0)}px; background: {barColor}; border-radius: 9999px; z-index: 1;"></div>
            {/if}
            {#each bar.dots as dotY, di (di)}
              <div class="lane-dot" style="position: absolute; left: 50%; transform: translateX(-50%); top: {dotY - DOT_SIZE/2}px; width: {DOT_SIZE}px; height: {DOT_SIZE}px; border-radius: 50%; background: #fff; border: 2px solid {barColor}; z-index: 2;"></div>
            {/each}
          {/if}
        </div>
      {/each}
    </div>

    <!-- Timeline cards -->
    <div bind:this={timelineEl} class="space-y-3 min-w-0" style="grid-row: 1;">
      {#each timeline as item (itemKey(item))}
        {#if item.kind === "release" && item.release}
          {@const release = item.release}
          <div data-release data-envs={release.dest_envs} class="border border-gray-200 rounded-lg overflow-hidden">
            <div class="px-4 py-3 flex items-center gap-3 flex-wrap">
              <div class="flex items-center gap-2 min-w-0 flex-1">
                <span class="inline-block w-6 h-6 rounded-full bg-gray-200 shrink-0" data-avatar></span>
                <a href="/orgs/{org}/projects/{release.project_name || project}/releases/{release.slug}" class="font-medium text-gray-900 hover:text-black truncate">
                  {release.title}
                </a>
              </div>
              <div class="flex items-center gap-4 text-xs text-gray-500 shrink-0 flex-wrap">
                {#if release.branch}
                  <span class="flex items-center gap-1">
                    <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A2 2 0 013 12V7a4 4 0 014-4z"/></svg>
                    {release.branch}
                  </span>
                {/if}
                {#if release.commit_sha}
                  <span class="font-mono">{release.commit_sha.slice(0, 7)}</span>
                {/if}
                <time>{timeAgo(release.created_at)}</time>
                {#if release.source_user}
                  <span class="flex items-center gap-1">
                    <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/></svg>
                    <a href="/users/{release.source_user}" class="hover:underline">{release.source_user}</a>
                  </span>
                {/if}
                {#if release.project_name && release.project_name !== project}
                  <a href="/orgs/{org}/projects/{release.project_name}" class="hover:underline">{release.project_name}</a>
                {/if}
              </div>
            </div>

            <!-- Summary + details -->
            <details class="border-t border-gray-100 group" on:toggle={scheduleComputeLaneBars}>
              <summary class="px-4 py-2 flex items-center gap-2 text-sm cursor-pointer list-none hover:bg-gray-50 flex-wrap">
                {#if release.has_pipeline && !pipelineSummary(release.pipeline_stages)}
                  <!-- Pipeline exists but not triggered yet -->
                  {@const envAllDone = release.env_groups && release.env_groups.length > 0 && release.env_groups.every(g => g.status === "SUCCEEDED")}
                  <svg class="w-3.5 h-3.5 text-purple-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
                  {#if envAllDone}
                    <svg class="w-4 h-4 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                    <span class="text-gray-500 text-sm">Deployed</span>
                  {:else}
                    <svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                    <span class="text-blue-600 text-sm">Queued</span>
                  {/if}
                {:else if release.has_pipeline && pipelineSummary(release.pipeline_stages)}
                  {@const summary = pipelineSummary(release.pipeline_stages)}
                  <svg class="w-3.5 h-3.5 text-purple-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
                  {#if summary.icon === "pulse"}
                    <span class="w-4 h-4 shrink-0 flex items-center justify-center"><span class="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></span></span>
                  {:else if summary.icon === "check-circle"}
                    <svg class="w-4 h-4 {summary.iconColor} shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else if summary.icon === "x-circle"}
                    <svg class="w-4 h-4 {summary.iconColor} shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else if summary.icon === "clock"}
                    <svg class="w-4 h-4 {summary.iconColor} shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else}
                    <svg class="w-4 h-4 text-gray-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2"/></svg>
                  {/if}
                  <span class="{summary.color} text-sm">{summary.label}</span>

                  {#each release.pipeline_stages as stage (stage.id || stage.environment || stage.stage_type)}
                    {#if stage.stage_type === "deploy" && summaryShowsStage(summary, stage.status)}
                      {@const badge = envBadgeClasses(stage.environment || "")}
                      {@const dot = statusDotColor(stage.status) || badge.dot}
                      <span class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full {badge.bg}">
                        {stage.environment}
                        <span class="w-1.5 h-1.5 rounded-full {dot}"></span>
                      </span>
                    {/if}
                  {/each}

                  <span class="text-xs text-gray-400">{summary.done}/{summary.total}</span>

                {:else if release.env_groups && release.env_groups.length > 0}
                  {@const allSucceeded = release.env_groups.every(g => g.status === "SUCCEEDED")}
                  {#if allSucceeded}
                    <svg class="w-4 h-4 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                    <span class="text-gray-500 text-sm">Deployed</span>
                  {:else}
                    {#each release.env_groups as group, gi (gi)}
                      {#if group.status !== "SUCCEEDED"}
                        {@const cfg = STATUS_CONFIG[group.status] || STATUS_CONFIG.SUCCEEDED}
                        {#if cfg.icon === "pulse"}
                          <span class="w-4 h-4 shrink-0 flex items-center justify-center"><span class="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></span></span>
                        {:else if cfg.icon === "check-circle"}
                          <svg class="w-4 h-4 {cfg.iconColor} shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                        {:else}
                          <svg class="w-4 h-4 {cfg.iconColor} shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                        {/if}
                        <span class="{cfg.color} text-sm">{cfg.label}</span>
                        {#each group.envs as env (env)}
                          {@const badge = envBadgeClasses(env)}
                          <span class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full {badge.bg}">
                            {env}
                            <span class="w-1.5 h-1.5 rounded-full {badge.dot}"></span>
                          </span>
                        {/each}
                      {/if}
                    {/each}
                  {/if}
                {:else}
                  <svg class="w-4 h-4 text-gray-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  <span class="text-gray-400 text-sm">Pending</span>
                {/if}

                <svg class="w-3 h-3 text-gray-400 shrink-0 ml-auto transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
              </summary>

              <!-- Release details -->
              <div class="px-4 py-3 border-t border-gray-100 space-y-3">
                {#if release.description}
                  <p class="text-sm text-gray-700">{release.description}</p>
                {/if}
                <div class="flex flex-wrap gap-x-6 gap-y-2 text-xs text-gray-500">
                  <span class="font-mono text-gray-400">{release.slug}</span>
                  {#if release.version}
                    <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">{release.version}</span>
                  {/if}
                </div>
              </div>

              <!-- Pipeline stages -->
              {#if release.has_pipeline}
                <div class="border-t border-gray-100">
                  {#each release.pipeline_stages as stage, i (stage.id || `${stage.stage_type}-${stage.environment}-${i}`)}
                    <div class="px-4 py-2.5 flex items-center gap-3 text-sm {i < release.pipeline_stages.length - 1 ? 'border-b border-gray-50' : ''} {stage.status === 'PENDING' ? 'opacity-50' : ''}">
                      {#if stage.status === "SUCCEEDED"}
                        <svg class="w-4 h-4 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                      {:else if stage.status === "RUNNING"}
                        <span class="w-4 h-4 shrink-0 flex items-center justify-center"><span class="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></span></span>
                      {:else if stage.status === "QUEUED"}
                        <svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                      {:else if stage.status === "FAILED"}
                        <svg class="w-4 h-4 text-red-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                      {:else}
                        <svg class="w-4 h-4 text-gray-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" stroke-width="2"/></svg>
                      {/if}

                      {#if stage.stage_type === "deploy"}
                        <span class="text-sm {stage.status === 'SUCCEEDED' ? 'text-gray-700' : stage.status === 'RUNNING' ? 'text-yellow-700' : stage.status === 'FAILED' ? 'text-red-700' : 'text-gray-400'}">
                          {deployStageLabel(stage.status)}
                        </span>
                        {@const badge = envBadgeClasses(stage.environment || "")}
                        <span class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full {badge.bg}">
                          {stage.environment}
                          <span class="w-1.5 h-1.5 rounded-full {badge.dot}"></span>
                        </span>
                      {:else if stage.stage_type === "wait"}
                        <span class="text-sm {stage.status === 'SUCCEEDED' ? 'text-gray-700' : stage.status === 'RUNNING' ? 'text-yellow-700' : 'text-gray-400'}">
                          {waitStageLabel(stage.status)} {stage.duration_seconds}s
                        </span>
                      {/if}

                      {#if stage.started_at && (stage.status === "RUNNING" || stage.status === "QUEUED" || stage.completed_at)}
                        <span class="text-xs text-gray-400 tabular-nums">{elapsedStr(stage.started_at, stage.completed_at, stage.status)}</span>
                      {/if}

                      <span class="ml-auto flex items-center gap-1 text-xs text-gray-400 shrink-0">
                        <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
                        pipeline
                      </span>
                    </div>
                  {/each}
                </div>
              {/if}

              <!-- Destinations -->
              {#each release.destinations as dest, i (dest.name)}
                {@const destBadge = envBadgeClasses(dest.environment || "")}
                <div class="px-4 py-2 flex items-center gap-3 text-sm {i < release.destinations.length - 1 ? 'border-b border-gray-50' : ''} border-t border-gray-100">
                  {#if dest.status === "SUCCEEDED"}
                    <svg class="w-4 h-4 text-green-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else if dest.status === "RUNNING" || dest.status === "ASSIGNED"}
                    <span class="w-4 h-4 shrink-0 flex items-center justify-center"><span class="w-2.5 h-2.5 rounded-full bg-yellow-500 animate-pulse"></span></span>
                  {:else if dest.status === "QUEUED"}
                    <svg class="w-4 h-4 text-blue-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else if dest.status === "FAILED"}
                    <svg class="w-4 h-4 text-red-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {:else}
                    <svg class="w-4 h-4 text-gray-300 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
                  {/if}
                  <span class="inline-flex items-center gap-1 text-xs font-medium px-2 py-0.5 rounded-full {destBadge.bg}">
                    {dest.environment}
                    <span class="w-1.5 h-1.5 rounded-full {destBadge.dot}"></span>
                  </span>
                  <span class="text-gray-400 text-xs">{dest.name}</span>
                  {#if dest.status === "SUCCEEDED"}
                    <span class="text-xs text-green-600">Deployed</span>
                  {:else if dest.status === "RUNNING"}
                    <span class="text-xs text-yellow-600">Deploying</span>
                  {:else if dest.status === "QUEUED"}
                    <span class="text-xs text-blue-600">Queued{dest.queue_position ? ` #${dest.queue_position}` : ""}</span>
                  {:else if dest.status === "FAILED"}
                    <span class="text-xs text-red-600">Failed</span>
                  {/if}
                  {#if dest.completed_at}
                    <time class="text-xs text-gray-400 ml-auto">{timeAgo(dest.completed_at)}</time>
                  {/if}
                </div>
              {/each}
            </details>
          </div>

        {:else if item.kind === "hidden"}
          <details class="group" on:toggle={scheduleComputeLaneBars}>
            <summary class="flex items-center gap-2 py-2 px-1 text-sm text-gray-400 cursor-pointer hover:text-gray-600 list-none">
              <svg class="w-3 h-3 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
              {item.count} hidden commit{item.count !== 1 ? "s" : ""}
              <span class="text-gray-300">&middot;</span>
              <span class="group-open:hidden">Show commit{item.count !== 1 ? "s" : ""}</span>
              <span class="hidden group-open:inline">Hide commit{item.count !== 1 ? "s" : ""}</span>
            </summary>
            <div class="space-y-3 mt-1">
              {#each item.releases || [] as release (release.slug)}
                <div data-release data-envs="" class="border border-gray-200 rounded-lg overflow-hidden opacity-75">
                  <div class="px-4 py-3 flex items-center gap-3 flex-wrap">
                    <div class="flex items-center gap-2 min-w-0 flex-1">
                      <span class="inline-block w-6 h-6 rounded-full bg-gray-200 shrink-0" data-avatar></span>
                      <a href="/orgs/{org}/projects/{release.project_name || project}/releases/{release.slug}" class="font-medium text-gray-900 hover:text-black truncate">
                        {release.title}
                      </a>
                    </div>
                    <div class="flex items-center gap-4 text-xs text-gray-500 shrink-0">
                      {#if release.commit_sha}
                        <span class="font-mono">{release.commit_sha.slice(0, 7)}</span>
                      {/if}
                      <time>{timeAgo(release.created_at)}</time>
                    </div>
                  </div>
                </div>
              {/each}
            </div>
          </details>
        {/if}
      {/each}
    </div>

    <!-- Lane labels (row 2, column 1) -->
    <div class="flex pt-1" style="grid-row: 2; grid-column: 1; height: 56px;">
      {#each lanes as lane (lane.name)}
        <div style="width: {BAR_WIDTH}px; margin-right: {BAR_GAP}px; display: flex; justify-content: center;">
          <span style="writing-mode: vertical-rl; transform: rotate(180deg); font-size: 10px; font-weight: 500; color: {lane.color}; white-space: nowrap;">{lane.name}</span>
        </div>
      {/each}
    </div>
  </div>
{/if}

<style>
  @keyframes lane-pulse {
    0%, 100% { opacity: 0.6; }
    50% { opacity: 1; }
  }
  :global(.lane-pulse) {
    animation: lane-pulse 2s ease-in-out infinite;
  }
</style>
