<script lang="ts">
  import { onMount } from 'svelte'
  import { statusStore } from '../stores/status.svelte.js'
  import { systemStore } from '../stores/system.svelte.js'
  import { api, getHfToken } from '@core/api.js'
  import { esc, toast, confirmModal, infoDot } from '@core/ui.js'

  const GGUF_HELP = 'GGUF ist das lokale Dateiformat für KI-Modelle. Jede Datei ist eine quantisierte Variante desselben Modells — Q4_K_M ist meist der beste Kompromiss aus Qualität und Geschwindigkeit.'
  const CTX_HELP = 'Kontext = das Kurzzeitgedächtnis des Modells. Größer = merkt sich mehr, braucht aber mehr Speicher und wird etwas langsamer.'
  const MOE_HELP = 'MoE (Mixture of Experts): viele Parameter fürs Wissen, aber pro Wort ist nur ein kleiner Teil aktiv → fast große-Modell-Qualität bei hohem Tempo. Ideal für deine APU.'
  const FIT_RANK: Record<string, number> = { perfect: 0, marginal: 1, too_tight: 2 }
  const ROLE_LABEL: Record<string, string> = { vision: 'Bilder', coder: 'Coden', reasoning: 'Logik', agent: 'Agenten & Tools', scout: 'Allrounder', reviewer: 'Review', manager: 'Manager' }
  const CAP_KW = [
    { label: 'Bilder', kw: ['-vl-', '-vl', 'vision', 'llava', 'multimodal', '-mm-', 'pixtral'] },
    { label: 'Coden', kw: ['coder', '-code-', 'code-', 'codestral', 'starcoder'] },
    { label: 'Logik', kw: ['-r1', 'deepseek-r1', 'reasoning', 'qwq', 'magistral', '-think', 'thinking', '-o1'] },
    { label: 'Agenten & Tools', kw: ['hermes', '-tool', 'command-r', 'watt', '-fc-', 'function'] },
  ]

  function capLabel(name: string) {
    const l = (name || '').toLowerCase()
    for (const c of CAP_KW) if (c.kw.some((k: string) => l.includes(k))) return c.label
    return 'Allrounder'
  }
  function isMoe(s = '') {
    const l = s.toLowerCase()
    return /\d+x\d+(\.\d+)?b/.test(l) || /(?<![a-z])a\d+(\.\d+)?b\b/.test(l) || l.includes('mixtral') || /\bmoe\b/.test(l)
  }
  function moeActive(s = '') {
    const m = s.toLowerCase().match(/(?<![a-z])a(\d+(?:\.\d+)?)b\b/)
    return m ? m[1] : null
  }
  function fitCls(l: string) { return l === 'perfect' ? 'ok' : l === 'marginal' ? 'warn' : 'bad' }
  function fitWord(l: string) { return l === 'perfect' ? 'Passt' : l === 'marginal' ? 'Knapp' : 'Zu groß' }
  function metricLine(fit: any) { return `~${(fit.req_gb ?? 0).toFixed(1)} GB · ~${Math.round(fit.tps ?? 0)} Tok/s` }

  const sys = $derived(systemStore.value)

  // ---- Recipes ----
  let recipes = $state<any[]>([])
  let recommended = $state<string | null>(null)
  let recipeLoading = $state(true)

  // ---- Discover ----
  let discoverData = $state<any | null>(null)
  let discoverLoading = $state(false)
  let discoverStale = $state(false)

  // ---- Search ----
  let searchQuery = $state('')
  let searchResults = $state<any[]>([])
  let searchLoading = $state(false)
  let activeFilter = $state('')
  let sortBy = $state('downloads')
  let cardFits = $state<(string | null)[]>([])
  let bestResultIdx = $state(-1)

  // ---- Recipe detail modal ----
  let recipeModal = $state<any | null>(null)

  // ---- Model detail modal ----
  let modelModal = $state(false)
  let modelModalTitle = $state('')
  let modelModalRepo = $state('')
  let modelAnalysis = $state<any | null>(null)
  let modelFile = $state('')
  let modelRole = $state('')
  let modelCtx = $state(8192)
  let modelLoading = $state(false)
  let modelFit = $state<any | null>(null)

  // ---- New/Edit recipe modal ----
  let newRecipeOpen = $state(false)
  let newRecipeTitle = $state('')
  let newRecipeDesc = $state('')
  let newRecipeModels = $state<{ repo: string; role: string; quant: string }[]>([{ repo: '', role: '', quant: 'Q4_K_M' }])
  let editRecipeId = $state<string | null>(null)

  function refresh() { document.dispatchEvent(new Event('mc:refresh')) }
  function goView(v: string) { document.querySelector(`.nav-item[data-view="${v}"]`)?.dispatchEvent(new MouseEvent('click')) }

  onMount(() => {
    loadRecipes()
    loadDiscover()
  })

  async function loadRecipes() {
    recipeLoading = true
    try {
      const d = await api('/api/cookbook/recipes')
      recipes = d.recipes || []
      recommended = d.recommended_id || null
    } catch (e: any) { toast(e.message, true) }
    recipeLoading = false
  }

  async function loadDiscover(force = false) {
    discoverLoading = true
    try {
      const d = await api('/api/cookbook/discover' + (force ? '?force=true' : ''))
      discoverData = d
      discoverStale = d.stale
    } catch (e: any) { toast('Empfehlungen nicht ladbar: ' + e.message, true) }
    discoverLoading = false
  }

  // ---- Search ----
  function parseHfRepo(s: string) {
    s = s.trim()
    const u = s.match(/huggingface\.co\/([^\s/]+\/[^\s/?#]+)/i)
    if (u) return u[1]
    if (/^[\w.-]+\/[\w.-]+$/.test(s)) return s
    return null
  }

  async function doSearch() {
    const raw = searchQuery.trim()
    const repo = parseHfRepo(raw)
    let q = raw
    if (activeFilter && !repo) q = q ? q + ' ' + activeFilter : activeFilter
    if (!q && !repo) { searchResults = []; return }
    searchLoading = true; cardFits = []; bestResultIdx = -1
    try {
      if (repo) {
        searchResults = [{ id: repo, author: repo.split('/')[0], downloads: 0 }]
      } else {
        const url = `https://huggingface.co/api/models?search=${encodeURIComponent(q)}&filter=gguf&sort=${encodeURIComponent(sortBy)}&direction=-1&limit=40`
        const r = await fetch(url)
        searchResults = await r.json()
      }
      // Fetch fits in background
      searchResults.forEach((m: any, i: number) => fetchFitForCard(i, m.id))
    } catch (e: any) { toast(e.message, true); searchResults = [] }
    searchLoading = false
  }

  async function fetchFitForCard(i: number, repo_id: string) {
    try {
      const res = await api('/api/cookbook/analyze', { method: 'POST', body: JSON.stringify({ repo_id, ctx: 8192 }) })
      if (!res.files?.length) { cardFits[i] = null; updateBest(); return }
      const best = res.files.find((f: any) => f.quant?.includes('Q4_K_M')) || res.files[0]
      cardFits[i] = best.fit.level; updateBest()
    } catch { cardFits[i] = null; updateBest() }
  }

  function updateBest() {
    let bi = -1, br = 99
    cardFits.forEach((lvl, i) => {
      if (lvl == null) return
      const r = FIT_RANK[lvl] ?? 99
      if (r < br) { br = r; bi = i }
    })
    bestResultIdx = (bi >= 0 && br <= 1) ? bi : -1
  }

  // ---- Recipe detail modal ----
  function openRecipe(id: string) {
    recipeModal = recipes.find((r: any) => r.id === id)
  }

  async function installRecipe(id: string) {
    const btn = document.getElementById('cb-r-install-btn') as HTMLButtonElement
    if (btn) { btn.disabled = true; btn.textContent = 'Starte…' }
    try {
      const r = await api('/api/cookbook/install-recipe', { method: 'POST', body: JSON.stringify({ recipe_id: id, hf_token: getHfToken() }) })
      toast(`${r.count} Download${r.count !== 1 ? 's' : ''} läuft — Modelle erscheinen danach automatisch im Modelle-Tab.`)
      recipeModal = null; goView('activity')
    } catch (e: any) { toast(e.message, true); if (btn) { btn.disabled = false; btn.textContent = 'Installieren' } }
  }

  // ---- Model detail modal (from search/discover) ----
  async function openModel(repo: string, title: string) {
    modelModal = true; modelModalTitle = title; modelModalRepo = repo
    modelRole = ''; modelCtx = 8192; modelAnalysis = null; modelFit = null; modelFile = ''
    modelLoading = true
    try {
      modelAnalysis = await api('/api/cookbook/analyze', { method: 'POST', body: JSON.stringify({ repo_id: repo, ctx: 8192 }) })
      if (modelAnalysis?.files?.length) {
        const best = modelAnalysis.files.find((f: any) => f.quant?.includes('Q4_K_M')) || modelAnalysis.files[0]
        modelFile = best.filename
        // Auto-set optimal ctx
        if (best.optimal_ctx && modelCtx === 8192) modelCtx = best.optimal_ctx
        updateModelFit()
      }
    } catch (e: any) { toast(e.message, true) }
    modelLoading = false
  }

  function updateModelFit() {
    if (!modelAnalysis?.files?.length || !modelFile) return
    modelFit = modelAnalysis.files.find((f: any) => f.filename === modelFile) || null
    if (modelFit?.optimal_ctx && modelCtx === 8192) modelCtx = modelFit.optimal_ctx
  }

  async function doDownload() {
    if (!modelModalRepo || !modelFile) return toast('Bitte eine GGUF-Datei wählen.', true)
    modelLoading = true
    try {
      const params_b = modelAnalysis?.params_b ?? 7.0
      const quant = modelFit?.quant ?? 'Q4_K_M'
      await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({
        repo: modelModalRepo, role: modelRole, params_b, quant,
        file: modelFile, ctx: modelCtx, hf_token: getHfToken()
      })})
      toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.')
      modelModal = false; goView('activity')
    } catch (e: any) { toast('Fehler: ' + e.message, true) }
    modelLoading = false
  }

  // ---- Install discovered model ----
  async function installDiscovered(repo: string, role: string, params_b: number, btnEl: HTMLButtonElement) {
    btnEl.disabled = true; btnEl.textContent = 'Starte…'
    try {
      await api('/api/cookbook/install-model', { method: 'POST', body: JSON.stringify({ repo, role, params_b, quant: 'Q4_K_M', hf_token: getHfToken() }) })
      toast('Download läuft — Modell erscheint danach automatisch im Modelle-Tab.'); goView('activity')
    } catch (e: any) { toast(e.message, true); btnEl.disabled = false; btnEl.textContent = 'Installieren' }
  }

  // ---- New/edit recipe ----
  function openNewRecipe() {
    editRecipeId = null; newRecipeTitle = ''; newRecipeDesc = ''
    newRecipeModels = [{ repo: '', role: '', quant: 'Q4_K_M' }]
    newRecipeOpen = true
  }
  function openEditRecipe(id: string) {
    const r = recipes.find((x: any) => x.id === id); if (!r) return
    editRecipeId = id; newRecipeTitle = r.title; newRecipeDesc = r.desc || ''
    newRecipeModels = r.models.map((m: any) => ({ repo: m.repo, role: m.role, quant: m.quant || 'Q4_K_M' }))
    newRecipeOpen = true
  }
  async function saveRecipe() {
    if (!newRecipeTitle.trim()) return toast('Bitte einen Titel angeben.', true)
    const models = newRecipeModels.filter((m: any) => m.repo.trim())
    if (!models.length) return toast('Mindestens ein Modell (Repo) angeben.', true)
    try {
      if (editRecipeId) {
        await api('/api/cookbook/user-recipe/' + encodeURIComponent(editRecipeId), { method: 'PUT', body: JSON.stringify({ title: newRecipeTitle, desc: newRecipeDesc, models }) })
        toast('Setup aktualisiert.')
      } else {
        await api('/api/cookbook/user-recipe', { method: 'POST', body: JSON.stringify({ title: newRecipeTitle, desc: newRecipeDesc, models }) })
        toast('Setup gespeichert.')
      }
      newRecipeOpen = false; loadRecipes()
    } catch (e: any) { toast(e.message, true) }
  }
  async function deleteRecipe(id: string) {
    const r = recipes.find((x: any) => x.id === id)
    if (!await confirmModal({ title: `„${r?.title || id}" löschen?`, body: 'Dein eigenes Setup wird aus dem Cookbook entfernt. Bereits installierte Modelle bleiben unangetastet.', confirmLabel: 'Löschen', danger: true })) return
    try { await api('/api/cookbook/user-recipe/' + encodeURIComponent(id), { method: 'DELETE' }); toast('Setup gelöscht.'); loadRecipes() }
    catch (e: any) { toast(e.message, true) }
  }

  // ---- MoE chips ----
  function moeChipHtml(name: string) {
    if (!isMoe(name)) return ''
    const a = moeActive(name)
    return `<span class="chip" title="${esc(MOE_HELP)}">MoE${a ? ` · ~${a}B aktiv` : ''}</span>`
  }
  function capChipHtml(label: string) { return label ? `<span class="chip">${esc(label)}</span>` : '' }
</script>

<!-- ============================================================ -->
<!-- RECIPES (Use-Case Setups) -->
<div class="pagehead">
  <div><h1>Cookbook</h1><div class="sub">Modelle finden, installieren und eigene Setups zusammenstellen.</div></div>
</div>

<div class="card">
  <div class="card-h">
    <h3>1 · Anwendungsfall wählen &amp; installieren</h3>
    <button class="ghost" style="margin-left:auto" onclick={openNewRecipe}>+ Eigenes Setup</button>
  </div>
  <div class="card-sub">Hier starten: Fertige Modell-Bundles für deinen Anwendungsfall — ein Klick installiert alles. Die Hardware-Ampel zeigt vorab, ob das Setup auf deinen PC passt.</div>
  {#if recipeLoading}
    <div class="empty" style="text-align:center;padding:30px">Lade Setups…</div>
  {:else}
    <div id="cb-recipes" class="grid grid-3" style="gap:10px;margin-top:6px">
      {#each recipes as r}
        {@const best = r.id === recommended}
        <button class="card-btn{best ? ' cb-best' : ''}" style="text-align:left;position:relative"
          onclick={() => openRecipe(r.id)}>
          {#if r.user}
            <span title="Setup bearbeiten" style="position:absolute;top:10px;right:36px;color:var(--accent);cursor:pointer;font-size:14px;line-height:1" role="button" tabindex="0"
              onclick={e => { e.stopPropagation(); openEditRecipe(r.id) }} onkeydown={e => { if(e.key==='Enter'){e.stopPropagation();openEditRecipe(r.id)} }}>✎</span>
            <span title="Setup löschen" style="position:absolute;top:9px;right:12px;color:var(--err);cursor:pointer;font-size:18px;line-height:1" role="button" tabindex="0"
              onclick={e => { e.stopPropagation(); deleteRecipe(r.id) }} onkeydown={e => { if(e.key==='Enter'){e.stopPropagation();deleteRecipe(r.id)} }}>×</span>
          {/if}
          {#if best}<div class="cb-best-tag">★ Beste Wahl für dein System</div>
          {:else if r.user}<div class="cb-best-tag" style="background:var(--act);color:#fff">Dein Setup</div>{/if}
          <div class="flex justify-between items-center">
            <span class="flex items-center gap-2">
              <h3 style="margin:0;font-size:15px">{r.title}</h3>
            </span>
            <span class="fit-badge {fitCls(r.fit_level)}">{fitWord(r.fit_level)}</span>
          </div>
          <p>{r.desc}</p>
          <div class="text-xs text-mut">{r.models.length} Modell{r.models.length > 1 ? 'e' : ''} im Setup</div>
        </button>
      {/each}
    </div>
  {/if}
</div>

<!-- ============================================================ -->
<!-- DISCOVER -->
<div class="card">
  <div class="card-h">
    <h3>Aktuell die besten Modelle für dein System</h3>
    <span class="meta" style="margin-left:auto">
      {#if discoverData?.updated}
        {@const ageMin = Math.round((Date.now() / 1000 - discoverData.updated) / 60)}
        {#if ageMin < 60}
          Quellen befragt vor {ageMin} min
        {:else if ageMin < 1440}
          Quellen befragt vor {Math.round(ageMin / 60)} h
        {:else}
          Quellen befragt vor {Math.round(ageMin / 1440)} Tg.
        {/if}
        {#if discoverStale}<span style="color:var(--warn)"> · ⚠ Cache veraltet</span>{/if}
      {/if}
    </span>
    <button class="ghost" style="margin-left:8px" disabled={discoverLoading} onclick={() => loadDiscover(true)}>
      {discoverLoading ? 'Suche…' : 'Aktualisieren'}
    </button>
  </div>
  <div class="card-sub">Top-Modelle je Kategorie von HuggingFace — Hardware-Fit wird live für dein System berechnet.</div>
  {#if discoverLoading && !discoverData}
    <div class="empty" style="text-align:center;padding:30px">Frage Quellen ab…</div>
  {:else if discoverData?.categories}
    {#each discoverData.categories as cat}
      <div style="margin-bottom:8px">
        <div class="flex items-center gap-2" style="margin:6px 0 8px">
          <h4 style="margin:0;font-size:13.5px;font-weight:600">{cat.title}</h4>
        </div>
        <div class="grid grid-3">
          {#each cat.models as m}
            {@const best = cat.recommended && m.repo === cat.recommended}
            <div class="card{best ? ' res-best' : ''}" style="display:flex;flex-direction:column;position:relative">
              {#if best}<div class="cb-best-tag">★ Beste Wahl für dein System</div>{/if}
              <div class="flex justify-between" style="align-items:flex-start;gap:8px">
                <div style="min-width:0">
                  <h3 style="margin:0;font-size:14px;font-weight:500;word-break:break-word">{m.name}</h3>
                  <div class="text-xs text-mut" style="margin-top:3px">{m.author} · {m.params_b}B</div>
                </div>
                <span class="fit-badge {fitCls(m.fit.level)}">{m.fit.text}</span>
              </div>
              <div class="flex gap-2" style="flex-wrap:wrap;margin-top:7px">
                {@html capChipHtml(ROLE_LABEL[m.role] || capLabel(m.repo))}
                {@html moeChipHtml(m.repo)}
              </div>
              <div class="mono-sm text-mut" style="margin-top:8px;font-size:11.5px">{metricLine(m.fit)} · ⬇ {(m.downloads || 0).toLocaleString()}</div>
              {#each (m.requirements || []) as req}
                <div class="li-sub" style="color:var(--warn);margin-top:3px">⚠ {req}</div>
              {/each}
              <div style="flex:1;min-height:8px"></div>
              <div class="flex justify-end" style="margin-top:10px">
                {#if m.installed}
                  <span class="fit-badge ok" title="bereits eingerichtet als {m.installed}">✓ installiert</span>
                {:else}
                  <button class="primary" style="padding:6px 12px;font-size:12.5px"
                    onclick={e => installDiscovered(m.repo, m.role, m.params_b, e.currentTarget as HTMLButtonElement)}>
                    Installieren
                  </button>
                {/if}
              </div>
            </div>
          {/each}
        </div>
      </div>
    {/each}
  {/if}
</div>

<!-- ============================================================ -->
<!-- PRO SEARCH -->
<div class="card">
  <div class="card-h"><h3>Profi-Suche (HuggingFace)</h3></div>
  <div class="card-sub">Direkt HuggingFace durchsuchen — oder eine Modell-URL einfügen für den direkten Sprung.</div>
  <div class="flex gap-2" style="align-items:stretch;margin-bottom:10px">
    <input bind:value={searchQuery} placeholder="Suchbegriff oder HuggingFace-URL…" style="flex:1;margin:0"
      onkeydown={e => e.key === 'Enter' && doSearch()}>
    <select bind:value={sortBy} style="width:auto;margin:0">
      <option value="downloads">Downloads</option>
      <option value="trending">Trending</option>
      <option value="created_at">Neu</option>
    </select>
    <button class="primary" disabled={searchLoading} onclick={doSearch}>{searchLoading ? 'Lade…' : 'Suchen'}</button>
  </div>
  <!-- Filters -->
  <div class="flex gap-2" style="flex-wrap:wrap;margin-bottom:12px">
    {#each ['', 'coder', 'vision', 'reasoning', 'chat', 'agent'] as f}
      <button class="{activeFilter === f ? 'primary' : 'ghost'}" style="padding:4px 10px;border-radius:999px;font-size:12px"
        onclick={() => { activeFilter = f; doSearch() }}>
        {f || 'Alle'}
      </button>
    {/each}
  </div>
  <!-- Results -->
  {#if searchResults.length}
    <div class="grid grid-3" id="cb-grid">
      {#each searchResults as m, i}
        <div class="card{i === bestResultIdx ? ' res-best' : ''}" style="display:flex;flex-direction:column;cursor:pointer" onclick={() => openModel(m.id, m.id.split('/').pop())}>
          {#if i === bestResultIdx}<div class="cb-best-tag">★ Beste Wahl für dein System</div>{/if}
          <div class="flex justify-between" style="align-items:flex-start;gap:8px">
            <div style="min-width:0">
              <h3 style="margin:0;font-size:14.5px;font-weight:500;word-break:break-word">{m.id.split('/').pop()}</h3>
              <div class="text-xs text-mut" style="margin-top:3px">{m.author || ''}</div>
            </div>
            <span class="fit-badge {cardFits[i] ? fitCls(cardFits[i]!) : 'warn'}">{cardFits[i] ? fitWord(cardFits[i]!) : 'prüfe…'}</span>
          </div>
          <div class="flex gap-2" style="flex-wrap:wrap;margin-top:7px">
            {@html capChipHtml(capLabel(m.id))}
            {@html moeChipHtml(m.id)}
          </div>
          <div style="flex:1;margin-top:12px"></div>
          <div class="flex justify-between items-center text-xs text-mut" style="border-top:1px solid var(--line);padding-top:11px">
            <span class="mono-sm">{cardFits[i] ? metricLine({ req_gb: 0, tps: 0 }) : 'Hardware-Fit…'}</span>
            <span class="mono-sm">⬇ {(m.downloads || 0).toLocaleString()}</span>
          </div>
        </div>
      {/each}
    </div>
  {/if}
</div>

<!-- ============================================================ -->
<!-- RECIPE DETAIL MODAL -->
{#if recipeModal}
  {@const r = recipeModal}
  <div class="modal-overlay" role="dialog" onclick={e => { if ((e.target as HTMLElement).classList.contains('modal-overlay')) recipeModal = null }}>
    <div class="modal-card" style="max-width:560px">
      <button class="ghost" style="position:absolute;top:14px;right:14px" onclick={() => recipeModal = null}>Schließen</button>
      <h3>{r.title}</h3>
      <p class="text-mut text-sm" style="margin:-4px 0 16px">{r.desc}</p>
      <div class="list">
        {#each r.models as m}
          <div class="li" style="align-items:flex-start">
            <div class="li-main">
              <div class="flex items-center gap-2" style="flex-wrap:wrap">
                <span class="li-id">{m.name}</span>
                <span class="tag text">{m.role}</span>
                {@html moeChipHtml(m.repo)}
              </div>
              {#if m.why}<div class="li-sub">{m.why}</div>{/if}
              {#if m.fit}
                <div class="li-sub mono-sm">~{(m.fit.req_gb ?? 0).toFixed(1)} GB · ~{Math.round(m.fit.tps ?? 0)} Tok/s · optimal ~{Math.round((m.optimal_ctx || 8192) / 1024)}k Kontext</div>
              {/if}
            </div>
            {#if m.fit}<span class="fit-badge {fitCls(m.fit.level)}">{m.fit.text}</span>{/if}
          </div>
        {/each}
      </div>
      {#if r.models.length && r.models[0].fit}
        {@const maxRam = Math.max(...r.models.map((m: any) => m.fit?.req_gb ?? 0))}
        <div class="hint" style="margin:12px 0">Größter Spitzenbedarf: <b>~{maxRam.toFixed(1)} GB</b>. Es läuft immer nur <b>ein</b> Modell gleichzeitig — das größte bestimmt, ob das Setup passt.</div>
      {/if}
      {#if r.fit_level === 'too_tight'}
        {@const smallerRec = recipes.find((x: any) => !x.user && x.fit_level !== 'too_tight' && x.id !== r.id)}
        <div class="hint" style="color:var(--warn);margin-bottom:10px;border-left:2px solid var(--warn);padding-left:10px">
          ⚠ Dieses Setup übersteigt deinen verfügbaren Speicher.
          {#if smallerRec}
            Alternative: <a href="#" style="color:var(--accent)"
              onclick={e => { e.preventDefault(); recipeModal = null; setTimeout(() => openRecipe(smallerRec.id), 50) }}>
              „{smallerRec.title}" ansehen
            </a> — passt auf dein System.
          {/if}
        </div>
      {/if}
      <button id="cb-r-install-btn" class="primary{r.fit_level === 'too_tight' ? ' warn' : ''}" style="width:100%"
        onclick={() => installRecipe(r.id)}>
        {r.fit_level === 'too_tight' ? 'Trotzdem installieren (nicht empfohlen)' : 'Komplettes Setup installieren'}
      </button>
    </div>
  </div>
{/if}

<!-- ============================================================ -->
<!-- MODEL DETAIL MODAL -->
{#if modelModal}
  <div class="modal-overlay" role="dialog" onclick={e => { if ((e.target as HTMLElement).classList.contains('modal-overlay')) modelModal = false }}>
    <div class="modal-card" style="max-width:560px">
      <button class="ghost" style="position:absolute;top:14px;right:14px" onclick={() => modelModal = false}>Schließen</button>
      <h3>{modelModalTitle}</h3>
      <p class="mono-sm" style="margin:-4px 0 16px">{modelModalRepo}</p>
      {#if modelLoading && !modelAnalysis}
        <div class="hint">Lade Dateien von HuggingFace…</div>
      {:else if modelAnalysis?.files?.length}
        <label>Quantisierung (GGUF-Datei) wählen</label>
        <select bind:value={modelFile} onchange={updateModelFit}>
          {#each modelAnalysis.files as f}
            {@const mark = f.fit.level === 'perfect' ? '●' : f.fit.level === 'marginal' ? '◐' : '○'}
            <option value={f.filename}>{mark} {f.filename}</option>
          {/each}
        </select>
        <div class="row" style="margin-top:4px">
          <div>
            <label>Rolle (optional)</label>
            <input bind:value={modelRole} placeholder="z.B. coder, vision, scout">
          </div>
          <div>
            <label>Kontext-Größe {@html infoDot(CTX_HELP)}</label>
            <input type="number" bind:value={modelCtx}>
          </div>
        </div>
        {#if modelFit?.optimal_ctx}
          <div class="hint" style="margin:-6px 0 14px">
            Empfohlener Kontext für deine Hardware: <b>~{Math.round(modelFit.optimal_ctx / 1024)}k</b> —
            <a href="#" onclick={e => { e.preventDefault(); modelCtx = modelFit.optimal_ctx }}>übernehmen</a>
          </div>
        {/if}
        {#if modelFit}
          <div class="tile" style="display:flex;justify-content:space-between;align-items:center;margin:8px 0 18px">
            <div>
              <div style="font-size:13px">Ressourcen-Check</div>
              <div class="hint" style="margin:4px 0 0">~{(modelFit.fit.req_gb ?? 0).toFixed(1)} GB · ~{Math.round(modelFit.fit.tps ?? 0)} Tok/s · {modelFit.quant || 'GGUF'}</div>
            </div>
            <span class="fit-badge {fitCls(modelFit.fit.level)}">{modelFit.fit.text}</span>
          </div>
        {/if}
        {#if modelFit?.fit.level === 'too_tight' && modelAnalysis?.files}
          {@const better = modelAnalysis.files.find((f: any) => f.fit.level !== 'too_tight' && f.filename !== modelFile)}
          {#if better}
            <div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
              ⚠ Diese Datei ist zu groß für deinen Speicher. Kleinere Alternative:
              <a href="#" style="color:var(--accent)" onclick={e => { e.preventDefault(); modelFile = better.filename; updateModelFit() }}>
                {better.filename} ({fitWord(better.fit.level)})
              </a> — einfach auswählen.
            </div>
          {:else}
            <div class="hint" style="color:var(--warn);margin:-8px 0 14px;border-left:2px solid var(--warn);padding-left:10px">
              ⚠ Alle Varianten dieses Modells sind zu groß für deinen Speicher. Ein kleineres Modell aus den Empfehlungen wäre die bessere Wahl.
            </div>
          {/if}
        {/if}
        <button class="primary{modelFit?.fit.level === 'too_tight' ? ' warn' : ''}" style="width:100%" disabled={modelLoading}
          onclick={doDownload}>
          {modelLoading ? 'Starte…' : modelFit?.fit.level === 'too_tight' ? 'Trotzdem holen (zu groß — nicht empfohlen)' : 'Herunterladen & Einpflegen'}
        </button>
      {:else if !modelLoading}
        <div class="hint">Keine GGUF-Dateien im Repo gefunden.</div>
      {/if}
    </div>
  </div>
{/if}

<!-- ============================================================ -->
<!-- NEW/EDIT RECIPE MODAL -->
{#if newRecipeOpen}
  <div class="modal-overlay" role="dialog" onclick={e => { if ((e.target as HTMLElement).classList.contains('modal-overlay')) newRecipeOpen = false }}>
    <div class="modal-card" style="max-width:640px">
      <button class="ghost" style="position:absolute;top:14px;right:14px" onclick={() => newRecipeOpen = false}>Schließen</button>
      <h3>{editRecipeId ? 'Setup bearbeiten' : 'Eigenes Setup erstellen'}</h3>
      <p class="text-mut text-sm" style="margin:-4px 0 16px">Stell dir aus beliebigen Modellen ein eigenes Setup zusammen.</p>
      <label>Titel</label>
      <input bind:value={newRecipeTitle} placeholder="z.B. Mein Schreib-Stack">
      <label>Kurzbeschreibung (optional)</label>
      <input bind:value={newRecipeDesc} placeholder="Wofür ist dieses Setup gut?">
      <label style="margin-top:10px">Modelle</label>
      {#each newRecipeModels as m, i}
        <div class="flex gap-2" style="margin-bottom:8px;align-items:center">
          <input bind:value={m.repo} placeholder="Repo, z.B. unsloth/Qwen3-8B-GGUF" style="flex:2;margin:0">
          <input bind:value={m.role} placeholder="Rolle (z.B. coder)" style="flex:1;margin:0">
          <select bind:value={m.quant} style="width:auto;margin:0">
            {#each ['Q4_K_M','Q5_K_M','Q6_K','Q8_0','Q3_K_M'] as q}<option>{q}</option>{/each}
          </select>
          <button class="ghost del" style="margin:0;padding:6px 10px" onclick={() => newRecipeModels = newRecipeModels.filter((_, j) => j !== i)}>×</button>
        </div>
      {/each}
      <button class="ghost" style="margin-top:8px" onclick={() => newRecipeModels = [...newRecipeModels, { repo: '', role: '', quant: 'Q4_K_M' }]}>+ Modell hinzufügen</button>
      <div class="hint" style="margin-top:10px">Die <b>Repo-ID</b> findest du über die <b>Profi-Suche</b> weiter unten (z.B. <code>unsloth/Qwen3-8B-GGUF</code>).</div>
      <button class="primary" style="width:100%;margin-top:14px" onclick={saveRecipe}>
        {editRecipeId ? 'Änderungen speichern' : 'Setup speichern'}
      </button>
    </div>
  </div>
{/if}
