import { apiFetch, basePath, cardImageLayoutClasses, cardImageLayoutDataAttrs, escapeHtml } from 'ect/site';
import { openCardModal } from 'ect/card-modal';
import { identitySummaryMarkup, manaCostMarkup, typeLineMarkup } from 'ect/mana';
import { comboInputs, filterComboOptions } from './multi-combo.js';
import { clearViewportPanel, createActivationTracker, observeViewport, positionViewportPanel } from 'ect/viewport-ui';

const app = document.getElementById('card-search-app');
if (!app) throw new Error('Card search root is missing.');

const isBuilder = app.dataset.builder === '1';
const isCardBrowser = app.dataset.cardBrowser === '1';
const form = document.getElementById('card-search-form');
const gallery = document.getElementById('card-gallery');
const summary = document.getElementById('result-summary');
const pagination = document.getElementById('card-pagination');
const subtypeCombo = document.getElementById('filter-subtype');
const dialog = document.getElementById('card-dialog');
const dialogContent = document.getElementById('card-dialog-content');
const dialogClose = dialog?.querySelector('.dialog-close');
const sentinel = document.getElementById('gallery-sentinel');
const queryInput = document.getElementById('card-query');
const queryClearButton = document.querySelector('[data-clear-card-query]');
const syntaxDialog = document.getElementById('search-syntax-dialog');
const syntaxOpenButton = document.querySelector('[data-open-search-syntax]');
const syntaxCloseButton = document.querySelector('[data-close-search-syntax]');
const leaderFilterInput = document.getElementById('filter-leader-search');
const leaderFilterOptions = document.getElementById('filter-leader-options');
const leaderFilterClearButton = document.querySelector('[data-clear-leader-filter]');
const deckLeaderClearButton = document.querySelector('[data-clear-deck-leader-context]');
const resetFiltersButton = document.getElementById('reset-card-filters');
const deckContextInput = document.getElementById('active-deck-select');
const deckContextTrigger = document.getElementById('active-deck-trigger');
const deckContextLabel = document.getElementById('active-deck-label');
const deckContextOptions = document.getElementById('active-deck-options');
const multiCombos = [...form.querySelectorAll('[data-filter-name]')];
const userPrintingBias = window.ECTMTG_USER?.printingBias || 'usable';
const scrollThroughPagination = isCardBrowser && window.ECTMTG_USER?.scrollThroughPagination !== false;
const hideCardDetails = isCardBrowser && window.ECTMTG_USER?.hideCardDetails === true;
const api = (path) => `${basePath}/api/v1${path}`;
const limitText = (value, limit) => String(value || '').slice(0, limit);
const limitLine = (value, limit) => String(value || '').replace(/\s+/g, ' ').trim().slice(0, limit);
const policyLabel = (status) => ({ banned: 'Banned', watch_high: 'High Risk', watch_low: 'Low Risk', pillar: 'Pillar', released: 'Released' }[status] || status);
const cardBackImage = `${basePath}/assets/images/ect-card-back.png`;
const cardImageFallbackAttr = ` onerror="this.onerror=null;this.src='${escapeHtml(cardBackImage)}';"`;

let abortController = null;
let currentPage = 1;
let totalPages = 1;
let totalResults = 0;
let loadingNextPage = false;
let autoPagingPaused = false;
let activeSearchToken = 0;
let resultItems = [];
let renderedCount = 0;
let selectedDeckState = null;
let pendingCardAdd = null;
let stageObserver = null;
let leaderFilterController = null;
let leaderFilterTimer = null;
let leaderFilterResults = [];
let activeLeaderContext = null;
let contextBonusColor = '';
let deckSelectorItems = [];
const colorOrder = ['C', 'W', 'U', 'B', 'R', 'G'];

function updateSearchClearButtons() {
  if (queryClearButton) queryClearButton.hidden = !queryInput?.value;
  if (leaderFilterClearButton) {
    const hasLeader = !!(document.getElementById('search-leader-id')?.value || leaderFilterInput?.value?.trim());
    leaderFilterClearButton.hidden = !hasLeader;
  }
}

function initSearchSyntaxHelp() {
  if (!syntaxDialog || !syntaxOpenButton) return;
  syntaxOpenButton.addEventListener('click', () => syntaxDialog.showModal());
  syntaxCloseButton?.addEventListener('click', () => syntaxDialog.close());
  syntaxDialog.addEventListener('click', (event) => {
    if (event.target === syntaxDialog) syntaxDialog.close();
    const example = event.target.closest('[data-search-example]');
    if (!example || !queryInput) return;
    queryInput.value = example.dataset.searchExample || '';
    updateSearchClearButtons();
    syntaxDialog.close();
    queryInput.focus();
  });
}

function updateDeckLeaderClearVisibility() {
  if (!deckLeaderClearButton) return;
  const hasDeck = hasSelectedDeck() || !!document.getElementById('active-deck-select')?.value;
  const hasLeader = !!(document.getElementById('search-leader-id')?.value || leaderFilterInput?.value?.trim());
  deckLeaderClearButton.hidden = !(hasDeck || hasLeader);
  updateSearchClearButtons();
}

