<?php

declare(strict_types=1);

namespace Drupal\flowdrop_stategraph\DTO;

/**
 * Configuration for graph execution.
 *
 * This DTO holds all configuration options for a stategraph execution,
 * including:
 * - Checkpointer type selection.
 * - Iteration limits for loop protection.
 * - Thread configuration for conversation continuity.
 * - Debug and logging options.
 *
 * Example usage:
 * @code
 * $config = new ExecutionConfig(
 *   checkpointerType: "entity",
 *   maxIterations: 50,
 *   threadId: "existing_thread_123",
 * );
 * @endcode
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class ExecutionConfig implements \JsonSerializable {

  /**
   * Constructs a new ExecutionConfig.
   *
   * @param string $checkpointerType
   *   The checkpointer type ("memory" or "entity").
   * @param int $maxIterations
   *   Maximum loop iterations before throwing exception.
   * @param string|null $threadId
   *   Optional thread ID to resume from existing conversation.
   * @param string|null $resumeFromCheckpoint
   *   Optional checkpoint ID to resume execution from.
   * @param bool $debugMode
   *   Enable verbose logging for debugging.
   * @param bool $createCheckpoints
   *   Whether to create checkpoints during execution.
   * @param array<string, mixed> $metadata
   *   Additional execution metadata.
   * @param float|null $maxExecutionTime
   *   Maximum execution time in seconds before the loop pauses the
   *   pipeline; NULL means no time limit. Populated from the orchestrator
   *   settings' common `timeout` field — which the settings form defaults
   *   to 300, so most form-saved workflows carry a five-minute budget.
   *   A budget pause stamps a `paused_reason` on the pipeline's execution
   *   context, which flowdrop_interrupt converts into a pending system
   *   Pause signal — `POST .../resume` (or inbox resolution) re-executes
   *   the pipeline with a fresh budget window. Authors of long-running
   *   workflows should still raise the timeout or set it to 0 (no limit)
   *   rather than rely on repeated resumes.
   */
  public function __construct(
    public readonly string $checkpointerType = "memory",
    public readonly int $maxIterations = 100,
    public readonly ?string $threadId = NULL,
    public readonly ?string $resumeFromCheckpoint = NULL,
    public readonly bool $debugMode = FALSE,
    public readonly bool $createCheckpoints = TRUE,
    public readonly array $metadata = [],
    public readonly ?float $maxExecutionTime = NULL,
  ) {}

  /**
   * Creates a config for memory-based execution (development/testing).
   *
   * @param int $maxIterations
   *   Maximum iterations.
   *
   * @return self
   *   A new ExecutionConfig with memory checkpointer.
   */
  public static function forDevelopment(int $maxIterations = 100): self {
    return new self(
      checkpointerType: "memory",
      maxIterations: $maxIterations,
      debugMode: TRUE,
      createCheckpoints: TRUE,
    );
  }

  /**
   * Creates a config for entity-based execution (production).
   *
   * @param int $maxIterations
   *   Maximum iterations.
   * @param string|null $threadId
   *   Optional thread ID for conversation continuity.
   *
   * @return self
   *   A new ExecutionConfig with entity checkpointer.
   */
  public static function forProduction(
    int $maxIterations = 100,
    ?string $threadId = NULL,
  ): self {
    return new self(
      checkpointerType: "entity",
      maxIterations: $maxIterations,
      threadId: $threadId,
      debugMode: FALSE,
      createCheckpoints: TRUE,
    );
  }

  /**
   * Creates a config for resuming from a checkpoint.
   *
   * @param string $checkpointId
   *   The checkpoint ID to resume from.
   * @param string $checkpointerType
   *   The checkpointer type to use.
   *
   * @return self
   *   A new ExecutionConfig configured for resume.
   */
  public static function forResume(
    string $checkpointId,
    string $checkpointerType = "entity",
  ): self {
    return new self(
      checkpointerType: $checkpointerType,
      resumeFromCheckpoint: $checkpointId,
      createCheckpoints: TRUE,
    );
  }

  /**
   * Creates a new config with updated thread ID.
   *
   * @param string $threadId
   *   The thread ID.
   *
   * @return self
   *   A new ExecutionConfig with updated thread ID.
   */
  public function withThreadId(string $threadId): self {
    return new self(
      checkpointerType: $this->checkpointerType,
      maxIterations: $this->maxIterations,
      threadId: $threadId,
      resumeFromCheckpoint: $this->resumeFromCheckpoint,
      debugMode: $this->debugMode,
      createCheckpoints: $this->createCheckpoints,
      metadata: $this->metadata,
      maxExecutionTime: $this->maxExecutionTime,
    );
  }

  /**
   * Creates a new config with added metadata.
   *
   * @param array<string, mixed> $metadata
   *   Additional metadata to merge.
   *
   * @return self
   *   A new ExecutionConfig with merged metadata.
   */
  public function withMetadata(array $metadata): self {
    return new self(
      checkpointerType: $this->checkpointerType,
      maxIterations: $this->maxIterations,
      threadId: $this->threadId,
      resumeFromCheckpoint: $this->resumeFromCheckpoint,
      debugMode: $this->debugMode,
      createCheckpoints: $this->createCheckpoints,
      metadata: array_merge($this->metadata, $metadata),
      maxExecutionTime: $this->maxExecutionTime,
    );
  }

  /**
   * Creates an ExecutionConfig from an array.
   *
   * Accepts both camelCase keys (the {@see toArray()} shape used by
   * programmatic callers) and snake_case keys (the shape produced by the
   * orchestrator settings form and SessionExecutionService). camelCase wins
   * when both are present.
   *
   * @param array<string, mixed> $data
   *   The config data.
   *
   * @return self
   *   A new ExecutionConfig instance.
   */
  public static function fromArray(array $data): self {
    $threadId = $data["threadId"] ?? $data["thread_id"] ?? NULL;
    $resumeFromCheckpoint = $data["resumeFromCheckpoint"]
      ?? $data["resume_from_checkpoint"]
      ?? NULL;
    // The time budget reuses the orchestrator settings' common `timeout`
    // field — there is deliberately no second knob.
    $maxExecutionTime = $data["maxExecutionTime"]
      ?? $data["max_execution_time"]
      ?? $data["timeout"]
      ?? NULL;

    return new self(
      checkpointerType: (string) ($data["checkpointerType"] ?? $data["checkpointer_type"] ?? "memory"),
      maxIterations: (int) ($data["maxIterations"] ?? $data["max_iterations"] ?? 100),
      threadId: $threadId !== NULL && $threadId !== "" ? (string) $threadId : NULL,
      resumeFromCheckpoint: $resumeFromCheckpoint !== NULL && $resumeFromCheckpoint !== ""
        ? (string) $resumeFromCheckpoint
        : NULL,
      debugMode: (bool) ($data["debugMode"] ?? $data["debug_mode"] ?? FALSE),
      createCheckpoints: (bool) ($data["createCheckpoints"] ?? $data["create_checkpoints"] ?? TRUE),
      metadata: is_array($data["metadata"] ?? NULL) ? $data["metadata"] : [],
      maxExecutionTime: is_numeric($maxExecutionTime) && (float) $maxExecutionTime > 0
        ? (float) $maxExecutionTime
        : NULL,
    );
  }

  /**
   * Converts the config to an array.
   *
   * @return array<string, mixed>
   *   The config as an array.
   */
  public function toArray(): array {
    return [
      "checkpointerType" => $this->checkpointerType,
      "maxIterations" => $this->maxIterations,
      "threadId" => $this->threadId,
      "resumeFromCheckpoint" => $this->resumeFromCheckpoint,
      "debugMode" => $this->debugMode,
      "createCheckpoints" => $this->createCheckpoints,
      "metadata" => $this->metadata,
      "maxExecutionTime" => $this->maxExecutionTime,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function jsonSerialize(): mixed {
    return $this->toArray();
  }

}
