const escapeHtml = (value: string) =>
    value
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')

const escapeAttribute = (value: string) => escapeHtml(value)

const parseLink = (value: string) => {
    return value.replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, (_match, text: string, url: string) => {
        const safeUrl = escapeAttribute(url)
        return `<a href="${safeUrl}">${text}</a>`
    })
}

const parseInlineMarkdown = (value: string) => {
    let html = escapeHtml(value)
    html = parseLink(html)
    html = html.replace(/`([^`]+)`/g, '<code>$1</code>')
    html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
    html = html.replace(/(^|[^\*])\*([^\*\n]+)\*(?!\*)/g, '$1<em>$2</em>')
    return html
}

export const renderMarkdownToHtml = (value: string) => {
    const source = String(value || '').replace(/\r\n?/g, '\n').trim()
    if (!source) return ''

    const lines = source.split('\n')
    const htmlParts: string[] = []
    const paragraphLines: string[] = []
    const codeLines: string[] = []
    const quoteLines: string[] = []
    const tableLines: string[] = []
    let listType: 'ul' | 'ol' | '' = ''
    let inCodeBlock = false

    const flushParagraph = () => {
        if (!paragraphLines.length) return
        htmlParts.push(`<p>${paragraphLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</p>`)
        paragraphLines.length = 0
    }

    const closeList = () => {
        if (!listType) return
        htmlParts.push(`</${listType}>`)
        listType = ''
    }

    const flushQuote = () => {
        if (!quoteLines.length) return
        htmlParts.push(
            `<blockquote>${quoteLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</blockquote>`
        )
        quoteLines.length = 0
    }

    const flushCodeBlock = () => {
        if (!codeLines.length) return
        htmlParts.push(`<pre><code>${escapeHtml(codeLines.join('\n'))}</code></pre>`)
        codeLines.length = 0
    }

    const parseTableCells = (line: string) => {
        const cells = line
            .trim()
            .replace(/^\|/, '')
            .replace(/\|$/, '')
            .split('|')
            .map((cell) => cell.trim())
        return cells.filter((_cell, index) => index < cells.length)
    }

    const isTableRow = (line: string) => /^\|.*\|$/.test(line) && parseTableCells(line).length >= 2

    const isTableSeparatorLine = (line: string) => {
        if (!isTableRow(line)) return false
        return parseTableCells(line).every((cell) => /^:?-{3,}:?$/.test(cell))
    }

    const flushTable = () => {
        if (!tableLines.length) return

        if (tableLines.length >= 2 && isTableSeparatorLine(tableLines[1])) {
            const headers = parseTableCells(tableLines[0])
            const bodyRows = tableLines.slice(2).map(parseTableCells).filter((cells) => cells.length > 0)
            const thead = `<thead><tr>${headers.map((cell) => `<th>${parseInlineMarkdown(cell)}</th>`).join('')}</tr></thead>`
            const tbody = bodyRows.length
                ? `<tbody>${bodyRows
                    .map((cells) => `<tr>${cells.map((cell) => `<td>${parseInlineMarkdown(cell)}</td>`).join('')}</tr>`)
                    .join('')}</tbody>`
                : ''
            htmlParts.push(`<table>${thead}${tbody}</table>`)
        } else {
            htmlParts.push(`<p>${tableLines.map((line) => parseInlineMarkdown(line)).join('<br />')}</p>`)
        }

        tableLines.length = 0
    }

    for (const rawLine of lines) {
        const line = rawLine.trim()

        if (/^```/.test(line)) {
            flushParagraph()
            closeList()
            flushQuote()
            flushTable()
            if (inCodeBlock) {
                flushCodeBlock()
            }
            inCodeBlock = !inCodeBlock
            continue
        }

        if (inCodeBlock) {
            codeLines.push(rawLine)
            continue
        }

        if (!line) {
            flushParagraph()
            closeList()
            flushQuote()
            flushTable()
            continue
        }

        const headingMatch = line.match(/^(#{1,6})\s+(.*)$/)
        if (headingMatch) {
            flushParagraph()
            closeList()
            flushQuote()
            flushTable()
            const level = Math.min(6, headingMatch[1].length)
            htmlParts.push(`<h${level}>${parseInlineMarkdown(headingMatch[2])}</h${level}>`)
            continue
        }

        const quoteMatch = line.match(/^>\s?(.*)$/)
        if (quoteMatch) {
            flushParagraph()
            closeList()
            flushTable()
            quoteLines.push(quoteMatch[1])
            continue
        }
        flushQuote()

        if (/^(-{3,}|\*{3,}|_{3,})$/.test(line)) {
            flushParagraph()
            closeList()
            flushTable()
            htmlParts.push('<hr />')
            continue
        }

        const unorderedMatch = line.match(/^[-*+]\s+(.*)$/)
        if (unorderedMatch) {
            flushParagraph()
            flushTable()
            if (listType !== 'ul') {
                closeList()
                listType = 'ul'
                htmlParts.push('<ul>')
            }
            htmlParts.push(`<li>${parseInlineMarkdown(unorderedMatch[1])}</li>`)
            continue
        }

        const orderedMatch = line.match(/^\d+\.\s+(.*)$/)
        if (orderedMatch) {
            flushParagraph()
            flushTable()
            if (listType !== 'ol') {
                closeList()
                listType = 'ol'
                htmlParts.push('<ol>')
            }
            htmlParts.push(`<li>${parseInlineMarkdown(orderedMatch[1])}</li>`)
            continue
        }

        if (isTableRow(line)) {
            flushParagraph()
            closeList()
            flushQuote()
            tableLines.push(line)
            continue
        }

        flushTable()
        closeList()
        paragraphLines.push(line)
    }

    flushParagraph()
    closeList()
    flushQuote()
    flushTable()
    if (inCodeBlock) {
        flushCodeBlock()
    }

    return htmlParts.join('')
}
