"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/util/packer/index.ts
var packer_exports = {};
__export(packer_exports, {
  CouchDBToStaticPacker: () => CouchDBToStaticPacker
});
module.exports = __toCommonJS(packer_exports);

// src/util/logger.ts
var isDevelopment = typeof process !== "undefined" && process.env.NODE_ENV === "development";
var logger = {
  /**
   * Debug-level logging - only shown in development
   */
  debug: (message, ...args) => {
    if (isDevelopment) {
      console.debug(`[DB:DEBUG] ${message}`, ...args);
    }
  },
  /**
   * Info-level logging - general information
   */
  info: (message, ...args) => {
    console.info(`[DB:INFO] ${message}`, ...args);
  },
  /**
   * Warning-level logging - potential issues
   */
  warn: (message, ...args) => {
    console.warn(`[DB:WARN] ${message}`, ...args);
  },
  /**
   * Error-level logging - serious problems
   */
  error: (message, ...args) => {
    console.error(`[DB:ERROR] ${message}`, ...args);
  },
  /**
   * Log function for backward compatibility with existing log() usage
   */
  log: (message, ...args) => {
    if (isDevelopment) {
      console.log(`[DB:LOG] ${message}`, ...args);
    }
  }
};

// src/util/packer/CouchDBToStaticPacker.ts
var CouchDBToStaticPacker = class {
  config;
  sourceDB = null;
  constructor(config = {}) {
    this.config = {
      chunkSize: 1e3,
      includeAttachments: true,
      ...config
    };
  }
  /**
   * Pack a CouchDB course database into static data structures
   */
  async packCourse(sourceDB, courseId) {
    logger.info(`Starting static pack for course: ${courseId}`);
    this.sourceDB = sourceDB;
    const manifest = {
      version: "1.0.0",
      courseId,
      courseName: "",
      courseConfig: null,
      lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
      documentCount: 0,
      chunks: [],
      indices: [],
      designDocs: []
    };
    const courseConfig = await this.extractCourseConfig(sourceDB);
    manifest.courseName = courseConfig.name;
    manifest.courseConfig = courseConfig;
    manifest.designDocs = await this.extractDesignDocs(sourceDB);
    const docsByType = await this.extractDocumentsByType(sourceDB);
    const attachments = /* @__PURE__ */ new Map();
    if (this.config.includeAttachments) {
      await this.extractAllAttachments(docsByType, attachments);
    }
    const chunks = /* @__PURE__ */ new Map();
    for (const [docType, docs] of Object.entries(docsByType)) {
      const chunkMetadata = this.createChunks(docs, docType);
      manifest.chunks.push(...chunkMetadata);
      manifest.documentCount += docs.length;
      this.prepareChunkData(chunkMetadata, docs, chunks);
    }
    const indices = /* @__PURE__ */ new Map();
    manifest.indices = await this.buildIndices(docsByType, manifest.designDocs, indices);
    return {
      manifest,
      chunks,
      indices,
      attachments
    };
  }
  /**
   * Pack a CouchDB course database and write the static files to disk
   */
  async packCourseToFiles(sourceDB, courseId, outputDir, fsAdapter) {
    logger.info(`Packing course ${courseId} to files in ${outputDir}`);
    const packedData = await this.packCourse(sourceDB, courseId);
    const filesWritten = await this.writePackedDataToFiles(packedData, outputDir, fsAdapter);
    return {
      manifest: packedData.manifest,
      filesWritten,
      attachmentsFound: packedData.attachments ? packedData.attachments.size : 0
    };
  }
  /**
   * Write packed course data to files using FileSystemAdapter
   */
  async writePackedDataToFiles(packedData, outputDir, fsAdapter) {
    let totalFiles = 0;
    await fsAdapter.ensureDir(outputDir);
    const manifestPath = fsAdapter.joinPath(outputDir, "manifest.json");
    await fsAdapter.writeJson(manifestPath, packedData.manifest, { spaces: 2 });
    totalFiles++;
    logger.info(`Wrote manifest: ${manifestPath}`);
    const chunksDir = fsAdapter.joinPath(outputDir, "chunks");
    const indicesDir = fsAdapter.joinPath(outputDir, "indices");
    await fsAdapter.ensureDir(chunksDir);
    await fsAdapter.ensureDir(indicesDir);
    for (const [chunkId, chunkData] of packedData.chunks) {
      const chunkPath = fsAdapter.joinPath(chunksDir, `${chunkId}.json`);
      await fsAdapter.writeJson(chunkPath, chunkData);
      totalFiles++;
    }
    logger.info(`Wrote ${packedData.chunks.size} chunk files`);
    for (const [indexName, indexData] of packedData.indices) {
      const indexPath = fsAdapter.joinPath(indicesDir, `${indexName}.json`);
      await fsAdapter.writeJson(indexPath, indexData, { spaces: 2 });
      totalFiles++;
    }
    logger.info(`Wrote ${packedData.indices.size} index files`);
    if (packedData.attachments && packedData.attachments.size > 0) {
      for (const [attachmentPath, attachmentData] of packedData.attachments) {
        const fullAttachmentPath = fsAdapter.joinPath(outputDir, attachmentPath);
        const attachmentDir = fsAdapter.dirname(fullAttachmentPath);
        await fsAdapter.ensureDir(attachmentDir);
        await fsAdapter.writeFile(fullAttachmentPath, attachmentData.buffer);
        totalFiles++;
      }
      logger.info(`Wrote ${packedData.attachments.size} attachment files`);
    }
    return totalFiles;
  }
  async extractCourseConfig(db) {
    try {
      return await db.get("CourseConfig");
    } catch (error) {
      logger.error("Failed to extract course config:", error);
      throw new Error("Course config not found");
    }
  }
  async extractDesignDocs(db) {
    const result = await db.allDocs({
      startkey: "_design/",
      endkey: "_design/\uFFF0",
      include_docs: true
    });
    return result.rows.map((row) => ({
      _id: row.id,
      views: row.doc.views || {}
    }));
  }
  async extractDocumentsByType(db) {
    const allDocs = await db.allDocs({ include_docs: true });
    const docsByType = {};
    for (const row of allDocs.rows) {
      if (row.id.startsWith("_")) continue;
      const doc = row.doc;
      if (doc.docType) {
        if (!docsByType[doc.docType]) {
          docsByType[doc.docType] = [];
        }
        docsByType[doc.docType].push(doc);
      }
    }
    return docsByType;
  }
  createChunks(docs, docType) {
    const chunks = [];
    const sortedDocs = docs.sort((a, b) => a._id.localeCompare(b._id));
    for (let i = 0; i < sortedDocs.length; i += this.config.chunkSize) {
      const chunk = sortedDocs.slice(i, i + this.config.chunkSize);
      const chunkId = `${docType}-${String(Math.floor(i / this.config.chunkSize)).padStart(4, "0")}`;
      chunks.push({
        id: chunkId,
        docType,
        startKey: chunk[0]._id,
        endKey: chunk[chunk.length - 1]._id,
        documentCount: chunk.length,
        path: `chunks/${chunkId}.json`
      });
    }
    return chunks;
  }
  prepareChunkData(chunkMetadata, docs, chunks) {
    const sortedDocs = docs.sort((a, b) => a._id.localeCompare(b._id));
    for (const chunk of chunkMetadata) {
      const chunkDocs = sortedDocs.filter(
        (doc) => doc._id >= chunk.startKey && doc._id <= chunk.endKey
      );
      const cleanedDocs = chunkDocs.map((doc) => {
        const cleaned = { ...doc };
        delete cleaned._rev;
        if (this.config.includeAttachments && cleaned._attachments) {
          cleaned._attachments = this.transformAttachmentStubs(cleaned._attachments, cleaned._id);
        } else if (!this.config.includeAttachments) {
          delete cleaned._attachments;
        }
        return cleaned;
      });
      chunks.set(chunk.id, cleanedDocs);
    }
  }
  async buildIndices(docsByType, designDocs, indices) {
    const indexMetadata = [];
    if (docsByType["CARD" /* CARD */]) {
      const eloIndexMeta = await this.buildEloIndex(
        docsByType["CARD" /* CARD */],
        indices
      );
      indexMetadata.push(eloIndexMeta);
    }
    if (docsByType["TAG" /* TAG */]) {
      const tagIndexMeta = await this.buildTagIndex(docsByType["TAG" /* TAG */], indices);
      indexMetadata.push(tagIndexMeta);
    }
    for (const designDoc of designDocs) {
      for (const [viewName, viewDef] of Object.entries(designDoc.views)) {
        if (viewDef.map) {
          logger.info(`Processing view: ${designDoc._id}/${viewName}`);
          const indexMeta = await this.buildViewIndex(viewName, designDoc, indices);
          if (indexMeta) {
            indexMetadata.push(indexMeta);
            logger.info(`Successfully built index: ${indexMeta.name}`);
          } else {
            logger.warn(`Skipped view index: ${designDoc._id}/${viewName}`);
          }
        }
      }
    }
    return indexMetadata;
  }
  async buildEloIndex(cards, indices) {
    const eloIndex = [];
    for (const card of cards) {
      if (card.elo?.global?.score) {
        eloIndex.push({
          elo: card.elo.global.score,
          cardId: card._id
        });
      }
    }
    eloIndex.sort((a, b) => a.elo - b.elo);
    const buckets = {};
    const bucketSize = 50;
    for (const entry of eloIndex) {
      const bucket = Math.floor(entry.elo / bucketSize) * bucketSize;
      if (!buckets[bucket]) buckets[bucket] = [];
      buckets[bucket].push(entry.cardId);
    }
    indices.set("elo", {
      sorted: eloIndex,
      buckets,
      stats: {
        min: eloIndex[0]?.elo || 0,
        max: eloIndex[eloIndex.length - 1]?.elo || 0,
        count: eloIndex.length
      }
    });
    return {
      name: "elo",
      type: "btree",
      path: "indices/elo.json"
    };
  }
  async buildTagIndex(tags, indices) {
    const tagIndex = {};
    for (const tag of tags) {
      tagIndex[tag.name] = {
        cardIds: tag.taggedCards,
        snippet: tag.snippet,
        count: tag.taggedCards.length
      };
    }
    const cardToTags = {};
    for (const tag of tags) {
      for (const cardId of tag.taggedCards) {
        if (!cardToTags[cardId]) cardToTags[cardId] = [];
        cardToTags[cardId].push(tag.name);
      }
    }
    indices.set("tags", {
      byTag: tagIndex,
      byCard: cardToTags
    });
    return {
      name: "tags",
      type: "hash",
      path: "indices/tags.json"
    };
  }
  /**
   * Build view index by querying CouchDB views directly instead of parsing map functions
   */
  async buildViewIndex(viewName, designDoc, indices) {
    if (!this.sourceDB) {
      logger.error("Source database not available for view querying");
      return null;
    }
    try {
      const designDocId = designDoc._id;
      const designDocName = designDocId.replace("_design/", "");
      const viewPath = `${designDocName}/${viewName}`;
      logger.info(`Querying CouchDB view: ${viewPath}`);
      const viewResults = await this.sourceDB.query(viewPath, {
        include_docs: false
      });
      if (!viewResults.rows || viewResults.rows.length === 0) {
        logger.warn(`View ${viewPath} returned no results`);
        return null;
      }
      logger.info(`Successfully queried view ${viewPath}: ${viewResults.rows.length} results`);
      const formattedResults = this.formatViewResults(viewName, viewResults.rows, designDoc);
      const indexName = `view-${designDoc._id.replace("_design/", "")}-${viewName}`;
      indices.set(indexName, formattedResults);
      return {
        name: indexName,
        type: "view",
        path: `indices/${indexName}.json`
      };
    } catch (error) {
      logger.error(`Failed to query view ${designDoc._id}/${viewName}:`, error);
      return null;
    }
  }
  /**
   * Format CouchDB view results for static consumption
   */
  formatViewResults(viewName, viewRows, designDoc) {
    const baseResult = {
      type: "couchdb-view",
      viewName,
      designDoc: designDoc._id,
      results: viewRows,
      metadata: {
        resultCount: viewRows.length,
        generatedAt: (/* @__PURE__ */ new Date()).toISOString()
      }
    };
    switch (viewName) {
      case "elo":
        return this.formatEloViewIndex(viewRows, baseResult);
      case "getTags":
        return this.formatTagsViewIndex(viewRows, baseResult);
      case "cardsByInexperience":
        return this.formatInexperienceViewIndex(viewRows, baseResult);
      default:
        return this.formatGenericViewIndex(viewRows, baseResult);
    }
  }
  /**
   * Format ELO view results - convert to sorted array format
   */
  formatEloViewIndex(viewRows, baseResult) {
    const sortedResults = viewRows.sort((a, b) => {
      if (typeof a.key === "number" && typeof b.key === "number") {
        return a.key - b.key;
      }
      return 0;
    });
    return {
      ...baseResult,
      sorted: sortedResults,
      stats: {
        min: sortedResults[0]?.key || 0,
        max: sortedResults[sortedResults.length - 1]?.key || 0,
        count: sortedResults.length
      }
    };
  }
  /**
   * Format tags view results - convert to tag mapping format
   */
  formatTagsViewIndex(viewRows, baseResult) {
    const tagMap = {};
    for (const row of viewRows) {
      const tagName = row.key;
      if (typeof tagName === "string") {
        if (!tagMap[tagName]) {
          tagMap[tagName] = [];
        }
        tagMap[tagName].push(row.id);
      }
    }
    return {
      ...baseResult,
      byTag: tagMap,
      tagCount: Object.keys(tagMap).length
    };
  }
  /**
   * Format inexperience view results - convert to experience-based format
   */
  formatInexperienceViewIndex(viewRows, baseResult) {
    const sortedResults = viewRows.sort((a, b) => {
      if (typeof a.key === "number" && typeof b.key === "number") {
        return a.key - b.key;
      }
      return 0;
    });
    return {
      ...baseResult,
      sorted: sortedResults,
      stats: {
        minInexperience: sortedResults[0]?.key || 0,
        maxInexperience: sortedResults[sortedResults.length - 1]?.key || 0,
        count: sortedResults.length
      }
    };
  }
  /**
   * Format generic view results - fallback for unknown views
   */
  formatGenericViewIndex(_viewRows, baseResult) {
    return {
      ...baseResult
      // Keep results as-is for unknown view types
    };
  }
  /**
   * Extract all attachments from documents and download binary data
   */
  async extractAllAttachments(docsByType, attachments) {
    logger.info("Extracting attachments...");
    const allDocs = [];
    for (const docs of Object.values(docsByType)) {
      allDocs.push(...docs);
    }
    const docsWithAttachments = allDocs.filter((doc) => doc._attachments && Object.keys(doc._attachments).length > 0);
    if (docsWithAttachments.length === 0) {
      logger.info("No attachments found");
      return;
    }
    logger.info(`Found ${docsWithAttachments.length} documents with attachments`);
    const extractionPromises = docsWithAttachments.map(
      (doc) => this.extractDocumentAttachments(doc, attachments)
    );
    await Promise.all(extractionPromises);
    logger.info(`Extracted ${attachments.size} attachment files`);
  }
  /**
   * Extract attachments for a single document
   */
  async extractDocumentAttachments(doc, attachments) {
    if (!doc._attachments || !this.sourceDB) {
      return;
    }
    const docId = doc._id;
    for (const [attachmentName, metadata] of Object.entries(doc._attachments)) {
      try {
        const attachmentResponse = await this.sourceDB.getAttachment(docId, attachmentName);
        let buffer;
        if (attachmentResponse instanceof ArrayBuffer) {
          buffer = Buffer.from(attachmentResponse);
        } else if (Buffer.isBuffer(attachmentResponse)) {
          buffer = attachmentResponse;
        } else {
          const blob = attachmentResponse;
          buffer = Buffer.from(await blob.arrayBuffer());
        }
        const extension = this.getFileExtension(metadata.content_type);
        const filename = `${attachmentName}${extension}`;
        const attachmentPath = `attachments/${docId}/${filename}`;
        attachments.set(attachmentPath, {
          docId,
          attachmentName,
          filename,
          path: attachmentPath,
          contentType: metadata.content_type,
          length: metadata.length || buffer.length,
          digest: metadata.digest,
          buffer
        });
        logger.debug(`Extracted attachment: ${attachmentPath}`);
      } catch (error) {
        logger.error(`Failed to extract attachment ${docId}/${attachmentName}:`, error);
        throw new Error(`Failed to extract attachment ${docId}/${attachmentName}: ${error}`);
      }
    }
  }
  /**
   * Transform attachment stubs to include file paths
   */
  transformAttachmentStubs(attachments, docId) {
    const transformed = {};
    for (const [attachmentName, metadata] of Object.entries(attachments)) {
      const extension = this.getFileExtension(metadata.content_type);
      const filename = `${attachmentName}${extension}`;
      const attachmentPath = `attachments/${docId}/${filename}`;
      transformed[attachmentName] = {
        path: attachmentPath,
        content_type: metadata.content_type,
        length: metadata.length,
        digest: metadata.digest,
        stub: false
        // No longer a stub - we have the actual file
      };
    }
    return transformed;
  }
  /**
   * Get file extension from content type
   */
  getFileExtension(contentType) {
    const extensionMap = {
      "image/jpeg": ".jpg",
      "image/jpg": ".jpg",
      "image/png": ".png",
      "image/gif": ".gif",
      "image/webp": ".webp",
      "audio/mpeg": ".mp3",
      "audio/mp3": ".mp3",
      "audio/wav": ".wav",
      "audio/ogg": ".ogg",
      "video/mp4": ".mp4",
      "video/webm": ".webm",
      "application/pdf": ".pdf",
      "text/plain": ".txt",
      "application/json": ".json"
    };
    return extensionMap[contentType] || "";
  }
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
  CouchDBToStaticPacker
});
//# sourceMappingURL=index.js.map