import {
  type DocumentChild,
  getTextOfChild,
  getTextOfRange,
  parseDocument,
} from "@lib/documents.ts";
import { parse, stringify } from "yaml";
import { createCrud } from "@lib/crud.ts";
import { extractHashTags } from "@lib/string.ts";
import { Ingredient, IngredientGroup } from "@lib/recipeSchema.ts";
import { fixRenderedMarkdown } from "@lib/helpers.ts";
import { parseIngredient } from "@lib/parseIngredient.ts";

export type Recipe = {
  type: "recipe";
  id: string;
  name: string;
  description?: string;
  markdown?: string;
  ingredients: (Ingredient | IngredientGroup)[];
  instructions?: string[];
  notes?: string[];
  tags: string[];
  meta?: {
    time?: string;
    link?: string;
    image?: string;
    rating?: number;
    portion?: number;
    author?: string;
    average?: string;
    thumbnail?: string;
  };
};

function parseIngredientItem(listItem: DocumentChild): Ingredient | undefined {
  if (listItem.type === "listItem") {
    const children: DocumentChild[] = listItem.children[0]?.children ||
      listItem.children;

    const text = children.map((c) => getTextOfChild(c)).join(" ").trim();

    return parseIngredient(text);
  }
}

const isIngredient = (item: Ingredient | undefined): item is Ingredient => {
  return !!item;
};

function parseIngredientsList(list: DocumentChild): Ingredient[] {
  if (list.type === "list" && "children" in list) {
    return list.children.map((listItem) => {
      return parseIngredientItem(listItem);
    }).filter(isIngredient);
  }
  return [];
}

function parseIngredients(children: DocumentChild[]): Recipe["ingredients"] {
  const ingredients: (Ingredient | IngredientGroup)[] = [];
  if (!children) return [];
  let skip = false;
  for (let i = 0; i < children.length; i++) {
    if (skip) {
      skip = false;
      continue;
    }
    const child = children[i];

    if (child.type === "paragraph") {
      const nextChild = children[i + 1];

      if (!nextChild || nextChild.type !== "list") continue;

      const name = getTextOfChild(child);
      ingredients.push({
        name: name || "",
        items: parseIngredientsList(nextChild),
      });
      skip = true;
      continue;
    }

    if (child.type === "list") {
      ingredients.push(...parseIngredientsList(child));
    }
  }

  return ingredients;
}

function extractSteps(
  content: string,
  seperator: RegExp = /\n(?=\d+\.)/g,
): string[] {
  const steps = content.split(seperator).map((step) => {
    const match = step.match(/^(\d+)\.\s*(.*)/);
    if (!match) return;
    const [, , text] = match;
    return text;
  }).filter((step) => !!step);
  return steps as string[];
}

export function parseRecipe(original: string, id: string): Recipe {
  const doc = parseDocument(original);

  let name = "";
  let meta: Recipe["meta"] = {};

  const groups: DocumentChild[][] = [];
  let group: DocumentChild[] = [];
  for (const child of doc.children) {
    if (child.type === "yaml") {
      try {
        meta = parse(child.value) as Recipe["meta"];
      } catch (err) {
        console.log("Error parsing YAML", err);
      }
      continue;
    }
    if (
      child.type === "heading" && child.depth === 1 && !name &&
      child.children.length === 1 && child.children[0].type === "text"
    ) {
      name = child.children[0].value;
      continue;
    }
    if (child.type === "thematicBreak") {
      groups.push(group);
      group = [];
      continue;
    }
    group.push(child);
  }

  if (group.length) {
    groups.push(group);
  }

  let description = getTextOfRange(groups[0], original);

  const ingredients = parseIngredients(groups[1]);

  const instructionText = getTextOfRange(groups[2], original);
  let instructions = extractSteps(instructionText || "");
  if (instructions.length <= 1) {
    const d = extractSteps(instructionText || "", /\n/g);
    if (d.length > instructions.length) {
      instructions = d;
    }
  }

  const tags = extractHashTags(description || "");
  if (description) {
    for (const tag of tags) {
      description = description.replace("#" + tag, "");
    }
  }

  return {
    type: "recipe",
    id,
    meta,
    name,
    tags,
    markdown: original,
    notes: getTextOfRange(groups[3], original)?.split("\n"),
    description,
    ingredients,
    instructions,
  };
}

function filterUndefinedFromObject<T extends { [key: string]: unknown }>(
  obj: T,
) {
  return Object.fromEntries(
    Object.entries(obj).filter(([_, v]) => v !== undefined),
  );
}

export function renderRecipe(recipe: Recipe) {
  const meta = filterUndefinedFromObject(recipe.meta || {});

  // Clean up meta properties
  delete meta.thumbnail;
  delete meta.average;

  const recipeImage = meta.image ? `![](${meta.image})` : "";

  // Format ingredient groups and standalone ingredients
  const ingredients = recipe.ingredients
    .map((item) => {
      if ("items" in item) {
        return `\n*${item.name}*\n${
          item.items
            .map((ing) => {
              if (ing.quantity && ing.unit) {
                return `- **${ing.quantity.trim() || ""}${
                  ing.unit.trim() || ""
                }** ${ing.name}`;
              }
              return `- ${ing.name}`;
            })
            .join("\n")
        }`;
      }
      if (item.quantity && item.unit) {
        return `- **${item.quantity?.trim() || ""}${
          item.unit?.trim() || ""
        }** ${item.name}`;
      }
      return `- ${item.name}`;
    })
    .join("\n");

  // Format instructions as a numbered list
  const instructions = recipe.instructions
    ? recipe.instructions.map((step, i) => `${i + 1}. ${step}`).join("\n")
    : "";

  // Render the final markdown
  return fixRenderedMarkdown(`${
    Object.keys(meta).length
      ? `---
${stringify(meta)}
---`
      : `---
---`
  }
# ${recipe.name}
${recipe.meta?.image ? recipeImage : ""}
${recipe.tags.map((t) => `#${t.replaceAll(" ", "-")}`).join(" ")}
${recipe.description || ""}

---

${ingredients ? `## Ingredients\n\n${ingredients}\n\n---\n` : ""}
${instructions ? `${instructions}\n\n---` : ""}
${recipe.notes?.length ? `\n${recipe.notes.join("\n")}` : ""}
`);
}

const crud = createCrud<Recipe>({
  prefix: `Recipes/`,
  parse: parseRecipe,
  render: renderRecipe,
  hasThumbnails: true,
});

export const getAllRecipes = crud.readAll;
export const getRecipe = crud.read;
export const updateRecipe = crud.update;
export const createRecipe = crud.create;