function visibleComboInputs(combo) { return comboInputs(combo).filter((input) => !input.closest('label')?.hidden); }
function updateComboSummary(combo) {
  const visible = visibleComboInputs(combo);
  const selected = visible.filter((input) => input.checked);
  const node = combo.querySelector('[data-multi-summary]');
  if (!node) return;
  if (!selected.length) node.textContent = combo.dataset.placeholder || 'Any';
  else if (selected.length === 1) {
    const labelNode = selected[0].closest('label')?.querySelector('[data-option-label]');
    if (labelNode?.querySelector('.card-type-line,.mana-option-label')) node.innerHTML = labelNode.innerHTML;
    else node.textContent = labelNode?.dataset.optionLabel || labelNode?.textContent || selected[0].value;
  }
  else if (selected.length === visible.length) node.textContent = `All ${selected.length} selected`;
  else node.textContent = `${selected.length} selected`;
}
function hasSelectedDeck() { return !!(selectedDeckState?.deckKey); }
function deckCards() { return Array.isArray(selectedDeckState?.cards) ? selectedDeckState.cards : []; }
function deckOracleCounts() {
  const counts = new Map();
  for (const line of deckCards()) {
    const oracleId = String(line.oracleId || '').trim();
    if (!oracleId) continue;
    const quantity = Math.max(0, Number(line.quantity || 0));
    if (quantity <= 0) continue;
    counts.set(oracleId, (counts.get(oracleId) || 0) + quantity);
  }
  return counts;
}
function deckCountForCard(card) { return deckOracleCounts().get(String(card?.oracleId || '')) || 0; }
function colorFilterValues(identity = '', bonusColor = '') {
  const identitySet = new Set(String(identity || '').toUpperCase().replace(/[^WUBRG]/g, '').split('').filter(Boolean));
  const bonus = String(bonusColor || '').toUpperCase();
  if (['W', 'U', 'B', 'R', 'G'].includes(bonus)) identitySet.add(bonus);
  const values = colorOrder.filter((symbol) => symbol !== 'C' && identitySet.has(symbol));
  return values.length ? values : ['C'];
}
function resetEligibilityDefaults(trigger = false) {
  setMultiFilter('eligibility', hasSelectedDeck() ? ['nonleader', 'leader', 'in_deck', 'not_in_deck'] : ['nonleader', 'leader'], trigger);
}
function setDeckEligibilityVisibility() {
  const enabled = hasSelectedDeck();
  document.querySelectorAll('[data-deck-eligibility]').forEach((node) => { node.hidden = !enabled; });
  const combo = multiCombos.find((candidate) => candidate.dataset.filterName === 'eligibility');
  if (!enabled) {
    combo?.querySelectorAll('input[value="in_deck"],input[value="not_in_deck"]').forEach((input) => { input.checked = false; });
  }
  if (combo) updateComboSummary(combo);
}

function applyLeaderDerivedFilters(context, bonusColor = '', trigger = false) {
  const inclusion = document.getElementById('filter-color-inclusion');
  if (!context) {
    if (inclusion) inclusion.value = 'at_least';
    setMultiFilter('color', [], false);
    setMultiFilter('subtype', [], false);
  } else {
    if (inclusion) inclusion.value = 'at_most';
    setMultiFilter('color', colorFilterValues(context.colorIdentity || '', bonusColor), false);
    setMultiFilter('subtype', Array.isArray(context.subtypes) ? context.subtypes : [], false);
  }
  if (trigger) form.dispatchEvent(new Event('change', { bubbles: true }));
}
function setMultiFilter(name, values, trigger = true) {
  const combo = multiCombos.find((candidate) => candidate.dataset.filterName === name);
  if (!combo) return;
  const wanted = new Set(values.map(String));
  comboInputs(combo).forEach((input) => { input.checked = wanted.has(input.value); });
  updateComboSummary(combo);
  if (trigger) form.dispatchEvent(new Event('change', { bubbles: true }));
}
window.ectSetMultiFilter = setMultiFilter;
function fitComboPanel(combo) {
  if (!combo) return;
  const panel = combo.querySelector('.multi-combo-panel');
  const summary = combo.querySelector('summary');
  if (!panel || !summary) return;
  if (!combo.open) { clearViewportPanel(panel); return; }
  positionViewportPanel(panel, summary);
}
function initMultiCombos() {
  multiCombos.forEach((combo) => {
    const activation = createActivationTracker(combo.querySelector('summary'));
    updateComboSummary(combo);
    combo.querySelector('[data-multi-select-all]')?.addEventListener('click', () => {
      comboInputs(combo).filter((input) => !input.closest('label')?.hidden).forEach((input) => { input.checked = true; });
      updateComboSummary(combo);
      form.dispatchEvent(new Event('change', { bubbles: true }));
    });
    combo.querySelector('[data-multi-clear]')?.addEventListener('click', () => {
      comboInputs(combo).forEach((input) => { input.checked = false; });
      updateComboSummary(combo);
      form.dispatchEvent(new Event('change', { bubbles: true }));
    });
    combo.querySelector('[data-multi-search]')?.addEventListener('input', (event) => filterComboOptions(combo, event.target.value));
    combo.addEventListener('change', () => updateComboSummary(combo));
    combo.addEventListener('toggle', () => {
      if (!combo.open) { fitComboPanel(combo); return; }
      multiCombos.forEach((candidate) => { if (candidate !== combo) candidate.open = false; });
      requestAnimationFrame(() => {
        fitComboPanel(combo);
        if (activation.isKeyboard()) combo.querySelector('[data-multi-search]')?.focus({ preventScroll: true });
      });
    });
  });
  observeViewport(() => multiCombos.filter((combo) => combo.open).forEach(fitComboPanel));
  document.addEventListener('click', (event) => {
    if (!event.target.closest('.multi-combo')) multiCombos.forEach((combo) => { combo.open = false; });
  });
}

