<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\Component\Serialization\Json;
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 JSON string to structured data.
 *
 * Takes a JSON string as input and outputs a decoded PHP array/object.
 *
 * Supports two parse modes:
 * - strict (default): standard JSON decode only.
 * - tolerant: tries direct JSON decode first; if that fails, attempts to
 *   extract JSON from the first markdown code fence in the input. Useful when
 *   the input comes from an LLM that may wrap JSON in prose or code fences.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "json_to_data",
  label: new \Drupal\Core\StringTranslation\TranslatableMarkup("JSON to Data"),
  description: "Convert JSON string to structured data (array/object)",
  version: "1.1.0"
)]
class JsonToData 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 {
    // Accept any input.
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $input = $params->get("json");
    $mode = $params->getString("parse_mode", "strict");

    if ($mode === "tolerant") {
      [$result, $source] = $this->convertToDataTolerant($input);
    }
    else {
      $result = $this->convertToData($input);
      $source = NULL;
    }

    $output = [
      "data" => $result,
      "success" => $result !== NULL,
    ];

    if ($mode === "tolerant") {
      $output["parse_source"] = $source;
    }

    return $output;
  }

  /**
   * Convert JSON string to structured data (strict).
   *
   * @param mixed $input
   *   The input (JSON string or already-decoded data).
   *
   * @return mixed
   *   The decoded data, or NULL on failure.
   */
  private function convertToData(mixed $input): mixed {
    // If already an array/object, return as-is.
    if (is_array($input) || is_object($input)) {
      return $input;
    }

    // If not a string, return empty array.
    if (!is_string($input)) {
      return [];
    }

    // Decode JSON string.
    $result = JSON::decode($input);

    if ($result === NULL && json_last_error() !== JSON_ERROR_NONE) {
      return [];
    }

    return $result;
  }

  /**
   * Convert to data with tolerant LLM-output handling.
   *
   * Tries direct JSON decode first. If that fails, looks for the first
   * markdown code fence (```json ... ``` or ``` ... ```) and tries to decode
   * its contents.
   *
   * @param mixed $input
   *   The input value.
   *
   * @return array{mixed, string|null}
   *   A tuple of [decoded data or NULL, source ('direct'|'code_fence'|NULL)].
   */
  private function convertToDataTolerant(mixed $input): array {
    // If already structured, return as-is.
    if (is_array($input) || is_object($input)) {
      return [$input, 'direct'];
    }

    if (!is_string($input)) {
      return [NULL, NULL];
    }

    $trimmed = trim($input);

    // Attempt 1: direct JSON decode.
    $result = json_decode($trimmed, TRUE);
    if (json_last_error() === JSON_ERROR_NONE) {
      return [$result, 'direct'];
    }

    // Attempt 2: extract from the first markdown code fence.
    if (preg_match('/^```(?:json)?\s*\r?\n(.*?)\r?\n```/ms', $trimmed, $matches)) {
      $fenced = trim($matches[1]);
      $result = json_decode($fenced, TRUE);
      if (json_last_error() === JSON_ERROR_NONE) {
        return [$result, 'code_fence'];
      }
    }

    return [NULL, NULL];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "json" => [
          "type" => "string",
          "title" => "JSON String",
          "description" => "The JSON string to decode into structured data",
          "default" => "",
        ],
        "parse_mode" => [
          "type" => "string",
          "title" => "Parse Mode",
          "description" => "strict: standard JSON decode only. tolerant: also tries to extract JSON from a markdown code fence — useful for LLM responses that mix prose and JSON.",
          "default" => "strict",
          "enum" => ["strict", "tolerant"],
          "options" => [
            ["value" => "strict", "label" => "Strict"],
            ["value" => "tolerant", "label" => "Tolerant (LLM output)"],
          ],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "data" => [
          "type" => ["object", "array", "null"],
          "description" => "The decoded structured data (array/object)",
        ],
        "success" => [
          "type" => "boolean",
          "description" => "Whether the conversion was successful",
        ],
        "parse_source" => [
          "type" => "string",
          "description" => "How the JSON was found: 'direct' (valid JSON as-is) or 'code_fence' (extracted from markdown). Only present in tolerant mode.",
        ],
      ],
    ];
  }

}
