import { unzipSync } from 'fflate';
import * as cheerio from 'cheerio';
import type { Cache } from './cache.ts';
import type { ChapterView } from '../types.ts';

const dec = new TextDecoder();

function parseContainerXml(xml: string): string {
  const $ = cheerio.load(xml, { xmlMode: true });
  const opfPath = $('rootfile').attr('full-path');
  if (!opfPath) throw new Error('EPUB container.xml is missing full-path attribute');
  return opfPath;
}

function parseOpfSpine(xml: string): string[] {
  const $ = cheerio.load(xml, { xmlMode: true });

  const manifest: Record<string, string> = {};
  $('manifest item').each((_, el) => {
    const id = $(el).attr('id');
    const href = $(el).attr('href');
    if (id && href) manifest[id] = href;
  });

  const spine: string[] = [];
  $('spine itemref').each((_, el) => {
    const idref = $(el).attr('idref');
    if (idref) {
      const href = manifest[idref];
      if (href) spine.push(href);
    }
  });

  return spine;
}

function resolveInZip(opfPath: string, relative: string): string {
  const dir = opfPath.includes('/')
    ? opfPath.slice(0, opfPath.lastIndexOf('/') + 1)
    : '';
  return dir + decodeURIComponent(relative);
}

function extractDivText(xhtml: string): string {
  const $ = cheerio.load(xhtml);
  const lines: string[] = [];
  $('div').each((_, el) => {
    const text = $(el).text().trim();
    if (text) lines.push(text);
  });
  return lines.join('\n');
}

export async function fetchEpub(
  url: string,
  id: number,
  from: number,
  to: number,
  cache: Cache,
  onRangeKnown: (startsAt: number, endsAt: number) => void,
): Promise<ChapterView> {
  const cached = cache.tryGetChapters(id, from, to);
  if (cached) return cached;

  const response = await fetch(url, { redirect: 'follow' });
  if (!response.ok) {
    throw new Error(`Failed to download EPUB from ${url}: HTTP ${response.status} ${response.statusText}`);
  }

  const buffer = await response.arrayBuffer();
  const zip = unzipSync(new Uint8Array(buffer));

  const containerEntry = zip['META-INF/container.xml'];
  if (!containerEntry) throw new Error('Invalid EPUB: missing META-INF/container.xml');

  const opfPath = parseContainerXml(dec.decode(containerEntry));
  const opfEntry = zip[opfPath];
  if (!opfEntry) throw new Error(`Invalid EPUB: OPF not found at "${opfPath}"`);

  const spineHrefs = parseOpfSpine(dec.decode(opfEntry));
  if (spineHrefs.length === 0) throw new Error('EPUB spine is empty — no readable chapters found');

  const toSave: { index: number; content: string }[] = [];
  let chapterIndex = 0;

  for (const href of spineHrefs) {
    const zipKey = resolveInZip(opfPath, href);
    const entry = zip[zipKey];
    if (!entry) continue;

    const text = extractDivText(dec.decode(entry));
    if (!text.trim()) continue;

    toSave.push({ index: chapterIndex, content: text });
    chapterIndex++;
  }

  if (toSave.length === 0) throw new Error('EPUB parsed but all chapters were empty');

  cache.saveChapters(id, toSave);
  onRangeKnown(0, chapterIndex - 1);

  const effectiveTo = Math.min(to, chapterIndex - 1);
  const view = cache.tryGetChapters(id, from, effectiveTo);
  if (!view) throw new Error('Cache read failed immediately after EPUB save — this is a bug');
  return view;
}