function readState(page = currentPage) {
  const data = new FormData(form);
  const params = new URLSearchParams();
  for (const [key, value] of data.entries()) if (String(value).trim() !== '') params.append(key, String(value));
  const paperField = form.elements.paperOnly;
  if (paperField?.type === 'checkbox' && !paperField.checked) params.delete('paperOnly');
  else if (paperField?.value === '1') params.set('paperOnly', '1');
  if (hasSelectedDeck()) {
    for (const oracleId of deckOracleCounts().keys()) params.append('deckOracleId[]', oracleId);
  }
  const pageSize = Number(document.getElementById('card-page-size')?.value || data.get('pageSize') || 60);
  params.set('page', String(page));
  params.set('pageSize', String(pageSize));
  params.set('printingBias', userPrintingBias);
  return params;
}
function updateUrl(params) {
  const existing = new URLSearchParams(location.search);
  const copy = new URLSearchParams();
  for (const key of ['deck', 'share']) for (const value of existing.getAll(key)) copy.append(key, value);
  for (const [key, value] of params) {
    if (['bonusColor', 'eligibleOnly', 'printingBias', 'deckOracleId', 'deckOracleId[]'].includes(key)) continue;
    copy.append(key, value);
  }
  history.replaceState(null, '', `${location.pathname}${copy.toString() ? `?${copy}` : ''}`);
}
function restoreState() {
  const params = new URLSearchParams(location.search);
  for (const combo of multiCombos) {
    const name = combo.dataset.filterName;
    const values = [...params.getAll(`${name}[]`), ...params.getAll(name)];
    if (values.length) setMultiFilter(name, values, false);
  }
  for (const [key, value] of params) {
    if (key.endsWith('[]')) continue;
    const field = form.elements.namedItem(key);
    if (!field || field instanceof RadioNodeList) continue;
    if (field.type === 'checkbox') field.checked = ['1', 'on', 'true'].includes(value);
    else field.value = value;
  }
  currentPage = Math.max(1, Number(params.get('page') || 1));
  multiCombos.forEach(updateComboSummary);
}
function skeletons() {
  loadingNextPage = false;
  gallery.innerHTML = '<div class="loading-grid">' + Array.from({ length: 12 }, () => '<div class="skeleton-card"></div>').join('') + '</div>';
  summary.textContent = 'Searching the ECT catalog…';
  pagination.replaceChildren();
  if (sentinel) {
    sentinel.hidden = true;
    sentinel.textContent = '';
    sentinel.removeAttribute('data-loading');
  }
}

