import { existsSync, readFileSync, readdirSync } from "node:fs";
import { extname, join, relative, resolve } from "node:path";

export interface ContentSchema<T> {
  parse(input: unknown): T;
}
export interface ContentEntry<T = Record<string, unknown>> {
  id: string;
  slug: string;
  collection: string;
  data: T;
  body: string;
  html: string;
  excerpt: string;
  headings: ContentHeading[];
  draft: boolean;
  version?: string;
  source: string;
}
export interface ContentHeading {
  depth: number;
  text: string;
  slug: string;
}
export interface ContentLoaderResult {
  id: string;
  source: string;
  content: string;
}
export interface ContentLoader {
  load(): ContentLoaderResult[] | Promise<ContentLoaderResult[]>;
}
export interface ContentCollectionOptions<T> {
  name: string;
  schema: ContentSchema<T>;
  loader: ContentLoader;
  includeDrafts?: boolean;
  previewToken?: string;
  references?: Record<string, ContentCollection<unknown>>;
}
export interface ContentCollection<T> {
  name: string;
  load(options?: {
    drafts?: boolean;
    previewToken?: string;
    version?: string;
  }): Promise<ContentEntry<T>[]>;
  get(
    id: string,
    options?: { drafts?: boolean; previewToken?: string; version?: string },
  ): Promise<ContentEntry<T> | null>;
}

