import * as fs from "node:fs/promises";
import * as cheerio from "cheerio";
import { matter } from "toml-matter";
import { Marked } from "marked";
import { PurgeCSS } from "purgecss";
import log from "loglevel";

import config from "./config";
import { getPost, readPosts, type Post } from "./posts";

const css = await Bun.file("templates/style.css").text();

const marked = new Marked();
marked.use({
  gfm: true,
});

const purgeCSS = new PurgeCSS();

function addMenu(
  parent: cheerio.Cheerio<cheerio.AnyNode>,
  child: cheerio.Cheerio<cheerio.AnyNode>,
) {
  parent.empty();
  for (const link of config.menus.main) {
    parent.append(child.clone().attr("href", link.url).text(link.name));
  }
}

export async function layout(
  html: string,
  pageTitle: string,
): Promise<cheerio.CheerioAPI> {
  const ccss = (
    await purgeCSS.purge({
      content: [
        {
          raw: html,
          extension: ".html",
        },
      ],
      css: [{ raw: css }],
    })
  )[0].css;
  const $ = cheerio.load(html);
  $("html").attr("lang", config.default_language);
  $("head > link[rel=alternate]").attr("title", config.title);
  addMenu($("nav"), $("nav a"));
  $(".title").text(config.title);
  $("title").text(pageTitle);
  $(".p-name").text(pageTitle);
  $("head").children("style").text(ccss);
  return $;
}

async function render404Page(): Promise<string> {
  const $ = await layout(
    await fs.readFile("templates/404.html", "utf-8"),
    "404 Not Found",
  );
  return $.html();
}

async function renderHomepage(posts: Array<Post>): Promise<string> {
  const file = matter(await fs.readFile("content/_index.md", "utf-8"));
  const $ = await layout(
    await fs.readFile("templates/homepage.html", "utf-8"),
    config.title,
  );

  $("body").addClass("h-card");
  $(".title").addClass("p-name").addClass("u-url");
  $("#content").html(await marked.parse(file.content));
  const $feed = $(".h-feed");
  const $entry = $(".h-entry").remove();

  for (const post of posts) {
    const $post = $entry.clone();
    $post.find(".p-name").text(post.title);
    $post.find(".u-url").attr("href", post.url);
    $post
      .find(".dt-published")
      .attr("datetime", post.date.toISOString().replace(/\.\d{3}/, ""))
      .text(post.date.toISOString().slice(0, 10));
    $post.appendTo($feed);
  }

  $(".u-email").attr("href", `mailto:${config.email}`).text(config.email);
  const $elsewhere = $(".elsewhere");
  const $linkRelMe = $elsewhere.find(".u-url[rel=me]").parentsUntil("ul");
  $linkRelMe.remove();
  for (const link of config.menus.me) {
    const $link = $linkRelMe.clone();
    $link.find("a").attr("href", link.url).text(link.name);
    $link.appendTo($elsewhere);
  }
  return $.html();
}

async function renderPost(file: Post, content: string) {
  const $ = await layout(
    await fs.readFile("templates/post.html", "utf-8"),
    file.title,
  );

  $(".title").addClass("h-card p-author").attr("rel", "author");
  $(".h-entry .dt-published")
    .attr("datetime", file.date.toISOString().replace(/\.\d{3}/, ""))
    .text(file.date.toISOString().slice(0, 10));
  $(".h-entry .e-content").html(content);
  const categories = $(".h-entry .p-categories");
  const cat = categories.find(".p-category").parentsUntil(categories);
  cat.remove();
  for (const tag of file.taxonomies["tags"].sort()) {
    categories.append(
      cat
        .clone()
        .find(".p-category")
        .attr("href", `/tags/${tag}/`)
        .text(`#${tag}`)
        .parent(),
    );
  }

  return $.html();
}

async function renderListPage(tag: string, posts: Post[]) {
  const $ = await layout(
    await fs.readFile("templates/list.html", "utf-8"),
    tag || config.title,
  );
  const $feed = $(".h-feed");
  const $tpl = $(".h-entry").remove();
  $(".title").addClass("p-author h-card").attr("rel", "author");
  if (tag === "") {
    $(".filter").remove();
  } else {
    $(".filter").find("h3").text(`#${tag}`);
  }

  for (const post of posts) {
    const $post = $tpl.clone();
    $post.find(".p-name").text(post.title);
    $post.find(".u-url").attr("href", post.url);
    $post
      .find(".dt-published")
      .attr("datetime", post.date.toISOString().replace(/\.\d{3}/, ""))
      .text(post.date.toISOString().slice(0, 10));
    $post.appendTo($feed);
  }
  return $.html();
}