function emitAdd(card, zone, printingId = null) {
  window.dispatchEvent(new CustomEvent('ect-card-add', { detail: { card, zone, printingId: printingId || card.defaultPrinting?.id || null } }));
}
function isTokenCard(card) { return card?.isToken === true || card?.catalogRole === 'token'; }
function addControls(card, compact = false) {
  if (!isBuilder || !hasSelectedDeck()) return '';
  const blocked = card.ectEligible === false;
  const token = isTokenCard(card);
  const title = blocked ? ' title="Card conflicts with current strict filters"' : '';
  const buttons = [];
  if (token) {
    buttons.push(`<button class="zone-token" type="button" data-add-zone="tokens">+T</button>`);
    buttons.push('<button class="zone-tbd" type="button" data-add-zone="maybeboard">+U</button>');
  } else {
    if (card.nonLeaderEligible) {
    buttons.push(`<button class="zone-main" type="button" data-add-zone="mainboard"${title}>+M</button>`);
    buttons.push('<button class="zone-side" type="button" data-add-zone="sideboard">+S</button>');
    }
    if (card.nonLeaderEligible || card.leaderEligible) buttons.push('<button class="zone-tbd" type="button" data-add-zone="maybeboard">+U</button>');
    if (card.leaderEligible) {
      buttons.push('<button class="zone-leader" type="button" data-add-zone="leader">+L</button>');
    }
  }
  if (!buttons.length) return '';
  return `<div class="card-add-controls card-zone-controls ${compact ? 'compact' : ''}">${buttons.join('')}</div>`;
}
function imageMarkup(printing, name, index, layout = 'normal') {
  const normal = printing?.imageNormal || printing?.imageSmall || '';
  const large = printing?.imageLarge || normal;
  const src = normal || cardBackImage;
  const eager = index < 24;
  const srcset = normal && large && large !== normal ? ` srcset="${escapeHtml(normal)} 488w, ${escapeHtml(large)} 672w" sizes="(max-width:560px) 48vw, (max-width:1100px) 30vw, 260px"` : '';
  return `<img class="${escapeHtml(cardImageLayoutClasses(layout))}" src="${escapeHtml(src)}"${srcset} alt="${escapeHtml(name || 'Card back')}" width="488" height="680" loading="${eager ? 'eager' : 'lazy'}" decoding="async" fetchpriority="${index < 12 ? 'high' : 'low'}"${cardImageFallbackAttr}>`;
}
function cardTile(card, index) {
  const article = document.createElement('article');
  article.className = `card-tile${card.leaderEligible ? ' is-leader-eligible' : ''}${card.ectEligible === false ? ' context-illegal' : ''}`;
  article.dataset.oracleId = card.oracleId;
  article.dataset.printingId = card.defaultPrinting?.id || '';
  if (isBuilder) {
    article.draggable = true;
    article.addEventListener('dragstart', (event) => {
      article.classList.add('is-dragging');
      const payload = {
        source: 'gallery',
        card,
        printingId: card.defaultPrinting?.id || null,
      };
      event.dataTransfer.setData('application/x-ect-card', JSON.stringify(payload));
      event.dataTransfer.setData('text/plain', card.oracleId);
      event.dataTransfer.effectAllowed = 'copy';
      window.dispatchEvent(new CustomEvent('ect-card-drag-state', { detail: payload }));
    });
    article.addEventListener('dragend', () => {
      article.classList.remove('is-dragging');
      window.dispatchEvent(new CustomEvent('ect-card-drag-state', { detail: null }));
    });
  }
  const badges = [];
  if (card.leaderEligible) badges.push('<span class="badge leader">Leader</span>');
  if (card.nonLeaderEligible) badges.push('<span class="badge main">C/U</span>');
  if (card.policy?.status) badges.push(`<span class="badge ${escapeHtml(card.policy.status)}" title="${escapeHtml(card.policy.publicReason || policyLabel(card.policy.status))}">${escapeHtml(policyLabel(card.policy.status))}</span>`);
  if (card.ectEligible === false) badges.push('<span class="badge banned">Conflict</span>');
  const deckCount = deckCountForCard(card);
  const deckBadge = deckCount > 0 ? `<span class="selected-deck-count-badge" data-deck-count-badge aria-label="${deckCount} in selected deck">${deckCount}</span>` : '';
  const details = hideCardDetails ? '' : `<div class="card-meta"><h2 title="${escapeHtml(card.name)}">${escapeHtml(card.name)}</h2><p>${typeLineMarkup(card.typeLine || 'Unknown type')}</p><div class="mana-line"><span>${manaCostMarkup(card.manaCost)}</span><span>${escapeHtml(String(card.defaultPrinting?.setCode || '').toUpperCase())}${card.defaultPrinting?.setCode ? ' · ' : ''}MV ${escapeHtml(card.manaValue)}</span></div></div>`;
  article.innerHTML = `<button class="card-open" type="button" aria-label="Open ${escapeHtml(card.name)}"><div class="card-image-wrap ${escapeHtml(cardImageLayoutClasses(card.layout, 'card-frame-layout'))}" ${cardImageLayoutDataAttrs(card.layout, card.typeLine, 'card', card.oracleText, card.hasAftermath)}>${imageMarkup(card.defaultPrinting, card.name, index, card.layout)}${deckBadge}<div class="card-badges">${badges.join('')}</div></div>${details}</button>${addControls(card)}`;
  article.querySelector('.card-open')?.addEventListener('click', () => openCard(card.oracleId));
  article.querySelectorAll('[data-add-zone]').forEach((button) => button.addEventListener('click', () => emitAdd(card, button.dataset.addZone)));
  return article;
}
function hasNextResultPage() {
  return scrollThroughPagination && !autoPagingPaused && Number(currentPage) < Number(totalPages || 1);
}
function updateSearchSummary() {
  const total = Number(totalResults || 0);
  const pageCopy = totalPages > 1 ? ` · Page ${Number(currentPage).toLocaleString()} of ${Number(totalPages).toLocaleString()}` : '';
  summary.textContent = `${total.toLocaleString()} card${total === 1 ? '' : 's'} found${pageCopy} · showing ${Number(resultItems.length).toLocaleString()}`;
}
function updateSentinelState(isLoading = false) {
  if (!sentinel) return;
  sentinel.hidden = !(isLoading || renderedCount < resultItems.length || hasNextResultPage());
  sentinel.toggleAttribute('data-loading', isLoading);
  sentinel.textContent = isLoading ? 'Loading next page…' : '';
}
function appendResultBatch(size = 24) {
  const end = Math.min(resultItems.length, renderedCount + size);
  if (end <= renderedCount) {
    updateSentinelState();
    return;
  }
  const fragment = document.createDocumentFragment();
  for (let index = renderedCount; index < end; index += 1) fragment.append(cardTile(resultItems[index], index));
  gallery.append(fragment);
  renderedCount = end;
  updateSentinelState();
}
function initStagedRendering() {
  if (sentinel && 'IntersectionObserver' in window) {
    stageObserver?.disconnect();
    stageObserver = new IntersectionObserver((entries) => {
      if (!entries.some((entry) => entry.isIntersecting)) return;
      if (renderedCount < resultItems.length) appendResultBatch(isCardBrowser ? resultItems.length - renderedCount : 24);
      else loadNextResultPage();
    }, { root: isCardBrowser ? null : gallery, rootMargin: '900px 0px' });
    stageObserver.observe(sentinel);
  }
  if (!isCardBrowser) {
    gallery.addEventListener('scroll', () => {
      if (gallery.scrollTop + gallery.clientHeight >= gallery.scrollHeight - Math.max(800, gallery.clientHeight)) appendResultBatch(24);
    });
  }
}
function renderResults(data) {
  gallery.replaceChildren();
  resultItems = data.items || [];
  renderedCount = 0;
  currentPage = Number(data.page || currentPage || 1);
  totalPages = Number(data.pages || 1);
  totalResults = Number(data.total || 0);
  if (!data.catalogReady) {
    gallery.innerHTML = '<div class="empty-gallery"><div><h2>Catalog not synchronized</h2><p>Run the initial Scryfall synchronization from SSH.</p></div></div>';
    summary.textContent = 'Catalog unavailable';
    updateSentinelState();
    return;
  }
  updateSearchSummary();
  if (!resultItems.length) {
    gallery.innerHTML = '<div class="empty-gallery"><div><h2>No matching cards</h2><p>Remove a filter or reset filters.</p></div></div>';
    updateSentinelState();
  }
  else appendResultBatch(isCardBrowser ? resultItems.length : 24);
  renderPagination(data.page, data.pages);
}
function renderPagination(page, pages) {
  pagination.replaceChildren();
  if (pages <= 1) return;
  const values = new Set([1, pages, page - 2, page - 1, page, page + 1, page + 2]);
  const sequence = [...values].filter((value) => value >= 1 && value <= pages).sort((a, b) => a - b);
  let previous = 0;
  for (const value of sequence) {
    if (value - previous > 1) pagination.append(Object.assign(document.createElement('span'), { textContent: '…' }));
    const button = Object.assign(document.createElement('button'), { type: 'button', textContent: String(value) });
    if (value === page) button.setAttribute('aria-current', 'page');
    button.addEventListener('click', () => {
      search(value);
      document.querySelector('.browser-gallery-shell, .gallery-shell, .editor-gallery-shell')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
    });
    pagination.append(button);
    previous = value;
  }
}
async function loadNextResultPage() {
  if (!hasNextResultPage() || loadingNextPage) return;
  const nextPage = Number(currentPage) + 1;
  const token = activeSearchToken;
  loadingNextPage = true;
  updateSentinelState(true);
  const params = readState(nextPage);
  updateUrl(params);
  try {
    const response = await fetch(`${api('/cards/search')}?${params}`, { headers: { Accept: 'application/json' } });
    const payload = await response.json();
    if (token !== activeSearchToken) return;
    if (!response.ok || !payload.ok) throw new Error(payload.error?.message || 'Card search failed.');
    const data = payload.data || {};
    currentPage = Number(data.page || nextPage);
    totalPages = Number(data.pages || totalPages || 1);
    totalResults = Number(data.total || totalResults || 0);
    const incoming = Array.isArray(data.items) ? data.items : [];
    autoPagingPaused = false;
    resultItems = resultItems.concat(incoming);
    appendResultBatch(incoming.length);
    updateSearchSummary();
    renderPagination(currentPage, totalPages);
  } catch (error) {
    if (token !== activeSearchToken) return;
    autoPagingPaused = true;
    summary.textContent = `Next page failed: ${error.message}`;
  } finally {
    if (token === activeSearchToken) {
      loadingNextPage = false;
      updateSentinelState();
    }
  }
}
async function search(page = 1) {
  currentPage = page;
  loadingNextPage = false;
  autoPagingPaused = false;
  const token = activeSearchToken + 1;
  activeSearchToken = token;
  const params = readState(page);
  updateUrl(params);
  abortController?.abort();
  abortController = new AbortController();
  skeletons();
  try {
    const response = await fetch(`${api('/cards/search')}?${params}`, { signal: abortController.signal, headers: { Accept: 'application/json' } });
    const payload = await response.json();
    if (token !== activeSearchToken) return;
    if (!response.ok || !payload.ok) throw new Error(payload.error?.message || 'Card search failed.');
    renderResults(payload.data);
    if (!isCardBrowser) gallery.scrollTop = 0;
  } catch (error) {
    if (error.name === 'AbortError' || token !== activeSearchToken) return;
    gallery.innerHTML = `<div class="empty-gallery"><div><h2>Search unavailable</h2><p>${escapeHtml(error.message)}</p></div></div>`;
    summary.textContent = 'Search failed';
  }
}

