<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

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 with deliberately distinct failure contracts:
 * - strict (default): assert-or-fail. Standard JSON decode only; a parse
 *   failure throws, which the runtime turns into an error-status output routed
 *   down the node's error edge (if wired) or fails the run. Output is
 *   {data} only.
 * - tolerant: best-effort. Tries direct JSON decode first; if that fails,
 *   attempts to extract JSON from the first markdown code fence in the input
 *   (useful for LLM output that wraps JSON in prose or fences). Never throws;
 *   reports the outcome via {data, success, parse_source} so the author can
 *   branch on it.
 *
 * Valid non-container JSON (numbers, strings, booleans, null) is accepted as
 * data in both modes.
 *
 * @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: "2.0.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 {
    $tolerant = $params->getString("parse_mode", "strict") === "tolerant";
    $outcome = $this->tryDecode($params->get("json"), $tolerant);

    if (!$tolerant) {
      // Strict mode is assert-or-fail. A \RuntimeException is caught by
      // NodeRuntimeService and converted to an error-status output, which the
      // orchestrator routes down the node's error edge (if the author wired
      // one) or uses to fail the run. Parse failure is deterministic, so the
      // exception is not retryable.
      if (!$outcome['ok']) {
        throw new \RuntimeException('json_to_data: input is not valid JSON (strict mode).');
      }
      return ["data" => $outcome['data']];
    }

    // Tolerant mode never throws; it reports the outcome so downstream nodes
    // can branch on it.
    return [
      "data" => $outcome['data'],
      "success" => $outcome['ok'],
      "parse_source" => $outcome['source'],
    ];
  }

  /**
   * Attempt to decode the input into structured data.
   *
   * Already-decoded arrays/objects pass through. Strings are JSON-decoded
   * directly; in tolerant mode a failed direct decode falls back to extracting
   * JSON from the first markdown code fence (```json ... ``` or ``` ... ```).
   *
   * @param mixed $input
   *   The input (JSON string or already-decoded data).
   * @param bool $tolerant
   *   Whether to attempt code-fence extraction on a failed direct decode.
   *
   * @return array{data: mixed, ok: bool, source: string|null}
   *   The decoded data (NULL on failure), whether decoding succeeded, and how
   *   the JSON was found ('direct'|'code_fence', or NULL on failure).
   */
  private function tryDecode(mixed $input, bool $tolerant): array {
    // Already-structured input passes through.
    if (is_array($input) || is_object($input)) {
      return ['data' => $input, 'ok' => TRUE, 'source' => 'direct'];
    }

    if (is_string($input)) {
      $trimmed = trim($input);

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

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

    return ['data' => NULL, 'ok' => FALSE, 'source' => 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", "string", "number", "integer", "boolean", "null"],
          "description" => "The decoded data. Objects/arrays for structured JSON; scalars for JSON primitives. NULL when tolerant mode fails to parse.",
        ],
        "success" => [
          "type" => "boolean",
          "description" => "Whether the JSON was successfully parsed. Tolerant mode only — strict mode throws on failure and omits this key.",
        ],
        "parse_source" => [
          "type" => "string",
          "description" => "How the JSON was found: 'direct' (valid JSON as-is) or 'code_fence' (extracted from markdown), or NULL on failure. Tolerant mode only.",
        ],
      ],
    ];
  }

}
