import { readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import type { Locale } from "@/content/site";

export type ArticleFrontmatter = {
  title: string;
  slug: string;
  description: string;
  excerpt: string;
  category: string;
  tags: string[];
  coverImage: string;
  coverImageAlt: string;
  author: string;
  publishedAt: string;
  updatedAt?: string;
  readingTime: string;
  featured: boolean;
  draft: boolean;
  locale: Locale;
  seoTitle: string;
  seoDescription: string;
  translationKey: string;
};

export type MarkdownBlock =
  | { type: "heading"; level: 2 | 3; text: string; id: string }
  | { type: "paragraph"; text: string }
  | { type: "list"; ordered: boolean; items: string[] }
  | { type: "blockquote"; text: string }
  | { type: "image"; src: string; alt: string }
  | { type: "table"; headers: string[]; rows: string[][] };

export type ArticleSummary = ArticleFrontmatter;

export type LibraryArticle = ArticleFrontmatter & {
  blocks: MarkdownBlock[];
  tableOfContents: { id: string; text: string; level: 2 | 3 }[];
};

const contentRoot = path.join(process.cwd(), "content", "library");

function parseScalar(value: string): unknown {
  const trimmed = value.trim();
  if (trimmed === "true") return true;
  if (trimmed === "false") return false;
  if (trimmed === "") return undefined;
  if (trimmed.startsWith("[") || trimmed.startsWith('"')) return JSON.parse(trimmed);
  return trimmed;
}

function parseDocument(filePath: string) {
  const source = readFileSync(filePath, "utf8").replace(/\r\n/g, "\n");
  const match = source.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
  if (!match) throw new Error(`Invalid article frontmatter: ${filePath}`);

  const frontmatter = Object.fromEntries(
    match[1]
      .split("\n")
      .filter((line) => line.trim() && !line.trimStart().startsWith("#"))
      .map((line) => {
        const separator = line.indexOf(":");
        if (separator < 1) throw new Error(`Invalid frontmatter line in ${filePath}: ${line}`);
        return [line.slice(0, separator).trim(), parseScalar(line.slice(separator + 1))];
      }),
  );

  return { frontmatter, body: match[2].trim() };
}

function assertFrontmatter(
  value: Record<string, unknown>,
  locale: Locale,
  filePath: string,
): ArticleFrontmatter {
  const requiredStrings = [
    "title",
    "slug",
    "description",
    "excerpt",
    "category",
    "coverImage",
    "coverImageAlt",
    "author",
    "publishedAt",
    "readingTime",
    "seoTitle",
    "seoDescription",
    "translationKey",
  ] as const;
  for (const field of requiredStrings) {
    if (typeof value[field] !== "string" || !value[field]) {
      throw new Error(`Missing ${field} in ${filePath}`);
    }
  }
  if (!Array.isArray(value.tags) || !value.tags.every((tag) => typeof tag === "string")) {
    throw new Error(`Invalid tags in ${filePath}`);
  }
  if (typeof value.featured !== "boolean" || typeof value.draft !== "boolean") {
    throw new Error(`Invalid publication flags in ${filePath}`);
  }
  if (value.locale !== locale) throw new Error(`Locale mismatch in ${filePath}`);
  const fileSlug = path.basename(filePath, ".md");
  if (value.slug !== fileSlug) throw new Error(`Slug and filename mismatch in ${filePath}`);

  return value as ArticleFrontmatter;
}

function slugifyHeading(value: string) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLocaleLowerCase()
    .replace(/[^a-z0-9\u0600-\u06ff]+/g, "-")
    .replace(/^-|-$/g, "");
}

