/**
 * Single-file PDF → PNG screen (WebView + pdf.js + JSZip).
 * Route: /pdf-to-images-simple
 *
 * Copy-paste friendly: no imports from this repo’s `assets/`. pdf.js is fetched from CDN into the
 * session folder (`pdf.min.js` / `pdf.worker.min.js`) — device needs Internet for that step.
 * Peer deps (Expo app): expo-document-picker, expo-file-system, expo-keep-awake, expo-sharing,
 *   jszip, react-native-safe-area-context, react-native-webview.
 */
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system/legacy';
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake';
import { useRouter } from 'expo-router';
import * as Sharing from 'expo-sharing';
import JSZip from 'jszip';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  ActivityIndicator,
  Alert,
  Image,
  Platform,
  Pressable,
  ScrollView,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { WebView, type WebViewMessageEvent } from 'react-native-webview';

/** Same major line as `pdfjs-dist` in package.json — legacy UMD build on unpkg. */
const PDFJS_DIST_VERSION = '3.11.174';
const PDFJS_MAIN_URL = `https://unpkg.com/pdfjs-dist@${PDFJS_DIST_VERSION}/legacy/build/pdf.min.js`;
const PDFJS_WORKER_URL = `https://unpkg.com/pdfjs-dist@${PDFJS_DIST_VERSION}/legacy/build/pdf.worker.min.js`;

const MAX_PAGES = 30;
const MAX_PAGE_WIDTH_PX = 1200;
const KEEP_AWAKE_TAG = 'pdf-to-images-simple';

const SESSION_DIR = 'pdf_to_images_session';

/** Poll until pdf.js is on `window`, then start the pipeline (used on load + Android reinject). */
const START_PDF_PIPELINE_JS = `(function(){
  var tries = 0;
  function tick() {
    if (window.__pdfToPngStarted) return;
    var PDF = window.pdfjsLib;
    if (PDF && window.startPdfToPngPipeline) {
      window.startPdfToPngPipeline();
      return;
    }
    if (++tries > 280) {
      try {
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'error', message: 'PDF engine timeout' }));
      } catch (e) {}
      return;
    }
    setTimeout(tick, 50);
  }
  tick();
})();true;`;

function ensureFileUrl(path: string): string {
  const p = path.trim();
  if (p.startsWith('file://')) return p;
  return `file://${p.startsWith('/') ? p : `/${p}`}`;
}

async function downloadPdfJsToWorkDir(workDir: string): Promise<void> {
  const mainPath = `${workDir}pdf.min.js`;
  const workerPath = `${workDir}pdf.worker.min.js`;
  const [mainRes, workerRes] = await Promise.all([
    FileSystem.downloadAsync(PDFJS_MAIN_URL, mainPath),
    FileSystem.downloadAsync(PDFJS_WORKER_URL, workerPath),
  ]);
  if (mainRes.status !== 200 || workerRes.status !== 200) {
    throw new Error(
      `PDF.js download failed (HTTP ${mainRes.status} / ${workerRes.status}). Check network.`
    );
  }
}

async function resolveReadableUri(uri: string, fileName: string): Promise<string> {
  if (!uri.startsWith('content://')) return uri;
  if (Platform.OS !== 'android') return uri;
  const base = FileSystem.cacheDirectory;
  if (!base) throw new Error('CACHE_UNAVAILABLE');
  const ext = fileName.includes('.') ? fileName.slice(fileName.lastIndexOf('.')) : '';
  const dest = `${base}doc_import_${Date.now()}${ext}`;
  await FileSystem.copyAsync({ from: uri, to: dest });
  return dest;
}

