<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\Component\Serialization\Yaml;
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;

/**
 * Extracts structured data from a markdown string.
 *
 * Parses LLM-generated markdown into discrete fields — title, body,
 * sections, front matter — without requiring external libraries.
 * Useful as a bridge between a Chat node and downstream nodes that
 * expect structured data (e.g. EntitySave, DataShaper).
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "markdown_extractor",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("Markdown Extractor"),
  description: "Extract title, body, sections, and front matter from a markdown string",
  version: "1.0.0"
)]
class MarkdownExtractor 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');

    $front_matter = $this->extractFrontMatter($text);
    // Remove front matter block from text before further parsing.
    $body_text = preg_replace('/^---\r?\n.*?\r?\n---\r?\n/s', '', $text, 1) ?? $text;

    $title = $this->extractTitle($body_text);
    $body = $this->extractBody($body_text);
    $sections = $this->extractSections($body_text);
    $plain_text = $this->stripMarkdown($body_text);

    return [
      'title' => $title,
      'body' => $body,
      'sections' => $sections,
      'front_matter' => $front_matter,
      'plain_text' => $plain_text,
      'raw' => $text,
    ];
  }

  /**
   * Extracts YAML or JSON front matter from between leading --- delimiters.
   *
   * @param string $text
   *   Raw markdown text.
   *
   * @return array
   *   Parsed front matter as associative array, empty array if none.
   */
  private function extractFrontMatter(string $text): array {
    if (!preg_match('/^---\r?\n(.*?)\r?\n---\r?\n/s', $text, $matches)) {
      return [];
    }

    $raw = trim($matches[1]);

    // Try JSON first (some LLMs output JSON front matter).
    $json = json_decode($raw, TRUE);
    if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
      return $json;
    }

    // Fall back to YAML.
    try {
      $parsed = Yaml::decode($raw);
      return is_array($parsed) ? $parsed : [];
    }
    catch (\Exception) {
      return [];
    }
  }

  /**
   * Extracts the first H1 heading as the title.
   *
   * @param string $text
   *   Markdown text (front matter already removed).
   *
   * @return string
   *   The title text, empty string if no H1 found.
   */
  private function extractTitle(string $text): string {
    if (preg_match('/^#\s+(.+)$/m', $text, $matches)) {
      return trim($matches[1]);
    }
    return '';
  }

  /**
   * Returns content after stripping the first H1 heading.
   *
   * @param string $text
   *   Markdown text (front matter already removed).
   *
   * @return string
   *   Body text with leading H1 removed and trimmed.
   */
  private function extractBody(string $text): string {
    // Remove only the first H1 line.
    $body = preg_replace('/^#\s+.+\r?\n?/m', '', $text, 1) ?? $text;
    return trim($body);
  }

  /**
   * Splits text into sections by heading (any level ## through ######).
   *
   * Each section is an associative array with 'heading', 'level', 'content'.
   * Text before the first heading is returned as a section with heading = ''.
   *
   * @param string $text
   *   Markdown text (front matter already removed).
   *
   * @return array
   *   Array of section objects.
   */
  private function extractSections(string $text): array {
    // Remove first H1 before splitting into sections.
    $text = preg_replace('/^#\s+.+\r?\n?/m', '', $text, 1) ?? $text;

    // Split on ## through ###### headings, keeping the delimiter.
    $parts = preg_split('/^(#{2,6})\s+(.+)$/m', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
    if ($parts === FALSE) {
      return [];
    }

    $sections = [];

    // First element is any content before the first heading.
    $preamble = trim(array_shift($parts));
    if ($preamble !== '') {
      $sections[] = ['heading' => '', 'level' => 0, 'content' => $preamble];
    }

    // Remaining elements come in triplets: hashes, heading text, content.
    $chunks = array_chunk($parts, 3);
    foreach ($chunks as $chunk) {
      if (count($chunk) < 3) {
        continue;
      }
      [$hashes, $heading, $content] = $chunk;
      $sections[] = [
        'heading' => trim($heading),
        'level' => strlen($hashes),
        'content' => trim($content),
      ];
    }

    return $sections;
  }

  /**
   * Strips common markdown syntax, returning plain text.
   *
   * Handles: headings, bold, italic, inline code, links, images,
   * horizontal rules, and blockquote markers.
   *
   * @param string $text
   *   Markdown text.
   *
   * @return string
   *   Plain text.
   */
  private function stripMarkdown(string $text): string {
    // Headings.
    $text = preg_replace('/^#{1,6}\s+/m', '', $text) ?? $text;
    // Bold and italic (**, __, *, _).
    $text = preg_replace('/(\*{1,3}|_{1,3})(.+?)\1/', '$2', $text) ?? $text;
    // Inline code.
    $text = preg_replace('/`([^`]+)`/', '$1', $text) ?? $text;
    // Fenced code blocks.
    $text = preg_replace('/^```.*?^```/ms', '', $text) ?? $text;
    // Links: [text](url) → text.
    $text = preg_replace('/\[([^\]]+)\]\([^)]+\)/', '$1', $text) ?? $text;
    // Images: ![alt](url) → alt.
    $text = preg_replace('/!\[([^\]]*)\]\([^)]+\)/', '$1', $text) ?? $text;
    // Blockquote markers.
    $text = preg_replace('/^>\s?/m', '', $text) ?? $text;
    // Horizontal rules.
    $text = preg_replace('/^[-*_]{3,}\s*$/m', '', $text) ?? $text;

    return trim($text);
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'text' => [
          'type' => 'string',
          'title' => 'Markdown Text',
          'description' => 'The markdown string to parse',
          'default' => '',
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'title' => [
          'type' => 'string',
          'description' => 'First H1 heading',
        ],
        'body' => [
          'type' => 'string',
          'description' => 'All content after the first H1 (markdown preserved)',
        ],
        'sections' => [
          'type' => 'array',
          'description' => 'Content split by headings. Each item has heading, level (int), and content keys.',
        ],
        'front_matter' => [
          'type' => 'object',
          'description' => 'Parsed YAML/JSON front matter from between leading --- delimiters',
        ],
        'plain_text' => [
          'type' => 'string',
          'description' => 'Full text with markdown syntax stripped',
        ],
        'raw' => [
          'type' => 'string',
          'description' => 'Original input unchanged',
        ],
      ],
    ];
  }

}
