import { readFile, readdir, stat } from "node:fs/promises";
import { basename, extname, join, relative, resolve, sep } from "node:path";

const videoExtensions = new Set([".mov", ".mp4", ".m4v", ".mkv", ".webm"]);
const imageExtensions = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic", ".heif", ".tif", ".tiff"]);
const textExtensions = new Set([".md", ".txt", ".srt", ".vtt"]);

function portablePath(path) {
  return path.split(sep).join("/");
}

function slugify(value) {
  return value
    .normalize("NFD")
    .replace(/[\u0300-\u036f]/g, "")
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-|-$/g, "");
}

function stripMarkdown(value) {
  return value
    .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
    .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
    .replace(/[`*_>#]/g, "")
    .replace(/\s+/g, " ")
    .trim();
}

function section(markdown, names) {
  const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
  const match = markdown.match(new RegExp(`^##\\s+(?:${escaped})\\s*$([\\s\\S]*?)(?=^##\\s+|(?![\\s\\S]))`, "im"));
  return match?.[1]?.trim() || "";
}

function listFromSection(markdown, names) {
  return section(markdown, names)
    .split("\n")
    .filter((line) => /^\s*-\s+/.test(line))
    .map((line) => stripMarkdown(line.replace(/^\s*-\s+/, "")))
    .filter(Boolean);
}

function paragraphAfterTitle(markdown) {
  const afterTitle = markdown.replace(/^#\s+.*$/m, "").trim();
  return stripMarkdown(afterTitle.split(/\n\s*\n/).find((block) => block.trim() && !block.trim().startsWith("#")) || "");
}

function fact(markdown, labels) {
  const overview = section(markdown, ["At a glance", "Resumen"]);
  for (const label of labels) {
    const match = overview.match(new RegExp(`^-\\s+\\*\\*${label}:?\\*\\*\\s*(.+)$`, "im"));
    if (match) return stripMarkdown(match[1]);
  }
  return null;
}

function instructionSteps(markdown) {
  const instructions = section(markdown, ["Instructions", "Instrucciones", "Sensory workflow", "Flujo sensorial"]);
  const matches = [...instructions.matchAll(/^###\s+(?:\d+\.?\s*)?(.+?)\s*$([\s\S]*?)(?=^###\s+|(?![\s\S]))/gm)];
  return matches.map((match) => {
    const image = match[2].match(/!\[([^\]]*)\]\(([^)]+)\)/);
    const paragraphs = match[2]
      .split(/\n\s*\n/)
      .map(stripMarkdown)
      .filter((value) => value && !/^\|/.test(value));
    return {
      title: stripMarkdown(match[1]),
      body: paragraphs.join(" "),
      image: image ? { alt: image[1].trim(), path: decodeURIComponent(image[2]) } : null,
    };
  });
}

function variantLabel(filename, language) {
  const base = basename(filename, extname(filename))
    .replace(/\s*-\s*(Guide|Guía)\s*\([^)]*\)$/i, "")
    .replace(/\s*\(V\d+\)$/i, "")
    .trim();
  return `${base} · ${language}`;
}

function categoryFor(folder) {
  if (/stock|broth/i.test(folder)) return "Stocks & broths";
  if (/mirepoix/i.test(folder)) return "Flavor foundations";
  if (/gravy|sauce/i.test(folder)) return "Sauces";
  return "Recipes";
}

async function findFirst(root, candidates) {
  for (const candidate of candidates) {
    try {
      if ((await stat(join(root, candidate))).isFile()) return portablePath(candidate);
    } catch {
      // Try the next conventional file.
    }
  }
  return null;
}

async function recipeFromFolder(cookingRoot, folderPath, folderName) {
  const entries = await readdir(folderPath, { withFileTypes: true });
  const guides = entries
    .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md") && entry.name.toLowerCase() !== "readme.md")
    .sort((a, b) => a.name.localeCompare(b.name));
  if (!guides.length) return null;
  const rootRelative = portablePath(relative(cookingRoot, folderPath));

  const variants = await Promise.all(guides.map(async (entry) => {
    const markdown = await readFile(join(folderPath, entry.name), "utf8");
    const language = /español|guía|gravy de pollo|caldo de pollo|señales sensoriales/i.test(entry.name) ? "Español" : "English";
    const title = stripMarkdown(markdown.match(/^#\s+(.+)$/m)?.[1] || folderName);
    return {
      id: slugify(basename(entry.name, ".md")),
      label: variantLabel(entry.name, language),
      language,
      title,
      description: paragraphAfterTitle(markdown),
      ingredients: listFromSection(markdown, ["Ingredients", "Ingredientes"]),
      equipment: listFromSection(markdown, ["Equipment", "Equipo"]),
      steps: instructionSteps(markdown).map((step) => ({
        title: step.title,
        body: step.body,
        imagePath: step.image ? portablePath(`${rootRelative}/${step.image.path}`) : null,
        imageAlt: step.image?.alt || null,
      })),
      sourcePath: portablePath(relative(cookingRoot, join(folderPath, entry.name))),
    };
  }));

  const canonicalSlug = slugify(folderName);
  variants.sort((a, b) => {
    const priority = (variant) =>
      Number(variant.language !== "English") * 100
      + Number(!variant.id.startsWith(canonicalSlug)) * 10
      + Number(/sensory|sensorial|senales/.test(variant.id)) * 2;
    return priority(a) - priority(b) || a.label.localeCompare(b.label);
  });
  const primary = variants[0];
  const primaryMarkdown = await readFile(join(cookingRoot, primary.sourcePath), "utf8");
  const files = entries.filter((entry) => entry.isFile());
  const video = files.find((entry) => videoExtensions.has(extname(entry.name).toLowerCase()));
  const image = await findFirst(folderPath, ["Media/Video Thumbnail.jpg", "Media/Video Thumbnail.png"]);
  const caption = await findFirst(folderPath, [
    "Source Files/Captions - English.srt",
    "Source Files/Captions - English Original.srt",
  ]);

  return {
    slug: slugify(folderName),
    title: primary.title,
    eyebrow: categoryFor(folderName),
    description: primary.description,
    category: categoryFor(folderName),
    image: image ? `${rootRelative}/${image}` : null,
    imageAlt: `${folderName} video thumbnail`,
    time: fact(primaryMarkdown, ["Total time", "Tiempo total", "Active gravy time", "Tiempo activo para la salsa"]) || "See guide",
    difficulty: "Guided",
    yield: fact(primaryMarkdown, ["Yield", "Rendimiento", "Gravy yield", "Rendimiento de la salsa"]) || "See guide",
    tags: [categoryFor(folderName), ...new Set(variants.map((variant) => variant.language))],
    sourceVariantCount: variants.length,
    defaultVariantId: primary.id,
    variants,
    storage: null,
    relatedSlugs: [],
    media: {
      videoPath: video ? `${rootRelative}/${video.name}` : null,
      posterPath: image ? `${rootRelative}/${image}` : null,
      captionPath: caption ? `${rootRelative}/${caption}` : null,
    },
  };
}

async function walkFiles(root, directory = root) {
  const entries = await readdir(directory, { withFileTypes: true });
  const files = [];
  for (const entry of entries) {
    if (entry.name.startsWith(".")) continue;
    const path = join(directory, entry.name);
    if (entry.isDirectory()) files.push(...(await walkFiles(root, path)));
    else if (entry.isFile()) {
      const fileStat = await stat(path);
      const extension = extname(entry.name).toLowerCase();
      files.push({
        path: portablePath(relative(root, path)),
        name: entry.name,
        extension,
        kind: videoExtensions.has(extension)
          ? "video"
          : imageExtensions.has(extension)
            ? "image"
            : extension === ".pdf"
              ? "pdf"
              : textExtensions.has(extension)
                ? "text"
                : "metadata",
        size: fileStat.size,
        modifiedAt: fileStat.mtime.toISOString(),
      });
    }
  }
  return files;
}

export async function buildCookingCatalog(cookingDirectory, sourceName = "Mac") {
  const root = resolve(cookingDirectory);
  const recipesRoot = join(root, "Recipes");
  const recipeEntries = await readdir(recipesRoot, { withFileTypes: true });
  const recipes = (await Promise.all(
    recipeEntries
      .filter((entry) => entry.isDirectory())
      .map((entry) => recipeFromFolder(root, join(recipesRoot, entry.name), entry.name)),
  )).filter(Boolean);
  const recipeOrder = new Map([["chicken-stock", 0], ["mirepoix", 1], ["chicken-gravy", 2]]);
  recipes.sort((a, b) => (recipeOrder.get(a.slug) ?? 99) - (recipeOrder.get(b.slug) ?? 99) || a.title.localeCompare(b.title));
  for (const recipe of recipes) {
    recipe.relatedSlugs = recipes.filter((candidate) => candidate.slug !== recipe.slug).map((candidate) => candidate.slug);
  }

  const files = await walkFiles(root);
  return {
    source: {
      name: sourceName,
      scannedAt: new Date().toISOString(),
      directoryName: basename(root),
    },
    stats: {
      files: files.length,
      bytes: files.reduce((total, file) => total + file.size, 0),
      recipes: recipes.length,
      variants: recipes.reduce((total, recipe) => total + recipe.variants.length, 0),
      videos: files.filter((file) => file.kind === "video").length,
    },
    recipes,
    files,
  };
}

export function resolveCookingFile(cookingDirectory, requestedPath) {
  const root = resolve(cookingDirectory);
  const candidate = resolve(root, requestedPath || "");
  if (candidate === root || !candidate.startsWith(`${root}${sep}`)) return null;
  return candidate;
}

export function srtToVtt(source) {
  return `WEBVTT\n\n${source.replace(/^\uFEFF/, "").replace(/(\d{2}:\d{2}:\d{2}),(\d{3})/g, "$1.$2")}`;
}
