(function () {
  "use strict";

  const DATABASE_NAME = "senliz-library";
  const DATABASE_VERSION = 3;
  const BOOKS_STORE = "books";
  const PROGRESS_STORE = "progress";
  const PREFERENCES_STORE = "preferences";

  let databasePromise;

  function requestResult(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  function transactionDone(transaction) {
    return new Promise((resolve, reject) => {
      transaction.oncomplete = () => resolve();
      transaction.onabort = () => reject(transaction.error || new Error("The database transaction was cancelled."));
      transaction.onerror = () => reject(transaction.error || new Error("The database transaction failed."));
    });
  }

  function openDatabase() {
    if (databasePromise) return databasePromise;

    databasePromise = new Promise((resolve, reject) => {
      const request = indexedDB.open(DATABASE_NAME, DATABASE_VERSION);

      request.onupgradeneeded = () => {
        const database = request.result;
        if (!database.objectStoreNames.contains(BOOKS_STORE)) {
          const books = database.createObjectStore(BOOKS_STORE, { keyPath: "id" });
          books.createIndex("fileKey", "fileKey", { unique: false });
          books.createIndex("lastOpenedDate", "lastOpenedDate", { unique: false });
        } else {
          const books = request.transaction.objectStore(BOOKS_STORE);
          if (!books.indexNames.contains("fileKey")) books.createIndex("fileKey", "fileKey", { unique: false });
          if (!books.indexNames.contains("lastOpenedDate")) {
            books.createIndex("lastOpenedDate", "lastOpenedDate", { unique: false });
          }
        }

        if (!database.objectStoreNames.contains(PROGRESS_STORE)) {
          database.createObjectStore(PROGRESS_STORE, { keyPath: "bookId" });
        }

        if (!database.objectStoreNames.contains(PREFERENCES_STORE)) {
          database.createObjectStore(PREFERENCES_STORE, { keyPath: "key" });
        }
      };

      request.onsuccess = () => {
        const database = request.result;
        database.onversionchange = () => database.close();
        resolve(database);
      };
      request.onblocked = () => reject(new Error("Close other Senliz Library windows and try again."));
      request.onerror = () => reject(request.error || new Error("The local library could not be opened."));
    });

    return databasePromise;
  }

  function normalizeFileKey(fileName) {
    return String(fileName || "")
      .normalize("NFKC")
      .trim()
      .toLocaleLowerCase("en-US");
  }

  function fallbackTitle(fileName) {
    return String(fileName || "Untitled EPUB")
      .replace(/\.epub$/i, "")
      .replace(/[_-]+/g, " ")
      .replace(/\s+/g, " ")
      .trim() || "Untitled EPUB";
  }

  function createId() {
    if (globalThis.crypto && typeof globalThis.crypto.randomUUID === "function") {
      return globalThis.crypto.randomUUID();
    }
    return `book-${Date.now()}-${Math.random().toString(16).slice(2)}`;
  }

  function cleanString(value, fallback = "") {
    if (Array.isArray(value)) value = value.filter(Boolean).join(", ");
    const text = String(value || "").replace(/\s+/g, " ").trim();
    return text || fallback;
  }

  function createBookRecord(file, metadata) {
    const importedDate = new Date().toISOString();
    return {
      id: metadata.id || createId(),
      epubBlob: file instanceof Blob ? file : new Blob([file], { type: "application/epub+zip" }),
      fileName: metadata.fileName,
      fileKey: normalizeFileKey(metadata.fileName),
      title: cleanString(metadata.title, fallbackTitle(metadata.fileName)),
      author: cleanString(metadata.author, "Unknown Author"),
      language: cleanString(metadata.language),
      direction: metadata.direction === "rtl" ? "rtl" : metadata.direction === "ltr" ? "ltr" : "",
      coverBlob: metadata.coverBlob instanceof Blob ? metadata.coverBlob : null,
      identifier: cleanString(metadata.identifier),
      importedDate,
      lastOpenedDate: "",
      fixedLayout: Boolean(metadata.fixedLayout),
      size: Number(metadata.size || file.size || 0),
      schemaVersion: 2,
      migrationState: "native"
    };
  }

  async function getAllBooks() {
    const database = await openDatabase();
    const transaction = database.transaction(BOOKS_STORE, "readonly");
    const result = await requestResult(transaction.objectStore(BOOKS_STORE).getAll());
    await transactionDone(transaction);
    return result;
  }

  async function getBook(id) {
    const database = await openDatabase();
    const transaction = database.transaction(BOOKS_STORE, "readonly");
    const result = await requestResult(transaction.objectStore(BOOKS_STORE).get(id));
    await transactionDone(transaction);
    return result;
  }

  async function findByFileName(fileName) {
    const fileKey = normalizeFileKey(fileName);
    const books = await getAllBooks();
    return books.find((book) => normalizeFileKey(book.fileKey || book.fileName) === fileKey) || null;
  }

  async function putBook(book) {
    const database = await openDatabase();
    const transaction = database.transaction(BOOKS_STORE, "readwrite");
    transaction.objectStore(BOOKS_STORE).put(book);
    await transactionDone(transaction);
    return book;
  }

  async function updateBook(id, fields) {
    const database = await openDatabase();
    const transaction = database.transaction(BOOKS_STORE, "readwrite");
    const store = transaction.objectStore(BOOKS_STORE);
    const book = await requestResult(store.get(id));
    if (!book) {
      transaction.abort();
      throw new Error("This book is no longer available on this device.");
    }
    const updated = { ...book, ...fields, id: book.id };
    store.put(updated);
    await transactionDone(transaction);
    return updated;
  }

  async function removeBook(id) {
    const database = await openDatabase();
    const stores = [BOOKS_STORE, PROGRESS_STORE];
    const transaction = database.transaction(stores, "readwrite");
    transaction.objectStore(BOOKS_STORE).delete(id);
    transaction.objectStore(PROGRESS_STORE).delete(id);
    await transactionDone(transaction);
  }

  async function getProgress(bookId) {
    const database = await openDatabase();
    const transaction = database.transaction(PROGRESS_STORE, "readonly");
    const result = await requestResult(transaction.objectStore(PROGRESS_STORE).get(bookId));
    await transactionDone(transaction);
    return result || null;
  }

  async function putProgress(progress) {
    const database = await openDatabase();
    const transaction = database.transaction(PROGRESS_STORE, "readwrite");
    transaction.objectStore(PROGRESS_STORE).put({
      ...progress,
      bookId: progress.bookId,
      updatedAt: new Date().toISOString()
    });
    await transactionDone(transaction);
  }

  async function getAllProgress() {
    const database = await openDatabase();
    const transaction = database.transaction(PROGRESS_STORE, "readonly");
    const result = await requestResult(transaction.objectStore(PROGRESS_STORE).getAll());
    await transactionDone(transaction);
    return result;
  }

  async function getPreference(key, fallback) {
    const database = await openDatabase();
    const transaction = database.transaction(PREFERENCES_STORE, "readonly");
    const result = await requestResult(transaction.objectStore(PREFERENCES_STORE).get(key));
    await transactionDone(transaction);
    return result ? result.value : fallback;
  }

  async function setPreference(key, value) {
    const database = await openDatabase();
    const transaction = database.transaction(PREFERENCES_STORE, "readwrite");
    transaction.objectStore(PREFERENCES_STORE).put({ key, value });
    await transactionDone(transaction);
  }

  function escapeXml(value) {
    return String(value || "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&apos;");
  }

  function sanitizeLegacyHtml(html) {
    const source = String(html || "");
    if (globalThis.DOMPurify && globalThis.DOMPurify.isSupported) {
      return globalThis.DOMPurify.sanitize(source, {
        USE_PROFILES: { html: true, svg: true, svgFilters: false },
        FORBID_TAGS: ["script", "iframe", "object", "embed", "form", "input", "button", "textarea", "select"],
        FORBID_ATTR: ["srcset"],
        ALLOW_UNKNOWN_PROTOCOLS: false
      });
    }
    const document = new DOMParser().parseFromString(source, "text/html");
    document.querySelectorAll("script,iframe,object,embed,form,input,button,textarea,select").forEach((node) => node.remove());
    document.querySelectorAll("*").forEach((element) => {
      for (const attribute of [...element.attributes]) {
        if (/^on/i.test(attribute.name) || /^\s*javascript:/i.test(attribute.value)) {
          element.removeAttribute(attribute.name);
        }
      }
    });
    return document.body.innerHTML;
  }

  function legacyChapters(book) {
    if (Array.isArray(book.chapters) && book.chapters.length) {
      return book.chapters.map((chapter, index) => ({
        title: cleanString(chapter.title, `Chapter ${index + 1}`),
        html: sanitizeLegacyHtml(chapter.html || chapter.content || "")
      }));
    }

    if (Array.isArray(book.pages) && book.pages.length) {
      const groups = new Map();
      for (const page of book.pages) {
        const index = Number.isInteger(page.chapterIndex) ? page.chapterIndex : 0;
        if (!groups.has(index)) groups.set(index, []);
        groups.get(index).push(page.html || "");
      }
      return [...groups.entries()].map(([index, pages]) => ({
        title: `Chapter ${index + 1}`,
        html: sanitizeLegacyHtml(pages.join("\n"))
      }));
    }

    return [];
  }

  async function convertLegacyBook(book) {
    const chapters = legacyChapters(book);
    if (!chapters.length || !globalThis.JSZip) {
      return { ...book, migrationState: "reimport-required", schemaVersion: 2 };
    }

    const zip = new globalThis.JSZip();
    zip.file("mimetype", "application/epub+zip", { compression: "STORE" });
    zip.file(
      "META-INF/container.xml",
      "<?xml version=\"1.0\" encoding=\"UTF-8\"?><container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\"><rootfiles><rootfile full-path=\"EPUB/package.opf\" media-type=\"application/oebps-package+xml\"/></rootfiles></container>"
    );

    const manifestItems = [];
    const spineItems = [];
    const navItems = [];
    chapters.forEach((chapter, index) => {
      const number = index + 1;
      const id = `chapter-${number}`;
      const href = `chapter-${number}.xhtml`;
      manifestItems.push(`<item id="${id}" href="${href}" media-type="application/xhtml+xml"/>`);
      spineItems.push(`<itemref idref="${id}"/>`);
      navItems.push(`<li><a href="${href}">${escapeXml(chapter.title)}</a></li>`);
      zip.file(
        `EPUB/${href}`,
        `<?xml version="1.0" encoding="UTF-8"?>
        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml" lang="${escapeXml(book.language || "en")}" dir="${book.direction === "rtl" ? "rtl" : "ltr"}">
          <head><title>${escapeXml(chapter.title)}</title><meta charset="utf-8"/></head>
          <body><h1>${escapeXml(chapter.title)}</h1>${chapter.html}</body>
        </html>`
      );
    });

    zip.file(
      "EPUB/nav.xhtml",
      `<?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE html>
      <html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" lang="${escapeXml(book.language || "en")}">
        <head><title>Contents</title></head>
        <body><nav epub:type="toc" id="toc"><h1>Contents</h1><ol>${navItems.join("")}</ol></nav></body>
      </html>`
    );

    zip.file(
      "EPUB/package.opf",
      `<?xml version="1.0" encoding="UTF-8"?>
      <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="book-id" page-progression-direction="${book.direction === "rtl" ? "rtl" : "ltr"}">
        <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
          <dc:identifier id="book-id">${escapeXml(book.identifier || book.id)}</dc:identifier>
          <dc:title>${escapeXml(book.title || fallbackTitle(book.fileName))}</dc:title>
          <dc:creator>${escapeXml(book.author || "Unknown Author")}</dc:creator>
          <dc:language>${escapeXml(book.language || "en")}</dc:language>
          <meta property="dcterms:modified">${new Date().toISOString().replace(/\.\d{3}Z$/, "Z")}</meta>
        </metadata>
        <manifest><item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/>${manifestItems.join("")}</manifest>
        <spine>${spineItems.join("")}</spine>
      </package>`
    );

    const epubBlob = await zip.generateAsync({
      type: "blob",
      mimeType: "application/epub+zip",
      compression: "DEFLATE",
      compressionOptions: { level: 6 }
    });

    return {
      id: book.id || createId(),
      epubBlob,
      fileName: book.fileName || `${fallbackTitle(book.title)}.epub`,
      fileKey: normalizeFileKey(book.fileName || `${fallbackTitle(book.title)}.epub`),
      title: cleanString(book.title, fallbackTitle(book.fileName)),
      author: cleanString(book.author, "Unknown Author"),
      language: cleanString(book.language),
      direction: book.direction === "rtl" || book.lang === "fa" ? "rtl" : "",
      coverBlob: book.coverBlob instanceof Blob ? book.coverBlob : null,
      identifier: cleanString(book.identifier),
      importedDate: book.importedDate || new Date().toISOString(),
      lastOpenedDate: book.lastOpenedDate || "",
      fixedLayout: false,
      size: epubBlob.size,
      schemaVersion: 2,
      migrationState: "legacy-converted"
    };
  }

  async function migrateLegacyBooks() {
    const books = await getAllBooks();
    const failures = [];
    for (const book of books) {
      if (book.epubBlob instanceof Blob && book.schemaVersion >= 2) continue;
      try {
        const migrated = await convertLegacyBook(book);
        await putBook(migrated);
      } catch (error) {
        console.error("Legacy book migration failed.", { id: book.id, error });
        failures.push(book.id);
        try {
          await putBook({ ...book, migrationState: "reimport-required", schemaVersion: 2 });
        } catch {
          // The original record remains untouched if even the marker cannot be written.
        }
      }
    }
    return { total: books.length, failures };
  }

  function friendlyStorageError(error) {
    if (error && (error.name === "QuotaExceededError" || error.name === "NS_ERROR_DOM_QUOTA_REACHED")) {
      return "There is not enough storage space to save this book. Remove an unused book or free disk space, then try again.";
    }
    return "Senliz Library could not save this change on this device.";
  }

  globalThis.SenlizDB = Object.freeze({
    openDatabase,
    normalizeFileKey,
    fallbackTitle,
    createBookRecord,
    getAllBooks,
    getBook,
    findByFileName,
    putBook,
    updateBook,
    removeBook,
    getProgress,
    getAllProgress,
    putProgress,
    getPreference,
    setPreference,
    migrateLegacyBooks,
    friendlyStorageError
  });
})();
