export type MdxComponent = (props: Record<string, string>, children: string) => string;

const escapeHtml = (value: string) =>
  value.replace(
    /[&<>"']/g,
    (character) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
  );

/** Execute explicitly registered MDX components without evaluating arbitrary JavaScript. */
export function renderMdxComponents(
  source: string,
  components: Record<string, MdxComponent>,
): string {
  let output = source;
  const parseProps = (raw: string) =>
    Object.fromEntries(
      [...raw.matchAll(/([A-Za-z_$][\w$-]*)\s*=\s*["']([^"']*)["']/g)].map((match) => [
        match[1]!,
        match[2]!,
      ]),
    );
  for (let pass = 0; pass < 20; pass++) {
    let changed = false;
    output = output.replace(
      /<([A-Z][A-Za-z0-9_$]*)\b([^>]*)>([\s\S]*?)<\/\1>|<([A-Z][A-Za-z0-9_$]*)\b([^>]*)\/>/g,
      (whole, pairedName, pairedProps, children, singleName, singleProps) => {
        const name = pairedName ?? singleName;
        const component = components[name];
        if (!component) throw new Error(`WRN-CONTENT-MDX-COMPONENT: '${name}' is not registered.`);
        changed = true;
        return component(parseProps(pairedProps ?? singleProps ?? ""), children ?? "");
      },
    );
    if (!changed) break;
  }
  if (/<[A-Z][A-Za-z0-9_$]*\b/.test(output))
    throw new Error("WRN-CONTENT-MDX-DEPTH: component expansion exceeded its bound.");
  return output;
}

export interface SyntaxLanguageBundle {
  highlight(source: string): string;
}
export function createIncrementalHighlighter(
  loaders: Record<string, () => SyntaxLanguageBundle | Promise<SyntaxLanguageBundle>>,
) {
  const loaded = new Map<string, Promise<SyntaxLanguageBundle>>();
  return {
    languages: () => [...loaded.keys()],
    async highlight(language: string, source: string) {
      const load = loaders[language];
      if (!load) return escapeHtml(source);
      let bundle = loaded.get(language);
      if (!bundle) {
        bundle = Promise.resolve(load());
        loaded.set(language, bundle);
      }
      return (await bundle).highlight(source);
    },
    async render(html: string) {
      const matches = [
        ...html.matchAll(/<pre><code class="language-([\w-]+)">([\s\S]*?)<\/code><\/pre>/g),
      ];
      let output = html;
      for (const match of matches)
        output = output.replace(
          match[0],
          `<pre><code class="language-${match[1]}">${await this.highlight(match[1]!, match[2]!)}</code></pre>`,
        );
      return output;
    },
  };
}

export interface VendorAdapterOptions {
  fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
  token?: string;
  map?: (entry: any) => { id: string; content: string; source?: string };
}
const requestJson = async (
  url: string,
  options: VendorAdapterOptions,
  headers: HeadersInit = {},
) => {
  const response = await (options.fetch ?? globalThis.fetch)(url, { headers });
  if (!response.ok) throw new Error(`WRN-CONTENT-CMS: ${response.status}`);
  return response.json();
};
const normalize = (entry: any, source: string, map?: VendorAdapterOptions["map"]) =>
  map?.(entry) ?? {
    id: String(entry.id ?? entry.sys?.id ?? entry._id),
    content: String(entry.content ?? entry.body ?? entry.fields?.body ?? ""),
    source,
  };

export function contentfulAdapter(
  space: string,
  environment = "master",
  options: VendorAdapterOptions = {},
) {
  return {
    async list(collection: string) {
      const url = `https://cdn.contentful.com/spaces/${encodeURIComponent(space)}/environments/${encodeURIComponent(environment)}/entries?content_type=${encodeURIComponent(collection)}`;
      const value = await requestJson(
        url,
        options,
        options.token ? { authorization: `Bearer ${options.token}` } : {},
      );
      return (value.items ?? []).map((entry: any) =>
        normalize(entry, `contentful:${space}:${entry.sys?.id}`, options.map),
      );
    },
  };
}
export function sanityAdapter(
  project: string,
  dataset: string,
  options: VendorAdapterOptions & { apiVersion?: string } = {},
) {
  return {
    async list(collection: string) {
      const query = encodeURIComponent(`*[_type == $type]`);
      const url = `https://${encodeURIComponent(project)}.api.sanity.io/v${options.apiVersion ?? "2024-01-01"}/data/query/${encodeURIComponent(dataset)}?query=${query}&$type=${encodeURIComponent(JSON.stringify(collection))}`;
      const value = await requestJson(
        url,
        options,
        options.token ? { authorization: `Bearer ${options.token}` } : {},
      );
      return (value.result ?? []).map((entry: any) =>
        normalize(entry, `sanity:${project}:${entry._id}`, options.map),
      );
    },
  };
}
export function strapiAdapter(baseUrl: string, options: VendorAdapterOptions = {}) {
  const base = new URL(baseUrl);
  if (!/^https?:$/.test(base.protocol)) throw new Error("Strapi URL must use HTTP(S)");
  return {
    async list(collection: string) {
      if (!/^[A-Za-z0-9_-]+$/.test(collection)) throw new Error("Invalid Strapi collection");
      const value = await requestJson(
        new URL(`/api/${collection}`, base).href,
        options,
        options.token ? { authorization: `Bearer ${options.token}` } : {},
      );
      return (value.data ?? []).map((entry: any) =>
        normalize(entry, `strapi:${base.host}:${entry.id}`, options.map),
      );
    },
  };
}