async function renderTags(tags: string[]) {
  const $ = await layout(
    await fs.readFile("templates/tags.html", "utf-8"),
    config.title,
  );
  const $tags = $(".tags");
  const $tpl = $(".h-feed");
  $tpl.remove();
  for (const tag of tags) {
    const $tag = $tpl.clone();
    $tag.find("a").attr("href", `/tags/${tag}/`).text(`#${tag}`);
    $tag.appendTo($tags);
  }
  return $.html();
}

const makeTagURI = (specific: string) =>
  `tag:${config.original_domain},${config.domain_start_date}:${specific}`;

async function renderFeed(title: string, posts: Post[], tag?: string) {
  const $ = cheerio.load(await fs.readFile("templates/feed.xml", "utf-8"), {
    xml: true,
  });
  const $feed = $("feed");
  $feed.children("title").text(title);
  $feed.children("link").attr("href", config.base_url);
  $feed.children("id").text(makeTagURI(tag || "feed"));
  $feed
    .children("updated")
    .text(posts[0].date.toISOString().replace(/\.\d{3}/, ""));

  const $tpl = $("feed > entry").remove();
  for (const post of posts) {
    const $post = $tpl.clone();
    $post.children("title").text(post.title);
    $post
      .children("link")
      .attr("href", new URL(post.url, config.base_url).href);
    $post.children("id").text(makeTagURI(post.basename));
    $post
      .children("updated")
      .text(post.date.toISOString().replace(/\.\d{3}/, ""));
    $post.find("author > name").text(config.title);
    $post.children("summary").text(post.description || "");
    const content = marked.parse((await getPost(post.input)).content);
    $post.children("content").html(await content);
    $post.appendTo($feed);
  }

  return $.xml();
}

async function renderFeedStyles() {
  const $ = cheerio.load(
    await fs.readFile("templates/feed-styles.xsl", "utf-8"),
    {
      xml: true,
    },
  );
  $("style").text(css);
  return $.xml();
}

export default async function generateSite() {
  const tasks = [];
  const { posts, tags } = await readPosts("content", "post", "public");
  await fs.mkdir("public/post", { recursive: true });
  for (const post of posts) {
    const content = await marked.parse((await getPost(post.input)).content);
    await fs.mkdir(`public/post/${post.basename}`, { recursive: true });
    tasks.push(async () => {
      log.debug(`Rendering post ${post.basename} to ${post.output}`);
      return fs.writeFile(post.output, await renderPost(post, content));
    });
  }
  await fs.mkdir("public/tags", { recursive: true });
  tasks.push(async () => {
    log.debug("Rendering tags page to public/tags/index.html");
    return fs.writeFile(
      "public/tags/index.html",
      await renderTags([...tags].sort()),
    );
  });
  for (const tag of tags) {
    log.debug(`Processing tag ${tag}`);
    const matchingPosts = posts.filter((p) =>
      p.taxonomies["tags"].includes(tag),
    );
    await fs.mkdir(`public/tags/${tag}`, { recursive: true });
    tasks.push(async () => {
      log.debug(`Rendering tag ${tag} to public/tags/${tag}/index.html`);
      return fs.writeFile(
        `public/tags/${tag}/index.html`,
        await renderListPage(tag, matchingPosts),
      );
    });

    tasks.push(async () => {
      log.debug(`Rendering tag ${tag} feed to public/tags/${tag}/atom.xml`);
      return fs.writeFile(
        `public/tags/${tag}/atom.xml`,
        await renderFeed(`${config.title} - ${tag}`, matchingPosts, tag),
      );
    });
  }
  tasks.push(async () => {
    log.debug("Rendering posts page to public/post/index.html");
    return fs.writeFile(
      "public/post/index.html",
      await renderListPage("", posts),
    );
  });
  tasks.push(async () => {
    log.debug("Rendering site feed to public/atom.xml");
    return fs.writeFile(
      "public/atom.xml",
      await renderFeed(config.title, posts),
    );
  });
  tasks.push(async () => {
    log.debug("Rendering feed styles to public/feed-styles.xsl");
    return fs.writeFile("public/feed-styles.xsl", await renderFeedStyles());
  });
  tasks.push(async () => {
    log.debug("Rendering homepage to public/index.html");
    return fs.writeFile(
      "public/index.html",
      await renderHomepage(posts.slice(0, 3)),
    );
  });
  tasks.push(async () => {
    log.debug("Rendering 404 page to public/404.html");
    return fs.writeFile("public/404.html", await render404Page());
  });
  return Promise.all(tasks.map((f) => f()));
}