<?php

declare(strict_types=1);

final class MyronHtmlToMarkdownConverter
{
    /**
     * @param array<string, mixed> $options
     */
    public function convert(string $html, array $options = []): string
    {
        $html = trim($html);
        if ($html === '') {
            return '';
        }

        if (!str_contains($html, '<')) {
            return $this->normalizeMarkdown($this->escapeText($html));
        }

        $doc = new DOMDocument('1.0', 'UTF-8');
        $previous = libxml_use_internal_errors(true);
        $loaded = $doc->loadHTML('<?xml encoding="UTF-8"><div data-myron-md-root="true">' . $html . '</div>', LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
        libxml_clear_errors();
        libxml_use_internal_errors($previous);

        if (!$loaded) {
            return $this->normalizeMarkdown($this->escapeText(strip_tags($html)));
        }

        $root = null;
        foreach ($doc->getElementsByTagName('div') as $div) {
            if ($div instanceof DOMElement && $div->getAttribute('data-myron-md-root') === 'true') {
                $root = $div;
                break;
            }
        }

        if (!$root instanceof DOMElement) {
            return $this->normalizeMarkdown($this->escapeText(trim($doc->textContent)));
        }

        return $this->normalizeMarkdown($this->renderChildren($root, [
            'listDepth' => 0,
            'orderedStack' => [],
            'canonicalUrl' => (string) ($options['canonicalUrl'] ?? ''),
        ]));
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderNode(DOMNode $node, array $context): string
    {
        if ($node instanceof DOMText || $node instanceof DOMCdataSection) {
            return $this->escapeText((string) $node->nodeValue);
        }
        if (!$node instanceof DOMElement) {
            return '';
        }

        $tag = strtolower($node->tagName);
        if (in_array($tag, ['script', 'style', 'template', 'dialog'], true)) {
            return '';
        }

        if ($this->hasClass($node, 'myron-mic-table')) {
            $table = $this->firstDescendant($node, 'table');
            return $table instanceof DOMElement ? "\n\n" . $this->renderTable($table) . "\n\n" : $this->renderChildren($node, $context);
        }

        return match ($tag) {
            'h1', 'h2', 'h3', 'h4', 'h5', 'h6' => "\n\n" . str_repeat('#', (int) substr($tag, 1)) . ' ' . trim($this->renderChildren($node, $context)) . "\n\n",
            'p', 'section', 'article', 'main', 'header', 'footer' => "\n\n" . trim($this->renderChildren($node, $context)) . "\n\n",
            'br' => "  \n",
            'hr' => "\n\n---\n\n",
            'strong', 'b' => '**' . trim($this->renderChildren($node, $context)) . '**',
            'em', 'i' => '*' . trim($this->renderChildren($node, $context)) . '*',
            'code' => $this->renderInlineCode($node),
            'pre' => "\n\n```\n" . rtrim($node->textContent, "\r\n") . "\n```\n\n",
            's', 'del' => '~~' . trim($this->renderChildren($node, $context)) . '~~',
            'sub', 'sup' => '<' . $tag . '>' . trim($this->renderChildren($node, $context)) . '</' . $tag . '>',
            'a' => $this->renderLink($node, $context),
            'img' => $this->renderImage($node),
            'ul' => $this->renderList($node, false, $context),
            'ol' => $this->renderList($node, true, $context),
            'blockquote' => $this->renderBlockquote($node, $context),
            'table' => "\n\n" . $this->renderTable($node) . "\n\n",
            'form' => "\n\n" . $this->renderFormPlaceholder($node, $context, false) . "\n\n",
            'button' => $this->hasClass($node, 'myron-form-cta') ? "\n\n" . $this->renderFormPlaceholder($node, $context, true) . "\n\n" : $this->renderChildren($node, $context),
            default => $this->renderChildren($node, $context),
        };
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderChildren(DOMNode $node, array $context): string
    {
        $out = '';
        foreach ($node->childNodes as $child) {
            $out .= $this->renderNode($child, $context);
        }
        return $out;
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderLink(DOMElement $node, array $context): string
    {
        $text = trim($this->renderChildren($node, $context));
        $href = trim($node->getAttribute('href'));
        if ($href === '') {
            return $text;
        }
        return '[' . ($text !== '' ? $text : $href) . '](' . str_replace(')', '%29', $href) . ')';
    }

    private function renderImage(DOMElement $node): string
    {
        $src = trim($node->getAttribute('src'));
        if ($src === '') {
            return '';
        }
        return '![' . str_replace(']', '\\]', trim($node->getAttribute('alt'))) . '](' . str_replace(')', '%29', $src) . ')';
    }

    private function renderInlineCode(DOMElement $node): string
    {
        $text = str_replace('`', '\`', (string) $node->textContent);
        return '`' . $text . '`';
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderList(DOMElement $node, bool $ordered, array $context): string
    {
        $depth = (int) ($context['listDepth'] ?? 0);
        $lines = [];
        $index = 1;
        foreach ($node->childNodes as $child) {
            if (!$child instanceof DOMElement || strtolower($child->tagName) !== 'li') {
                continue;
            }
            $marker = $ordered ? $index . '. ' : '- ';
            $itemContext = $context;
            $itemContext['listDepth'] = $depth + 1;
            $body = trim($this->renderChildren($child, $itemContext));
            $body = preg_replace('/\n{3,}/', "\n\n", $body) ?? $body;
            $body = str_replace("\n", "\n" . str_repeat('  ', $depth + 1), $body);
            $lines[] = str_repeat('  ', $depth) . $marker . $body;
            $index++;
        }
        return "\n" . implode("\n", $lines) . "\n";
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderBlockquote(DOMElement $node, array $context): string
    {
        $body = trim($this->renderChildren($node, $context));
        $lines = preg_split('/\R/', $body) ?: [];
        return "\n\n" . implode("\n", array_map(static fn (string $line): string => '> ' . $line, $lines)) . "\n\n";
    }

    private function renderTable(DOMElement $table): string
    {
        $rows = [];
        $caption = '';
        foreach ($table->childNodes as $child) {
            if ($child instanceof DOMElement && strtolower($child->tagName) === 'caption') {
                $caption = trim($child->textContent);
            }
        }
        foreach ($table->getElementsByTagName('tr') as $tr) {
            $cells = [];
            foreach ($tr->childNodes as $cell) {
                if (!$cell instanceof DOMElement || !in_array(strtolower($cell->tagName), ['th', 'td'], true)) {
                    continue;
                }
                $cells[] = $this->tableCellText($cell);
            }
            if ($cells !== []) {
                $rows[] = $cells;
            }
        }
        if ($rows === []) {
            return '';
        }
        $width = max(array_map('count', $rows));
        $lines = [];
        foreach ($rows as $i => $row) {
            $row = array_pad($row, $width, '');
            $lines[] = '| ' . implode(' | ', $row) . ' |';
            if ($i === 0) {
                $lines[] = '| ' . implode(' | ', array_fill(0, $width, '---')) . ' |';
            }
        }
        $prefix = $caption !== '' ? '**' . $this->escapeText($caption) . "**\n\n" : '';
        return $prefix . implode("\n", $lines);
    }

    private function tableCellText(DOMElement $cell): string
    {
        $text = trim(preg_replace('/\s+/', ' ', $cell->textContent) ?? '');
        $text = str_replace(['|', "\n", "\r"], ['\\|', ' ', ' '], $text);
        return $this->escapeText($text);
    }

    /**
     * @param array<string, mixed> $context
     */
    private function renderFormPlaceholder(DOMElement $node, array $context, bool $modal): string
    {
        $formId = trim($node->getAttribute($modal ? 'data-mic-form-id' : 'data-form-id'));
        if ($formId === '') {
            $formId = trim($node->getAttribute('id'));
        }
        $formId = $formId !== '' ? $formId : 'unknown';
        $name = trim($node->getAttribute('aria-label'));
        if ($name === '') {
            $name = $modal ? 'Modal form' : 'Embedded form';
        }
        $canonical = trim((string) ($context['canonicalUrl'] ?? ''));
        $fields = $modal ? [] : $this->formFieldLabels($node);

        if ($modal) {
            $label = trim($node->getAttribute('data-cta-label')) ?: trim($node->textContent) ?: 'Open form';
            return '[Open form: ' . $this->escapeText($label) . '](#mic-form-' . $formId . ')' . "\n\n"
                . '> **Form (modal):** ' . $this->escapeText($name) . ' (id: `' . $formId . '`) - Click button above on the live page to open.';
        }

        $lines = ['> **Form:** ' . $this->escapeText($name) . ' (id: `' . $formId . '`)'];
        if ($fields !== []) {
            $lines[] = '> Fields: ' . implode(', ', $fields);
        }
        $lines[] = '> Interactive form not supported in Markdown export.' . ($canonical !== '' ? ' Visit the page to submit: ' . $canonical : '');
        return implode("\n", $lines);
    }

    /**
     * @return list<string>
     */
    private function formFieldLabels(DOMElement $form): array
    {
        $labels = [];
        foreach ($form->getElementsByTagName('label') as $label) {
            $text = trim(preg_replace('/\s+/', ' ', $label->textContent) ?? '');
            if ($text !== '') {
                $labels[] = $this->escapeText($text);
            }
        }
        return array_values(array_unique($labels));
    }

    private function firstDescendant(DOMElement $node, string $tag): ?DOMElement
    {
        foreach ($node->getElementsByTagName($tag) as $child) {
            return $child instanceof DOMElement ? $child : null;
        }
        return null;
    }

    private function hasClass(DOMElement $node, string $class): bool
    {
        $classes = preg_split('/\s+/', trim($node->getAttribute('class'))) ?: [];
        return in_array($class, $classes, true);
    }

    private function escapeText(string $text): string
    {
        $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
        return str_replace(['\\', '[', ']'], ['\\\\', '\\[', '\\]'], $text);
    }

    private function normalizeMarkdown(string $markdown): string
    {
        $markdown = str_replace(["\r\n", "\r"], "\n", $markdown);
        $lines = array_map(static fn (string $line): string => rtrim($line), explode("\n", $markdown));
        $markdown = implode("\n", $lines);
        $markdown = preg_replace("/\n{3,}/", "\n\n", $markdown) ?? $markdown;
        return trim($markdown) . "\n";
    }
}
