import { PDFDocument, PDFName, PDFString } from '@cantoo/pdf-lib';
import { registerFeature } from '../registry';
import { featureModal, pdfSource, footerButtons, deliver, type LoadedFile } from '../common';
import { h } from '../../core/dom';
import { icon } from '../../ui/icons';
import { loadPdf } from '../../core/pdf';
import { bufferOf, baseName, clamp } from '../../core/util';
import { toast } from '../../ui/toast';

interface Row { title: string; page: number }

registerFeature({
  id: 'bookmarks',
  title: 'Bookmarks / outline',
  desc: 'View and add PDF bookmarks that jump to a page.',
  icon: 'bookmark',
  category: 'organize',
  run(ctx) {
    let file: LoadedFile | null = null;
    let total = 0;
    let rows: Row[] = [];

    const m = featureModal('Bookmarks / outline', 'See existing bookmarks and add your own.', 'bookmark', 560);
    const list = h('div.bm-list');
    const info = h('p.hint', 'Choose a PDF to begin.');

    const renderRows = () => {
      list.replaceChildren(
        ...rows.map((r, i) =>
          h('div.bm-row',
            h('input.input bm-title', { value: r.title, placeholder: 'Bookmark title', oninput: (e: Event) => { r.title = (e.target as HTMLInputElement).value; } }),
            h('input.input bm-page', { type: 'number', min: 1, max: total || 1, value: String(r.page), oninput: (e: Event) => { r.page = clamp(parseInt((e.target as HTMLInputElement).value, 10) || 1, 1, total || 1); } }),
            h('button.bm-del', { title: 'Remove', onclick: () => { rows.splice(i, 1); renderRows(); } }, '×'),
          ),
        ),
        h('button.btn btn-soft btn-sm bm-add', { onclick: () => { rows.push({ title: `Bookmark ${rows.length + 1}`, page: 1 }); renderRows(); } }, icon('plus', 14), 'Add bookmark'),
      );
    };

    m.body.append(
      pdfSource(ctx, {
        multiple: false,
        onChange: async (files) => {
          file = files[0] ?? null;
          rows = [];
          if (!file) { info.textContent = 'Choose a PDF to begin.'; primary.disabled = true; renderRows(); return; }
          info.textContent = 'Reading…';
          try {
            const pjs = await loadPdf(file.bytes);
            total = pjs.numPages;
            const outline = await pjs.getOutline();
            if (outline && outline.length) {
              rows = await flatten(outline, pjs);
              info.textContent = `${rows.length} existing bookmark${rows.length === 1 ? '' : 's'} — edit or add below.`;
            } else {
              info.textContent = `No bookmarks yet. Add some below (pages 1–${total}).`;
              rows = [{ title: 'Introduction', page: 1 }];
            }
            primary.disabled = false;
            renderRows();
          } catch (e) {
            info.textContent = 'Could not read that PDF.';
            file = null;
            console.error(e);
          }
        },
      }),
      info,
      list,
    );

    const { primary } = footerButtons(m, 'Save bookmarks', async () => {
      if (!file) return;
      const clean = rows.filter((r) => r.title.trim());
      if (!clean.length) { toast('Add at least one bookmark.', { type: 'error' }); return; }
      m.setBusy(true, 'Writing bookmarks…');
      try {
        const bytes = await writeOutline(file.bytes, clean);
        m.close();
        deliver(bytes, baseName(file.name) + '-bookmarked.pdf');
      } catch (e) {
        m.setBusy(false);
        toast('Could not write bookmarks.', { type: 'error' });
        console.error(e);
      }
    }, 'bookmark');
    primary.disabled = true;
    renderRows();
  },
});

/** Flatten a pdf.js outline (with nesting) into title+page rows. */
async function flatten(items: any[], pjs: any, depth = 0): Promise<Row[]> {
  const out: Row[] = [];
  for (const it of items) {
    let page = 1;
    try {
      let dest = it.dest;
      if (typeof dest === 'string') dest = await pjs.getDestination(dest);
      if (Array.isArray(dest) && dest[0]) page = (await pjs.getPageIndex(dest[0])) + 1;
    } catch { /* leave page 1 */ }
    out.push({ title: '  '.repeat(depth) + (it.title || 'Untitled'), page });
    if (it.items?.length) out.push(...(await flatten(it.items, pjs, depth + 1)));
  }
  return out;
}

/** Build a flat /Outlines tree pointing at page destinations. */
async function writeOutline(bytes: Uint8Array, rows: Row[]): Promise<Uint8Array> {
  const doc = await PDFDocument.load(bufferOf(bytes), { ignoreEncryption: true });
  const ctx = doc.context;
  const pages = doc.getPages();

  const outlinesDict = ctx.obj({ Type: 'Outlines' });
  const outlinesRef = ctx.register(outlinesDict);

  const dicts = rows.map((r) => {
    const idx = clamp(r.page - 1, 0, pages.length - 1);
    const dest = ctx.obj([pages[idx].ref, 'Fit']);
    const d = ctx.obj({ Title: PDFString.of(r.title.trim()), Dest: dest });
    d.set(PDFName.of('Parent'), outlinesRef);
    return d;
  });
  const refs = dicts.map((d) => ctx.register(d));
  dicts.forEach((d, i) => {
    if (i > 0) d.set(PDFName.of('Prev'), refs[i - 1]);
    if (i < dicts.length - 1) d.set(PDFName.of('Next'), refs[i + 1]);
  });

  outlinesDict.set(PDFName.of('First'), refs[0]);
  outlinesDict.set(PDFName.of('Last'), refs[refs.length - 1]);
  outlinesDict.set(PDFName.of('Count'), ctx.obj(rows.length));
  doc.catalog.set(PDFName.of('Outlines'), outlinesRef);

  return doc.save({ useObjectStreams: true });
}
