<?php

namespace App\Support;

use Illuminate\Support\HtmlString;
use Illuminate\Support\Str;

/**
 * The small amount of Markdown a chat reply actually uses.
 *
 * Models write lists, bold and the occasional link, and rendering that as plain
 * text turns a readable answer into a wall of hyphens and asterisks.
 *
 * Deliberately not a full Markdown library. The input is model output rendered
 * straight into the page, so everything is escaped first and only a fixed set of
 * patterns is turned back into markup. A general parser would be a larger
 * surface for very little gain here.
 */
class Markdown
{
    public static function toHtml(?string $text): HtmlString
    {
        $escaped = e(trim((string) $text));

        return new HtmlString(
            self::paragraphs(
                self::inline($escaped)
            )
        );
    }

    /** Bold, italic, inline code and links. */
    private static function inline(string $text): string
    {
        $patterns = [
            '/`([^`]+)`/' => '<code>$1</code>',
            '/\*\*([^*]+)\*\*/' => '<strong>$1</strong>',
            '/(?<![\w*])\*([^*\n]+)\*(?![\w*])/' => '<em>$1</em>',
            '/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/' => '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
        ];

        $text = preg_replace(array_keys($patterns), array_values($patterns), $text);

        // Bare URLs, but not ones already inside an anchor from the step above.
        return preg_replace(
            '/(?<!href=")(?<!">)(https?:\/\/[^\s<]+)/',
            '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>',
            $text,
        );
    }

    /**
     * Blocks: bulleted lists, numbered lists, and everything else as paragraphs.
     *
     * Models mix "-", "*", "1." and "1)" freely, so all four are treated as list
     * markers rather than left as literal characters.
     */
    private static function paragraphs(string $text): string
    {
        $html = '';
        $list = null;

        foreach (preg_split('/\R/', $text) as $line) {
            $line = rtrim($line);

            if (trim($line) === '') {
                $html .= self::closeList($list);

                continue;
            }

            if (preg_match('/^\s*[-*]\s+(.*)$/u', $line, $matches)) {
                $html .= self::openList($list, 'ul');
                $html .= '<li>'.$matches[1].'</li>';

                continue;
            }

            if (preg_match('/^\s*\d+[.)]\s+(.*)$/u', $line, $matches)) {
                $html .= self::openList($list, 'ol');
                $html .= '<li>'.$matches[1].'</li>';

                continue;
            }

            $html .= self::closeList($list);
            $html .= '<p>'.$line.'</p>';
        }

        return $html.self::closeList($list);
    }

    private static function openList(?string &$list, string $tag): string
    {
        if ($list === $tag) {
            return '';
        }

        $html = self::closeList($list);
        $list = $tag;

        return $html."<{$tag}>";
    }

    private static function closeList(?string &$list): string
    {
        if ($list === null) {
            return '';
        }

        $html = "</{$list}>";
        $list = null;

        return $html;
    }

    /** A short single line, for places that cannot take block markup. */
    public static function toLine(?string $text): string
    {
        return Str::of((string) $text)->replaceMatches('/\s+/', ' ')->trim()->toString();
    }
}