async function openCard(oracleId) {
  const eligibilityCombo = multiCombos.find((combo) => combo.dataset.filterName === 'eligibility');
  const eligibility = comboInputs(eligibilityCombo).filter((input) => input.checked).map((input) => input.value);
  await openCardModal({
    dialog,
    content: dialogContent,
    oracleId,
    eligibility,
    actionMarkup: (card) => addControls(card, true),
    bindActions: (root, card, getPrinting) => {
      root.querySelectorAll('[data-add-zone]').forEach((button) => button.addEventListener('click', () => {
        emitAdd(card, button.dataset.addZone, getPrinting()?.scryfall_id || getPrinting()?.id || card.defaultPrinting?.id);
        if (button.dataset.addZone === 'leader') dialog.close();
      }));
    },
  });
}

async function loadFacets() {
  try {
    const payload = await apiFetch('/api/v1/cards/facets');
    if (!payload.data.catalogReady || !subtypeCombo) return;
    const options = subtypeCombo.querySelector('[data-multi-options]');
    const params = new URLSearchParams(location.search);
    const selected = new Set([...params.getAll('subtype[]'), ...params.getAll('subtype')]);
    const fragment = document.createDocumentFragment();
    for (const item of payload.data.subtypes) {
      const creatureCount = Number(item.creature_card_count ?? item.card_count ?? 0);
      if (creatureCount <= 0) continue;
      const label = document.createElement('label');
      const input = document.createElement('input');
      input.type = 'checkbox'; input.name = 'subtype[]'; input.value = item.subtype; input.checked = selected.has(item.subtype);
      const text = document.createElement('span'); text.textContent = `${item.subtype} (${creatureCount.toLocaleString()})`;
      label.append(input, text); fragment.append(label);
    }
    options.append(fragment); updateComboSummary(subtypeCombo);
  } catch {}
}

function deckPayload(state) {
  return {
    draftVersion: Number(state.draftVersion || 1),
    metadata: { name: limitLine(state.name, 32), description: limitText(state.description, 256), deckStatus: state.deckStatus, deckStrategy: state.deckStrategy || null, deckArchetype: state.deckArchetype || null, validationMode: state.validationMode, leaderOracleId: state.leaderOracleId, leaderPrintingId: state.leaderPrintingId, bonusColor: state.bonusColor },
    groups: (state.groups || []).map((g, index) => {
      const key = String(g.key || g.clientId || g.id || g.name);
      return { key, clientId: key, name: limitLine(g.name, 12), colorKey: g.colorKey || null, sortOrder: index };
    }),
    cards: (state.cards || []).map((c, index) => {
      const key = String(c.key || c.clientId || c.lineKey || `card-search-${c.oracleId}-${index}`);
      return { key, clientId: key, oracleId: c.oracleId, printingId: c.printingId || null, zone: c.zone, quantity: Number(c.quantity || 1), groupClientId: c.groupClientId || c.groupKey || (c.groupId !== null && c.groupId !== undefined ? String(c.groupId) : (c.groupName || null)), gameStage: c.gameStage || 'none', testing: !!c.testing, autoBasic: !!c.autoBasic, sortOrder: index, note: c.note ? limitText(c.note, 256) : null };
    }),
  };
}
async function addToSelectedDeck(detail) {
  const select = document.getElementById('active-deck-select');
  const key = select?.value;
  if (!key) { pendingCardAdd = detail; window.ectOpenCreateDeck?.(); return; }
  try {
    summary.textContent = `Adding ${detail.card.name}…`;
    const state = selectedDeckState?.deckKey === key && selectedDeckState?.isDraft
      ? selectedDeckState
      : (await apiFetch(`/api/v1/decks/${encodeURIComponent(key)}/draft`)).data;
    const printingId = detail.printingId || detail.card.defaultPrinting?.id || null;
    if (detail.zone === 'leader') {
      state.cards = state.cards.filter((line) => line.zone !== 'leader');
      state.leaderOracleId = detail.card.oracleId; state.leaderPrintingId = printingId;
      state.cards.push({ oracleId: detail.card.oracleId, printingId, zone: 'leader', quantity: 1, groupId: null, groupName: null, gameStage: 'none', testing: false, autoBasic: false, note: null });
    } else {
      const line = state.cards.find((item) => item.oracleId === detail.card.oracleId && item.zone === detail.zone && (item.printingId || null) === printingId);
      if (line) line.quantity = Number(line.quantity || 0) + 1;
      else state.cards.push({ oracleId: detail.card.oracleId, printingId, zone: detail.zone, quantity: 1, groupId: null, groupName: null, gameStage: 'none', testing: false, autoBasic: false, note: null });
    }
    selectedDeckState = (await apiFetch(`/api/v1/decks/${encodeURIComponent(key)}`, { method: 'PUT', body: deckPayload(state) })).data;
    summary.textContent = `${detail.card.name} added to ${selectedDeckState.name}.`;
    updateDeckContext(selectedDeckState);
  } catch (error) { summary.textContent = error.message; }
}
function updateDeckCountBadges() {
  gallery.querySelectorAll('.card-tile').forEach((article) => {
    const oracleId = article.dataset.oracleId || '';
    const count = deckOracleCounts().get(oracleId) || 0;
    article.querySelector('[data-deck-count-badge]')?.remove();
    if (count > 0) {
      const badge = document.createElement('span');
      badge.className = 'selected-deck-count-badge';
      badge.dataset.deckCountBadge = '';
      badge.setAttribute('aria-label', `${count} in selected deck`);
      badge.textContent = String(count);
      article.querySelector('.card-image-wrap')?.append(badge);
    }
  });
}
function setLeaderFilterContext(context, bonusColor = '', triggerSearch = false) {
  activeLeaderContext = context;
  contextBonusColor = String(bonusColor || '').toUpperCase();
  document.getElementById('search-leader-id').value = context?.oracleId || '';
  if (leaderFilterInput) leaderFilterInput.value = context?.name || '';
  updateDeckLeaderClearVisibility();
  applyLeaderDerivedFilters(context, contextBonusColor, false);
  closeLeaderFilterOptions();
  syncLeaderContext();
  if (triggerSearch) search(1);
}
function updateDeckContext(state, applyContextFilters = false) {
  selectedDeckState = state;
  updateDeckLeaderClearVisibility();
  setDeckEligibilityVisibility();
  if (applyContextFilters) {
    resetEligibilityDefaults(false);
    if (state?.leaderOracleId) {
      setLeaderFilterContext({
        oracleId: state.leaderOracleId,
        name: state.leaderName || '',
        colorIdentity: state.leaderColorIdentity || '',
        subtypes: Array.isArray(state.leaderSubtypes) ? state.leaderSubtypes : [],
      }, state.bonusColor || '', false);
    } else {
      setLeaderFilterContext(null, '', false);
    }
  }
  updateDeckCountBadges();
}
function syncLeaderContext() {
  if (!isCardBrowser) return;
  const leaderId = document.getElementById('search-leader-id')?.value?.trim() || '';
  document.getElementById('search-bonus-color').value = leaderId ? contextBonusColor : '';
  document.getElementById('search-eligible-only').value = '0';
}

