// EPUB Reader — open and read EPUB books entirely in the browser.
// Uses JSZip to unpack, parses OPF spine for reading order, renders
// sanitised XHTML chapters with embedded images converted to blob URLs.

window.TOOL_HANDLERS['epub-reader'] = function EpubReaderTool() {
  const [file, setFile] = React.useState(null);
  const [loading, setLoading] = React.useState(false);
  const [error, setError] = React.useState('');
  const [book, setBook] = React.useState(null);
  // book = { title, author, cover (blob url), chapters: [{ id, title, html }], toc: [] }
  const [chapterIdx, setChapterIdx] = React.useState(0);
  const [fontSize, setFontSize] = React.useState(18);
  const [sidebarOpen, setSidebarOpen] = React.useState(true);
  const [readerDark, setReaderDark] = React.useState(false);
  const contentRef = React.useRef(null);

  // Cleanup blob URLs on unmount or new book
  const blobUrls = React.useRef([]);
  React.useEffect(() => {
    return () => blobUrls.current.forEach((u) => URL.revokeObjectURL(u));
  }, []);

  // ── XML helpers ──────────────────────────────────────────────
  function parseXml(text) {
    return new DOMParser().parseFromString(text, 'application/xml');
  }

  function getTextContent(parent, tagNames) {
    for (const tag of tagNames) {
      // try with and without namespace prefix
      const el = parent.getElementsByTagName(tag)[0]
        || parent.querySelector(tag);
      if (el) return (el.textContent || '').trim();
    }
    return '';
  }

  // ── Sanitise HTML ────────────────────────────────────────────
  function sanitiseHtml(html) {
    // Strip <script> tags entirely
    let clean = html.replace(/<script[\s\S]*?<\/script>/gi, '');
    // Strip on* event attributes
    clean = clean.replace(/\s+on\w+\s*=\s*("[^"]*"|'[^']*'|[^\s>]*)/gi, '');
    // Strip javascript: hrefs
    clean = clean.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"');
    return clean;
  }

  // ── Resolve relative paths within the EPUB zip ───────────────
  function resolvePath(base, rel) {
    if (rel.startsWith('/')) return rel.slice(1);
    const parts = base.split('/');
    parts.pop(); // remove filename
    for (const seg of rel.split('/')) {
      if (seg === '..') parts.pop();
      else if (seg !== '.') parts.push(seg);
    }
    return parts.join('/');
  }

  // ── Replace image srcs in HTML with blob URLs from the zip ───
  async function resolveImages(html, zip, opfDir) {
    const imgRegex = /<img\s[^>]*src\s*=\s*["']([^"']+)["'][^>]*>/gi;
    const matches = [...html.matchAll(imgRegex)];
    const uniqueSrcs = [...new Set(matches.map((m) => m[1]))];

    // Resolve all images in parallel instead of sequentially
    const replacements = {};
    await Promise.all(uniqueSrcs.map(async (src) => {
      const decoded = src.replace(/&amp;/g, '&');
      const zipPath = resolvePath(opfDir + '/dummy', decoded);
      const entry = zip.file(zipPath) || zip.file(decodeURIComponent(zipPath));
      if (!entry) return;
      try {
        const data = await entry.async('blob');
        const url = URL.createObjectURL(data);
        blobUrls.current.push(url);
        replacements[src] = url;
      } catch (_) { /* skip broken images */ }
    }));

    let result = html;
    for (const [orig, blobUrl] of Object.entries(replacements)) {
      const escaped = orig.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
      result = result.replace(new RegExp(escaped, 'g'), blobUrl);
    }
    return result;
  }

  // ── Cover extraction ─────────────────────────────────────────
  async function extractCover(zip, manifest, opfDir) {
    // Look for common cover patterns
    const coverItem = manifest.find(
      (m) => m.id === 'cover-image' || m.id === 'cover' || m.id === 'coverimage'
        || (m.properties && m.properties.includes('cover-image'))
        || (m.mediaType && m.mediaType.startsWith('image/') && /cover/i.test(m.href))
    );
    if (coverItem) {
      const path = resolvePath(opfDir + '/dummy', coverItem.href);
      const entry = zip.file(path) || zip.file(decodeURIComponent(path));
      if (entry) {
        const blob = await entry.async('blob');
        const url = URL.createObjectURL(blob);
        blobUrls.current.push(url);
        return url;
      }
    }
    return null;
  }

  // ── Main EPUB parser ─────────────────────────────────────────
  const handleFile = async (f) => {
    if (!f) return;
    setFile(f);
    setLoading(true);
    setError('');
    setChapterIdx(0);

    // Cleanup previous blob URLs
    blobUrls.current.forEach((u) => URL.revokeObjectURL(u));
    blobUrls.current = [];

    try {
      await window.loadScript('https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js');
      const zip = await JSZip.loadAsync(await f.arrayBuffer());

      // 1. Read container.xml to find OPF
      const containerEntry = zip.file('META-INF/container.xml');
      if (!containerEntry) throw new Error('Invalid EPUB: missing META-INF/container.xml');
      const containerXml = await containerEntry.async('string');
      const containerDoc = parseXml(containerXml);
      const rootfileEl = containerDoc.getElementsByTagName('rootfile')[0];
      if (!rootfileEl) throw new Error('Invalid EPUB: no rootfile in container.xml');
      const opfPath = rootfileEl.getAttribute('full-path');
      const opfDir = opfPath.includes('/') ? opfPath.substring(0, opfPath.lastIndexOf('/')) : '';

      // 2. Parse the OPF
      const opfEntry = zip.file(opfPath);
      if (!opfEntry) throw new Error('Invalid EPUB: OPF file not found at ' + opfPath);
      const opfXml = await opfEntry.async('string');
      const opfDoc = parseXml(opfXml);

      // Metadata
      const title = getTextContent(opfDoc, ['dc:title', 'title']) || f.name.replace(/\.epub$/i, '');
      const author = getTextContent(opfDoc, ['dc:creator', 'creator']) || '';

      // Manifest: id → { href, mediaType, properties }
      const manifestEls = opfDoc.getElementsByTagName('item');
      const manifest = [];
      const manifestById = {};
      for (let i = 0; i < manifestEls.length; i++) {
        const el = manifestEls[i];
        const item = {
          id: el.getAttribute('id'),
          href: el.getAttribute('href'),
          mediaType: el.getAttribute('media-type') || '',
          properties: el.getAttribute('properties') || ''
        };
        manifest.push(item);
        manifestById[item.id] = item;
      }

      // Spine: ordered list of itemref idrefs
      const spineEls = opfDoc.getElementsByTagName('itemref');
      const spineIds = [];
      for (let i = 0; i < spineEls.length; i++) {
        spineIds.push(spineEls[i].getAttribute('idref'));
      }

      // Cover
      const cover = await extractCover(zip, manifest, opfDir);

      // 3. Try to parse the NCX or nav for a table of contents
      let tocLabels = {}; // href → label
      // Try nav (EPUB3)
      const navItem = manifest.find((m) => m.properties && m.properties.includes('nav'));
      if (navItem) {
        const navPath = resolvePath(opfDir + '/dummy', navItem.href);
        const navEntry = zip.file(navPath) || zip.file(decodeURIComponent(navPath));
        if (navEntry) {
          try {
            const navHtml = await navEntry.async('string');
            const navDoc = new DOMParser().parseFromString(navHtml, 'application/xhtml+xml');
            const anchors = navDoc.querySelectorAll('nav[*|type="toc"] a, nav.toc a, nav a');
            for (const a of anchors) {
              const href = (a.getAttribute('href') || '').split('#')[0];
              if (href) tocLabels[href] = a.textContent.trim();
            }
          } catch (_) { /* nav parse failed, continue */ }
        }
      }
      // Fallback: try NCX
      if (Object.keys(tocLabels).length === 0) {
        const ncxItem = manifest.find((m) => m.mediaType === 'application/x-dtbncx+xml');
        if (ncxItem) {
          const ncxPath = resolvePath(opfDir + '/dummy', ncxItem.href);
          const ncxEntry = zip.file(ncxPath) || zip.file(decodeURIComponent(ncxPath));
          if (ncxEntry) {
            try {
              const ncxXml = await ncxEntry.async('string');
              const ncxDoc = parseXml(ncxXml);
              const navPoints = ncxDoc.getElementsByTagName('navPoint');
              for (let i = 0; i < navPoints.length; i++) {
                const label = getTextContent(navPoints[i], ['text']);
                const contentEl = navPoints[i].getElementsByTagName('content')[0];
                if (contentEl && label) {
                  const src = (contentEl.getAttribute('src') || '').split('#')[0];
                  if (src) tocLabels[src] = label;
                }
              }
            } catch (_) { /* NCX parse failed */ }
          }
        }
      }

      // 4. Extract chapters from the spine
      const chapters = [];
      for (const idref of spineIds) {
        const item = manifestById[idref];
        if (!item) continue;
        const chPath = resolvePath(opfDir + '/dummy', item.href);
        const chEntry = zip.file(chPath) || zip.file(decodeURIComponent(chPath));
        if (!chEntry) continue;

        let chHtml = await chEntry.async('string');

        // Extract just the <body> content
        const bodyMatch = chHtml.match(/<body[^>]*>([\s\S]*)<\/body>/i);
        let bodyContent = bodyMatch ? bodyMatch[1] : chHtml;

        // Resolve images
        bodyContent = await resolveImages(bodyContent, zip, opfDir);

        // Sanitise
        bodyContent = sanitiseHtml(bodyContent);

        // Determine chapter title: from TOC, or first heading, or fallback
        let chTitle = tocLabels[item.href] || '';
        if (!chTitle) {
          const headingMatch = bodyContent.match(/<h[1-3][^>]*>([\s\S]*?)<\/h[1-3]>/i);
          if (headingMatch) {
            chTitle = headingMatch[1].replace(/<[^>]+>/g, '').trim();
          }
        }
        if (!chTitle) chTitle = 'Chapter ' + (chapters.length + 1);

        chapters.push({ id: idref, title: chTitle, html: bodyContent, href: item.href });
      }

      if (chapters.length === 0) throw new Error('No readable chapters found in this EPUB.');

      setBook({ title, author, cover, chapters });
    } catch (e) {
      setError(e.message || 'Failed to parse EPUB');
    } finally {
      setLoading(false);
    }
  };

  // Scroll content to top when chapter changes
  React.useEffect(() => {
    if (contentRef.current) contentRef.current.scrollTop = 0;
  }, [chapterIdx]);

  const reset = () => {
    blobUrls.current.forEach((u) => URL.revokeObjectURL(u));
    blobUrls.current = [];
    setFile(null);
    setBook(null);
    setError('');
    setChapterIdx(0);
  };

  // ── Render: Dropzone ─────────────────────────────────────────
  if (!file) {
    return <window.Dropzone onFile={handleFile} title="Drop an EPUB file" hint=".epub" accept=".epub,application/epub+zip" />;
  }

  // ── Render: Loading ──────────────────────────────────────────
  if (loading) {
    return <window.LoadingCard label="Opening book..." sub={file.name + ' (' + window.fmtBytes(file.size) + ')'} />;
  }

  // ── Render: Error ────────────────────────────────────────────
  if (error) {
    return (
      <window.ToolError
        error={error}
        hint="Make sure the file is a valid .epub"
        onRetry={reset}
      />
    );
  }

  if (!book) return null;

  const chapter = book.chapters[chapterIdx];
  const totalChapters = book.chapters.length;

  // ── Reader theme styles ──────────────────────────────────────
  const readerBg = readerDark ? '#1a1a2e' : 'var(--id-surface)';
  const readerText = readerDark ? '#d4d4d8' : 'var(--id-text)';
  const readerMuted = readerDark ? '#71717a' : 'var(--id-text-muted)';
  const readerBorder = readerDark ? '#2d2d44' : 'var(--id-border)';
  const readerSurface = readerDark ? '#252540' : 'var(--id-surface-alt)';

  return (
    <div className="mini-tool" style={{ padding: 0 }}>
      {/* ── Top bar ──────────────────────────────────────────── */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 10, padding: '10px 16px',
        borderBottom: '1px solid ' + readerBorder,
        background: readerSurface, borderRadius: '10px 10px 0 0',
        flexWrap: 'wrap'
      }}>
        <button
          className="btn btn-secondary"
          onClick={() => setSidebarOpen(!sidebarOpen)}
          title={sidebarOpen ? 'Hide contents' : 'Show contents'}
          style={{ padding: '6px 10px', minWidth: 0 }}
        >
          <window.Icon name="doc" size={14} />
        </button>

        <div style={{ flex: 1, minWidth: 120, overflow: 'hidden' }}>
          <div style={{
            fontWeight: 700, fontSize: 14, color: readerText,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
          }}>
            {book.title}
          </div>
          {book.author && (
            <div style={{ fontSize: 12, color: readerMuted }}>
              {book.author}
            </div>
          )}
        </div>

        {/* Chapter indicator */}
        <span className="cmp-meta" style={{ color: readerMuted, fontSize: 12, whiteSpace: 'nowrap' }}>
          {chapterIdx + 1} / {totalChapters}
        </span>

        {/* Font size controls */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
          <button
            className="filter-pill"
            onClick={() => setFontSize((s) => Math.max(12, s - 2))}
            title="Decrease font size"
            style={{ padding: '4px 8px', fontSize: 13 }}
          >
            A-
          </button>
          <span style={{ fontSize: 12, color: readerMuted, minWidth: 28, textAlign: 'center' }}>
            {fontSize}
          </span>
          <button
            className="filter-pill"
            onClick={() => setFontSize((s) => Math.min(32, s + 2))}
            title="Increase font size"
            style={{ padding: '4px 8px', fontSize: 13 }}
          >
            A+
          </button>
        </div>

        {/* Reading theme toggle */}
        <button
          className="filter-pill"
          onClick={() => setReaderDark(!readerDark)}
          title={readerDark ? 'Light reading mode' : 'Dark reading mode'}
          style={{ padding: '4px 8px' }}
        >
          <window.Icon name={readerDark ? 'sun' : 'moon'} size={14} />
        </button>

        {/* Back to drop */}
        <button
          className="btn btn-secondary"
          onClick={reset}
          title="Load another EPUB"
          style={{ padding: '6px 10px', minWidth: 0 }}
        >
          <window.Icon name="x" size={14} />
        </button>
      </div>

      {/* ── Main layout: sidebar + reading area ────────────── */}
      <div style={{ display: 'flex', minHeight: 460, background: readerBg, borderRadius: '0 0 10px 10px' }}>

        {/* ── Sidebar: TOC ──────────────────────────────────── */}
        {sidebarOpen && (
          <div style={{
            width: 240, minWidth: 200, borderRight: '1px solid ' + readerBorder,
            overflowY: 'auto', padding: '12px 0',
            background: readerSurface, flexShrink: 0,
            borderRadius: '0 0 0 10px'
          }}>
            {/* Cover thumbnail */}
            {book.cover && (
              <div style={{ padding: '0 14px 12px', textAlign: 'center' }}>
                <img
                  src={book.cover}
                  alt="Cover"
                  style={{
                    maxWidth: '100%', maxHeight: 160, borderRadius: 6,
                    boxShadow: '0 2px 8px rgba(0,0,0,0.15)'
                  }}
                />
              </div>
            )}

            <div style={{
              padding: '0 14px 8px', fontSize: 11, fontWeight: 700,
              textTransform: 'uppercase', letterSpacing: '0.05em',
              color: readerMuted
            }}>
              Contents
            </div>

            {book.chapters.map((ch, i) => (
              <button
                key={ch.id + '-' + i}
                onClick={() => setChapterIdx(i)}
                style={{
                  display: 'block', width: '100%', textAlign: 'left',
                  padding: '8px 14px', border: 'none', cursor: 'pointer',
                  fontSize: 13, lineHeight: 1.4,
                  background: i === chapterIdx
                    ? (readerDark ? 'rgba(37,99,235,0.2)' : 'var(--id-brand-blue-soft)')
                    : 'transparent',
                  color: i === chapterIdx ? 'var(--id-brand-blue)' : readerText,
                  fontWeight: i === chapterIdx ? 600 : 400,
                  borderLeft: i === chapterIdx ? '3px solid var(--id-brand-blue)' : '3px solid transparent',
                  transition: 'background 0.15s, color 0.15s'
                }}
                onMouseEnter={(e) => {
                  if (i !== chapterIdx) e.currentTarget.style.background = readerDark ? 'rgba(255,255,255,0.05)' : 'var(--id-gray-100)';
                }}
                onMouseLeave={(e) => {
                  if (i !== chapterIdx) e.currentTarget.style.background = 'transparent';
                }}
              >
                <span style={{
                  display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical',
                  overflow: 'hidden'
                }}>
                  {ch.title}
                </span>
              </button>
            ))}
          </div>
        )}

        {/* ── Reading area ──────────────────────────────────── */}
        <div
          ref={contentRef}
          style={{
            flex: 1, overflowY: 'auto', padding: '32px 24px',
            maxHeight: 560
          }}
        >
          {/* Progress bar */}
          <div style={{
            height: 3, background: readerDark ? '#2d2d44' : 'var(--id-gray-200)',
            borderRadius: 2, marginBottom: 24, overflow: 'hidden'
          }}>
            <div style={{
              height: '100%', borderRadius: 2,
              background: 'var(--id-brand-blue)',
              width: ((chapterIdx + 1) / totalChapters * 100) + '%',
              transition: 'width 0.3s ease'
            }} />
          </div>

          {/* Chapter title */}
          <h2 style={{
            fontSize: Math.min(fontSize + 8, 32), fontWeight: 700,
            color: readerText, margin: '0 auto 20px',
            maxWidth: 680, lineHeight: 1.3
          }}>
            {chapter.title}
          </h2>

          {/* Chapter content */}
          <div
            className="epub-reader-content"
            style={{
              maxWidth: 680, margin: '0 auto',
              fontSize: fontSize, lineHeight: 1.75,
              color: readerText, fontFamily: 'Georgia, "Times New Roman", serif',
              wordWrap: 'break-word', overflowWrap: 'break-word'
            }}
            dangerouslySetInnerHTML={{ __html: chapter.html }}
          />

          {/* ── Bottom navigation ──────────────────────────── */}
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            maxWidth: 680, margin: '32px auto 0', gap: 12, flexWrap: 'wrap'
          }}>
            <button
              className="btn btn-secondary"
              onClick={() => setChapterIdx((i) => Math.max(0, i - 1))}
              disabled={chapterIdx === 0}
              style={{ opacity: chapterIdx === 0 ? 0.4 : 1 }}
            >
              <window.Icon name="arrow" size={14} style={{ transform: 'rotate(180deg)' }} />
              {' '}Previous
            </button>

            <span style={{ fontSize: 13, color: readerMuted }}>
              Chapter {chapterIdx + 1} of {totalChapters}
            </span>

            <button
              className="btn btn-secondary"
              onClick={() => setChapterIdx((i) => Math.min(totalChapters - 1, i + 1))}
              disabled={chapterIdx === totalChapters - 1}
              style={{ opacity: chapterIdx === totalChapters - 1 ? 0.4 : 1 }}
            >
              Next{' '}
              <window.Icon name="arrow" size={14} />
            </button>
          </div>
        </div>
      </div>

      {/* ── Scoped styles for chapter HTML content ─────────── */}
      <style>{`
        .epub-reader-content p {
          margin: 0 0 1em;
        }
        .epub-reader-content h1, .epub-reader-content h2,
        .epub-reader-content h3, .epub-reader-content h4 {
          margin: 1.2em 0 0.5em;
          line-height: 1.3;
          font-family: var(--id-font-sans);
        }
        .epub-reader-content img {
          max-width: 100%;
          height: auto;
          border-radius: 6px;
          margin: 12px 0;
        }
        .epub-reader-content a {
          color: var(--id-brand-blue);
          text-decoration: underline;
        }
        .epub-reader-content blockquote {
          margin: 1em 0;
          padding: 12px 20px;
          border-left: 3px solid var(--id-brand-blue);
          font-style: italic;
          opacity: 0.85;
        }
        .epub-reader-content pre, .epub-reader-content code {
          font-family: var(--id-font-mono);
          font-size: 0.88em;
        }
        .epub-reader-content pre {
          padding: 12px 16px;
          border-radius: 8px;
          overflow-x: auto;
          background: rgba(0,0,0,0.05);
        }
        .epub-reader-content ul, .epub-reader-content ol {
          padding-left: 1.5em;
          margin: 0.5em 0;
        }
        .epub-reader-content li {
          margin-bottom: 0.3em;
        }
        .epub-reader-content table {
          border-collapse: collapse;
          width: 100%;
          margin: 1em 0;
        }
        .epub-reader-content td, .epub-reader-content th {
          padding: 6px 10px;
          border: 1px solid var(--id-border);
          text-align: left;
        }
        .epub-reader-content svg {
          max-width: 100%;
          height: auto;
        }
      `}</style>
    </div>
  );
};
