<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use League\CommonMark\CommonMarkConverter;
use Drupal\Component\Utility\UrlHelper;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Converts a markdown string to safe HTML.
 *
 * Uses league/commonmark when available (install via `composer require
 * league/commonmark`). Falls back to a regex-based converter that handles
 * the standard markdown patterns produced by LLMs.
 *
 * Output is XSS-safe in both paths:
 * - CommonMark: escapes raw HTML in input by default.
 * - Regex fallback: text nodes are htmlspecialchars-encoded; link URLs are
 *   sanitized via UrlHelper::stripDangerousProtocols().
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "markdown_to_html",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Markdown to HTML"),
  description: "Convert markdown text to safe HTML, preserving headings, links, and basic formatting",
  version: "1.0.0"
)]
class MarkdownToHtml extends AbstractFlowDropNodeProcessor {

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ): static {
    return new static($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $text = $params->getString('text');
    $strip_h1 = $params->getBool('strip_h1', FALSE);

    if ($strip_h1) {
      $text = preg_replace('/^#\s+.+\r?\n?/m', '', $text, 1) ?? $text;
    }

    $html = $this->convert($text);

    return [
      'html' => $html,
      'raw' => $params->getString('text'),
    ];
  }

  /**
   * Converts markdown to HTML using CommonMark if available, regex otherwise.
   *
   * @param string $markdown
   *   Markdown text.
   *
   * @return string
   *   Safe HTML string.
   */
  private function convert(string $markdown): string {
    if (class_exists('League\CommonMark\CommonMarkConverter')) {
      $converter = new CommonMarkConverter([
        'html_input' => 'escape',
        'allow_unsafe_links' => FALSE,
      ]);
      return (string) $converter->convert($markdown);
    }

    return $this->regexConvert($markdown);
  }

  /**
   * Regex-based markdown-to-HTML for standard LLM output patterns.
   *
   * Covers: fenced code blocks, headings (H1–H6), bold, italic, inline code,
   * unordered lists, ordered lists, blockquotes, links, images, horizontal
   * rules, and paragraphs.
   *
   * Text content is HTML-escaped to prevent XSS. Link URLs are sanitized.
   *
   * @param string $text
   *   Markdown text.
   *
   * @return string
   *   HTML string.
   */
  private function regexConvert(string $text): string {
    // Normalize line endings.
    $text = str_replace("\r\n", "\n", $text);

    // Fenced code blocks (process before others to protect content).
    $code_blocks = [];
    $text = preg_replace_callback(
      '/^```(\w*)\n(.*?)^```/ms',
      function (array $m) use (&$code_blocks): string {
        $placeholder = "\x02CODE" . count($code_blocks) . "\x03";
        $lang = htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $code = htmlspecialchars($m[2], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $attr = $lang !== '' ? " class=\"language-{$lang}\"" : '';
        $code_blocks[$placeholder] = "<pre><code{$attr}>{$code}</code></pre>";
        return $placeholder;
      },
      $text
    ) ?? $text;

    // --- Headings ---
    $text = preg_replace_callback(
      '/^(#{1,6})\s+(.+)$/m',
      function (array $m): string {
        $level = strlen($m[1]);
        $content = htmlspecialchars(trim($m[2]), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        return "<h{$level}>{$content}</h{$level}>";
      },
      $text
    ) ?? $text;

    // --- Blockquotes ---
    $text = preg_replace_callback(
      '/^((?:>\s?.+\n?)+)/m',
      function (array $m): string {
        $inner = preg_replace('/^>\s?/m', '', $m[1]) ?? $m[1];
        return '<blockquote>' . trim($inner) . '</blockquote>';
      },
      $text
    ) ?? $text;

    // --- Unordered lists ---
    $text = preg_replace_callback(
      '/^((?:[*\-+]\s.+\n?)+)/m',
      function (array $m): string {
        $items = preg_replace('/^[*\-+]\s(.+)/m', '<li>$1</li>', trim($m[1])) ?? $m[1];
        return "<ul>\n{$items}\n</ul>";
      },
      $text
    ) ?? $text;

    // --- Ordered lists ---
    $text = preg_replace_callback(
      '/^((?:\d+\.\s.+\n?)+)/m',
      function (array $m): string {
        $items = preg_replace('/^\d+\.\s(.+)/m', '<li>$1</li>', trim($m[1])) ?? $m[1];
        return "<ol>\n{$items}\n</ol>";
      },
      $text
    ) ?? $text;

    // --- Horizontal rules ---
    $text = preg_replace('/^[-*_]{3,}\s*$/m', '<hr>', $text) ?? $text;

    // --- Inline: images (before links) ---
    $text = preg_replace_callback(
      '/!\[([^\]]*)\]\(([^)]+)\)/',
      function (array $m): string {
        $alt = htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $src = UrlHelper::stripDangerousProtocols($m[2]);
        $src = htmlspecialchars($src, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        return "<img src=\"{$src}\" alt=\"{$alt}\">";
      },
      $text
    ) ?? $text;

    // --- Inline: links ---
    $text = preg_replace_callback(
      '/\[([^\]]+)\]\(([^)]+)\)/',
      function (array $m): string {
        $label = htmlspecialchars($m[1], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        $href = UrlHelper::stripDangerousProtocols(trim($m[2]));
        $href = htmlspecialchars($href, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        return "<a href=\"{$href}\">{$label}</a>";
      },
      $text
    ) ?? $text;

    // --- Inline: bold + italic (***text***) ---
    $text = preg_replace('/\*{3}(.+?)\*{3}/', '<strong><em>$1</em></strong>', $text) ?? $text;
    // --- Inline: bold (**text** or __text__) ---
    $text = preg_replace('/(\*{2}|_{2})(.+?)\1/', '<strong>$2</strong>', $text) ?? $text;
    // --- Inline: italic (*text* or _text_) ---
    $text = preg_replace('/(\*|_)(.+?)\1/', '<em>$2</em>', $text) ?? $text;
    // --- Inline: code (`code`) ---
    $text = preg_replace_callback(
      '/`([^`]+)`/',
      function (array $m): string {
        $code = htmlspecialchars($m[1], ENT_NOQUOTES | ENT_SUBSTITUTE, 'UTF-8');
        return "<code>{$code}</code>";
      },
      $text
    ) ?? $text;

    // Paragraphs: blank-line-separated blocks not already wrapped in tags.
    $blocks = preg_split('/\n{2,}/', $text) ?: [];
    $text = implode("\n\n", array_map(function (string $block): string {
      $block = trim($block);
      if ($block === '') {
        return '';
      }
      // Skip blocks that are already HTML tags or placeholders.
      if (preg_match('/^<[a-zA-Z\/\x02]/', $block)) {
        return $block;
      }
      return '<p>' . $block . '</p>';
    }, $blocks));

    // --- Restore fenced code blocks ---
    foreach ($code_blocks as $placeholder => $html) {
      $text = str_replace($placeholder, $html, $text);
    }

    return $text;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'text' => [
          'type' => 'string',
          'title' => 'Markdown Text',
          'description' => 'The markdown text to convert to HTML',
          'default' => '',
        ],
        'strip_h1' => [
          'type' => 'boolean',
          'title' => 'Strip H1',
          'description' => 'Remove the first H1 heading — useful when the title is stored separately',
          'default' => FALSE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'html' => [
          'type' => 'string',
          'description' => 'Safe HTML output',
        ],
        'raw' => [
          'type' => 'string',
          'description' => 'Original markdown input unchanged',
        ],
      ],
    ];
  }

}