function closeLeaderFilterOptions() {
  if (leaderFilterOptions) { leaderFilterOptions.hidden = true; leaderFilterOptions.replaceChildren(); }
  leaderFilterInput?.setAttribute('aria-expanded', 'false');
}
function leaderOptionMarkup(card, index) {
  const image = card.defaultPrinting?.imageSmall || card.defaultPrinting?.imageNormal || cardBackImage;
  return `<button class="search-combobox-option search-combobox-option-card" type="button" role="option" data-leader-option="${index}">
    <img class="search-combobox-card-image" src="${escapeHtml(image)}" alt="" onerror="this.onerror=null;this.src='${cardBackImage}'">
    <span><strong>${escapeHtml(card.name)}</strong><small><span class="search-combobox-card-mana">${manaCostMarkup(card.manaCost)}</span><span class="search-combobox-card-type">${typeLineMarkup(card.typeLine || '', { className: 'card-type-line-compact' })}</span></small></span>
  </button>`;
}
function selectLeaderFilter(card, triggerSearch = true) {
  if (!card) return;
  setLeaderFilterContext({
    oracleId: card.oracleId || '',
    name: card.name || '',
    colorIdentity: card.colorIdentity || '',
    subtypes: Array.isArray(card.frontSubtypes) ? card.frontSubtypes : [],
  }, '', triggerSearch);
}
function clearLeaderFilter(triggerSearch = true) {
  activeLeaderContext = null;
  contextBonusColor = '';
  document.getElementById('search-leader-id').value = '';
  if (leaderFilterInput) leaderFilterInput.value = '';
  updateDeckLeaderClearVisibility();
  updateSearchClearButtons();
  closeLeaderFilterOptions();
  applyLeaderDerivedFilters(null, '', false);
  syncLeaderContext();
  if (triggerSearch) search(1);
}
function renderLeaderFilterOptions(items) {
  leaderFilterResults = items;
  if (!leaderFilterOptions) return;
  leaderFilterOptions.innerHTML = items.length
    ? items.map(leaderOptionMarkup).join('')
    : '<div class="search-combobox-empty">No matching Leaders.</div>';
  leaderFilterOptions.hidden = false;
  leaderFilterInput?.setAttribute('aria-expanded', 'true');
  leaderFilterOptions.querySelectorAll('[data-leader-option]').forEach((button) => {
    button.addEventListener('mousedown', (event) => {
      event.preventDefault();
      selectLeaderFilter(leaderFilterResults[Number(button.dataset.leaderOption)]);
    });
  });
}
async function searchLeaderFilter(query) {
  const term = query.trim();
  if (term.length < 2) { closeLeaderFilterOptions(); return; }
  leaderFilterController?.abort();
  leaderFilterController = new AbortController();
  try {
    const params = new URLSearchParams({ q: term, nameOnly: '1', pageSize: '20', sort: 'relevance', paperOnly: '1' });
    params.append('eligibility[]', 'leader');
    const payload = await apiFetch(`/api/v1/cards/search?${params}`, { signal: leaderFilterController.signal });
    renderLeaderFilterOptions(payload.data.items || []);
  } catch (error) {
    if (error.name !== 'AbortError') renderLeaderFilterOptions([]);
  }
}
function initLeaderFilter() {
  if (!leaderFilterInput) return;
  leaderFilterInput.addEventListener('input', () => {
    activeLeaderContext = null;
    contextBonusColor = '';
    document.getElementById('search-leader-id').value = '';
    applyLeaderDerivedFilters(null, '', false);
    updateDeckLeaderClearVisibility();
    updateSearchClearButtons();
    syncLeaderContext();
    clearTimeout(leaderFilterTimer);
    leaderFilterTimer = setTimeout(() => searchLeaderFilter(leaderFilterInput.value), 220);
  });
  leaderFilterInput.addEventListener('keydown', (event) => {
    if (event.key === 'Escape') { event.preventDefault(); closeLeaderFilterOptions(); }
    if (event.key === 'Enter' && leaderFilterResults[0] && !leaderFilterOptions?.hidden) {
      event.preventDefault();
      selectLeaderFilter(leaderFilterResults[0]);
    }
  });
  leaderFilterInput.addEventListener('blur', () => {
    setTimeout(() => {
      if (!document.getElementById('search-leader-id').value && !leaderFilterInput.value.trim()) clearLeaderFilter(false);
      closeLeaderFilterOptions();
    }, 150);
  });
}
async function restoreLeaderFilter() {
  const leaderId = document.getElementById('search-leader-id')?.value?.trim() || '';
  if (!leaderId || !leaderFilterInput || activeLeaderContext?.oracleId === leaderId) return;
  try {
    const payload = await apiFetch(`/api/v1/cards/${encodeURIComponent(leaderId)}`);
    if (payload.data?.name) selectLeaderFilter(payload.data, false);
  } catch {}
}
function initSearchClearButtons() {
  queryInput?.addEventListener('input', updateSearchClearButtons);
  queryClearButton?.addEventListener('click', () => {
    if (!queryInput) return;
    queryInput.value = '';
    updateSearchClearButtons();
    queryInput.focus();
    search(1);
  });
  leaderFilterClearButton?.addEventListener('click', () => {
    clearLeaderFilter(true);
    leaderFilterInput?.focus();
  });
  updateSearchClearButtons();
}
function clearDeckAndLeaderContext(triggerSearch = true) {
  setDeckSelectorValue('');
  localStorage.removeItem('ectmtg.activeDeck');
  updateDeckContext(null, false);
  clearLeaderFilter(false);
  resetEligibilityDefaults(false);
  syncLeaderContext();
  updateDeckLeaderClearVisibility();
  if (triggerSearch) search(1);
}

