import { catalog as legacyCatalog } from './catalog.js';
import { files } from './files.js';
import { renderVoteWidget } from './vote.js';

// The Astro review-desk route serializes the typed content collection before
// this client module loads. Keeping the legacy fallback preserves the vanilla
// page until cutover removes it.
const catalog = window.__SKILLS_REVIEW_CATALOG || legacyCatalog;

const state = {
  selected: catalog[0],
  query: '',
  preview: 'original',
  file: null,
  sourceByPath: new Map(),
  lens: false,
  rendered: false,
  diff: false,
  searching: false,
  contentMatches: new Set(),
  searchTimer: null,
  searchRequest: 0,
};
const $ = (selector) => document.querySelector(selector);
const escape = (value) =>
  value.replace(
    /[&<>"']/g,
    (character) =>
      ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' })[character],
  );
const redact = (value, entry) => {
  const safe = value
    .replace(/(NDO_PASS[^\n=]*[=:]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
    .replace(/(password["']?\s*[:=]\s*["']?)[^\n"']+/gi, '$1[REDACTED]')
    .replace(/\b[\w.+-]+@[\w.-]+\.[a-z]{2,}\b/gi, '[REDACTED SERVICE ACCOUNT]')
    .replace(/\/home\/[A-Za-z0-9._-]+(?=\/)/g, '[REDACTED LOCAL USER]')
    .replace(/display\/~[A-Za-z0-9._-]+/gi, 'display/~[REDACTED USER]');
  return entry.id === 'ndo-repro'
    ? safe
        .replace(/https?:\/\/[^\s)>]+/gi, '[REDACTED URL]')
        .replace(/\b(?:[\w-]+\.)*netcracker\.[\w.-]+\b/gi, '[REDACTED HOST]')
        .replace(/\bpedro[._ -]?aranha\b/gi, '[REDACTED CONTRIBUTOR]')
    : safe;
};
const download = (name, content) => {
  const url = URL.createObjectURL(new Blob([content], { type: 'text/markdown' }));
  const a = document.createElement('a');
  a.href = url;
  a.download = name;
  a.click();
  URL.revokeObjectURL(url);
};
const copy = async (content) => {
  if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(content);
  const textarea = document.createElement('textarea');
  textarea.value = content;
  textarea.setAttribute('readonly', '');
  textarea.style.position = 'fixed';
  textarea.style.opacity = '0';
  document.body.append(textarea);
  textarea.select();
  document.execCommand('copy');
  textarea.remove();
};
const packageFiles = (entry = state.selected) =>
  files[entry.id] || [{ name: 'SKILL.md', path: entry.path, kind: 'skill' }];
const packageSearchText = (entry) =>
  packageFiles(entry)
    .map((file) => `${file.name} ${file.kind}`)
    .join(' ');
function packageSummary(entry) {
  const counts = packageFiles(entry).reduce((all, file) => {
    all[file.kind] = (all[file.kind] || 0) + 1;
    return all;
  }, {});
  const labels = {
    skill: 'skill',
    reference: 'ref',
    script: 'script',
    template: 'template',
    data: 'data',
    asset: 'asset',
  };
  return Object.entries(counts)
    .map(([kind, count]) => `${count} ${labels[kind] || kind}${count === 1 ? '' : 's'}`)
    .join(' · ');
}
const unchangedDraft = (file) =>
  `# ${file.name}\n\n> Kept as-is in the improved package\n\nThis ${file.kind} file was not rewritten. Select **Change lens** to see why the improved draft concentrates its changes in the main skill contract.`;
const currentSource = () => state.sourceByPath.get(state.file.path);
const currentContent = () =>
  state.preview === 'original'
    ? currentSource() || 'Loading original file…'
    : state.file.improved ||
      (state.file.name === 'SKILL.md'
        ? state.selected.improved
        : currentSource()
          ? `# ${state.file.name}\n\n> Kept as-is in the improved package\n\n${currentSource()}`
          : unchangedDraft(state.file));
const inlineMarkdown = (value) =>
  escape(value)
    .replace(/`([^`]+)`/g, '<code>$1</code>')
    .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
    .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
    .replace(
      /\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g,
      '<a href="$2" target="_blank" rel="noreferrer">$1 ↗</a>',
    );
const tableCells = (line) =>
  line
    .trim()
    .replace(/^\||\|$/g, '')
    .split('|')
    .map((cell) => cell.trim());
const isTableDivider = (line) => /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line);
function markdownMarkup(markdown) {
  const lines = markdown.replace(/\r/g, '').split('\n');
  const headings = markdownHeadings(markdown);
  let headingIndex = 0;
  let index = 0;
  const out = [];
  if (lines[0] === '---') {
    const end = lines.indexOf('---', 1);
    if (end > 0) {
      out.push(
        `<dl class="markdown-frontmatter">${lines
          .slice(1, end)
          .map((line) => {
            const [key, ...rest] = line.split(':');
            return rest.length
              ? `<dt>${escape(key)}</dt><dd>${inlineMarkdown(rest.join(':').trim())}</dd>`
              : '';
          })
          .join('')}</dl>`,
      );
      index = end + 1;
    }
  }
  const startsBlock = (line, next) =>
    !line ||
    /^#{1,6}\s+/.test(line) ||
    /^```/.test(line) ||
    /^[-*+]\s+/.test(line) ||
    /^\d+\.\s+/.test(line) ||
    /^>\s?/.test(line) ||
    /^---+$/.test(line) ||
    (line.includes('|') && isTableDivider(next || ''));
  while (index < lines.length) {
    const line = lines[index];
    if (!line.trim()) {
      index += 1;
      continue;
    }
    const heading = line.match(/^(#{1,6})\s+(.+)$/);
    if (heading) {
      const level = heading[1].length;
      const item = headings[headingIndex++];
      out.push(`<h${level} id="${item.id}">${inlineMarkdown(heading[2])}</h${level}>`);
      index += 1;
      continue;
    }
    if (/^```/.test(line)) {
      const language = line.slice(3).trim();
      const code = [];
      index += 1;
      while (index < lines.length && !/^```/.test(lines[index])) code.push(lines[index++]);
      if (index < lines.length) index += 1;
      out.push(
        `<pre><code${language ? ` data-language="${escape(language)}"` : ''}>${escape(code.join('\n'))}</code></pre>`,
      );
      continue;
    }
    if (line.includes('|') && isTableDivider(lines[index + 1] || '')) {
      const headings = tableCells(line);
      index += 2;
      const rows = [];
      while (index < lines.length && lines[index].includes('|') && lines[index].trim())
        rows.push(tableCells(lines[index++]));
      out.push(
        `<div class="markdown-table-wrap"><table><thead><tr>${headings.map((cell) => `<th>${inlineMarkdown(cell)}</th>`).join('')}</tr></thead><tbody>${rows.map((row) => `<tr>${headings.map((_, cell) => `<td>${inlineMarkdown(row[cell] || '')}</td>`).join('')}</tr>`).join('')}</tbody></table></div>`,
      );
      continue;
    }
    const list = line.match(/^([-*+]|\d+\.)\s+(.+)$/);
    if (list) {
      const ordered = /\d+\./.test(list[1]);
      const items = [];
      while (index < lines.length) {
        const item = lines[index].match(ordered ? /^\d+\.\s+(.+)$/ : /^[-*+]\s+(.+)$/);
        if (!item) break;
        items.push(`<li>${inlineMarkdown(item[1])}</li>`);
        index += 1;
      }
      out.push(`<${ordered ? 'ol' : 'ul'}>${items.join('')}</${ordered ? 'ol' : 'ul'}>`);
      continue;
    }
    if (/^>\s?/.test(line)) {
      const quote = [];
      while (index < lines.length && /^>\s?/.test(lines[index]))
        quote.push(lines[index++].replace(/^>\s?/, ''));
      out.push(`<blockquote>${inlineMarkdown(quote.join(' '))}</blockquote>`);
      continue;
    }
    if (/^---+$/.test(line)) {
      out.push('<hr>');
      index += 1;
      continue;
    }
    const paragraph = [line];
    index += 1;
    while (index < lines.length && !startsBlock(lines[index], lines[index + 1]))
      paragraph.push(lines[index++]);
    out.push(`<p>${inlineMarkdown(paragraph.join(' '))}</p>`);
  }
  return out.join('');
}
function markdownHeadings(markdown) {
  const used = new Map();
  let fenced = false;
  return markdown
    .replace(/\r/g, '')
    .split('\n')
    .flatMap((line) => {
      if (/^```/.test(line)) {
        fenced = !fenced;
        return [];
      }
      const match = !fenced && line.match(/^(#{1,6})\s+(.+)$/);
      if (!match) return [];
      const text = match[2].replace(/[`*_\[\]]/g, '').trim();
      const base =
        text
          .toLowerCase()
          .replace(/[^\p{L}\p{N}]+/gu, '-')
          .replace(/(^-|-$)/g, '') || 'section';
      const seen = used.get(base) || 0;
      used.set(base, seen + 1);
      return [{ level: match[1].length, text, id: seen ? `${base}-${seen + 1}` : base }];
    });
}
function markdownToc(markdown) {
  const headings = markdownHeadings(markdown);
  if (headings.length < 2) return '';
  return `<nav class="markdown-toc" aria-label="On this page"><span>ON THIS PAGE</span><ol>${headings.map((heading) => `<li class="level-${heading.level}"><a href="#${heading.id}">${escape(heading.text)}</a></li>`).join('')}</ol></nav>`;
}
const changeRows = (entry) =>
  entry.improve.map((why, index) => ({
    kind: ['SAFETY', 'SCOPE', 'EVIDENCE', 'STRUCTURE'][index] || 'CLARITY',
    before:
      index === 0
        ? 'The submitted guidance leaves a material decision implicit.'
        : 'The submitted package carries detail without a clear boundary.',
    after:
      index === 0
        ? 'The improved draft makes the operating rule explicit.'
        : 'The improved draft moves the decision into a smaller, reviewable contract.',
    why,
  }));

function visible() {
  return catalog.filter(
    (item) =>
      `${item.author} ${item.title} ${item.id} ${item.focus} ${packageSearchText(item)}`
        .toLowerCase()
        .includes(state.query) || state.contentMatches.has(item.id),
  );
}
function syncUrl() {
  const url = new URL(window.location.href);
  url.searchParams.set('author', state.selected.author);
  url.searchParams.set('skill', state.selected.id);
  url.searchParams.set('view', state.preview);
  if (state.file && state.file.name !== 'SKILL.md') url.searchParams.set('file', state.file.name);
  else url.searchParams.delete('file');
  if (state.preview === 'improved' && state.lens) url.searchParams.set('lens', 'changes');
  else url.searchParams.delete('lens');
  if (state.rendered) url.searchParams.set('render', 'preview');
  else url.searchParams.delete('render');
  if (state.diff) url.searchParams.set('compare', 'diff');
  else url.searchParams.delete('compare');
  history.replaceState({}, '', url);
}
function selectFromUrl() {
  const params = new URLSearchParams(window.location.search);
  const author = params.get('author');
  const id = params.get('skill');
  const view = params.get('view');
  const byAuthor =
    author && catalog.filter((item) => item.author.toLowerCase() === author.toLowerCase());
  const byId = id && catalog.find((item) => item.id === id);
  state.selected = byId || byAuthor?.[0] || catalog[0];
  state.query = byAuthor ? state.selected.author.toLowerCase() : '';
  state.preview = view === 'improved' ? 'improved' : 'original';
  state.lens = state.preview === 'improved' && params.get('lens') === 'changes';
  state.rendered = params.get('render') === 'preview';
  state.diff = params.get('compare') === 'diff';
  state.file = packageFiles().find((item) => item.name === params.get('file')) || packageFiles()[0];
  $('#skill-filter').value = byAuthor ? state.selected.author : '';
  $('#submission-count').textContent = `${catalog.length} submissions`;
}
function renderList() {
  const items = visible();
  $('#count').textContent = state.searching
    ? `Searching package files… ${items.length} of ${catalog.length}`
    : `${items.length} of ${catalog.length} reviewed`;
  $('#skill-list').innerHTML = items
    .map(
      (item) =>
        `<button role="option" aria-selected="${item.id === state.selected.id}" class="${item.id === state.selected.id ? 'active' : ''}" data-id="${item.id}"><span>AUTHOR · ${escape(item.author)}</span><strong>${escape(item.title)}</strong><small>SKILL · ${escape(item.id)} · ${escape(item.status)}</small><em>${escape(packageSummary(item))}</em></button>`,
    )
    .join('');
  $('#skill-list')
    .querySelectorAll('button')
    .forEach((button) => button.addEventListener('click', () => selectSkill(button.dataset.id)));
}
function selectSkill(id, focus = false) {
  state.selected = catalog.find((item) => item.id === id) || state.selected;
  state.file = packageFiles()[0];
  state.preview = 'original';
  state.lens = false;
  state.rendered = false;
  state.diff = false;
  syncUrl();
  renderList();
  renderDetail();
  loadSelectedFile();
  if (focus) $('#skill-list').querySelector(`[data-id="${state.selected.id}"]`)?.focus();
}
async function fetchSource(entry, file) {
  if (state.sourceByPath.has(file.path)) return state.sourceByPath.get(file.path);
  try {
    state.sourceByPath.set(file.path, redact(await (await fetch(file.path)).text(), entry));
  } catch {
    state.sourceByPath.set(
      file.path,
      '# Original preview unavailable\n\nServe this site from the repository root to load the submitted source.',
    );
  }
  return state.sourceByPath.get(file.path);
}
async function loadSelectedFile() {
  const entry = state.selected;
  const file = state.file;
  await fetchSource(entry, file);
  if (state.selected.id === entry.id && state.file.path === file.path) renderDetail();
  return state.sourceByPath.get(file.path);
}
function schedulePackageSearch() {
  clearTimeout(state.searchTimer);
  const query = state.query;
  const request = ++state.searchRequest;
  state.contentMatches.clear();
  if (query.length < 3) {
    state.searching = false;
    renderList();
    return;
  }
  state.searchTimer = setTimeout(async () => {
    state.searching = true;
    renderList();
    await Promise.all(
      catalog.flatMap((entry) => packageFiles(entry).map((file) => fetchSource(entry, file))),
    );
    if (request !== state.searchRequest) return;
    state.contentMatches = new Set(
      catalog
        .filter((entry) =>
          packageFiles(entry).some((file) =>
            state.sourceByPath.get(file.path)?.toLowerCase().includes(query),
          ),
        )
        .map((entry) => entry.id),
    );
    state.searching = false;
    renderList();
  }, 180);
}
function lensMarkup(entry) {
  return `<section class="change-lens" aria-label="Why this improved draft changed"><header><div><span>CHANGE LENS</span><h3>What changed — and why.</h3></div><button data-lens aria-pressed="true">Back to draft</button></header><p>The improved draft keeps the job, but narrows the decisions an agent must make from memory.</p><div class="change-rows">${changeRows(
    entry,
  )
    .map(
      (change, index) =>
        `<article><span>0${index + 1} / ${change.kind}</span><div><b>− Before</b><p>${escape(change.before)}</p></div><div><b>+ After</b><p>${escape(change.after)}</p></div><aside><b>Why</b><p>${escape(change.why)}</p></aside></article>`,
    )
    .join('')}</div></section>`;
}
function diffRows(before, after) {
  const oldLines = before.split('\n');
  const newLines = after.split('\n');
  const rows = [];
  let oldIndex = 0;
  let newIndex = 0;
  while (oldIndex < oldLines.length || newIndex < newLines.length) {
    if (oldLines[oldIndex] === newLines[newIndex]) {
      rows.push(
        `<p class="same"><span>${oldIndex + 1}</span>${escape(oldLines[oldIndex] || '')}</p>`,
      );
      oldIndex += 1;
      newIndex += 1;
      continue;
    }
    const oldAhead = oldLines.slice(oldIndex + 1, oldIndex + 9).indexOf(newLines[newIndex]);
    const newAhead = newLines.slice(newIndex + 1, newIndex + 9).indexOf(oldLines[oldIndex]);
    if (
      newIndex < newLines.length &&
      (oldIndex >= oldLines.length || (oldAhead === -1 && newAhead !== -1) || newAhead < oldAhead)
    ) {
      rows.push(`<p class="added"><span>+</span>${escape(newLines[newIndex++])}</p>`);
      continue;
    }
    if (oldIndex < oldLines.length) {
      rows.push(`<p class="removed"><span>−</span>${escape(oldLines[oldIndex++])}</p>`);
      continue;
    }
  }
  return rows.join('');
}
function diffMarkup(entry) {
  if (state.file.name !== 'SKILL.md')
    return `<section class="skill-diff" aria-label="Draft comparison"><header><div><span>PACKAGE DIFF</span><h3>Supporting file unchanged.</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>This review only rewrites the main skill contract. The selected ${escape(state.file.kind)} file remains available in its original form.</p></section>`;
  return `<section class="skill-diff" aria-label="Original and improved skill comparison"><header><div><span>SKILL DIFF</span><h3>Original → improved draft</h3></div><button data-diff aria-pressed="true">Back to draft</button></header><p>Green lines are additions; red lines are removals. Unmarked lines are shared context.</p><div class="diff-lines">${diffRows(currentSource() || 'Loading original Markdown…', entry.improved)}</div></section>`;
}
function previewMarkup(entry, available) {
  if (state.preview === 'improved' && state.lens) return lensMarkup(entry);
  if (state.diff) return diffMarkup(entry);
  const label =
    state.preview === 'original'
      ? 'ORIGINAL / SAFETY-REDACTED WHERE NEEDED'
      : 'IMPROVED DRAFT / PACKAGE-AWARE';
  const content = currentContent();
  const body = state.rendered
    ? `<div class="markdown-preview" aria-label="Rendered Markdown preview">${markdownToc(content)}${markdownMarkup(content)}</div>`
    : `<pre><code>${escape(content)}</code></pre>`;
  return `<section class="preview"><header><div class="preview-title"><span>FILE PREVIEW</span><small>${label}</small></div><div>${state.preview === 'improved' ? '<button data-lens aria-pressed="false">Change lens</button>' : ''}<button data-diff aria-pressed="false">Diff</button><button class="preview-markdown" data-render aria-pressed="${state.rendered}">${state.rendered ? 'View source' : 'Preview Markdown'}</button><button data-copy>Copy</button><button data-download>Download</button></div></header><nav class="file-tabs" aria-label="Skill package files">${available.map((item) => `<button class="${item.name === state.file.name ? 'active' : ''}" data-file="${escape(item.name)}"><span>${escape(item.kind)}</span>${escape(item.name)}</button>`).join('')}</nav>${body}</section>`;
}
function renderDetail() {
  const entry = state.selected;
  const available = packageFiles(entry);
  $('#detail').innerHTML =
    `<header><div><span class="status">${escape(entry.status)}</span><h2>${escape(entry.title)}</h2><p>Submitted by <a class="author-link" href="?author=${encodeURIComponent(entry.author)}">${escape(entry.author)}</a> · <a class="share-link" href="?author=${encodeURIComponent(entry.author)}&skill=${encodeURIComponent(entry.id)}&view=${state.preview}">share review ↗</a></p></div><div class="switch" role="group" aria-label="Preview version"><button class="${state.preview === 'original' ? 'active' : ''}" data-preview="original">Original</button><button class="${state.preview === 'improved' ? 'active' : ''}" data-preview="improved">Improved draft</button></div></header><div class="purpose"><span>THE JOB</span><p>${escape(entry.focus)}</p></div><div id="vote-widget"></div><div class="review-grid"><section><span>WHAT'S ALREADY WORKING</span><ul>${entry.wins.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section><section><span>HIGHEST-VALUE IMPROVEMENTS</span><ul>${entry.improve.map((item) => `<li>${escape(item)}</li>`).join('')}</ul></section></div><aside class="extras"><span>GOOD NEXT ADDITION</span><p>${escape(entry.extras)}</p></aside>${previewMarkup(entry, available)}`;
  renderVoteWidget($('#vote-widget'), entry.id);
  $('#detail')
    .querySelectorAll('[data-file]')
    .forEach((button) =>
      button.addEventListener('click', () => {
        state.file = available.find((item) => item.name === button.dataset.file) || available[0];
        state.rendered = false;
        state.diff = false;
        syncUrl();
        renderDetail();
        loadSelectedFile();
      }),
    );
  $('#detail')
    .querySelectorAll('[data-preview]')
    .forEach((button) =>
      button.addEventListener('click', () => {
        state.preview = button.dataset.preview;
        state.lens = false;
        state.rendered = false;
        state.diff = false;
        syncUrl();
        renderDetail();
        loadSelectedFile();
      }),
    );
  $('#detail')
    .querySelectorAll('[data-lens]')
    .forEach((button) =>
      button.addEventListener('click', () => {
        state.lens = !state.lens;
        state.rendered = false;
        state.diff = false;
        syncUrl();
        renderDetail();
      }),
    );
  $('#detail')
    .querySelectorAll('[data-diff]')
    .forEach((button) =>
      button.addEventListener('click', async () => {
        await loadSelectedFile();
        state.diff = !state.diff;
        state.rendered = false;
        state.lens = false;
        syncUrl();
        renderDetail();
      }),
    );
  $('[data-render]')?.addEventListener('click', async () => {
    await loadSelectedFile();
    state.rendered = !state.rendered;
    syncUrl();
    renderDetail();
  });
  $('[data-copy]')?.addEventListener('click', async () => {
    await loadSelectedFile();
    await copy(currentContent());
    $('[data-copy]').textContent = 'Copied';
  });
  $('[data-download]')?.addEventListener('click', async () => {
    await loadSelectedFile();
    download(
      `${entry.id}-${state.file.name.replaceAll('/', '-')}-${state.preview}.md`,
      currentContent(),
    );
  });
}

$('#skill-filter').addEventListener('input', (event) => {
  state.query = event.target.value.toLowerCase().trim();
  renderList();
  schedulePackageSearch();
});
document.addEventListener('keydown', (event) => {
  if (
    event.metaKey ||
    event.ctrlKey ||
    event.altKey ||
    /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName || '')
  )
    return;
  const items = visible();
  const current = items.findIndex((item) => item.id === state.selected.id);
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
    event.preventDefault();
    const offset = event.key === 'ArrowDown' ? 1 : -1;
    selectSkill(items[(current + offset + items.length) % items.length]?.id, true);
  }
  if (event.key.toLowerCase() === 'p') {
    event.preventDefault();
    $('[data-render]')?.click();
  }
});
window.addEventListener('popstate', () => {
  selectFromUrl();
  renderList();
  renderDetail();
  loadSelectedFile();
});
selectFromUrl();
renderList();
renderDetail();
loadSelectedFile();
