import fs from "fs";
import matter from "gray-matter";
import { serialize } from "next-mdx-remote/serialize";
import path from "path";

import type {
  MDXContent,
  MDXFrontMatter,
  MDXPage,
  MDXPageFrontMatter,
  MDXRawContent,
  MDXSection,
  MDXSectionFrontMatter,
} from "@/types";

interface MDXFetchOptions {
  includeDrafts?: boolean;
}

interface MDXPageFetchOptions extends MDXFetchOptions {
  by?: "filename" | "slug";
}

const defaultMDXFetchOptions: MDXFetchOptions = { includeDrafts: false };

const defaultMDXPageFetchOptions: MDXPageFetchOptions = {
  ...defaultMDXFetchOptions,
  by: "filename",
};

const root = process.cwd();
export const contentPath = path.join(root, "content");
export const sectionsPath = path.join(contentPath, "sections");

const ensureMDXExtension = (fileName: string) => {
  if (fileName.endsWith(".mdx") || fileName.endsWith(".md")) {
    return fileName;
  }
  return `${fileName}.mdx`;
};

const readMDXFilesFromDir = (dirPath: string) => {
  return fs
    .readdirSync(dirPath)
    .filter((item) => item.endsWith(".mdx") || item.endsWith(".md"));
};

const createPageFrontMatter = (
  data: MDXPageFrontMatter,
  id: string,
): MDXPageFrontMatter => {
  return {
    ...data,
    id,
    slug: data.slug || id,
    title: data.title || id,
  };
};

const getMDXContent = <T extends MDXFrontMatter>(
  basePath: string,
  fileName: string,
  options: MDXFetchOptions = defaultMDXFetchOptions,
): MDXRawContent<T> | null => {
  const fileNameWithExtension = ensureMDXExtension(fileName);
  const fullPath = path.join(basePath, fileNameWithExtension);

  if (!fs.existsSync(fullPath)) {
    console.warn(`Warning: Markdown/MDX file not found at ${fullPath}`);
    return null;
  }

  try {
    const file = fs.readFileSync(fullPath, "utf-8");
    const { data, content } = matter(file);
    const id = fileNameWithExtension.replace(/\.(mdx|md)$/, "");

    let frontMatter: MDXFrontMatter;

    if (basePath === contentPath) {
      frontMatter = createPageFrontMatter(data as MDXPageFrontMatter, id);
    } else {
      frontMatter = { id, ...data };
    }

    if (frontMatter.draft && options.includeDrafts === false) {
      return null;
    }

    return {
      frontMatter: frontMatter as T,
      content,
    };
  } catch (error) {
    console.warn(`Error reading Markdown/MDX file at ${fullPath}:`, error);
    return null;
  }
};

export const serializeMDXContent = async <T extends MDXFrontMatter>(
  mdxContentObj: MDXRawContent<T> | null,
): Promise<MDXContent<T> | null> => {
  if (!mdxContentObj) return null;

  const mdxSource = await serialize(mdxContentObj.content);
  return {
    frontMatter: mdxContentObj.frontMatter,
    mdxSource,
  };
};

/**
 * Pages
 * ===================================================================
 */

export const getMDXPage = (
  identifier: string,
  options: MDXPageFetchOptions = defaultMDXPageFetchOptions,
): MDXRawContent<MDXPageFrontMatter> | null => {
  const { by, includeDrafts } = options;

  switch (by) {
    case "filename": {
      const rawContent = getMDXContent<MDXPageFrontMatter>(
        contentPath,
        identifier,
        { includeDrafts },
      );
      if (!rawContent) return null;
      return rawContent;
    }
    case "slug": {
      const mdxFiles = readMDXFilesFromDir(contentPath);

      for (const mdxFile of mdxFiles) {
        const rawContent = getMDXContent<MDXPageFrontMatter>(
          contentPath,
          mdxFile,
          { includeDrafts },
        );
        if (!rawContent) continue;

        if (rawContent.frontMatter.slug === identifier) {
          return rawContent;
        }
      }

      return null;
    }
    default:
      return null;
  }
};

export const getSerializedMDXPage = async (
  identifier: string,
  options: MDXPageFetchOptions = defaultMDXPageFetchOptions,
): Promise<MDXPage | null> => {
  const rawContent = getMDXPage(identifier, options);
  if (!rawContent) return null;
  return await serializeMDXContent<MDXPageFrontMatter>(rawContent);
};

export const getAllMDXPages = (
  options: MDXFetchOptions = defaultMDXFetchOptions,
) => {
  return readMDXFilesFromDir(contentPath)
    .map((item) => getMDXPage(item, { by: "filename", ...options }))
    .filter((page): page is MDXRawContent<MDXPageFrontMatter> => page !== null);
};

export const getAllSerializedMDXPages = async (
  options: MDXFetchOptions = defaultMDXFetchOptions,
) => {
  const allRawContent = getAllMDXPages(options);
  const serializedPages = await Promise.all(
    allRawContent.map(async (rawContent) => {
      return await serializeMDXContent<MDXPageFrontMatter>(rawContent);
    }),
  );
  return serializedPages.filter((page): page is MDXPage => page !== null);
};

export const getAllMDXPagesSlugs = (
  options: MDXFetchOptions = defaultMDXFetchOptions,
) => {
  return getAllMDXPages(options).map((page) => page.frontMatter.slug);
};

/**
 * Sections
 * ===================================================================
 */

const getMDXSection = (fileName: string, options = defaultMDXFetchOptions) => {
  return getMDXContent<MDXSectionFrontMatter>(sectionsPath, fileName, options);
};

export const getSerializedMDXSection = async (
  fileName: string,
  options = defaultMDXFetchOptions,
): Promise<MDXSection | null> => {
  const rawContent = getMDXSection(fileName, options);
  if (!rawContent) return null;
  return await serializeMDXContent<MDXSectionFrontMatter>(rawContent);
};

export const getAllMDXSections = (options = defaultMDXFetchOptions) => {
  return readMDXFilesFromDir(sectionsPath)
    .map((item) => getMDXSection(item, options))
    .filter(
      (section): section is MDXRawContent<MDXSectionFrontMatter> =>
        section !== null,
    );
};

export const getAllSerializedMDXSections = async (
  options = defaultMDXFetchOptions,
): Promise<MDXSection[]> => {
  const allRawContent = getAllMDXSections(options);
  const serializedSections = await Promise.all(
    allRawContent.map(async (rawContent) => {
      return await serializeMDXContent<MDXSectionFrontMatter>(rawContent);
    }),
  );
  return serializedSections.filter(
    (section): section is MDXSection => section !== null,
  );
};