function deckContextMarkup(deck, selected = false) {
  const image = deck?.leaderImage || cardBackImage;
  const leader = deck?.leaderName || 'No Leader selected';
  const colors = identitySummaryMarkup(deck?.leaderColorIdentity || '', deck?.bonusColor || '');
  return `<span class="deck-context-option-art"><img src="${escapeHtml(image)}" alt="" onerror="this.onerror=null;this.src='${cardBackImage}'"></span><span class="deck-context-option-copy"><strong>${escapeHtml(deck?.name || 'Untitled deck')}</strong><small>${escapeHtml(leader)}</small></span><span class="deck-context-option-colors" aria-label="Leader colors and additional color">${colors}</span>${selected ? '<span class="deck-context-check" aria-hidden="true">✓</span>' : ''}`;
}

function closeDeckContextOptions() {
  if (deckContextOptions) deckContextOptions.hidden = true;
  deckContextTrigger?.setAttribute('aria-expanded', 'false');
}

function deckContextOptionButtons() {
  return deckContextOptions ? [...deckContextOptions.querySelectorAll('[data-deck-context-key]')] : [];
}

function focusDeckContextOption(offset = 0) {
  const options = deckContextOptionButtons();
  if (!options.length) return;
  const focused = options.indexOf(document.activeElement);
  const selected = options.findIndex((option) => option.getAttribute('aria-selected') === 'true');
  const start = focused >= 0 ? focused : Math.max(0, selected);
  options[(start + offset + options.length) % options.length].focus();
}

function renderDeckContextOptions() {
  if (!deckContextOptions) return;
  const selectedKey = deckContextInput?.value || '';
  const renderGroup = (label, items) => {
    return items.length ? `<div class="deck-context-group-label" role="presentation">${escapeHtml(label)}</div>${items.map((deck) => `<button class="deck-context-option" type="button" role="option" data-deck-context-key="${escapeHtml(deck.deckKey)}" aria-selected="${deck.deckKey === selectedKey}">${deckContextMarkup(deck, deck.deckKey === selectedKey)}</button>`).join('')}` : '';
  };
  const mine = deckSelectorItems.filter((deck) => deck.selectorSource === 'mine');
  const folderGroups = new Map();
  for (const deck of mine) {
    const label = String(deck.folderName || 'Misc.').trim() || 'Misc.';
    if (!folderGroups.has(label)) folderGroups.set(label, []);
    folderGroups.get(label).push(deck);
  }
  const groupedMine = [...folderGroups.entries()].sort(([a],[b]) => a === 'Misc.' ? 1 : b === 'Misc.' ? -1 : a.localeCompare(b, undefined, {sensitivity:'base'})).map(([label,items]) => renderGroup(label,items)).join('');
  const faves = deckSelectorItems.filter((deck) => deck.selectorSource === 'favorite');
  deckContextOptions.innerHTML = `<button class="deck-context-option deck-context-option-empty" type="button" role="option" data-deck-context-key="" aria-selected="${selectedKey === ''}"><span class="deck-context-option-copy"><strong>No deck selected</strong><small>Browse without a deck context</small></span>${selectedKey === '' ? '<span class="deck-context-check" aria-hidden="true">✓</span>' : ''}</button>${groupedMine}${renderGroup('Faves',faves)}`;
}

function setDeckSelectorValue(deckKey) {
  if (deckContextInput) deckContextInput.value = deckKey || '';
  const deck = deckSelectorItems.find((item) => item.deckKey === deckKey);
  if (deckContextLabel) {
    deckContextLabel.classList.toggle('is-empty', !deck);
    deckContextLabel.innerHTML = deck
      ? deckContextMarkup(deck)
      : '<span class="deck-context-option-copy"><strong>No deck selected</strong><small>Choose a deck context</small></span>';
  }
  renderDeckContextOptions();
}

