import { join } from 'path';
import fs from 'fs/promises';
import yaml from 'yaml';
import { marked } from 'marked';
import hljs from 'highlight.js';
import { getDateObject } from '../utils/date-object';
import type { ArchiveDto, BlogPost, BlogPostTag } from '../types/post';

const dir = join('content', 'blog');

marked.use({
  renderer: {
    code: ({ text, lang }): string => {
      const language = lang || 'plaintext';
      const created = hljs.highlight(text, { language }).value;
      return `<div class="codeblock"><pre><code class="hljs language-${language}">${created}</code></pre></div>`;
    },
    link: ({ href, title, text }): string =>
      `<a href="${href}" rel="nofollow" target="_blank"${
        title ? ` title="${title}"` : ''
      }>${text}</a>`,
  },
});

async function readBlogPosts() {
  const files = await fs.readdir(dir);
  const readMD = files.filter((file) => file.endsWith('.md'));
  const readFiles: BlogPost[] = [];

  for (const file of readMD) {
    const post = await readBlogPost(file.replace('.md', ''));
    if (!post) continue;
    readFiles.push(post);
  }

  readFiles.sort((a, b) =>
    new Date(b.date)
      .toISOString()
      .localeCompare(new Date(a.date).toISOString(), 'en', { numeric: true }),
  );

  return readFiles;
}

async function readBlogPost(slug: string): Promise<BlogPost> {
  const decoded = decodeURIComponent(slug);
  if (!slug || decoded.includes('/') || decoded.includes('.'))
    throw new Error('Invalid post slug');

  const file = `${slug}.md`;
  const mdpath = join(dir, file);
  const read = await fs.readFile(mdpath, { encoding: 'utf-8' });

  const { header: parsedHeader, length: headerLength } = await readHeader(read);
  const renderedMd = await marked(read.substring(headerLength), {
    async: true,
  });

  const boundary = renderedMd.indexOf('<!-- more -->');

  const { year, month, day } = getDateObject(parsedHeader);
  const content = {
    ...parsedHeader,
    file,
    slug,
    summary: renderedMd.substring(0, boundary > -1 ? boundary : 240),
    fullSlug: `${year}/${month}/${day}/${slug}`,
    markdown: read.substring(headerLength),
    html: renderedMd.replace('<!-- more -->', ''),
  };

  return content;
}

let blogPostCache: BlogPost[] = [];
export const blogPostList = async () => {
  if (!blogPostCache.length) {
    blogPostCache = await readBlogPosts();
  }
  return blogPostCache;
};

export const getFilteredBlogPosts = async (
  condition?: (body: BlogPost) => boolean,
  htmlContent = true,
  includeMarkdown = true,
): Promise<BlogPost[]> => {
  const posts = await blogPostList();
  return posts
    .filter((item) => (condition ? condition(item) : true))
    .map((item) => ({
      ...item,
      html: htmlContent ? item.html : undefined,
      markdown: includeMarkdown ? item.markdown : undefined,
    }));
};

export const getBlogPost = async (
  slug: string,
  htmlContent = true,
  includeMarkdown = true,
): Promise<BlogPost | undefined> => {
  const posts = await blogPostList();
  const item = posts.find((item) => item.slug === slug);
  if (!item) return;

  const index = posts.indexOf(item);
  const next = index > 0 ? posts[index - 1] : undefined;
  const prev = posts[index + 1];

  return {
    ...item,
    html: htmlContent ? item.html : undefined,
    markdown: includeMarkdown ? item.markdown : undefined,
    next: next
      ? { title: next.title, slug: next.slug, fullSlug: next.fullSlug }
      : undefined,
    prev: prev
      ? { title: prev.title, slug: prev.slug, fullSlug: prev.fullSlug }
      : undefined,
  };
};

export async function getTags(): Promise<BlogPostTag[]> {
  const posts = await getFilteredBlogPosts(undefined, false, false);
  const obj: BlogPostTag[] = [];

  for (const post of posts) {
    for (const tag of post.tags || []) {
      const find = obj.find((item) => item.name === tag);
      if (find) {
        find.count++;
        find.posts.push(post.slug);
        continue;
      }

      obj.push({
        name: tag,
        count: 1,
        posts: [post.slug],
      });
    }
  }

  obj.sort((a, b) => a.name.localeCompare(b.name, 'en'));

  return obj;
}

export async function getArchiveTree(condition?: (body: BlogPost) => boolean) {
  const posts = await getFilteredBlogPosts(condition, false, false);
  const obj: ArchiveDto = {};

  for (const post of posts) {
    const { year, month, day } = getDateObject(post);

    if (!obj[year]) {
      obj[year] = {};
    }

    if (!obj[year][month]) {
      obj[year][month] = {};
    }

    if (!obj[year][month][day]) {
      obj[year][month][day] = [];
    }

    obj[year][month][day].push(post.slug);
  }

  return obj;
}

export async function generatePaths(prefix = '/') {
  const tree = await getArchiveTree();
  const paths = [];

  for (const year of Object.keys(tree)) {
    paths.push(`${prefix}archive/${year}`);
    for (const month of Object.keys(tree[year])) {
      paths.push(`${prefix}archive/${year}/${month}`);
      for (const day of Object.keys(tree[year][month])) {
        paths.push(`${prefix}archive/${year}/${month}/${day}`);
      }
    }
  }

  paths.sort((a, b) => b.localeCompare(a, 'en', { numeric: true }));

  return paths;
}

async function readHeader(fileContents: string) {
  const splitter = fileContents.split('\n');
  let header = '';

  for (const line of splitter) {
    if (line === '---') {
      if (!header.length) continue;
      break;
    }
    header += line + '\n';
  }

  return {
    header: yaml.parse(header),
    length: header.length + 9,
  };
}