import fs from "fs";
import path from "path";
import { remarkCodeBlocksShiki } from "@kentcdodds/md-temp";
import { cachified } from "cachified";
import fsExtra from "fs-extra";
import md5 from "md5-hex";
import { bundleMDX } from "mdx-bundler";
import PQueue from "p-queue";
import remarkAutolinkHeadings from "remark-autolink-headings";
import emoji from "remark-emoji";
import gfm from "remark-gfm";
import { visit } from "unist-util-visit";
import {
  compiledMarkdownCache,
  embeddedFilesCache,
  shouldForceFresh
} from "./cache.server.js";
import {
  remarkCodeFile
} from "./codefile-mdx.server.js";
import { singleton } from "./singleton.server.js";
const cacheDir = path.join(
  process.env.KCDSHOP_CONTEXT_CWD ?? process.cwd(),
  "./node_modules/.cache/compile-mdx"
);
function trimCodeBlocks() {
  return async function transformer(tree) {
    visit(tree, "element", (preNode) => {
      if (preNode.tagName !== "pre" || !preNode.children.length) {
        return;
      }
      const codeNode = preNode.children[0];
      if (!codeNode || codeNode.type !== "element" || codeNode.tagName !== "code") {
        return;
      }
      const [codeStringNode] = codeNode.children;
      if (!codeStringNode)
        return;
      if (codeStringNode.type !== "text") {
        console.warn(
          `trimCodeBlocks: Unexpected: codeStringNode type is not "text": ${codeStringNode.type}`
        );
        return;
      }
      codeStringNode.value = codeStringNode.value.trimEnd();
    });
  };
}
function removePreContainerDivs() {
  return async function preContainerDivsTransformer(tree) {
    visit(
      tree,
      { type: "element", tagName: "pre" },
      function visitor(node, index, parent) {
        if (parent?.type !== "element")
          return;
        if (parent.tagName !== "div")
          return;
        if (parent.children.length !== 1 && index === 0)
          return;
        Object.assign(parent, node);
      }
    );
  };
}
const rehypePlugins = [
  trimCodeBlocks,
  remarkCodeBlocksShiki,
  removePreContainerDivs
];
function checkFileExists(file) {
  return fs.promises.access(file, fs.constants.F_OK).then(
    () => true,
    () => false
  );
}
const verboseLog = process.env.KCDSHOP_VERBOSE_LOG === "true" ? console.log : () => {
};
function validateEmbeddedFiles(embeddedFiles, lastCompiledTime) {
  if (process.env.NODE_ENV !== "development")
    return Promise.resolve(true);
  return Promise.all(
    Array.from(embeddedFiles).map(async ({ file }) => {
      const stat = await fs.promises.stat(file).catch(() => ({ mtimeMs: 0 }));
      return lastCompiledTime > stat.mtimeMs || Promise.reject();
    })
  ).then(
    () => true,
    () => false
  );
}
async function compileMdx(file, { request, forceFresh } = {}) {
  if (!await checkFileExists(file)) {
    throw new Error(`File does not exist: ${file}`);
  }
  let cachedEmbeddedFiles = /* @__PURE__ */ new Map();
  const stat = await fs.promises.stat(file);
  const cacheLocation = path.join(cacheDir, `${md5(file)}.json`);
  const requireFresh = await shouldForceFresh({
    forceFresh,
    request,
    key: cacheLocation
  });
  if (!requireFresh && await checkFileExists(cacheLocation)) {
    const cached = JSON.parse(
      await fs.promises.readFile(cacheLocation, "utf-8")
    );
    cachedEmbeddedFiles = new Map(
      Object.entries(cached.value.embeddedFiles ?? {})
    );
    const compiledTime = cached.value.compiledTime ?? 0;
    const warningCancled = process.env.NODE_ENV === "development" ? cached?.value?.warningCancled ?? false : false;
    if (compiledTime > stat.mtimeMs && !warningCancled && await validateEmbeddedFiles(cachedEmbeddedFiles.values(), compiledTime)) {
      return cached.value;
    }
  }
  let title = null;
  const epicVideoEmbeds = [];
  const codeFileData = {
    mdxFile: file,
    cacheLocation,
    cachedEmbeddedFiles,
    embeddedFiles: /* @__PURE__ */ new Map()
  };
  try {
    verboseLog(`Compiling ${file}`);
    const bundleResult = await queuedBundleMDX({
      file,
      cwd: path.dirname(file),
      esbuildOptions(options) {
        options.define = {
          "process.env.NODE_ENV": JSON.stringify(
            process.env.NODE_ENV ?? "production"
          ),
          ...options.define
        };
        return options;
      },
      mdxOptions(options) {
        options.remarkPlugins = [
          // @ts-expect-error - the types here are a mess
          ...options.remarkPlugins ?? [],
          // @ts-expect-error - the types here are a mess
          [remarkAutolinkHeadings, { behavior: "wrap" }],
          // @ts-expect-error - the types here are a mess
          gfm,
          // @ts-expect-error - the types here are a mess
          () => (tree) => {
            visit(tree, "heading", (node) => {
              if (title)
                return;
              if (node.depth === 1) {
                visit(node, "text", (textNode) => {
                  title = textNode.value.trim();
                });
              }
            });
            title = title ? title.replace(/^\d+\. /, "").trim() : null;
          },
          // @ts-expect-error - the types here are a mess
          () => (tree) => {
            visit(tree, "mdxJsxFlowElement", (jsxEl) => {
              if (jsxEl.name !== "EpicVideo")
                return;
              const urlAttr = jsxEl.attributes.find(
                (a) => a.type === "mdxJsxAttribute" && a.name === "url"
              );
              if (!urlAttr)
                return;
              let url = urlAttr.value;
              if (typeof url !== "string")
                return;
              if (url.endsWith("/"))
                url = url.slice(0, -1);
              epicVideoEmbeds.push(url);
            });
          },
          // @ts-expect-error - the types here are a mess
          () => remarkCodeFile(codeFileData),
          // @ts-expect-error - the types here are a mess
          emoji
        ];
        options.rehypePlugins = [
          ...options.rehypePlugins ?? [],
          ...rehypePlugins
        ];
        options.mdxExtensions = [".mdx", ".md"];
        options.format = "mdx";
        options.development = false;
        return options;
      }
    });
    if (!bundleResult)
      throw new Error(`Timeout for file: ${file}`);
    const result = { code: bundleResult.code, title, epicVideoEmbeds };
    await fsExtra.ensureDir(cacheDir);
    await fs.promises.writeFile(
      cacheLocation,
      JSON.stringify({
        value: {
          ...result,
          compiledTime: Date.now(),
          embeddedFiles: codeFileData.embeddedFiles.size ? Object.fromEntries(codeFileData.embeddedFiles) : void 0
        }
      })
    );
    await updateEmbeddedFilesCache(codeFileData);
    return result;
  } catch (error) {
    console.error(`Compilation error for file: `, file, error);
    throw error;
  } finally {
    verboseLog(`Successfully compiled ${file}`);
  }
}
async function compileMarkdownString(markdownString) {
  return cachified({
    key: markdownString,
    cache: compiledMarkdownCache,
    ttl: 1e3 * 60 * 60 * 24,
    getFreshValue: async () => {
      try {
        verboseLog(`Compiling string`, markdownString);
        const result = await queuedBundleMDX({
          source: markdownString,
          esbuildOptions(options) {
            options.define = {
              "process.env.NODE_ENV": JSON.stringify(
                process.env.NODE_ENV ?? "production"
              ),
              ...options.define
            };
            return options;
          },
          mdxOptions(options) {
            options.rehypePlugins = [
              ...options.rehypePlugins ?? [],
              ...rehypePlugins
            ];
            options.development = false;
            return options;
          }
        });
        if (!result)
          throw new Error(`Timed out compiling markdown string`);
        return result.code;
      } catch (error) {
        console.error(`Compilation error for code: `, markdownString, error);
        throw error;
      } finally {
        verboseLog(`Successfully compiled string`, markdownString);
      }
    }
  });
}
const modifiedEmbeddedFilesTime = singleton(
  "modified_embedded_files_time",
  () => /* @__PURE__ */ new Map()
);
const EMBEDDED_FILES_CACHE_KEY = "embeddedFilesCache";
async function updateEmbeddedFilesCache({
  mdxFile,
  embeddedFiles
}) {
  if (mdxFile.includes("playground"))
    return;
  let cachedList = await getEmbeddedFilesCache();
  const hash = cachedList ? md5(JSON.stringify(cachedList)) : null;
  if (cachedList) {
    for (const [key, value] of Object.entries(cachedList)) {
      cachedList[key] = value.filter((item) => item !== mdxFile);
      if (cachedList[key]?.length === 0) {
        delete cachedList[key];
      }
    }
  }
  if (embeddedFiles.size) {
    if (!cachedList) {
      cachedList = {};
    }
    const files = Array.from(
      new Set(Array.from(embeddedFiles.values()).map(({ file }) => file))
    ).sort();
    for (const file of files) {
      cachedList[file] = [...cachedList[file] || [], mdxFile];
    }
  }
  if (cachedList && hash !== md5(JSON.stringify(cachedList))) {
    await fsExtra.ensureDir(cacheDir);
    const embeddedFilesLocation = path.join(cacheDir, "embeddedFiles.json");
    modifiedEmbeddedFilesTime.set(EMBEDDED_FILES_CACHE_KEY, Date.now());
    await fs.promises.writeFile(
      embeddedFilesLocation,
      JSON.stringify({ ...cachedList })
    );
  }
}
async function getEmbeddedFilesCache() {
  const key = EMBEDDED_FILES_CACHE_KEY;
  function getForceFresh(cacheEntry) {
    if (!cacheEntry)
      return true;
    const latestModifiedTime = modifiedEmbeddedFilesTime.get(key);
    if (!latestModifiedTime)
      return void 0;
    return latestModifiedTime > cacheEntry.metadata.createdTime ? true : void 0;
  }
  return cachified({
    key,
    cache: embeddedFilesCache,
    ttl: 1e3 * 60 * 60 * 24,
    forceFresh: getForceFresh(await embeddedFilesCache.get(key)),
    getFreshValue: async () => {
      try {
        const embeddedFilesLocation = path.join(cacheDir, "embeddedFiles.json");
        if (await checkFileExists(embeddedFilesLocation)) {
          return JSON.parse(
            await fs.promises.readFile(embeddedFilesLocation, "utf-8")
          );
        }
      } catch (error) {
        console.error(`Unable to read 'embeddedFiles.json' from: `, cacheDir);
      }
      return void 0;
    }
  });
}
async function isEmbeddedFile(filePath) {
  if (process.env.NODE_ENV !== "development")
    return false;
  const embeddedFilesList = await getEmbeddedFilesCache();
  if (embeddedFilesList) {
    const embeddedFiles = Object.keys(embeddedFilesList);
    return embeddedFiles.includes(filePath.replace(/\\/g, "/"));
  }
  return false;
}
let _queue = null;
async function getQueue() {
  if (_queue)
    return _queue;
  _queue = new PQueue({
    concurrency: 1,
    throwOnTimeout: true,
    timeout: 1e3 * 20
  });
  return _queue;
}
async function queuedBundleMDX(...args) {
  const queue = await getQueue();
  const result = await queue.add(() => bundleMDX(...args));
  return result;
}
export {
  compileMarkdownString,
  compileMdx,
  isEmbeddedFile
};
