{% extends "base.html" %}
{% block title %}Marketplace — {{ config.INSTANCE_NAME }}{% endblock %}

{% block content %}
<style>
  .mp-page {
    --surface: #ffffff;
    --primary-light: rgba(0, 115, 209, 0.12);
    --border-light: #eceff1;
    --text-primary: #202124;
    --text-secondary: #5f6368;
    --success-color: #10b77f;
    --warn-color: #b45309;
    --font-medium: 500;
    --font-semibold: 600;
    --font-bold: 700;
    --font-primary: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  }

  /* ── Hero with integrated search ─────────────────────────────────── */
  /* Layout split: cover image is anchored absolutely to the right edge
     of the hero (z-index 1) so it doesn't claim layout space; the text
     content sits on top (z-index 2) and uses the hero's full width.
     `overflow: hidden` clips the cover to the hero's rounded corners. */
  .mp-hero {
    position: relative;
    overflow: hidden;
    background: linear-gradient(135deg, #0073D1 0%, #0056A3 100%);
    border-radius: 12px;
    padding: 28px 32px;
    margin-bottom: 24px;
    box-shadow: 0 4px 16px rgba(0, 115, 209, 0.2);
    color: #fff;
  }
  .mp-hero-content {
    position: relative;
    z-index: 2;
  }
  .mp-hero-cover {
    position: absolute;
    right: 0;
    top: 18px;
    bottom: 0;
    width: 46%;
    object-fit: cover;
    z-index: 1;
  }
  @media (max-width: 900px) {
    .mp-hero-cover { display: none; }
  }
  .mp-hero .eyebrow {
    font-size: 11px; font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.8px; color: rgba(255,255,255,0.75); margin-bottom: 8px;
  }
  .mp-hero h1 { margin: 0; font-size: 28px; font-weight: 700; letter-spacing: -0.4px; }
  .mp-hero .sub { margin: 6px 0 0; font-size: 14px; color: rgba(255,255,255,0.85); }

  .mp-hero .search-row {
    display: flex; align-items: stretch;
    margin-top: 18px;
    background: #fff;
    border-radius: 10px;
    padding: 4px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
    max-width: 760px;
  }
  .mp-hero .search-wrap { flex: 1; position: relative; min-width: 0; }
  .mp-hero input[type="search"] {
    width: 100%;
    padding: 11px 14px 11px 40px;
    border: none; border-radius: 8px;
    font-size: 14px; font-family: var(--font-primary);
    background: transparent; color: var(--text-primary); outline: none;
  }
  .mp-hero input[type="search"]::placeholder { color: var(--text-secondary); opacity: 0.75; }
  .mp-hero .search-icon {
    position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
    width: 16px; height: 16px; color: var(--text-secondary); pointer-events: none;
  }
  .mp-hero .search-btn {
    appearance: none; border: none; background: var(--primary); color: #fff;
    padding: 0 24px; border-radius: 8px;
    font-size: 13px; font-weight: 600; font-family: var(--font-primary);
    cursor: pointer; flex-shrink: 0;
    transition: background 0.15s ease;
  }
  .mp-hero .search-btn:hover { background: var(--primary-dark); }
  .mp-hero .search-btn:active { transform: scale(0.98); }

  .mp-hero .scope {
    display: flex; gap: 6px; align-items: center;
    margin-top: 10px; font-size: 12px;
  }
  .mp-hero .scope-label {
    font-weight: 600; text-transform: uppercase; letter-spacing: 0.6px;
    font-size: 11px; color: rgba(255,255,255,0.7); margin-right: 4px;
  }
  .mp-hero .scope label {
    display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
    user-select: none; padding: 4px 10px; border-radius: 6px; color: #fff;
  }
  .mp-hero .scope label:hover { background: rgba(255,255,255,0.12); }
  .mp-hero .scope input[type="checkbox"] { accent-color: #fff; }

  /* ── Tabs row + actions ──────────────────────────────────────────── */
  .mp-tabs-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 16px; margin-bottom: 16px; flex-wrap: wrap;
  }
  .mp-tabs {
    display: flex; gap: 4px; align-items: center;
    background: var(--surface); border: 1px solid var(--border);
    border-radius: 10px; padding: 4px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
  }
  .mp-tabs button {
    appearance: none; border: none; background: transparent;
    color: var(--text-secondary);
    padding: 8px 16px; border-radius: 7px;
    font-size: 13px; font-weight: 500; cursor: pointer;
    font-family: var(--font-primary);
    display: inline-flex; align-items: center; gap: 8px;
    transition: all 0.15s ease;
  }
  .mp-tabs button .tab-icon {
    width: 16px; height: 16px; flex-shrink: 0;
  }
  /* Per-tab icon tint reuses the established palette: blue = curated/
     vetted, purple = flea/community, amber = "in your stack" (mirrors
     the In Stack badge on installed cards). The active-tab override
     flips icons to white so they read against the blue active fill. */
  .mp-tabs button[data-tab="curated"] .tab-icon { color: #0073D1; }
  .mp-tabs button[data-tab="flea"] .tab-icon    { color: #6D28D9; }
  .mp-tabs button[data-tab="my"] .tab-icon      { color: #F59F0A; }
  .mp-tabs button.is-active .tab-icon           { color: #fff; }
  .mp-tabs button:hover { color: var(--text-primary); background: var(--bg); }
  .mp-tabs button.is-active { background: var(--primary); color: #fff; }
  .mp-tabs button .count {
    display: inline-flex; align-items: center; justify-content: center;
    min-width: 22px; height: 18px; padding: 0 6px; border-radius: 9px;
    background: rgba(0,0,0,0.08); color: inherit;
    font-size: 11px; font-weight: 600;
  }
  .mp-tabs button.is-active .count { background: rgba(255,255,255,0.2); }

  .mp-actions { display: flex; gap: 8px; align-items: center; }
  .mp-actions .btn {
    appearance: none;
    display: inline-flex; align-items: center; gap: 6px;
    padding: 8px 14px;
    border: 1px solid var(--border); background: var(--surface);
    color: var(--text-primary); border-radius: 8px;
    font-size: 13px; font-weight: 500; cursor: pointer;
    font-family: var(--font-primary); text-decoration: none;
    transition: all 0.15s ease;
  }
  .mp-actions .btn:hover { border-color: var(--primary); color: var(--primary); }
  .mp-actions .btn.primary {
    background: var(--primary); color: #fff; border-color: var(--primary);
  }
  .mp-actions .btn.primary:hover {
    background: var(--primary-dark); border-color: var(--primary-dark); color: #fff;
  }

  /* ── Info block (curated trust badge + flea open-shelf signal) ───── */
  .mp-curator-block {
    background: var(--surface);
    border: 1px solid var(--border);
    border-left: 3px solid var(--primary);
    border-radius: 10px;
    padding: 14px 18px;
    margin-bottom: 16px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap;
  }
  /* Flea variant — purple accent matches the flea badge color used on
     /marketplace cards and detail pages so the two info blocks read as
     visually distinct (blue = curated/vetted, purple = flea/community).
     The same purple is applied to the trailing link so it reads as part
     of the flea block, not a stray curated-blue accent. */
  .mp-curator-block.is-flea { border-left-color: #6D28D9; }
  .mp-curator-block.is-flea .link { color: #6D28D9; }
  /* My Stack variant — neutral slate. Not a trust/community signal like
     the other two; it's the user's personal shelf, so the accent stays
     intentionally non-branded so the user's own content is the focus. */
  .mp-curator-block.is-mystack { border-left-color: #64748B; }
  .mp-curator-block .text { flex: 1; min-width: 240px; }
  .mp-curator-block .title {
    font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px;
  }
  .mp-curator-block .body { font-size: 12px; color: var(--text-secondary); line-height: 1.5; }
  .mp-curator-block .link {
    font-size: 12px; color: var(--primary); font-weight: 500;
    white-space: nowrap; align-self: center;
  }

  /* ── Filter row ──────────────────────────────────────────────────── */
  .mp-filter-row {
    display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
    margin-bottom: 12px;
  }
  .mp-filter-row .pill {
    appearance: none; border: 1px solid var(--border);
    background: var(--surface); color: var(--text-primary);
    padding: 7px 12px; border-radius: 999px;
    font-size: 13px; font-weight: 500; cursor: pointer;
    font-family: var(--font-primary);
    display: inline-flex; align-items: center; gap: 6px;
    transition: all 0.15s ease;
  }
  .mp-filter-row .pill svg { width: 14px; height: 14px; flex-shrink: 0; }
  .mp-filter-row .pill:hover { border-color: var(--primary); color: var(--primary); }
  .mp-filter-row .pill.is-active {
    background: var(--primary-light); color: var(--primary); border-color: var(--primary);
  }
  .mp-filter-row .pill .count { color: var(--text-secondary); font-weight: 500; margin-left: 2px; }
  .mp-filter-row .pill.is-active .count { color: var(--primary); }

  .mp-type-row {
    display: flex; gap: 6px; align-items: center; margin-bottom: 24px;
  }
  .mp-type-row .pill {
    appearance: none; border: 1px solid var(--border);
    background: var(--surface); color: var(--text-primary);
    padding: 6px 12px; border-radius: 8px;
    font-size: 12px; font-weight: 500; cursor: pointer;
    font-family: var(--font-primary);
    transition: all 0.15s ease;
  }
  .mp-type-row .pill:hover { border-color: var(--primary); color: var(--primary); }
  .mp-type-row .pill.is-active {
    background: var(--primary); color: #fff; border-color: var(--primary);
  }

  /* ── Card grid ───────────────────────────────────────────────────── */
  .mp-grid {
    display: grid; gap: 16px;
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
  @media (max-width: 1100px) { .mp-grid { grid-template-columns: repeat(3, minmax(0,1fr)); } }
  @media (max-width: 820px)  { .mp-grid { grid-template-columns: repeat(2, minmax(0,1fr)); } }
  @media (max-width: 540px)  { .mp-grid { grid-template-columns: 1fr; } }

  .mp-card {
    position: relative;
    display: flex; flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border); border-radius: 12px;
    box-shadow: 0 1px 2px rgba(0,0,0,0.04);
    overflow: hidden; cursor: pointer;
    transition: all 0.15s ease;
    /* Card is an <a> so middle-click / ctrl-click open in a new tab; reset
       the link defaults so the styling matches the surrounding panel. */
    text-decoration: none;
    color: inherit;
  }
  .mp-card:hover {
    border-color: var(--primary);
    box-shadow: 0 6px 20px rgba(0, 115, 209, 0.12);
    transform: translateY(-2px);
  }
  .mp-card .photo {
    width: 100%; height: 120px;
    display: flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, var(--primary-light) 0%, #fce7f3 100%);
    color: var(--primary);
    font-size: 26px; font-weight: var(--font-bold); letter-spacing: 0.5px;
  }
  .mp-card .photo img {
    width: 100%; height: 100%; object-fit: cover;
  }
  .mp-card.is-installed { border-color: rgba(245, 159, 10, 0.55); }
  /* Solid filled pill in amber — signals "selected / in your stack"
     while staying readable on both the white card photo placeholder
     and any uploaded cover image. Card border matches at low opacity
     so the two installed-state cues read as a pair. */
  .mp-card .installed-badge {
    position: absolute; top: 10px; right: 10px;
    display: inline-flex; align-items: center; gap: 4px;
    padding: 4px 10px; border-radius: 999px;
    background: #f59f0a; color: #fff;
    font-size: 11px; font-weight: var(--font-semibold);
    letter-spacing: 0.2px;
    border: 1px solid rgba(255, 255, 255, 0.55);
  }
  .mp-card .body {
    padding: 14px 16px 12px; flex: 1;
    display: flex; flex-direction: column; gap: 6px;
  }
  .mp-card .badges { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
  .mp-card .type-badge {
    display: inline-block; padding: 2px 8px; border-radius: 4px;
    background: var(--primary-light); color: var(--primary);
    font-size: 10px; font-weight: var(--font-semibold);
    text-transform: uppercase; letter-spacing: 0.5px;
  }
  .mp-card .type-badge[data-type="plugin"] { background: rgba(0, 115, 209, 0.12); color: #0056A3; }
  .mp-card .type-badge[data-type="skill"]  { background: rgba(16, 183, 127, 0.14); color: #0e9b6a; }
  .mp-card .type-badge[data-type="agent"]  { background: rgba(124, 58, 237, 0.14); color: #6d28d9; }
  .mp-card .cat-badge {
    font-size: 10px; color: var(--text-secondary);
    border: 1px solid var(--border); border-radius: 4px;
    padding: 2px 7px;
    text-transform: uppercase; letter-spacing: 0.4px; font-weight: 500;
  }
  .mp-card .name {
    font-weight: var(--font-semibold); color: var(--text-primary);
    font-size: 15px; line-height: 1.3;
  }
  .mp-card .by {
    font-size: 11px; color: var(--text-secondary); margin-top: -2px;
  }
  .mp-card .by .todo { color: var(--warn-color); font-style: italic; }
  .mp-card .desc {
    font-size: 12px; color: var(--text-secondary); line-height: 1.5;
    display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .mp-card .footer {
    display: flex; align-items: center; justify-content: space-between;
    padding: 10px 16px; border-top: 1px solid var(--border-light);
    font-size: 11px; color: var(--text-secondary);
  }
  .mp-card .ver { font-variant-numeric: tabular-nums; }

  /* ── Pager + empty state ─────────────────────────────────────────── */
  .mp-pager { display: flex; gap: 6px; justify-content: center; margin: 32px 0 0; }
  .mp-pager button {
    padding: 7px 14px; border: 1px solid var(--border);
    background: var(--surface); color: var(--text-primary);
    border-radius: 8px; cursor: pointer; font-size: 13px;
    font-family: var(--font-primary); transition: all 0.15s ease;
  }
  .mp-pager button:hover { border-color: var(--primary); color: var(--primary); }
  .mp-pager button.is-active {
    background: var(--primary); color: #fff; border-color: var(--primary);
  }
  .mp-pager .ellipsis {
    display: inline-flex; align-items: center; padding: 0 4px;
    color: var(--text-secondary); font-size: 13px;
  }
  .mp-empty {
    text-align: center; padding: 56px 24px;
    color: var(--text-secondary); font-size: 14px;
    background: var(--surface);
    border: 1px dashed var(--border); border-radius: 12px;
  }
  .mp-empty h3 {
    margin: 0 0 8px; color: var(--text-primary); font-size: 16px; font-weight: 600;
  }
  .mp-empty .cta { margin-top: 16px; }
  .mp-empty .cta a {
    display: inline-block; margin: 0 6px;
    color: var(--primary); font-weight: 500; text-decoration: none;
  }
  .mp-empty .cta a:hover { text-decoration: underline; }

  [hidden] { display: none !important; }
</style>

<script id="category-icons-data" type="application/json">{{ category_icons_json | safe }}</script>

<div class="mp-page page-shell">

  <!-- Hero with integrated search -->
  <div class="mp-hero">
    <div class="mp-hero-content">
      <div class="eyebrow">Marketplace</div>
      <h1>Plugin Marketplace</h1>
      <p class="sub">Discover AI tools — from curated catalogs and the community.</p>

      <div class="search-row">
        <div class="search-wrap">
          <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
               stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>
          </svg>
          <input id="mp-search" type="search"
                 placeholder="Search plugins, skills, agents — by name, description, author…">
        </div>
        <button class="search-btn" id="mp-search-btn" type="button">Search</button>
      </div>
      <div class="scope">
        <span class="scope-label">Search in:</span>
        <label><input type="checkbox" id="mp-scope-curated"> Curated</label>
        <label><input type="checkbox" id="mp-scope-flea"> Flea Market</label>
      </div>
    </div>
    <img class="mp-hero-cover" src="/static/marketplace-cover.png"
         alt="" aria-hidden="true">
  </div>

  <!-- Tabs + actions -->
  <div class="mp-tabs-row">
    <div class="mp-tabs" role="tablist">
      {# Tab icons (Heroicons outline 24×24): shield-check signals trust/vetting
         for curated, building-storefront signals an open shelf for flea, and
         rectangle-stack signals the user's personal collection for My Stack. #}
      <button class="is-active" data-tab="curated" role="tab" aria-selected="true">
        <svg class="tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z"/>
        </svg>
        Curated Marketplace <span class="count" data-count-curated>0</span>
      </button>
      <button data-tab="flea" role="tab" aria-selected="false">
        <svg class="tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349M3.75 21V9.349m0 0a3.001 3.001 0 0 0 3.75-.615A2.993 2.993 0 0 0 9.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 0 0 2.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 0 0 3.75.614m-16.5 0a3.004 3.004 0 0 1-.621-4.72L4.318 3.44A1.5 1.5 0 0 1 5.378 3h13.243a1.5 1.5 0 0 1 1.06.44l1.19 1.189a3 3 0 0 1-.621 4.72M6.75 18h3.75a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75H6.75a.75.75 0 0 0-.75.75v3.75c0 .414.336.75.75.75Z"/>
        </svg>
        Flea Market <span class="count" data-count-flea>0</span>
      </button>
      <button data-tab="my" role="tab" aria-selected="false">
        <svg class="tab-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
          <path d="M6.429 9.75 2.25 12l4.179 2.25m0-4.5 5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0 4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0-5.571 3-5.571-3"/>
        </svg>
        My Stack <span class="count" data-count-my>0</span>
      </button>
    </div>
    <div class="mp-actions">
      <a class="btn" data-actions-for="curated" href="/marketplace/guide/curated">Submit a plugin</a>
      <!-- Flea has a self-service +Upload button below — no second
           "how to" CTA needed. Anyone uploads via the form directly. -->
      <a class="btn primary" data-actions-for="flea" href="/store/new" hidden>+ Upload</a>
    </div>
  </div>

  <!-- Curator info block (Curated tab only) — trust badge, not onboarding.
       Goal: signal "someone is accountable for what's in here", without
       prescribing the process (teams may or may not have a curator; we
       don't want the copy to imply a structure the org doesn't follow). -->
  <div class="mp-curator-block" data-show-on="curated">
    <div class="text">
      <div class="title">Each plugin here has a named curator accountable for it.</div>
      <div class="body">Each plugin in this marketplace has a named curator and meets a baseline review bar (security, telemetry hygiene, documentation).</div>
    </div>
    <a class="link" href="#">See all curators →</a>
  </div>

  <!-- Flea Market info block — open-shelf signal, mirror structure of
       the curated block. The trailing "Tips for sharing" link points at
       the publication guide; the +Upload button in the actions row above
       handles the direct CTA, so this link is informational, not a
       second action. Purple left-border + link colour distinguishes the
       block from curated. -->
  <div class="mp-curator-block is-flea" data-show-on="flea">
    <div class="text">
      <div class="title">Anyone in the company can upload here.</div>
      <div class="body">The Flea Market is the community shelf — anyone can share a skill, agent, or plugin. Browse what colleagues have built, or upload something new.</div>
    </div>
    <a class="link" href="/marketplace/guide/flea">Tips for sharing →</a>
  </div>

  <!-- My Stack info block — personal-shelf orientation. Answers two
       questions a non-technical user lands here with: "what is this?"
       (their own collected items from Curated + Flea) and "how does
       this connect to Claude Code?" (auto-sync at next session). No
       trailing link — the user is on their own tab, navigating away
       would be a distraction; the cards themselves are the action. -->
  <div class="mp-curator-block is-mystack" data-show-on="my">
    <div class="text">
      <div class="title">Your AI stack — everything you’ve added.</div>
      <div class="body">Plugins, skills, and agents you’ve picked from Curated or the Flea Market all live here. They sync to Claude Code on your laptop automatically the next time you start a session.</div>
    </div>
  </div>

  <!-- Filter row: categories -->
  <div class="mp-filter-row" id="mp-cat-row"></div>

  <!-- Type filter (Flea + My — both surface skill/agent/plugin items) -->
  <div class="mp-type-row" id="mp-type-row" hidden>
    <button class="pill is-active" data-type="">All</button>
    <button class="pill" data-type="skill">Skills</button>
    <button class="pill" data-type="agent">Agents</button>
    <button class="pill" data-type="plugin">Plugins</button>
  </div>

  <!-- Most Popular section: hidden until telemetry exists.
       <div class="mp-section-header"><h2>Most Popular</h2><div class="meta">Last 30 days</div></div>
       <div class="mp-grid">…8 cards…</div>
  -->

  <!-- Main grid -->
  <div class="mp-grid" id="mp-grid"></div>
  <div class="mp-empty" id="mp-empty" hidden>
    <h3 id="mp-empty-title">Nothing here yet</h3>
    <p id="mp-empty-body"></p>
    <div class="cta" id="mp-empty-cta"></div>
  </div>
  <div class="mp-pager" id="mp-pager"></div>

</div>

<script>
'use strict';

const ICON_PATHS = JSON.parse(document.getElementById('category-icons-data').textContent);
const PAGE_SIZE = 24;

const state = {
  tab: 'curated',
  q: '',
  category: null,
  type: null,
  page: 1,
  scope: { curated: true, flea: true },
};

// ── URL state ────────────────────────────────────────────────────────────
function loadFromURL() {
  const p = new URLSearchParams(window.location.search);
  const tab = p.get('tab');
  state.tab = (tab === 'flea' || tab === 'my') ? tab : 'curated';
  state.q = p.get('q') || '';
  state.category = p.get('category') || null;
  state.type = p.get('type') || null;
  state.page = Math.max(1, parseInt(p.get('page') || '1', 10));
}
function syncURL() {
  const u = new URL(window.location.href);
  u.searchParams.set('tab', state.tab);
  ['q','category','type'].forEach(k => {
    if (state[k]) u.searchParams.set(k, state[k]);
    else u.searchParams.delete(k);
  });
  if (state.page > 1) u.searchParams.set('page', String(state.page));
  else u.searchParams.delete('page');
  window.history.replaceState({}, '', u);
}

// ── DOM helpers ──────────────────────────────────────────────────────────
function esc(s) {
  return String(s ?? '').replace(/[&<>"']/g, ch => (
    {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[ch]
  ));
}
function typeIcon(t) {
  return t === 'skill' ? 'SK' : t === 'agent' ? 'AG' : t === 'plugin' ? 'PL' : '?';
}
function categorySVG(category) {
  const path = ICON_PATHS[category || 'Other'] || ICON_PATHS['Other'] || '';
  return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">${path}</svg>`;
}

// ── Category filter ──────────────────────────────────────────────────────
function renderCategories(items) {
  const row = document.getElementById('mp-cat-row');
  row.innerHTML = '';
  const allBtn = document.createElement('button');
  allBtn.className = 'pill' + (!state.category ? ' is-active' : '');
  allBtn.dataset.cat = '';
  allBtn.textContent = 'All';
  allBtn.addEventListener('click', () => {
    state.category = null; state.page = 1; syncURL();
    renderCategories(items); loadItems();
  });
  row.appendChild(allBtn);
  for (const c of items) {
    const btn = document.createElement('button');
    btn.className = 'pill' + (state.category === c.name ? ' is-active' : '');
    btn.dataset.cat = c.name;
    btn.innerHTML = `${categorySVG(c.name)} ${esc(c.name)} <span class="count">${c.count}</span>`;
    btn.addEventListener('click', () => {
      state.category = c.name; state.page = 1; syncURL();
      renderCategories(items); loadItems();
    });
    row.appendChild(btn);
  }
}

async function loadCategories() {
  const params = new URLSearchParams({ tab: state.tab });
  if (state.tab === 'flea' && state.type) params.set('type', state.type);
  try {
    const res = await fetch('/api/marketplace/categories?' + params);
    if (!res.ok) { renderCategories([]); return; }
    const data = await res.json();
    renderCategories(data.items || []);
  } catch (e) {
    renderCategories([]);
  }
}

// ── Items grid ───────────────────────────────────────────────────────────
function renderGrid(items) {
  const grid = document.getElementById('mp-grid');
  const empty = document.getElementById('mp-empty');
  grid.innerHTML = '';
  if (!items || !items.length) {
    grid.hidden = true;
    empty.hidden = false;
    fillEmptyState();
    return;
  }
  grid.hidden = false; empty.hidden = true;

  for (const it of items) {
    const card = document.createElement('a');
    card.className = 'mp-card' + (it.installed ? ' is-installed' : '');
    card.href = it.detail_url;
    // Cover photo with broken-image fallback. When the <img> 404s (e.g. an
    // marketplace-metadata.json `cover_photo` references a file the curator forgot
    // to commit, or an external mirror failed), browsers paint their default
    // broken-image icon — which looks worse than the gradient placeholder we
    // render when no cover_photo_url is set at all. The `onerror` swaps the
    // parent's content for the same fallback initials (PL/SK/AG) so the card
    // looks identical to the no-photo case.
    const fallbackInitials = esc(typeIcon(it.type));
    const photoMarkup = it.photo_url
      ? `<div class="photo"><img src="${esc(it.photo_url)}" alt=""
            onerror="this.parentElement.classList.add('photo-failed');
                     this.parentElement.textContent=this.dataset.fallback;"
            data-fallback="${fallbackInitials}"></div>`
      : `<div class="photo">${fallbackInitials}</div>`;
    // v39: when a plugin is system, the amber "Required" badge replaces
    // the green "In stack" badge — system plugins are always installed
    // (force-installed via mark_system materialization), so "in stack"
    // is implied and stacking both pills wastes vertical space. The
    // shield icon makes the org-policy semantic visually distinct from
    // the user-action "✓ in stack" semantic.
    let installedBadge = '';
    if (it.is_system) {
      installedBadge = `<div class="installed-badge" title="Required by your organization — managed by admin"
            style="background:#fef3c7;color:#92400e;border-color:#fde68a;">
         <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
           <path d="M12 2 4 6v6c0 5 3.4 9.5 8 10 4.6-.5 8-5 8-10V6l-8-4z"/>
         </svg>
         Required
       </div>`;
    } else if (it.installed) {
      installedBadge = `<div class="installed-badge" title="In your stack">
         <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor"
              stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
           <polyline points="20 6 9 17 4 12"/>
         </svg>
         In stack
       </div>`;
    }

    // v32+ quarantine: subtle corner badge on the submitter's own
    // non-approved cards. Approved cards + non-owner views omit it.
    let quarantineBadge = '';
    if (it.is_viewer_owner && it.visibility_status && it.visibility_status !== 'approved') {
      const isPending = it.visibility_status === 'pending'
        || it.visibility_status === 'pending_inline'
        || it.visibility_status === 'pending_llm';
      const label = isPending ? '⟳ Under review' : '⚠ Quarantined';
      const palette = isPending
        ? 'background:#fef3c7;color:#92400e;border:1px solid #fde68a;'
        : 'background:#fee2e2;color:#991b1b;border:1px solid #fecaca;';
      quarantineBadge = `<div class="quarantine-badge"
        title="Visible only to you. ${isPending ? 'Checks in progress.' : 'Failed automated/security checks — open the detail page to see why.'}"
        style="position:absolute;top:8px;left:8px;padding:3px 9px;border-radius:999px;font-size:11px;font-weight:600;letter-spacing:0.2px;${palette}z-index:2;">${label}</div>`;
    }

    let ownerLine;
    if (it.source === 'curated') {
      const ownerVal = it.owner === 'owner_todo'
        ? `<span class="todo">owner_todo</span>`
        : `<span>${esc(it.owner || '')}</span>`;
      const mp = it.marketplace_name ? `via <strong>${esc(it.marketplace_name)}</strong> · ` : '';
      ownerLine = `${mp}${ownerVal}`;
    } else {
      ownerLine = `by <strong>${esc(it.owner || '')}</strong>`;
    }
    const addedFmt = it.added ? new Date(it.added).toISOString().slice(0,10) : '';
    const verFmt = it.version ? `v${esc(it.version)}` : '';
    const catBadge = it.category ? `<span class="cat-badge">${esc(it.category)}</span>` : '';

    // Rich-content fallback chain:
    // - card name uses display_name (curator-friendly) when set; falls back
    //   to the raw `it.name` (plugin id from marketplace.json) otherwise.
    // - card description uses tagline (1-line punchy value prop) when set;
    //   falls back to the marketplace.json `description` (often verbose
    //   technical copy, clamped to 2 lines by CSS line-clamp).
    // Both fields come from marketplace-metadata.json — curated only;
    // flea entities don't have a metadata layer yet.
    const cardName = it.display_name || it.name;
    const cardDesc = it.tagline || it.description || '';
    card.innerHTML = `
      ${quarantineBadge}
      ${photoMarkup}
      ${installedBadge}
      <div class="body">
        <div class="badges">
          <span class="type-badge" data-type="${esc(it.type)}">${esc(it.type)}</span>
          ${catBadge}
        </div>
        <div class="name">${esc(cardName)}</div>
        <div class="by">${ownerLine}</div>
        <div class="desc">${esc(cardDesc)}</div>
      </div>
      <div class="footer">
        <span class="added">${addedFmt ? 'Added ' + addedFmt : ''}</span>
        <span class="ver">${verFmt}</span>
      </div>`;
    grid.appendChild(card);
  }
}

function fillEmptyState() {
  const t = document.getElementById('mp-empty-title');
  const b = document.getElementById('mp-empty-body');
  const c = document.getElementById('mp-empty-cta');
  if (state.q) {
    t.textContent = `Nothing matches "${state.q}"`;
    b.textContent = '';
    c.innerHTML = '';
    return;
  }
  if (state.tab === 'curated') {
    t.textContent = 'No curated plugins available.';
    b.textContent = 'Ask your admin to grant your group access.';
    c.innerHTML = `<a href="/marketplace/guide/curated">Submit a plugin →</a>`;
  } else if (state.tab === 'flea') {
    t.textContent = 'No community entities yet.';
    b.textContent = 'Be the first to share something.';
    c.innerHTML = `<a href="/store/new">+ Upload</a>`;
  } else {
    t.textContent = 'My Stack';
    b.textContent = "Plugins you've added to your stack will show up here. Browse the Curated Marketplace or Flea Market and click “Add to my stack”.";
    c.innerHTML = `<a href="/marketplace?tab=curated">Browse Curated →</a><a href="/marketplace?tab=flea">Browse Flea Market →</a>`;
  }
}

// ── Pager ────────────────────────────────────────────────────────────────
function renderPager(total) {
  const pager = document.getElementById('mp-pager');
  pager.innerHTML = '';
  const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
  if (totalPages <= 1) return;
  const mk = (label, page, active, isEll) => {
    if (isEll) {
      const s = document.createElement('span');
      s.className = 'ellipsis'; s.textContent = '…'; return s;
    }
    const b = document.createElement('button');
    b.textContent = label;
    if (active) b.classList.add('is-active');
    b.addEventListener('click', () => {
      state.page = page; syncURL(); loadItems();
      window.scrollTo({ top: 0, behavior: 'smooth' });
    });
    return b;
  };
  if (state.page > 1) pager.appendChild(mk('‹', state.page - 1, false));
  // Compact pagination: 1 … (cur-1) cur (cur+1) … last
  const pages = new Set([1, totalPages, state.page, state.page-1, state.page+1, 2, totalPages-1]);
  const sortedPages = [...pages].filter(p => p >= 1 && p <= totalPages).sort((a,b) => a-b);
  let prev = 0;
  for (const p of sortedPages) {
    if (p - prev > 1) pager.appendChild(mk('', 0, false, true));
    pager.appendChild(mk(String(p), p, p === state.page));
    prev = p;
  }
  if (state.page < totalPages) pager.appendChild(mk('›', state.page + 1, false));
}

// ── Tab counts (separate cheap fetches) ──────────────────────────────────
async function loadTabCounts() {
  const fetchCount = async (tab) => {
    try {
      const res = await fetch('/api/marketplace/items?tab=' + tab + '&page_size=1');
      if (!res.ok) return 0;
      const data = await res.json();
      return data.total || 0;
    } catch { return 0; }
  };
  const [c, f, m] = await Promise.all([
    fetchCount('curated'), fetchCount('flea'), fetchCount('my'),
  ]);
  document.querySelector('[data-count-curated]').textContent = c;
  document.querySelector('[data-count-flea]').textContent = f;
  document.querySelector('[data-count-my]').textContent = m;
}

// ── Items loader ─────────────────────────────────────────────────────────
async function loadItems() {
  // Search-results mode: when user is searching with both scopes checked,
  // we fan out two requests (one per scope) and merge client-side. With
  // exactly one scope checked we just hit the matching tab.
  if (state.q) {
    const wantC = document.getElementById('mp-scope-curated').checked;
    const wantF = document.getElementById('mp-scope-flea').checked;
    if (!wantC && !wantF) {
      document.getElementById('mp-scope-curated').checked = true;
      state.scope.curated = true;
    }
    const fetches = [];
    if (wantC) fetches.push(fetchOne('curated'));
    if (wantF) fetches.push(fetchOne('flea'));
    const results = await Promise.all(fetches);
    const merged = [];
    let total = 0;
    for (const r of results) {
      merged.push(...(r.items || []));
      total += r.total || 0;
    }
    // Client-side paginate the merged set: backend already paginated each
    // half so this is an approximate combined view, sufficient for search.
    renderGrid(merged);
    renderPager(total);
    return;
  }
  const r = await fetchOne(state.tab);
  renderGrid(r.items || []);
  renderPager(r.total || 0);
}

async function fetchOne(tab) {
  const params = new URLSearchParams({
    tab,
    page: String(state.page),
    page_size: String(PAGE_SIZE),
  });
  if (state.q) params.set('q', state.q);
  if (state.category) params.set('category', state.category);
  // My stack also surfaces skill/agent/plugin entries (curated subscriptions +
  // flea installs), so the type filter is meaningful there too. Curated
  // browse only lists plugins, so we still skip the param for that tab.
  if ((tab === 'flea' || tab === 'my') && state.type) params.set('type', state.type);
  try {
    const res = await fetch('/api/marketplace/items?' + params);
    if (!res.ok) return { items: [], total: 0 };
    return await res.json();
  } catch { return { items: [], total: 0 }; }
}

// ── Tab switching + filter pills ─────────────────────────────────────────
function setTab(tab) {
  state.tab = tab;
  state.category = null;
  state.type = null;
  state.page = 1;
  // Sync scope checkboxes with the active tab (user can manually flip back).
  document.getElementById('mp-scope-curated').checked = (tab === 'curated');
  document.getElementById('mp-scope-flea').checked = (tab === 'flea');
  document.querySelectorAll('.mp-tabs button').forEach(b => {
    const active = b.dataset.tab === tab;
    b.classList.toggle('is-active', active);
    b.setAttribute('aria-selected', active ? 'true' : 'false');
  });
  document.querySelectorAll('[data-actions-for]').forEach(a => {
    a.hidden = a.dataset.actionsFor !== tab;
  });
  document.querySelectorAll('[data-show-on]').forEach(el => {
    el.hidden = el.dataset.showOn !== tab;
  });
  // Category + type filters now show on both flea and my tabs (my surfaces
  // a mix of plugin/skill/agent entries the user has installed). Curated
  // browse only lists plugins, so type filter stays hidden there.
  document.getElementById('mp-type-row').hidden = (tab === 'curated');
  document.getElementById('mp-cat-row').hidden = false;
  // Type filter pills always reset to 'All' on tab switch.
  document.querySelectorAll('#mp-type-row .pill').forEach(p => {
    p.classList.toggle('is-active', !p.dataset.type);
  });
  syncURL();
  Promise.all([loadCategories(), loadItems()]);
}

// ── Boot ─────────────────────────────────────────────────────────────────
document.querySelectorAll('.mp-tabs button').forEach(b => {
  b.addEventListener('click', () => setTab(b.dataset.tab));
});
document.querySelectorAll('#mp-type-row .pill').forEach(p => {
  p.addEventListener('click', () => {
    document.querySelectorAll('#mp-type-row .pill').forEach(x => x.classList.remove('is-active'));
    p.classList.add('is-active');
    state.type = p.dataset.type || null;
    state.page = 1; syncURL();
    Promise.all([loadCategories(), loadItems()]);
  });
});

const searchInput = document.getElementById('mp-search');
const searchBtn = document.getElementById('mp-search-btn');
let searchTimer;
function runSearch() {
  state.q = searchInput.value.trim();
  state.page = 1;
  syncURL();
  loadItems();
}
searchInput.addEventListener('input', () => {
  clearTimeout(searchTimer);
  searchTimer = setTimeout(runSearch, 250);
});
searchInput.addEventListener('keydown', e => {
  if (e.key === 'Enter') { clearTimeout(searchTimer); runSearch(); }
});
searchBtn.addEventListener('click', () => {
  clearTimeout(searchTimer); runSearch();
});
document.getElementById('mp-scope-curated').addEventListener('change', () => {
  if (state.q) loadItems();
});
document.getElementById('mp-scope-flea').addEventListener('change', () => {
  if (state.q) loadItems();
});

(async function init() {
  loadFromURL();
  // Restore search input from URL state
  if (state.q) document.getElementById('mp-search').value = state.q;
  // Sync scope checkboxes with the active tab on first load — only the
  // active tab's source is searched by default. User can flip the other
  // checkbox manually to widen the scope.
  document.getElementById('mp-scope-curated').checked = (state.tab === 'curated');
  document.getElementById('mp-scope-flea').checked = (state.tab === 'flea');
  // For tab='my' fall back to Curated checked so a search query still has
  // a non-empty scope to match against.
  if (state.tab === 'my') {
    document.getElementById('mp-scope-curated').checked = true;
  }
  // Apply tab to UI
  document.querySelectorAll('.mp-tabs button').forEach(b => {
    const active = b.dataset.tab === state.tab;
    b.classList.toggle('is-active', active);
    b.setAttribute('aria-selected', active ? 'true' : 'false');
  });
  document.querySelectorAll('[data-actions-for]').forEach(a => {
    a.hidden = a.dataset.actionsFor !== state.tab;
  });
  document.querySelectorAll('[data-show-on]').forEach(el => {
    el.hidden = el.dataset.showOn !== state.tab;
  });
  // Same visibility rule as setTab() — see comment there.
  document.getElementById('mp-type-row').hidden = (state.tab === 'curated');
  document.getElementById('mp-cat-row').hidden = false;
  document.querySelectorAll('#mp-type-row .pill').forEach(p => {
    p.classList.toggle('is-active', (p.dataset.type || null) === state.type);
  });
  await Promise.all([
    loadTabCounts(),
    loadCategories(),
    loadItems(),
  ]);
})();
</script>
{% endblock %}