function scalar(value: string): unknown {
  const text = value.trim();
  if (/^(true|false)$/i.test(text)) return text.toLowerCase() === "true";
  if (/^-?\d+(?:\.\d+)?$/.test(text)) return Number(text);
  if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")))
    return text.slice(1, -1);
  if (text.startsWith("[") && text.endsWith("]"))
    return text
      .slice(1, -1)
      .split(",")
      .map((item) => scalar(item));
  return text;
}
export function parseFrontmatter(source: string): { data: Record<string, unknown>; body: string } {
  if (!source.startsWith("---\n") && !source.startsWith("---\r\n"))
    return { data: {}, body: source };
  const normalized = source.replace(/\r\n/g, "\n");
  const end = normalized.indexOf("\n---\n", 4);
  if (end < 0) throw new Error("WRN-CONTENT-FRONTMATTER: closing delimiter is missing.");
  const data: Record<string, unknown> = {};
  for (const line of normalized.slice(4, end).split("\n")) {
    if (!line.trim() || line.trimStart().startsWith("#")) continue;
    const match = /^([A-Za-z_$][\w$.-]*):\s*(.*)$/.exec(line);
    if (!match) throw new Error(`WRN-CONTENT-FRONTMATTER: invalid line '${line}'.`);
    data[match[1]!] = scalar(match[2]!);
  }
  return { data, body: normalized.slice(end + 5) };
}
const escape = (value: string) =>
  value.replace(
    /[&<>"']/g,
    (character) =>
      ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[character]!,
  );
const slugify = (value: string) =>
  value
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
export function renderMarkdown(source: string): {
  html: string;
  headings: ContentHeading[];
  excerpt: string;
} {
  const headings: ContentHeading[] = [];
  const lines = source.replace(/\r\n/g, "\n").split("\n");
  const output: string[] = [];
  let code: string[] | null = null;
  let language = "";
  for (const line of lines) {
    const fence = /^```([\w-]*)/.exec(line);
    if (fence) {
      if (code) {
        output.push(
          `<pre><code class="language-${escape(language)}">${escape(code.join("\n"))}</code></pre>`,
        );
        code = null;
      } else {
        code = [];
        language = fence[1] ?? "";
      }
      continue;
    }
    if (code) {
      code.push(line);
      continue;
    }
    const heading = /^(#{1,6})\s+(.+)$/.exec(line);
    if (heading) {
      const text = heading[2]!.trim();
      const item = { depth: heading[1]!.length, text, slug: slugify(text) };
      headings.push(item);
      output.push(`<h${item.depth} id="${escape(item.slug)}">${escape(text)}</h${item.depth}>`);
    } else if (line.trim())
      output.push(
        `<p>${escape(line.trim()).replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+|\/[^\s)]*)\)/g, '<a href="$2">$1</a>')}</p>`,
      );
  }
  if (code) throw new Error("WRN-CONTENT-MARKDOWN: code fence is not closed.");
  const excerpt =
    lines
      .find((line) => line.trim() && !line.startsWith("#") && !line.startsWith("---"))
      ?.trim()
      .slice(0, 240) ?? "";
  return { html: output.join("\n"), headings, excerpt };
}

export function localContentLoader(directory: string): ContentLoader {
  const root = resolve(directory);
  return {
    load() {
      if (!existsSync(root)) return [];
      const values: ContentLoaderResult[] = [];
      const walk = (current: string) => {
        for (const entry of readdirSync(current, { withFileTypes: true })) {
          if (entry.name.startsWith(".")) continue;
          const path = join(current, entry.name);
          if (entry.isDirectory()) walk(path);
          else if ([".md", ".mdx"].includes(extname(entry.name)))
            values.push({
              id: relative(root, path)
                .replace(/\\/g, "/")
                .replace(/\.mdx?$/, ""),
              source: path,
              content: readFileSync(path, "utf8"),
            });
        }
      };
      walk(root);
      return values.sort((left, right) => left.id.localeCompare(right.id));
    },
  };
}
export function remoteContentLoader(
  url: string,
  options: {
    fetch?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
    headers?: HeadersInit;
  } = {},
): ContentLoader {
  return {
    async load() {
      const response = await (options.fetch ?? globalThis.fetch)(url, { headers: options.headers });
      if (!response.ok) throw new Error(`WRN-CONTENT-REMOTE: ${response.status}`);
      const value = (await response.json()) as Array<{
        id: string;
        content: string;
        source?: string;
      }>;
      if (!Array.isArray(value)) throw new TypeError("WRN-CONTENT-REMOTE: expected an array.");
      return value.map((entry) => ({
        id: entry.id,
        content: entry.content,
        source: entry.source ?? url,
      }));
    },
  };
}
export function defineCollection<T>(options: ContentCollectionOptions<T>): ContentCollection<T> {
  const parse = (raw: ContentLoaderResult): ContentEntry<T> => {
    const parsed = parseFrontmatter(raw.content);
    const rendered = renderMarkdown(parsed.body);
    const data = options.schema.parse(parsed.data);
    const record = data as Record<string, unknown>;
    return {
      id: raw.id,
      slug: String(record.slug ?? raw.id),
      collection: options.name,
      data,
      body: parsed.body,
      ...rendered,
      draft: record.draft === true,
      version: typeof record.version === "string" ? record.version : undefined,
      source: raw.source,
    };
  };
  const load = async (
    filter: { drafts?: boolean; previewToken?: string; version?: string } = {},
  ) => {
    const preview = Boolean(options.previewToken && filter.previewToken === options.previewToken);
    const drafts = options.includeDrafts || filter.drafts || preview;
    return (await options.loader.load())
      .map(parse)
      .filter(
        (entry) =>
          (drafts || !entry.draft) && (!filter.version || entry.version === filter.version),
      );
  };
  return {
    name: options.name,
    load,
    async get(id, filter) {
      return (await load(filter)).find((entry) => entry.id === id || entry.slug === id) ?? null;
    },
  };
}
export function resolveContentReference(
  collections: Record<string, ContentCollection<unknown>>,
  reference: string,
) {
  const [collection, id] = reference.split(":", 2);
  if (!collection || !id || !collections[collection])
    throw new Error(`WRN-CONTENT-REFERENCE: '${reference}' is invalid.`);
  return collections[collection].get(id);
}
export function paginateContent<T>(entries: T[], page = 1, pageSize = 10) {
  if (!Number.isInteger(page) || page < 1 || !Number.isInteger(pageSize) || pageSize < 1)
    throw new RangeError("content pagination values must be positive integers");
  const totalPages = Math.max(1, Math.ceil(entries.length / pageSize));
  return {
    items: entries.slice((page - 1) * pageSize, page * pageSize),
    page,
    pageSize,
    total: entries.length,
    totalPages,
    hasNext: page < totalPages,
    hasPrevious: page > 1,
  };
}
export function createSearchIndex(entries: ContentEntry[]) {
  return entries.map((entry) => ({
    id: entry.id,
    slug: entry.slug,
    title: String((entry.data as Record<string, unknown>).title ?? entry.id),
    text: `${entry.excerpt} ${entry.headings.map((heading) => heading.text).join(" ")}`.toLowerCase(),
  }));
}
export function searchContent(index: ReturnType<typeof createSearchIndex>, query: string) {
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
  return index.filter((entry) =>
    terms.every((term) => `${entry.title} ${entry.text}`.toLowerCase().includes(term)),
  );
}
export function contentSitemap(entries: ContentEntry[], baseUrl: string) {
  return `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${entries.map((entry) => `<url><loc>${escape(new URL(entry.slug, baseUrl).href)}</loc></url>`).join("")}</urlset>`;
}
export function contentRss(
  entries: ContentEntry[],
  options: { title: string; baseUrl: string; description?: string },
) {
  return `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>${escape(options.title)}</title><link>${escape(options.baseUrl)}</link><description>${escape(options.description ?? options.title)}</description>${entries.map((entry) => `<item><title>${escape(String((entry.data as Record<string, unknown>).title ?? entry.id))}</title><link>${escape(new URL(entry.slug, options.baseUrl).href)}</link><guid>${escape(entry.id)}</guid><description>${escape(entry.excerpt)}</description></item>`).join("")}</channel></rss>`;
}
export interface CmsAdapter {
  list(collection: string): Promise<Array<{ id: string; content: string; source?: string }>>;
}
export function cmsContentLoader(adapter: CmsAdapter, collection: string): ContentLoader {
  return {
    async load() {
      return (await adapter.list(collection)).map((entry) => ({
        ...entry,
        source: entry.source ?? `cms:${collection}:${entry.id}`,
      }));
    },
  };
}
export {
  renderMdxComponents,
  createIncrementalHighlighter,
  contentfulAdapter,
  sanityAdapter,
  strapiAdapter,
} from "./advanced.ts";
export type { MdxComponent, SyntaxLanguageBundle, VendorAdapterOptions } from "./advanced.ts";
