import { useEffect, useState } from 'react'

type AdminItem = {
  id: number
  slug: string
  name: string
  slot: string
  rarity: string
  itemLevel: number
  healingPower: number
  maxResourceBonus: number
  glyph: string
  imageUrl: string
  description: string
}

type AdminEncounter = {
  id: number
  dungeonId: number
  sequence: number
  slug: string
  enemyName: string
  encounterType: string
  imageUrl: string
}

type AdminDifficulty = {
  id: number
  slug: string
  name: string
  droppedItemLevel: number
}

type AdminLootEntry = {
  encounterId: number
  itemId: number
  difficultyId: number
  dropWeight: number
  dropChance: number
}

type AdminRecipeComponent = {
  itemId: number
  quantity: number
}

type AdminRecipe = {
  id: number
  itemId: number
  difficultyId: number | null
  sourceDungeonId: number | null
  sourceEncounterId: number | null
  components: AdminRecipeComponent[]
}

type AdminDungeon = {
  id: number
  slug: string
  name: string
}

type AdminData = {
  items: AdminItem[]
  encounters: AdminEncounter[]
  difficulties: AdminDifficulty[]
  encounterLoot: AdminLootEntry[]
  craftingRecipes: AdminRecipe[]
  dungeons: AdminDungeon[]
}

const API = '/api/admin'

async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, init)
  const body = await res.json()
  if (!res.ok) throw new Error(body.error ?? 'Request failed')
  return body
}