async function selectDeckContext(deckKey, { triggerSearch = true } = {}) {
  closeDeckContextOptions();
  if (deckKey) {
    const state = (await apiFetch(`/api/v1/decks/${encodeURIComponent(deckKey)}`)).data;
    setDeckSelectorValue(deckKey);
    localStorage.setItem('ectmtg.activeDeck', deckKey);
    updateDeckContext(state, true);
  } else {
    setDeckSelectorValue('');
    localStorage.removeItem('ectmtg.activeDeck');
    updateDeckContext(null, false);
  }
  if (triggerSearch) search(1);
}

function initDeckContextCombobox() {
  if (!deckContextTrigger || !deckContextOptions) return;
  deckContextTrigger.addEventListener('click', () => {
    const opening = deckContextOptions.hidden;
    if (opening) renderDeckContextOptions();
    deckContextOptions.hidden = !opening;
    deckContextTrigger.setAttribute('aria-expanded', String(opening));
  });
  deckContextTrigger.addEventListener('keydown', (event) => {
    if (!['ArrowDown', 'ArrowUp'].includes(event.key)) return;
    event.preventDefault();
    renderDeckContextOptions();
    deckContextOptions.hidden = false;
    deckContextTrigger.setAttribute('aria-expanded', 'true');
    focusDeckContextOption(event.key === 'ArrowDown' ? 0 : -1);
  });
  deckContextOptions.addEventListener('click', async (event) => {
    const option = event.target.closest('[data-deck-context-key]');
    if (!option) return;
    try { await selectDeckContext(option.dataset.deckContextKey || ''); }
    catch (error) { alert(error.message); }
    finally { deckContextTrigger.focus(); }
  });
  deckContextOptions.addEventListener('keydown', (event) => {
    if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
      event.preventDefault();
      focusDeckContextOption(event.key === 'ArrowDown' ? 1 : -1);
    } else if (event.key === 'Home' || event.key === 'End') {
      event.preventDefault();
      const options = deckContextOptionButtons();
      options[event.key === 'Home' ? 0 : options.length - 1]?.focus();
    } else if (event.key === 'Escape') {
      event.preventDefault();
      closeDeckContextOptions();
      deckContextTrigger.focus();
    }
  });
  document.addEventListener('pointerdown', (event) => {
    if (!event.target.closest('[data-deck-context-combobox]')) closeDeckContextOptions();
  });
  document.addEventListener('keydown', (event) => { if (event.key === 'Escape') closeDeckContextOptions(); });
}

async function loadDeckSelector() {
  if (!isCardBrowser || !window.ECTMTG_USER) return;
  try {
    const [minePayload, favoritesPayload] = await Promise.all([apiFetch('/api/v1/me/decks'), apiFetch('/api/v1/me/favorites')]);
    const mine = (minePayload.data.items || []).map((deck) => ({...deck, selectorSource:'mine'}));
    const mineKeys = new Set(mine.map((deck) => deck.deckKey));
    const favorites = (favoritesPayload.data.items || []).filter((deck) => !mineKeys.has(deck.deckKey)).map((deck) => ({...deck, selectorSource:'favorite'}));
    deckSelectorItems = [...mine, ...favorites];
    renderDeckContextOptions();
    const remembered = localStorage.getItem('ectmtg.activeDeck');
    const selectedKey = remembered && deckSelectorItems.some((deck) => deck.deckKey === remembered) ? remembered : '';
    await selectDeckContext(selectedKey, { triggerSearch: false });
  } catch {}
}

initMultiCombos();
initLeaderFilter();
initDeckContextCombobox();
initSearchClearButtons();
initSearchSyntaxHelp();
restoreState();
updateSearchClearButtons();
initStagedRendering();
form.addEventListener('submit', (event) => { event.preventDefault(); search(1); });
form.addEventListener('change', () => { syncLeaderContext(); search(1); });
window.addEventListener('ect-search-context-changed', () => search(1));
if (isCardBrowser) {
  window.addEventListener('ect-card-add', (event) => addToSelectedDeck(event.detail));
  window.addEventListener('ect-deck-created', (event) => {
    if (!pendingCardAdd) return;
    event.preventDefault();
    deckSelectorItems.push(event.detail.deck);
    setDeckSelectorValue(event.detail.deck.deckKey);
    localStorage.setItem('ectmtg.activeDeck', event.detail.deck.deckKey);
    updateDeckContext(event.detail.deck, true);
    const add = pendingCardAdd; pendingCardAdd = null;
    addToSelectedDeck(add);
  });
  deckLeaderClearButton?.addEventListener('click', () => clearDeckAndLeaderContext(true));
  resetFiltersButton?.addEventListener('click', () => {
    form.reset();
    document.getElementById('card-query').value = '';
    document.getElementById('filter-color-inclusion').value = 'at_least';
    document.getElementById('filter-sort').value = 'relevance';
    document.getElementById('card-page-size').value = '60';
    resetEligibilityDefaults(false);
    if (activeLeaderContext) setLeaderFilterContext(activeLeaderContext, contextBonusColor, false);
    updateSearchClearButtons();
    syncLeaderContext();
    search(1);
  });
}
document.querySelectorAll('[data-card-size]').forEach((button) => button.addEventListener('click', () => {
  const size = button.dataset.cardSize;
  gallery.dataset.cardSize = size;
  document.querySelectorAll('[data-card-size]').forEach((candidate) => candidate.setAttribute('aria-pressed', String(candidate === button)));
  localStorage.setItem('ectmtg.cardSize', size);
}));
const storedSize = localStorage.getItem('ectmtg.cardSize') || window.ECTMTG_USER?.cardSize;
if (storedSize && ['large', 'medium', 'small'].includes(storedSize)) document.querySelector(`[data-card-size="${storedSize}"]`)?.click();
dialogClose?.addEventListener('click', () => dialog.close());
dialog?.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); });
await loadFacets();
await loadDeckSelector();
await restoreLeaderFilter();
syncLeaderContext();
updateDeckLeaderClearVisibility();
await search(currentPage);
