<?php

declare(strict_types=1);

final class MyronMarkdownToHtmlConverter
{
    public function convert(string $markdown): string
    {
        $markdown = str_replace(["\r\n", "\r"], "\n", trim($markdown));
        if ($markdown === '') {
            return '';
        }

        $lines = explode("\n", $markdown);
        $html = [];
        $paragraph = [];
        $list = [];
        $listType = '';
        $blockquote = [];
        $inFence = false;
        $fence = [];

        $flushParagraph = function () use (&$paragraph, &$html): void {
            if ($paragraph === []) {
                return;
            }
            $html[] = '<p>' . $this->inline(implode(' ', array_map('trim', $paragraph))) . '</p>';
            $paragraph = [];
        };
        $flushList = function () use (&$list, &$listType, &$html): void {
            if ($list === []) {
                return;
            }
            $tag = $listType === 'ol' ? 'ol' : 'ul';
            $html[] = '<' . $tag . '>' . implode('', array_map(fn (string $item): string => '<li>' . $this->inline($item) . '</li>', $list)) . '</' . $tag . '>';
            $list = [];
            $listType = '';
        };
        $flushBlockquote = function () use (&$blockquote, &$html): void {
            if ($blockquote === []) {
                return;
            }
            $html[] = '<blockquote><p>' . $this->inline(implode(' ', array_map('trim', $blockquote))) . '</p></blockquote>';
            $blockquote = [];
        };
        $flushBlocks = function () use ($flushParagraph, $flushList, $flushBlockquote): void {
            $flushParagraph();
            $flushList();
            $flushBlockquote();
        };

        foreach ($lines as $line) {
            if (preg_match('/^\s*```/', $line) === 1) {
                if ($inFence) {
                    $html[] = '<pre><code>' . htmlspecialchars(rtrim(implode("\n", $fence), "\n"), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</code></pre>';
                    $fence = [];
                    $inFence = false;
                } else {
                    $flushBlocks();
                    $inFence = true;
                }
                continue;
            }
            if ($inFence) {
                $fence[] = $line;
                continue;
            }

            $trimmed = trim($line);
            if ($trimmed === '') {
                $flushBlocks();
                continue;
            }
            if (preg_match('/^\[mic-[a-z0-9._-]+-mic_[a-f0-9]{16}\]$/', $trimmed) === 1) {
                $flushBlocks();
                $html[] = $trimmed;
                continue;
            }
            if (preg_match('/^(\*\s*){3,}$|^(-\s*){3,}$/', $trimmed) === 1) {
                $flushBlocks();
                $html[] = '<hr>';
                continue;
            }
            if (preg_match('/^(#{1,4})\s+(.+)$/', $trimmed, $matches) === 1) {
                $flushBlocks();
                $level = max(2, min(4, strlen($matches[1])));
                $html[] = '<h' . $level . '>' . $this->inline(trim($matches[2])) . '</h' . $level . '>';
                continue;
            }
            if (preg_match('/^>\s?(.*)$/', $trimmed, $matches) === 1) {
                $flushParagraph();
                $flushList();
                $blockquote[] = (string) $matches[1];
                continue;
            }
            if (preg_match('/^[-*+]\s+(.+)$/', $trimmed, $matches) === 1) {
                $flushParagraph();
                $flushBlockquote();
                if ($listType !== '' && $listType !== 'ul') {
                    $flushList();
                }
                $listType = 'ul';
                $list[] = (string) $matches[1];
                continue;
            }
            if (preg_match('/^\d+[.)]\s+(.+)$/', $trimmed, $matches) === 1) {
                $flushParagraph();
                $flushBlockquote();
                if ($listType !== '' && $listType !== 'ol') {
                    $flushList();
                }
                $listType = 'ol';
                $list[] = (string) $matches[1];
                continue;
            }

            $flushList();
            $flushBlockquote();
            $paragraph[] = $trimmed;
        }

        if ($inFence) {
            $html[] = '<pre><code>' . htmlspecialchars(rtrim(implode("\n", $fence), "\n"), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</code></pre>';
        }
        $flushBlocks();

        return implode("\n", $html);
    }

    private function inline(string $text): string
    {
        $escaped = htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $escaped = preg_replace_callback('/!\[([^\]]*)\]\(([^)\s]+)\)/', static function (array $matches): string {
            $alt = htmlspecialchars($matches[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            $src = htmlspecialchars($matches[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            return '<img src="' . $src . '" alt="' . $alt . '">';
        }, $escaped) ?? $escaped;
        $escaped = preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)\)/', static function (array $matches): string {
            $label = $matches[1];
            $href = htmlspecialchars($matches[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            return '<a href="' . $href . '">' . $label . '</a>';
        }, $escaped) ?? $escaped;
        $escaped = preg_replace('/`([^`]+)`/', '<code>$1</code>', $escaped) ?? $escaped;
        $escaped = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $escaped) ?? $escaped;
        $escaped = preg_replace('/(^|[^*])\*([^*]+)\*/', '$1<em>$2</em>', $escaped) ?? $escaped;

        return $escaped;
    }
}