export function AdminScreen({ onBack }: { onBack: () => void }) {
  const [data, setData] = useState<AdminData | null>(null)
  const [tab, setTab] = useState<'items' | 'bosses' | 'loot' | 'crafting'>('items')
  const [error, setError] = useState('')
  const [saving, setSaving] = useState<Record<string, boolean>>({})

  useEffect(() => {
    fetchJson<AdminData>(`${API}/data`)
      .then(setData)
      .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load'))
  }, [])

  if (error) return <section className="content-screen"><p className="error-message">{error}</p></section>
  if (!data) return <section className="content-screen"><p>Loading admin data...</p></section>

  return (
    <section className="content-screen admin-screen">
      <div className="screen-heading">
        <div><p className="eyebrow">Developer Tools</p><h1>Admin Panel</h1></div>
        <button className="back-button" onClick={onBack} type="button">Back</button>
      </div>
      <nav className="admin-tabs">
        {(['items', 'bosses', 'loot', 'crafting'] as const).map((t) => (
          <button key={t} className={`admin-tab ${tab === t ? 'active' : ''}`}
            onClick={() => setTab(t)} type="button">
            {t === 'items' ? 'Items' : t === 'bosses' ? 'Boss Images' : t === 'loot' ? 'Boss Loot' : 'Crafting'}
          </button>
        ))}
      </nav>
      {tab === 'items' && <ItemsTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
      {tab === 'bosses' && <BossImagesTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
      {tab === 'loot' && <LootTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
      {tab === 'crafting' && <CraftingTab data={data} setData={setData} setSaving={setSaving} saving={saving} />}
    </section>
  )
}

function ItemsTab({ data, setData, setSaving, saving }: {
  data: AdminData | null
  setData: React.Dispatch<React.SetStateAction<AdminData | null>>
  setSaving: (s: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void
  saving: Record<string, boolean>
}) {
  if (!data) return null
  const [filter, setFilter] = useState('')
  const [editId, setEditId] = useState<number | null>(null)
  const [form, setForm] = useState<Partial<AdminItem>>({})

  const groups = groupBy(data.items.filter((i) =>
    i.name.toLowerCase().includes(filter.toLowerCase()) || i.slug.includes(filter)),
    (i) => i.slot,
  )

  async function saveItem(id: number) {
    setSaving((prev) => ({ ...prev, [`item-${id}`]: true }))
    try {
      const body: Record<string, string | number> = {}
      if (form.name !== undefined) body.name = form.name
      if (form.glyph !== undefined) body.glyph = form.glyph
      if (form.description !== undefined) body.description = form.description
      if (form.rarity !== undefined) body.rarity = form.rarity
      if (form.slot !== undefined) body.slot = form.slot
      if (form.itemLevel !== undefined) body.item_level = form.itemLevel
      if (form.healingPower !== undefined) body.healing_power = form.healingPower
      if (form.maxResourceBonus !== undefined) body.max_resource_bonus = form.maxResourceBonus
      await fetchJson(`${API}/items/${id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(body),
      })
      setData((prev) => prev ? {
        ...prev,
        items: prev.items.map((i) => i.id === id ? { ...i, ...form } : i),
      } : prev)
      setEditId(null)
      setForm({})
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Save failed')
    } finally {
      setSaving((prev) => ({ ...prev, [`item-${id}`]: false }))
    }
  }

  return (
    <div className="admin-panel">
      <input className="admin-search" placeholder="Search items..." value={filter}
        onChange={(e) => setFilter(e.target.value)} />
      {Object.entries(groups).map(([slot, items]) => (
        <details key={slot} open>
          <summary className="admin-group-header">{slot} ({items.length})</summary>
          <div className="admin-grid">
            {items.map((item) => (
              <div key={item.id} className="admin-card">
                {editId === item.id ? (
                  <div className="admin-edit-form">
                    <label>Glyph <input value={form.glyph ?? item.glyph} onChange={(e) => setForm({ ...form, glyph: e.target.value })} /></label>
                    <label>Name <input value={form.name ?? item.name} onChange={(e) => setForm({ ...form, name: e.target.value })} /></label>
                    <label>Slot
                      <select value={form.slot ?? item.slot} onChange={(e) => setForm({ ...form, slot: e.target.value })}>
                        {['weapon', 'helmet', 'chest', 'gloves', 'boots', 'pants', 'ring', 'necklace', 'trinket', 'component'].map((s) => (
                          <option key={s} value={s}>{s}</option>
                        ))}
                      </select>
                    </label>
                    <label>Rarity
                      <select value={form.rarity ?? item.rarity} onChange={(e) => setForm({ ...form, rarity: e.target.value })}>
                        {['common', 'uncommon', 'rare', 'epic', 'legendary'].map((r) => (
                          <option key={r} value={r}>{r}</option>
                        ))}
                      </select>
                    </label>
                    <label>iLvl <input type="number" value={form.itemLevel ?? item.itemLevel} onChange={(e) => setForm({ ...form, itemLevel: Number(e.target.value) })} /></label>
                    <label>Healing <input type="number" value={form.healingPower ?? item.healingPower} onChange={(e) => setForm({ ...form, healingPower: Number(e.target.value) })} /></label>
                    <label>Resource <input type="number" value={form.maxResourceBonus ?? item.maxResourceBonus} onChange={(e) => setForm({ ...form, maxResourceBonus: Number(e.target.value) })} /></label>
                    <label>Description <textarea value={form.description ?? item.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></label>
                    <div className="admin-edit-actions">
                      <button className="primary-button" onClick={() => saveItem(item.id)} disabled={saving[`item-${item.id}`]} type="button">
                        {saving[`item-${item.id}`] ? 'Saving...' : 'Save'}
                      </button>
                      <button className="text-button" onClick={() => { setEditId(null); setForm({}) }} type="button">Cancel</button>
                    </div>
                  </div>
                ) : (
                  <>
                    <div className="admin-item-header">
                      <span className={`admin-glyph rarity-${item.rarity}`}>{item.glyph}</span>
                      <div>
                        <strong>{item.name}</strong>
                        <small className="admin-item-meta">{item.slot} · iLvl {item.itemLevel} · {item.rarity}</small>
                      </div>
                    </div>
                    <p className="admin-item-desc">{item.description}</p>
                    <div className="admin-item-stats">
                      <span>+{item.healingPower} healing</span>
                      <span>+{item.maxResourceBonus} resource</span>
                    </div>
                    <button className="text-button" onClick={() => { setEditId(item.id); setForm({}) }} type="button">Edit</button>
                  </>
                )}
              </div>
            ))}
          </div>
        </details>
      ))}
    </div>
  )
}

function BossImagesTab({ data, setData, setSaving, saving }: {
  data: AdminData | null
  setData: React.Dispatch<React.SetStateAction<AdminData | null>>
  setSaving: (s: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void
  saving: Record<string, boolean>
}) {
  if (!data) return null

  async function uploadBossImage(encounterId: number, file: File | undefined) {
    if (!file) return
    setSaving((prev) => ({ ...prev, [`boss-image-${encounterId}`]: true }))
    try {
      const imageData = await fileToDataUrl(file)
      const result = await fetchJson<{ imageUrl: string }>(`${API}/encounters/${encounterId}/image`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ imageData }),
      })
      setData((prev) => prev ? {
        ...prev,
        encounters: prev.encounters.map((encounter) => (
          encounter.id === encounterId
            ? { ...encounter, imageUrl: result.imageUrl }
            : encounter
        )),
      } : prev)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Upload failed')
    } finally {
      setSaving((prev) => ({ ...prev, [`boss-image-${encounterId}`]: false }))
    }
  }

  const bosses = data.encounters.filter((encounter) => encounter.encounterType === 'boss')

  return (
    <div className="admin-panel">
      <div className="admin-grid boss-image-grid">
        {bosses.map((boss) => (
          <div key={boss.id} className="admin-card boss-image-card">
            <img src={boss.imageUrl} alt={`${boss.enemyName} icon`} />
            <div>
              <strong>{boss.enemyName}</strong>
              <small className="admin-item-meta">Dungeon {boss.dungeonId} · Encounter {boss.sequence}</small>
            </div>
            <label className="boss-upload-button">
              {saving[`boss-image-${boss.id}`] ? 'Uploading...' : 'Upload Image'}
              <input
                accept="image/png,image/jpeg,image/webp,image/gif"
                disabled={saving[`boss-image-${boss.id}`]}
                onChange={(event) => uploadBossImage(boss.id, event.target.files?.[0])}
                type="file"
              />
            </label>
          </div>
        ))}
      </div>
    </div>
  )
}

function LootTab({ data, setData, setSaving, saving }: {
  data: AdminData | null
  setData: React.Dispatch<React.SetStateAction<AdminData | null>>
  setSaving: (s: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void
  saving: Record<string, boolean>
}) {
  const [encounterId, setEncounterId] = useState(data?.encounters.filter(e => e.encounterType === 'boss')[0]?.id ?? 0)
  const [difficultyId, setDifficultyId] = useState(data?.difficulties[0]?.id ?? 0)
  const [addItemId, setAddItemId] = useState(0)
  const [addDropWeight, setAddDropWeight] = useState(100)
  const [addDropChance, setAddDropChance] = useState(1)
  const [renameItemId, setRenameItemId] = useState<number | null>(null)
  const [renameValue, setRenameValue] = useState('')
  const [bossSort, setBossSort] = useState<'dungeon' | 'boss'>('dungeon')

  if (!data) return null

  const enc = data.encounters.find(e => e.id === encounterId)
  const bossOptions = data.encounters
    .filter((e) => e.encounterType === 'boss')
    .sort((a, b) => bossSort === 'boss'
      ? a.enemyName.localeCompare(b.enemyName) || a.dungeonId - b.dungeonId || a.sequence - b.sequence
      : a.dungeonId - b.dungeonId || a.sequence - b.sequence || a.enemyName.localeCompare(b.enemyName))
  const bossLoot = data.encounterLoot.filter(
    (l) => l.encounterId === encounterId && l.difficultyId === difficultyId,
  )

  const items = data.items
  function itemName(id: number) { return items.find(i => i.id === id)?.name ?? `#${id}` }
  function itemGlyph(id: number) { return items.find(i => i.id === id)?.glyph ?? '?' }

  async function renameItem(itemId: number) {
    if (renameValue.trim() === '' || renameValue === itemName(itemId)) return
    setSaving((prev) => ({ ...prev, [`loot-rename-${itemId}`]: true }))
    try {
      await fetchJson(`${API}/items/${itemId}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: renameValue.trim() }),
      })
      setData((prev) => prev ? {
        ...prev,
        items: prev.items.map((item) =>
          item.id === itemId ? { ...item, name: renameValue.trim() } : item,
        ),
      } : prev)
      setRenameItemId(null)
      setRenameValue('')
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Rename failed')
    } finally {
      setSaving((prev) => ({ ...prev, [`loot-rename-${itemId}`]: false }))
    }
  }

  async function deleteLoot(eId: number, dId: number, iId: number) {
    try {
      await fetchJson(`${API}/encounter-loot/${eId}/${dId}/${iId}`, { method: 'DELETE' })
      setData((prev) => prev ? {
        ...prev,
        encounterLoot: prev.encounterLoot.filter(
          (l) => !(l.encounterId === eId && l.difficultyId === dId && l.itemId === iId),
        ),
      } : prev)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Delete failed')
    }
  }

  async function addLoot() {
    if (!addItemId) return
    try {
      await fetchJson(`${API}/encounter-loot`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ encounterId, itemId: addItemId, difficultyId, dropWeight: addDropWeight, dropChance: addDropChance }),
      })
      setData((prev) => prev ? {
        ...prev,
        encounterLoot: [
          ...prev.encounterLoot.filter(
            (l) => !(l.encounterId === encounterId && l.difficultyId === difficultyId && l.itemId === addItemId),
          ),
          { encounterId, itemId: addItemId, difficultyId, dropWeight: addDropWeight, dropChance: addDropChance },
        ],
      } : prev)
      setAddItemId(0)
      setAddDropWeight(100)
      setAddDropChance(1)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Add failed')
    }
  }

  return (
    <div className="admin-panel">
      <div className="admin-loot-selectors">
        <label>Boss
          <select value={encounterId} onChange={(e) => setEncounterId(Number(e.target.value))}>
            {bossOptions.map((e) => (
              <option key={e.id} value={e.id}>{e.enemyName} (dungeon {e.dungeonId})</option>
            ))}
          </select>
        </label>
        <label>Sort Bosses
          <select value={bossSort} onChange={(e) => setBossSort(e.target.value as 'dungeon' | 'boss')}>
            <option value="dungeon">Dungeon order</option>
            <option value="boss">Boss name</option>
          </select>
        </label>
        <label>Difficulty
          <select value={difficultyId} onChange={(e) => setDifficultyId(Number(e.target.value))}>
            {data.difficulties.map((d) => (
              <option key={d.id} value={d.id}>{d.name} (iLvl {d.droppedItemLevel})</option>
            ))}
          </select>
        </label>
      </div>

      <h3 className="admin-loot-title">
        {enc?.enemyName ?? 'Unknown'} — {data.difficulties.find(d => d.id === difficultyId)?.name ?? '?'}
      </h3>

      {bossLoot.length === 0 && <p className="admin-empty">No loot entries for this boss + difficulty.</p>}

      <div className="admin-loot-list">
        {bossLoot.map((entry) => (
          <div key={`${entry.itemId}`} className="admin-loot-row">
            <span className={`admin-glyph rarity-${data.items.find(i => i.id === entry.itemId)?.rarity ?? 'common'}`}>
              {itemGlyph(entry.itemId)}
            </span>
            {renameItemId === entry.itemId ? (
              <span className="admin-loot-name">
                <input
                  className="admin-rename-input"
                  ref={(node) => {
                    if (node) window.requestAnimationFrame(() => node.focus({ preventScroll: true }))
                  }}
                  value={renameValue}
                  onChange={(e) => setRenameValue(e.target.value)}
                  onKeyDown={(e) => { if (e.key === 'Enter') renameItem(entry.itemId); if (e.key === 'Escape') { setRenameItemId(null); setRenameValue('') } }}
                />
              </span>
            ) : (
              <span className="admin-loot-name">{itemName(entry.itemId)}</span>
            )}
            <span className="admin-loot-weight">Weight: {entry.dropWeight}</span>
            <span className="admin-loot-chance">Chance: {(entry.dropChance * 100).toFixed(0)}%</span>
            {renameItemId === entry.itemId ? (
              <>
                <button className="primary-button" disabled={saving[`loot-rename-${entry.itemId}`]} onClick={() => renameItem(entry.itemId)} type="button">
                  {saving[`loot-rename-${entry.itemId}`] ? 'Saving...' : 'Save'}
                </button>
                <button className="text-button" onClick={() => { setRenameItemId(null); setRenameValue('') }} type="button">Cancel</button>
              </>
            ) : (
              <>
                <button className="text-button" onClick={() => { setRenameItemId(entry.itemId); setRenameValue(itemName(entry.itemId)) }} type="button">Rename</button>
                <button className="danger-button" onClick={() => deleteLoot(entry.encounterId, entry.difficultyId, entry.itemId)} type="button">X</button>
              </>
            )}
          </div>
        ))}
      </div>

      <details className="admin-add-section">
        <summary>Add Item to Loot Table</summary>
        <div className="admin-add-form">
          <label>Item
            <select value={addItemId} onChange={(e) => setAddItemId(Number(e.target.value))}>
              <option value={0}>Select item...</option>
              {data.items.filter((i) => !bossLoot.some((l) => l.itemId === i.id)).map((i) => (
                <option key={i.id} value={i.id}>[{i.glyph}] {i.name} ({i.slot})</option>
              ))}
            </select>
          </label>
          <label>Drop Weight <input type="number" value={addDropWeight} onChange={(e) => setAddDropWeight(Number(e.target.value))} /></label>
          <label>Drop Chance <input type="number" min="0" max="1" step="0.05" value={addDropChance} onChange={(e) => setAddDropChance(Number(e.target.value))} /></label>
          <button className="primary-button" onClick={addLoot} disabled={!addItemId} type="button">Add</button>
        </div>
      </details>
    </div>
  )
}

function CraftingTab({ data, setData, setSaving, saving }: {
  data: AdminData | null
  setData: React.Dispatch<React.SetStateAction<AdminData | null>>
  setSaving: (s: Record<string, boolean> | ((prev: Record<string, boolean>) => Record<string, boolean>)) => void
  saving: Record<string, boolean>
}) {
  const [recipeId, setRecipeId] = useState(data?.craftingRecipes[0]?.id ?? 0)
  const [itemLevelFilter, setItemLevelFilter] = useState('all')
  const [bossFilterId, setBossFilterId] = useState(0)
  const [addItemId, setAddItemId] = useState(0)
  const [addQty, setAddQty] = useState(1)
  const [outputNameByItem, setOutputNameByItem] = useState<Record<number, string>>({})

  if (!data) return null

  const bossEncounters = data.encounters.filter((encounter) => encounter.encounterType === 'boss')
  const bossLootItemIds = new Set(
    data.encounterLoot
      .filter((entry) => bossEncounters.some((boss) => boss.id === entry.encounterId))
      .map((entry) => entry.itemId),
  )
  const itemLevels = Array.from(new Set(
    data.craftingRecipes
      .map((candidate) => data.items.find((item) => item.id === candidate.itemId)?.itemLevel)
      .filter((itemLevel): itemLevel is number => itemLevel !== undefined),
  )).sort((a, b) => a - b)
  const filteredRecipes = data.craftingRecipes.filter((candidate) => {
    const item = data.items.find((i) => i.id === candidate.itemId)
    if (!item) return false
    if (itemLevelFilter !== 'all' && item.itemLevel !== Number(itemLevelFilter)) return false
    if (bossFilterId === 0) return true
    return candidate.sourceEncounterId === bossFilterId
      || candidate.components.some((component) => data.encounterLoot.some(
        (entry) => entry.encounterId === bossFilterId && entry.itemId === component.itemId,
      ))
  })
  const recipe = filteredRecipes.find((r) => r.id === recipeId) ?? filteredRecipes[0] ?? null
  const outputItem = recipe ? data.items.find((i) => i.id === recipe.itemId) : null
  const outputName = outputItem ? outputNameByItem[outputItem.id] ?? outputItem.name : ''
  const bossComponentOptions = data.items.filter((item) => (
    item.slot === 'component'
    && bossLootItemIds.has(item.id)
    && !recipe?.components.some((component) => component.itemId === item.id)
  ))
  const addComponentIsValid = bossComponentOptions.some((item) => item.id === addItemId)

  const items = data.items
  function itemName(id: number) { return items.find(i => i.id === id)?.name ?? `#${id}` }
  function itemGlyph(id: number) { return items.find(i => i.id === id)?.glyph ?? '?' }
  function bossName(id: number | null) {
    return id ? bossEncounters.find((boss) => boss.id === id)?.enemyName ?? `Boss #${id}` : 'Any boss'
  }
  function componentBossNames(itemId: number) {
    return (data!).encounterLoot
      .filter((entry) => entry.itemId === itemId)
      .map((entry) => bossEncounters.find((boss) => boss.id === entry.encounterId)?.enemyName ?? `Boss #${entry.encounterId}`)
      .join(', ')
  }

  async function saveOutputName() {
    if (!outputItem || outputName.trim() === '' || outputName === outputItem.name) return
    setSaving((prev) => ({ ...prev, [`item-name-${outputItem.id}`]: true }))
    try {
      await fetchJson(`${API}/items/${outputItem.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: outputName.trim() }),
      })
      setData((prev) => prev ? {
        ...prev,
        items: prev.items.map((item) => (
          item.id === outputItem.id ? { ...item, name: outputName.trim() } : item
        )),
      } : prev)
      setOutputNameByItem((prev) => ({ ...prev, [outputItem.id]: outputName.trim() }))
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Rename failed')
    } finally {
      setSaving((prev) => ({ ...prev, [`item-name-${outputItem.id}`]: false }))
    }
  }

  async function uploadItemImage(itemId: number, file: File | undefined) {
    if (!file) return
    setSaving((prev) => ({ ...prev, [`item-image-${itemId}`]: true }))
    try {
      const imageData = await fileToDataUrl(file)
      const result = await fetchJson<{ imageUrl: string }>(`${API}/items/${itemId}/image`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ imageData }),
      })
      setData((prev) => prev ? {
        ...prev,
        items: prev.items.map((item) => (
          item.id === itemId ? { ...item, imageUrl: result.imageUrl } : item
        )),
      } : prev)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Upload failed')
    } finally {
      setSaving((prev) => ({ ...prev, [`item-image-${itemId}`]: false }))
    }
  }

  async function deleteComponent(rId: number, iId: number) {
    try {
      await fetchJson(`${API}/crafting-recipes/${rId}/components/${iId}`, { method: 'DELETE' })
      setData((prev) => prev ? {
        ...prev,
        craftingRecipes: prev.craftingRecipes.map((r) =>
          r.id === rId ? { ...r, components: r.components.filter((c) => c.itemId !== iId) } : r,
        ),
      } : prev)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Delete failed')
    }
  }

  async function addComponent() {
    if (!addComponentIsValid || !recipe) return
    try {
      await fetchJson(`${API}/crafting-recipes/${recipe.id}/components`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ itemId: addItemId, quantity: addQty }),
      })
      setData((prev) => prev ? {
        ...prev,
        craftingRecipes: prev.craftingRecipes.map((r) =>
          r.id === recipe!.id
            ? { ...r, components: [...r.components.filter((c) => c.itemId !== addItemId), { itemId: addItemId, quantity: addQty }] }
            : r,
        ),
      } : prev)
      setAddItemId(0)
      setAddQty(1)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Add failed')
    }
  }

  async function updateQty(rId: number, iId: number, quantity: number) {
    try {
      await fetchJson(`${API}/crafting-recipes/${rId}/components`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ itemId: iId, quantity }),
      })
      setData((prev) => prev ? {
        ...prev,
        craftingRecipes: prev.craftingRecipes.map((r) =>
          r.id === rId ? {
            ...r,
            components: r.components.map((c) => c.itemId === iId ? { ...c, quantity } : c),
          } : r,
        ),
      } : prev)
    } catch (e: unknown) {
      alert(e instanceof Error ? e.message : 'Update failed')
    }
  }

  return (
    <div className="admin-panel">
      <div className="admin-crafting-filters">
        <label>Item Level
          <select value={itemLevelFilter} onChange={(e) => setItemLevelFilter(e.target.value)}>
            <option value="all">All levels</option>
            {itemLevels.map((itemLevel) => (
              <option key={itemLevel} value={itemLevel}>iLvl {itemLevel}</option>
            ))}
          </select>
        </label>
        <label>Boss
          <select value={bossFilterId} onChange={(e) => setBossFilterId(Number(e.target.value))}>
            <option value={0}>All bosses</option>
            {bossEncounters.map((boss) => (
              <option key={boss.id} value={boss.id}>{boss.enemyName}</option>
            ))}
          </select>
        </label>
        <label>Recipe
          <select value={recipe?.id ?? 0} onChange={(e) => setRecipeId(Number(e.target.value))}>
            {filteredRecipes.length === 0 && <option value={0}>No matching recipes</option>}
            {filteredRecipes.map((r) => (
              <option key={r.id} value={r.id}>
                [{itemGlyph(r.itemId)}] {itemName(r.itemId)} - {bossName(r.sourceEncounterId)}
              </option>
            ))}
          </select>
        </label>
      </div>

      {outputItem && (
        <div className="admin-recipe-header">
          <img className="admin-recipe-image" src={outputItem.imageUrl || '/equipment-placeholder.svg'} alt={`${outputItem.name} icon`} />
          <div>
            <label className="admin-inline-field">Name
              <input
                value={outputName}
                onChange={(e) => {
                  if (!outputItem) return
                  setOutputNameByItem((prev) => ({ ...prev, [outputItem.id]: e.target.value }))
                }}
              />
            </label>
            <small className="admin-item-meta">{outputItem.slot} · iLvl {outputItem.itemLevel} · {outputItem.rarity}</small>
          </div>
          <div className="admin-recipe-actions">
            <button
              className="primary-button"
              disabled={saving[`item-name-${outputItem.id}`] || outputName.trim() === '' || outputName === outputItem.name}
              onClick={saveOutputName}
              type="button"
            >
              {saving[`item-name-${outputItem.id}`] ? 'Saving...' : 'Rename'}
            </button>
            <label className="boss-upload-button">
              {saving[`item-image-${outputItem.id}`] ? 'Uploading...' : 'Upload Image'}
              <input
                accept="image/png,image/jpeg,image/webp,image/gif"
                disabled={saving[`item-image-${outputItem.id}`]}
                onChange={(event) => uploadItemImage(outputItem.id, event.target.files?.[0])}
                type="file"
              />
            </label>
          </div>
        </div>
      )}

      <h3 className="admin-loot-title">Required Components</h3>

      {(!recipe || recipe.components.length === 0) && (
        <p className="admin-empty">No component requirements.</p>
      )}

      <div className="admin-loot-list">
        {recipe?.components.map((comp) => (
          <div key={comp.itemId} className="admin-loot-row">
            <span className={`admin-glyph rarity-${data.items.find(i => i.id === comp.itemId)?.rarity ?? 'common'}`}>
              {itemGlyph(comp.itemId)}
            </span>
            <span className="admin-loot-name">{itemName(comp.itemId)}</span>
            <span className="admin-loot-weight">Qty:
              <input className="admin-qty-input" type="number" min="1" value={comp.quantity}
                onChange={(e) => updateQty(recipe.id, comp.itemId, Number(e.target.value))} />
            </span>
            <button className="danger-button" onClick={() => deleteComponent(recipe.id, comp.itemId)} type="button">X</button>
          </div>
        ))}
      </div>

      <details className="admin-add-section">
        <summary>Add Component</summary>
        <div className="admin-add-form">
          <label>Component
            <select value={addItemId} onChange={(e) => setAddItemId(Number(e.target.value))}>
              <option value={0}>Select component...</option>
              {bossComponentOptions.map((i) => (
                <option key={i.id} value={i.id}>[{i.glyph}] {i.name} ({componentBossNames(i.id)})</option>
              ))}
            </select>
          </label>
          <label>Quantity <input type="number" min="1" value={addQty} onChange={(e) => setAddQty(Number(e.target.value))} /></label>
          <button className="primary-button" onClick={addComponent} disabled={!recipe || !addComponentIsValid} type="button">Add</button>
        </div>
      </details>
    </div>
  )
}

function fileToDataUrl(file: File) {
  return new Promise<string>((resolve, reject) => {
    const reader = new FileReader()
    reader.addEventListener('load', () => {
      if (typeof reader.result === 'string') resolve(reader.result)
      else reject(new Error('Unable to read image.'))
    })
    reader.addEventListener('error', () => reject(reader.error ?? new Error('Unable to read image.')))
    reader.readAsDataURL(file)
  })
}

function groupBy<T>(items: T[], keyFn: (item: T) => string): Record<string, T[]> {
  const groups: Record<string, T[]> = {}
  for (const item of items) {
    const key = keyFn(item)
    if (!groups[key]) groups[key] = []
    groups[key].push(item)
  }
  return groups
}