function buildPdfToPngHtml(maxPages: number, maxWidthPx: number): string {
  const safeMaxPages = Math.max(1, Math.floor(maxPages));
  const safeMaxW = Math.max(512, Math.floor(maxWidthPx));
  return `<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
  <script src="pdf.min.js"></script>
</head>
<body>
<script>
(function () {
  var __MAX_PAGES = ${safeMaxPages};
  var __MAX_WIDTH_PX = ${safeMaxW};

  function xhrGetArrayBuffer(url) {
    return new Promise(function (resolve, reject) {
      var xhr = new XMLHttpRequest();
      xhr.open('GET', url, true);
      xhr.responseType = 'arraybuffer';
      xhr.onload = function () {
        var st = xhr.status;
        if (st === 200 || st === 0) {
          if (xhr.response) resolve(xhr.response);
          else reject(new Error('Empty response for ' + url));
        } else {
          reject(new Error('Could not read ' + url + ' (HTTP ' + st + ')'));
        }
      };
      xhr.onerror = function () {
        reject(new Error('Could not read ' + url + ' (XHR error)'));
      };
      xhr.send();
    });
  }

  async function loadPdfArrayBuffer() {
    try {
      return await xhrGetArrayBuffer('input.pdf');
    } catch (xhrErr) {
      try {
        var r = await fetch('input.pdf');
        if (!r.ok) throw xhrErr;
        return await r.arrayBuffer();
      } catch (fetchErr) {
        throw xhrErr;
      }
    }
  }

  function withTimeout(promise, ms, errMsg) {
    return new Promise(function (resolve, reject) {
      var done = false;
      var tid = setTimeout(function () {
        if (done) return;
        done = true;
        reject(new Error(errMsg || 'Operation timed out'));
      }, ms);
      promise.then(
        function (v) {
          if (done) return;
          done = true;
          clearTimeout(tid);
          resolve(v);
        },
        function (e) {
          if (done) return;
          done = true;
          clearTimeout(tid);
          reject(e);
        }
      );
    });
  }

  window.startPdfToPngPipeline = async function () {
    if (window.__pdfToPngStarted) return;
    var pdfjsLib = window.pdfjsLib;
    if (!pdfjsLib) return;
    window.__pdfToPngStarted = true;
    try {
      try {
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'stage', stage: 'worker' }));
      } catch (e0) {}
      pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.min.js';

      var buf = await loadPdfArrayBuffer();
      try {
        window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'stage', stage: 'open' }));
      } catch (e1) {}
      var annDisable =
        pdfjsLib.AnnotationMode && pdfjsLib.AnnotationMode.DISABLE != null
          ? pdfjsLib.AnnotationMode.DISABLE
          : 0;
      var pdf = await pdfjsLib.getDocument({
        data: buf,
        verbosity: 0,
        useSystemFonts: true,
        isEvalSupported: false,
        useWorkerFetch: false,
        disableFontFace: true,
        isOffscreenCanvasSupported: false,
      }).promise;
      var cap = Math.min(pdf.numPages, __MAX_PAGES);
      window.ReactNativeWebView.postMessage(
        JSON.stringify({ type: 'meta', numPages: pdf.numPages, processing: cap })
      );
      var BRIDGE_CHUNK = 450000;
      for (var pgn = 1; pgn <= cap; pgn++) {
        try {
          window.ReactNativeWebView.postMessage(
            JSON.stringify({ type: 'pageStart', index: pgn, total: cap })
          );
        } catch (psErr) {}
        var page = await pdf.getPage(pgn);
        var baseVp = page.getViewport({ scale: 1 });
        var scale = Math.min(2.5, __MAX_WIDTH_PX / baseVp.width);
        var viewport = page.getViewport({ scale: scale });
        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        var renderTask = page.render({
          canvasContext: ctx,
          viewport: viewport,
          annotationMode: annDisable,
        });
        await withTimeout(renderTask.promise, 180000, 'Page render timed out');
        var b64 = canvas.toDataURL('image/png').split(',')[1] || '';
        for (var off = 0; off < b64.length; off += BRIDGE_CHUNK) {
          try {
            window.ReactNativeWebView.postMessage(
              JSON.stringify({
                type: 'pagePart',
                index: pgn,
                data: b64.slice(off, off + BRIDGE_CHUNK),
              })
            );
          } catch (bridgeErr) {
            throw bridgeErr;
          }
        }
        try {
          window.ReactNativeWebView.postMessage(
            JSON.stringify({ type: 'pageDone', index: pgn, total: cap })
          );
        } catch (pdErr) {}
      }
      await pdf.destroy();
      window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'done' }));
    } catch (err) {
      window.__pdfToPngStarted = false;
      var msg = err && err.message ? String(err.message) : String(err);
      window.ReactNativeWebView.postMessage(JSON.stringify({ type: 'error', message: msg }));
    }
  };
})();
</script>
</body>
</html>`;
}

