import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
import { remark } from 'remark'
import remarkParse from 'remark-parse'
import gfm from 'remark-gfm'
import remarkMath from 'remark-math'
import remarkRehype from 'remark-rehype'
import rehypeHighlight from 'rehype-highlight'
import rehypeKatex from 'rehype-katex'
import rehypeReact from 'rehype-react'
import { cloneElement, isValidElement, type ReactElement, Fragment, type ReactNode } from 'react'
import { jsx, jsxs } from 'react/jsx-runtime'
import { rehypeCodeMeta } from './rehype-code-meta'
import { CodeWrapper } from '@/components/common/CodeWrapper'
import { ImageWrapper } from '@/components/common/ImageWrapper'
import { ExternalLink } from '@/components/common/ExternalLink'
import readingTime from 'reading-time'
import { rehypeUnwrapImages } from './rehype-unwrap-images'
import { rehypeMermaidSsg } from './rehype-mermaid-ssg'
import { stripLeadingMatch } from './react-node-text'
import { BlockquoteWrapper } from '@/components/common/BlockquoteWrapper'
import type { NoteType } from '@/types/Note.type'
import { rehypeMark } from './rehype-mark'
import { rehypeDefinition } from './rehype-definition'
import { slugify } from '@/utils/slug'
import { PostMeta } from '@/types/Post.type'

const postsDirectory = path.join(process.cwd(), 'content/posts')

function getTextContent(node: ReactNode): string {
  if (typeof node === 'string') return node
  if (typeof node === 'number') return String(node)
  if (Array.isArray(node)) return node.map(getTextContent).join('')
  if (node && typeof node === 'object' && 'props' in node) {
    const props = node.props as { children?: ReactNode }
    return getTextContent(props.children)
  }
  return ''
}

export function getAllPostSlugs(): string[] {
  return fs
    .readdirSync(postsDirectory)
    .filter((f) => f.endsWith('.md'))
    .map((f) => f.replace(/\.md$/, ''))
}

export function getAllPostsMeta(): PostMeta[] {
  return getAllPostSlugs()
    .map((slug) => {
      const fullPath = path.join(postsDirectory, `${slug}.md`)
      const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))
      const readingTimeText = readingTime(content).text
      return Object.assign({ slug }, {
        ...data,
        readingTimeText,
      } as Omit<PostMeta, 'slug'>)
    })
    .toSorted((a, b) => (a.date < b.date ? 1 : -1))
}

export async function getPostBySlug(slug: string) {
  const fullPath = path.join(postsDirectory, `${slug}.md`)
  const { data, content } = matter(fs.readFileSync(fullPath, 'utf8'))

  let processor = remark().use(remarkParse).use(gfm)

  if (data.math) {
    processor = processor.use(remarkMath)
  }

  processor = processor
    .use(remarkRehype, { allowDangerousHtml: true })
    .use(rehypeHighlight)
    .use(rehypeCodeMeta)
    .use(rehypeMark)
    .use(rehypeDefinition)

  if (data.math) {
    processor = processor.use(rehypeKatex, { output: 'mathml' })
  }

  if (data.mermaid) {
    processor = processor.use(rehypeMermaidSsg)
  }

  processor = processor.use(rehypeUnwrapImages).use(rehypeReact, {
    Fragment,
    jsx,
    jsxs,
    components: {
      pre: ({ children, ...props }: { children: ReactNode; [key: string]: unknown }) => {
        const language =
          typeof props['data-language'] === 'string' ? props['data-language'] : undefined
        return <CodeWrapper language={language}>{children}</CodeWrapper>
      },
      img: ({ src, alt }: { src: string; alt: string }) => <ImageWrapper src={src} alt={alt} />,
      a: ({ href, children }: { href: string; children: ReactNode }) =>
        href?.startsWith('http') ? (
          <ExternalLink href={href}>{children}</ExternalLink>
        ) : (
          <a href={href}>{children}</a>
        ),
      blockquote: ({ children }: { children: ReactNode }) => {
        const textContent = getTextContent(children).trimStart()
        const type = textContent.match(/^\[!(\w+)\]/)?.[1]?.toLowerCase() as NoteType | undefined
        return (
          <BlockquoteWrapper type={type}>
            {stripLeadingMatch(children, /^\[!(\w+)\]/)}
          </BlockquoteWrapper>
        )
      },
    },
  })

  const file = await processor.process(content)
  const rawContentNode = file.result as ReactNode

  const readingTimeText = readingTime(content).text

  const tableOfContents: { text: string; id: string; level: number }[] = []
  const seenIds = new Map<string, number>()

  function withHeadingIds(node: ReactNode): ReactNode {
    if (!isValidElement(node)) return node

    const element = node as ReactElement<{ children?: ReactNode; id?: string }>
    if (element.type === 'h2' || element.type === 'h3') {
      const text = getTextContent(element.props.children ?? [])
      let id = slugify(text)

      const seenCount = seenIds.get(id) ?? 0
      seenIds.set(id, seenCount + 1)
      if (seenCount > 0) id = `${id}-${seenCount}`

      const level = element.type === 'h2' ? 2 : 3
      tableOfContents.push({ text, id, level })

      return cloneElement(element, { id })
    }

    return node
  }

  const rawChildren = isValidElement(rawContentNode)
    ? (rawContentNode.props as { children?: ReactNode }).children
    : rawContentNode
  const childrenArray = Array.isArray(rawChildren) ? rawChildren : [rawChildren]

  const processedChildren = childrenArray.map(withHeadingIds)

  const processedContent = isValidElement(rawContentNode)
    ? cloneElement(rawContentNode, {}, processedChildren)
    : rawContentNode

  return {
    meta: { ...(data as Omit<PostMeta, 'slug'>), readingTimeText },
    content: processedContent,
    tableOfContents,
  }
}