function parseMarkdown(body: string): MarkdownBlock[] {
  const lines = body.split("\n");
  const blocks: MarkdownBlock[] = [];
  const usedIds = new Map<string, number>();
  let index = 0;

  while (index < lines.length) {
    const line = lines[index].trim();
    if (!line) {
      index += 1;
      continue;
    }

    const heading = line.match(/^(##|###)\s+(.+)$/);
    if (heading) {
      const baseId = slugifyHeading(heading[2]) || `section-${index + 1}`;
      const count = usedIds.get(baseId) ?? 0;
      usedIds.set(baseId, count + 1);
      blocks.push({
        type: "heading",
        level: heading[1] === "##" ? 2 : 3,
        text: heading[2],
        id: count ? `${baseId}-${count + 1}` : baseId,
      });
      index += 1;
      continue;
    }

    const image = line.match(/^!\[([^\]]*)\]\(([^)]+)\)$/);
    if (image) {
      blocks.push({ type: "image", alt: image[1], src: image[2] });
      index += 1;
      continue;
    }

    if (line.startsWith("> ")) {
      const quote: string[] = [];
      while (index < lines.length && lines[index].trim().startsWith("> ")) {
        quote.push(lines[index].trim().slice(2));
        index += 1;
      }
      blocks.push({ type: "blockquote", text: quote.join(" ") });
      continue;
    }

    const unordered = /^[-*]\s+/.test(line);
    const ordered = /^\d+\.\s+/.test(line);
    if (unordered || ordered) {
      const items: string[] = [];
      const pattern = ordered ? /^\d+\.\s+/ : /^[-*]\s+/;
      while (index < lines.length && pattern.test(lines[index].trim())) {
        items.push(lines[index].trim().replace(pattern, ""));
        index += 1;
      }
      blocks.push({ type: "list", ordered, items });
      continue;
    }

    if (
      line.includes("|") &&
      index + 1 < lines.length &&
      /^\|?\s*:?-{3,}/.test(lines[index + 1].trim())
    ) {
      const splitRow = (row: string) =>
        row.replace(/^\||\|$/g, "").split("|").map((cell) => cell.trim());
      const headers = splitRow(line);
      index += 2;
      const rows: string[][] = [];
      while (index < lines.length && lines[index].trim().includes("|")) {
        rows.push(splitRow(lines[index].trim()));
        index += 1;
      }
      blocks.push({ type: "table", headers, rows });
      continue;
    }

    const paragraph = [line];
    index += 1;
    while (index < lines.length) {
      const next = lines[index].trim();
      if (
        !next ||
        /^(##|###)\s+/.test(next) ||
        /^!\[/.test(next) ||
        /^>\s+/.test(next) ||
        /^[-*]\s+/.test(next) ||
        /^\d+\.\s+/.test(next) ||
        (next.includes("|") && index + 1 < lines.length && /^\|?\s*:?-{3,}/.test(lines[index + 1].trim()))
      ) break;
      paragraph.push(next);
      index += 1;
    }
    blocks.push({ type: "paragraph", text: paragraph.join(" ") });
  }

  return blocks;
}

function readLocaleFiles(locale: Locale) {
  const directory = path.join(contentRoot, locale);
  return readdirSync(directory)
    .filter((file) => file.endsWith(".md") && !file.startsWith("README"))
    .map((file) => path.join(directory, file));
}

export function getArticleSummaries(locale: Locale): ArticleSummary[] {
  return readLocaleFiles(locale)
    .map((filePath) => {
      const { frontmatter } = parseDocument(filePath);
      return assertFrontmatter(frontmatter, locale, filePath);
    })
    .filter((article) => !article.draft)
    .sort((a, b) => b.publishedAt.localeCompare(a.publishedAt));
}

export function getArticle(locale: Locale, slug: string): LibraryArticle | undefined {
  const filePath = readLocaleFiles(locale).find((candidate) => path.basename(candidate, ".md") === slug);
  if (!filePath) return undefined;
  const { frontmatter, body } = parseDocument(filePath);
  const metadata = assertFrontmatter(frontmatter, locale, filePath);
  if (metadata.draft) return undefined;
  const blocks = parseMarkdown(body);
  return {
    ...metadata,
    blocks,
    tableOfContents: blocks
      .filter((block): block is Extract<MarkdownBlock, { type: "heading" }> => block.type === "heading")
      .map(({ id, text, level }) => ({ id, text, level })),
  };
}

export function getArticleTranslations(translationKey: string) {
  return (["fr", "en", "ar"] as const)
    .flatMap((locale) => getArticleSummaries(locale).filter((article) => article.translationKey === translationKey))
    .map((article) => ({ locale: article.locale, slug: article.slug }));
}

export function getRelatedArticles(article: LibraryArticle, limit = 3) {
  return getArticleSummaries(article.locale)
    .filter((candidate) => candidate.slug !== article.slug)
    .map((candidate) => ({
      candidate,
      score:
        (candidate.category === article.category ? 3 : 0) +
        candidate.tags.filter((tag) => article.tags.includes(tag)).length,
    }))
    .sort((a, b) => b.score - a.score || b.candidate.publishedAt.localeCompare(a.candidate.publishedAt))
    .slice(0, limit)
    .map(({ candidate }) => candidate);
}