type WebPayload =
  | { type: 'stage'; stage: 'worker' | 'open' }
  | { type: 'meta'; numPages: number; processing: number }
  | { type: 'pageStart'; index: number; total: number }
  | { type: 'pagePart'; index: number; data: string }
  | { type: 'pageDone'; index: number; total: number }
  | { type: 'done' }
  | { type: 'error'; message: string };

type PageAcc = { index: number; parts: string[] };

export default function PdfToImagesSimpleScreen() {
  const router = useRouter();
  const [busy, setBusy] = useState(false);
  const [progress, setProgress] = useState(0);
  const [status, setStatus] = useState('');
  const [webSession, setWebSession] = useState<{
    html: string;
    baseUrl: string;
    readAccessUrl: string | undefined;
  } | null>(null);
  const [resultZipPath, setResultZipPath] = useState<string | null>(null);
  const [previewUri, setPreviewUri] = useState<string | null>(null);
  const [pageCount, setPageCount] = useState(0);

  const zipRef = useRef<JSZip>(new JSZip());
  const workDirRef = useRef<string | null>(null);
  const pageAccRef = useRef<PageAcc | null>(null);
  const webRef = useRef<WebView>(null);
  const abortedRef = useRef(false);

  useEffect(() => {
    return () => {
      abortedRef.current = true;
      void deactivateKeepAwake(KEEP_AWAKE_TAG);
    };
  }, []);

  const teardown = useCallback(async (dir: string | null) => {
    if (!dir) return;
    try {
      await FileSystem.deleteAsync(dir, { idempotent: true });
    } catch {
      /* ignore */
    }
  }, []);

  const finishZip = useCallback(
    async (workDir: string) => {
      if (abortedRef.current) {
        await teardown(workDir);
        await deactivateKeepAwake(KEEP_AWAKE_TAG);
        setBusy(false);
        setWebSession(null);
        setProgress(0);
        setStatus('');
        workDirRef.current = null;
        return;
      }

      try {
        setStatus('Creating ZIP…');
        setProgress(90);
        const zip = zipRef.current;
        const pngKeys = Object.keys(zip.files)
          .filter((k) => !zip.files[k].dir && /\.png$/i.test(k))
          .sort();
        const count = pngKeys.length;
        let firstDataUri: string | null = null;
        if (pngKeys[0]) {
          const raw = await zip.file(pngKeys[0])!.async('base64');
          firstDataUri = `data:image/png;base64,${raw}`;
        }

        const zipBase64 = await zip.generateAsync({
          type: 'base64',
          compression: 'DEFLATE',
          compressionOptions: { level: 6 },
        });

        const docRoot = FileSystem.documentDirectory;
        if (!docRoot) throw new Error('No document directory');

        const outDir = `${docRoot}PdfImageExports`;
        await FileSystem.makeDirectoryAsync(outDir, { intermediates: true });
        const outName = `pdf_pages_${Date.now()}.zip`;
        const outPath = `${outDir}/${outName}`;
        await FileSystem.writeAsStringAsync(outPath, zipBase64, {
          encoding: FileSystem.EncodingType.Base64,
        });

        setPageCount(count);
        setPreviewUri(firstDataUri);
        setResultZipPath(outPath);
        setProgress(100);
        setStatus('Done');
      } catch (e) {
        const msg = e instanceof Error ? e.message : 'Unknown error';
        Alert.alert('Export failed', msg);
      } finally {
        await teardown(workDir);
        await deactivateKeepAwake(KEEP_AWAKE_TAG);
        workDirRef.current = null;
        setBusy(false);
        setWebSession(null);
        setProgress(0);
        setStatus('');
      }
    },
    [teardown]
  );

  const onMessage = useCallback(
    (ev: WebViewMessageEvent) => {
      const workDir = workDirRef.current;
      if (!workDir) return;

      let payload: WebPayload;
      try {
        payload = JSON.parse(ev.nativeEvent.data) as WebPayload;
      } catch {
        return;
      }
      if (abortedRef.current) return;

      if (payload.type === 'stage') {
        setStatus(payload.stage === 'worker' ? 'Loading PDF engine…' : 'Opening PDF…');
        return;
      }

      if (payload.type === 'meta') {
        pageAccRef.current = null;
        setStatus(`Rendering 0 / ${payload.processing} pages…`);
        return;
      }

      if (payload.type === 'pageStart') {
        pageAccRef.current = { index: payload.index, parts: [] };
        setStatus(`Rendering page ${payload.index}…`);
        return;
      }

      if (payload.type === 'pagePart') {
        const acc = pageAccRef.current;
        if (!acc || acc.index !== payload.index) return;
        if (payload.data) acc.parts.push(payload.data);
        return;
      }

      if (payload.type === 'pageDone') {
        const acc = pageAccRef.current;
        if (!acc || acc.index !== payload.index) return;
        pageAccRef.current = null;
        const joined = acc.parts.join('');
        if (joined) {
          zipRef.current.file(`page-${String(payload.index).padStart(3, '0')}.png`, joined, {
            base64: true,
          });
        }
        const pct = Math.min(88, Math.round((payload.index / Math.max(1, payload.total)) * 88));
        setProgress(pct);
        setStatus(`Rendering ${payload.index} / ${payload.total}…`);
        return;
      }

      if (payload.type === 'done') {
        void finishZip(workDir);
        return;
      }

      if (payload.type === 'error') {
        void (async () => {
          await deactivateKeepAwake(KEEP_AWAKE_TAG);
          await teardown(workDir);
          workDirRef.current = null;
          pageAccRef.current = null;
          setBusy(false);
          setWebSession(null);
          setProgress(0);
          setStatus('');
          Alert.alert('PDF error', payload.message);
        })();
      }
    },
    [finishZip, teardown]
  );

  const injectStart = useCallback(() => {
    webRef.current?.injectJavaScript(START_PDF_PIPELINE_JS);
  }, []);

  const onLoadEnd = useCallback(() => {
    injectStart();
    if (Platform.OS === 'android') {
      setTimeout(injectStart, 200);
      setTimeout(injectStart, 700);
    }
  }, [injectStart]);

  const pickAndConvert = useCallback(async () => {
    if (busy || Platform.OS === 'web') return;

    setBusy(true);
    setResultZipPath(null);
    setPreviewUri(null);
    setPageCount(0);
    abortedRef.current = false;
    zipRef.current = new JSZip();
    pageAccRef.current = null;
    setProgress(0);
    setStatus('Preparing…');

    try {
      await activateKeepAwakeAsync(KEEP_AWAKE_TAG);

      const res = await DocumentPicker.getDocumentAsync({
        type: 'application/pdf',
        copyToCacheDirectory: true,
      });
      if (res.canceled || !res.assets?.[0]) {
        await deactivateKeepAwake(KEEP_AWAKE_TAG);
        setBusy(false);
        setStatus('');
        return;
      }

      const asset = res.assets[0];
      const mime = (asset.mimeType ?? '').toLowerCase();
      const lower = (asset.name ?? '').toLowerCase();
      if (mime !== 'application/pdf' && !lower.endsWith('.pdf')) {
        await deactivateKeepAwake(KEEP_AWAKE_TAG);
        setBusy(false);
        Alert.alert('Not a PDF', 'Please choose a PDF file.');
        return;
      }

      const readable = await resolveReadableUri(asset.uri, asset.name ?? 'doc.pdf');
      const cacheBase = FileSystem.cacheDirectory;
      if (!cacheBase) throw new Error('Cache unavailable');

      const sessionId = `${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
      const workDir = `${cacheBase}${SESSION_DIR}/${sessionId}/`;
      await FileSystem.makeDirectoryAsync(workDir, { intermediates: true });
      workDirRef.current = workDir;

      const inputPath = `${workDir}input.pdf`;
      await FileSystem.copyAsync({ from: readable, to: inputPath });
      await downloadPdfJsToWorkDir(workDir);

      const html = buildPdfToPngHtml(MAX_PAGES, MAX_PAGE_WIDTH_PX);
      const basePath = workDir.endsWith('/') ? workDir : `${workDir}/`;
      const baseUrl = ensureFileUrl(basePath);
      const readAccessUrl = ensureFileUrl(workDir.replace(/\/+$/, ''));

      setWebSession({ html, baseUrl, readAccessUrl });
      setStatus('Starting engine…');
    } catch (e) {
      await deactivateKeepAwake(KEEP_AWAKE_TAG);
      workDirRef.current = null;
      setBusy(false);
      setWebSession(null);
      setStatus('');
      const msg = e instanceof Error ? e.message : 'Unknown error';
      Alert.alert('Error', msg);
    }
  }, [busy]);

  const shareZip = useCallback(async () => {
    if (!resultZipPath) return;
    const ok = await Sharing.isAvailableAsync();
    if (!ok) {
      Alert.alert('Sharing unavailable', 'Sharing is not available on this device.');
      return;
    }
    try {
      await Sharing.shareAsync(ensureFileUrl(resultZipPath), {
        mimeType: 'application/zip',
        dialogTitle: 'Save or share ZIP',
        UTI: 'public.zip-archive',
      });
    } catch {
      Alert.alert('Share failed', 'Could not open the share sheet.');
    }
  }, [resultZipPath]);

  const isWeb = Platform.OS === 'web';

  return (
    <SafeAreaView style={styles.safe}>
      <View style={styles.header}>
        <Pressable onPress={() => router.back()} style={styles.backBtn} hitSlop={12}>
          <Text style={styles.backText}>← Back</Text>
        </Pressable>
        <Text style={styles.title}>PDF → Images</Text>
        <View style={styles.headerSpacer} />
      </View>

      <ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled">
        {isWeb ? (
          <Text style={styles.hint}>This demo runs on iOS and Android, not in the browser.</Text>
        ) : (
          <Text style={styles.sub}>
            Pick a PDF. Each page is rendered to a PNG inside a ZIP you can save via the share sheet
            (Files, iCloud, AirDrop). Up to {MAX_PAGES} pages. The PDF engine downloads once per
            conversion (needs Internet).
          </Text>
        )}

        <Pressable
          style={({ pressed }) => [
            styles.primaryBtn,
            pressed && styles.primaryPressed,
            (busy || isWeb) && styles.disabled,
          ]}
          onPress={pickAndConvert}
          disabled={busy || isWeb}
        >
          {busy ? (
            <View style={styles.row}>
              <ActivityIndicator color="#fff" />
              <Text style={styles.primaryLabel}> Working…</Text>
            </View>
          ) : (
            <Text style={styles.primaryLabel}>Choose PDF</Text>
          )}
        </Pressable>

        {busy ? (
          <View style={styles.card}>
            <Text style={styles.status}>{status}</Text>
            <View style={styles.track}>
              <View style={[styles.fill, { width: `${Math.min(100, progress)}%` }]} />
            </View>
            <Text style={styles.progressNum}>{Math.round(progress)}%</Text>
          </View>
        ) : null}

        {resultZipPath && previewUri && !busy ? (
          <View style={styles.card}>
            <Text style={styles.cardTitle}>Exported</Text>
            <Text style={styles.meta}>{pageCount} PNG pages in ZIP</Text>
            <Image source={{ uri: previewUri }} style={styles.preview} resizeMode="contain" />
            <Pressable style={styles.secondaryBtn} onPress={shareZip}>
              <Text style={styles.secondaryLabel}>Share / Save ZIP</Text>
            </Pressable>
            <Text style={styles.pathHint} numberOfLines={2}>
              {resultZipPath}
            </Text>
          </View>
        ) : null}
      </ScrollView>

      {webSession ? (
        <View style={styles.hiddenWeb} pointerEvents="none" collapsable={false}>
          <WebView
            key={webSession.baseUrl}
            ref={webRef}
            style={styles.webview}
            source={{ html: webSession.html, baseUrl: webSession.baseUrl }}
            onMessage={onMessage}
            onLoadStart={() => setStatus('Loading WebView…')}
            injectedJavaScript={START_PDF_PIPELINE_JS}
            onLoadEnd={onLoadEnd}
            onError={() => {
              void (async () => {
                const wd = workDirRef.current;
                await deactivateKeepAwake(KEEP_AWAKE_TAG);
                if (wd) await teardown(wd);
                workDirRef.current = null;
                setBusy(false);
                setWebSession(null);
                Alert.alert('WebView error', 'Could not load the PDF engine.');
              })();
            }}
            javaScriptEnabled
            domStorageEnabled
            originWhitelist={['*']}
            allowFileAccess
            allowFileAccessFromFileURLs
            allowUniversalAccessFromFileURLs
            allowingReadAccessToURL={Platform.OS === 'ios' ? webSession.readAccessUrl : undefined}
            mixedContentMode="always"
            setSupportMultipleWindows={false}
            {...(Platform.OS === 'android' ? { androidLayerType: 'software' as const } : {})}
          />
        </View>
      ) : null}
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safe: { flex: 1, backgroundColor: '#0f1419' },
  header: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: StyleSheet.hairlineWidth,
    borderBottomColor: '#2a3441',
  },
  backBtn: { paddingVertical: 4 },
  backText: { color: '#7eb8ff', fontSize: 17 },
  title: { color: '#f2f5f9', fontSize: 18, fontWeight: '600' },
  headerSpacer: { width: 56 },
  scroll: { padding: 20, paddingBottom: 40 },
  hint: { color: '#aeb8c4', fontSize: 15, lineHeight: 22 },
  sub: { color: '#aeb8c4', fontSize: 15, lineHeight: 22, marginBottom: 20 },
  primaryBtn: {
    backgroundColor: '#2563eb',
    paddingVertical: 16,
    borderRadius: 12,
    alignItems: 'center',
  },
  primaryPressed: { opacity: 0.9 },
  disabled: { opacity: 0.55 },
  primaryLabel: { color: '#fff', fontSize: 17, fontWeight: '600' },
  row: { flexDirection: 'row', alignItems: 'center' },
  card: {
    marginTop: 24,
    backgroundColor: '#1a2332',
    borderRadius: 14,
    padding: 16,
    borderWidth: 1,
    borderColor: '#2a3441',
  },
  status: { color: '#e8edf4', fontSize: 15, marginBottom: 10 },
  track: {
    height: 8,
    borderRadius: 4,
    backgroundColor: '#2a3441',
    overflow: 'hidden',
  },
  fill: {
    height: '100%',
    backgroundColor: '#3b82f6',
    borderRadius: 4,
  },
  progressNum: { color: '#8b9cb3', marginTop: 8, fontSize: 13 },
  cardTitle: { color: '#f2f5f9', fontSize: 17, fontWeight: '600', marginBottom: 6 },
  meta: { color: '#8b9cb3', fontSize: 14, marginBottom: 12 },
  preview: {
    width: '100%',
    aspectRatio: 3 / 4,
    backgroundColor: '#0f1419',
    borderRadius: 8,
    marginBottom: 16,
  },
  secondaryBtn: {
    backgroundColor: '#334155',
    paddingVertical: 14,
    borderRadius: 10,
    alignItems: 'center',
  },
  secondaryLabel: { color: '#f2f5f9', fontSize: 16, fontWeight: '600' },
  pathHint: { color: '#5c6b7f', fontSize: 11, marginTop: 12 },
  hiddenWeb: {
    position: 'absolute',
    width: 1,
    height: 1,
    opacity: 0.01,
    overflow: 'hidden',
    left: 0,
    bottom: 0,
  },
  webview: { width: 1, height: 1 },
});
