<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use League\HTMLToMarkdown\HtmlConverter;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Converts HTML content to Markdown format.
 *
 * Takes HTML string as input and outputs a Markdown string using
 * the league/html-to-markdown library.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "html_to_markdown",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("HTML to Markdown"),
  description: "Convert HTML content to Markdown format",
  version: "1.0.0"
)]
class HtmlToMarkdown extends AbstractFlowDropNodeProcessor {

  /**
   * Valid heading styles.
   */
  private const HEADING_STYLES = ['atx', 'setext'];

  /**
   * Valid bold styles.
   */
  private const BOLD_STYLES = ['**', '__'];

  /**
   * Valid italic styles.
   */
  private const ITALIC_STYLES = ['*', '_'];

  /**
   * Maximum allowed HTML input size in bytes (1 MB).
   */
  private const MAX_INPUT_SIZE = 1048576;

  /**
   * URI schemes allowed in markdown links and images.
   *
   * Any URI whose scheme is not in this list will be neutralized to (#).
   */
  private const ALLOWED_URI_SCHEMES = ['http', 'https', 'mailto', 'ftp', 'tel'];

  /**
   * HTML elements whose content should be completely removed.
   */
  private const REMOVE_NODES = 'script style iframe object embed form noscript base meta svg math link applet';

  /**
   * Constructs an HtmlToMarkdown plugin instance.
   *
   * @param array $configuration
   *   The plugin configuration.
   * @param string $plugin_id
   *   The plugin ID.
   * @param mixed $plugin_definition
   *   The plugin definition.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger channel factory.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    private readonly LoggerChannelFactoryInterface $loggerFactory,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get("logger.factory"),
    );
  }

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

    if (isset($params['heading_style']) && !in_array($params['heading_style'], self::HEADING_STYLES, TRUE)) {
      $result = $result->merge(ValidationResult::error(
        'heading_style',
        sprintf('Invalid heading_style "%s". Must be one of: %s', $params['heading_style'], implode(', ', self::HEADING_STYLES)),
      ));
    }

    if (isset($params['bold_style']) && !in_array($params['bold_style'], self::BOLD_STYLES, TRUE)) {
      $result = $result->merge(ValidationResult::error(
        'bold_style',
        sprintf('Invalid bold_style "%s". Must be one of: %s', $params['bold_style'], implode(', ', self::BOLD_STYLES)),
      ));
    }

    if (isset($params['italic_style']) && !in_array($params['italic_style'], self::ITALIC_STYLES, TRUE)) {
      $result = $result->merge(ValidationResult::error(
        'italic_style',
        sprintf('Invalid italic_style "%s". Must be one of: %s', $params['italic_style'], implode(', ', self::ITALIC_STYLES)),
      ));
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $html = $params->get("html");

    if (!is_string($html) || $html === "") {
      return [
        "markdown" => "",
        "success" => TRUE,
      ];
    }

    if (strlen($html) > self::MAX_INPUT_SIZE) {
      $this->loggerFactory->get('flowdrop_node_processor')->warning(
        'HtmlToMarkdown: Input exceeds maximum size of @max bytes (received @actual bytes).',
        ['@max' => self::MAX_INPUT_SIZE, '@actual' => strlen($html)],
      );
      return [
        "markdown" => "",
        "success" => FALSE,
      ];
    }

    try {
      $converter = new HtmlConverter([
        'header_style' => $params->get("heading_style") ?? "atx",
        'bold_style' => $params->get("bold_style") ?? "**",
        'italic_style' => $params->get("italic_style") ?? "*",
        'strip_tags' => TRUE,
        'strip_placeholder_links' => $params->get("strip_placeholder_links") ?? FALSE,
        'remove_nodes' => self::REMOVE_NODES,
      ]);

      $markdown = $converter->convert($html);
      $markdown = $this->sanitizeUris($markdown);

      return [
        "markdown" => $markdown,
        "success" => TRUE,
      ];
    }
    catch (\Throwable $e) {
      $this->loggerFactory->get('flowdrop_node_processor')->debug(
        'HtmlToMarkdown: Conversion failed: @message',
        ['@message' => $e->getMessage()],
      );
      return [
        "markdown" => "",
        "success" => FALSE,
      ];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "html" => [
          "type" => "string",
          "title" => "HTML",
          "description" => "The HTML content to convert to Markdown",
          "required" => TRUE,
          "maxLength" => self::MAX_INPUT_SIZE,
        ],
        "heading_style" => [
          "type" => "string",
          "title" => "Heading Style",
          "description" => "Heading format: atx (# Heading) or setext (underlined)",
          "enum" => self::HEADING_STYLES,
          "default" => "atx",
        ],
        "bold_style" => [
          "type" => "string",
          "title" => "Bold Style",
          "description" => "Bold marker style",
          "enum" => self::BOLD_STYLES,
          "default" => "**",
        ],
        "italic_style" => [
          "type" => "string",
          "title" => "Italic Style",
          "description" => "Italic marker style",
          "enum" => self::ITALIC_STYLES,
          "default" => "*",
        ],
        "strip_placeholder_links" => [
          "type" => "boolean",
          "title" => "Strip Placeholder Links",
          "description" => "Remove links with empty href attributes",
          "default" => FALSE,
        ],
      ],
    ];
  }

  /**
   * Sanitizes URIs in markdown output using an allowlist approach.
   *
   * Only URIs whose scheme matches ALLOWED_URI_SCHEMES are preserved.
   * All others (javascript:, data:, vbscript:, and any future dangerous
   * schemes) are neutralized to (#). Also handles reference-style links.
   */
  private function sanitizeUris(string $markdown): string {
    $allowedPattern = implode('|', array_map('preg_quote', self::ALLOWED_URI_SCHEMES));

    // Sanitize inline links: [text](scheme:...) where scheme is not allowed.
    $markdown = (string) preg_replace_callback(
      '/\[([^\]]*)\]\(\s*([a-zA-Z][a-zA-Z0-9+.\-]*)\s*:[^)]*\)/i',
      function (array $matches) use ($allowedPattern): string {
        if (preg_match('/^(' . $allowedPattern . ')$/i', $matches[2])) {
          return $matches[0];
        }
        return '[' . $matches[1] . '](#)';
      },
      $markdown,
    );

    // Sanitize images: ![alt](scheme:...) where scheme is not allowed.
    $markdown = (string) preg_replace_callback(
      '/!\[([^\]]*)\]\(\s*([a-zA-Z][a-zA-Z0-9+.\-]*)\s*:[^)]*\)/i',
      function (array $matches) use ($allowedPattern): string {
        if (preg_match('/^(' . $allowedPattern . ')$/i', $matches[2])) {
          return $matches[0];
        }
        return '![' . $matches[1] . '](#)';
      },
      $markdown,
    );

    // Sanitize reference-style definitions: [label]: scheme:...
    $markdown = (string) preg_replace_callback(
      '/^\s*\[([^\]]+)\]:\s*([a-zA-Z][a-zA-Z0-9+.\-]*):[^\n]*$/im',
      function (array $matches) use ($allowedPattern): string {
        if (preg_match('/^(' . $allowedPattern . ')$/i', $matches[2])) {
          return $matches[0];
        }
        return '';
      },
      $markdown,
    );

    return $markdown;
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "markdown" => [
          "type" => "string",
          "description" => "The Markdown representation of the HTML content",
        ],
        "success" => [
          "type" => "boolean",
          "description" => "Whether the conversion was successful",
        ],
      ],
    ];
  }

}
