<?php

declare(strict_types=1);

namespace Drupal\flowdrop_stategraph\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\flowdrop_stategraph\Attribute\StateField;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\DTO\GraphState;
use Drupal\flowdrop_stategraph\DTO\StateUpdate;
use Drupal\flowdrop_stategraph\Exception\MaxIterationsException;
use Drupal\flowdrop_stategraph\Exception\StateValidationException;
use Drupal\flowdrop_stategraph\Reducer\AppendReducer;
use Drupal\flowdrop_stategraph\Reducer\MergeReducer;
use Drupal\flowdrop_stategraph\Reducer\ReducerInterface;
use Drupal\flowdrop_stategraph\Reducer\ReplaceReducer;
use Psr\Log\LoggerInterface;

/**
 * State lifecycle management service.
 *
 * The StateManager is responsible for:
 * - Applying state updates using configured reducers.
 * - Creating checkpoints from current state.
 * - Enforcing iteration limits for loop protection.
 * - Serializing/deserializing state for persistence.
 *
 * This service is the central coordinator for all state operations
 * within the StateGraph orchestrator.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class StateManager implements StateManagerInterface {

  /**
   * Map of reducer ID to reducer instance.
   *
   * @var array<string, \Drupal\flowdrop_stategraph\Reducer\ReducerInterface>
   */
  private readonly array $reducers;

  /**
   * The logger service.
   *
   * @var \Psr\Log\LoggerInterface
   */
  private readonly LoggerInterface $logger;

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  private readonly ConfigFactoryInterface $configFactory;

  /**
   * Constructs a new StateManager.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   */
  public function __construct(
    LoggerInterface $logger,
    ConfigFactoryInterface $configFactory,
  ) {
    $this->logger = $logger;
    $this->configFactory = $configFactory;

    // Initialize built-in reducers.
    $this->reducers = $this->initializeReducers();
  }

  /**
   * Initializes the built-in reducer instances.
   *
   * @return array<string, \Drupal\flowdrop_stategraph\Reducer\ReducerInterface>
   *   Map of reducer ID to instance.
   */
  private function initializeReducers(): array {
    $appendReducer = new AppendReducer();
    $replaceReducer = new ReplaceReducer();
    $mergeReducer = new MergeReducer();

    return [
      $appendReducer->getId() => $appendReducer,
      $replaceReducer->getId() => $replaceReducer,
      $mergeReducer->getId() => $mergeReducer,
    ];
  }

  /**
   * Creates a new initial state.
   *
   * @param string|null $threadId
   *   Optional thread ID for conversation continuity.
   * @param array<string, mixed> $initialData
   *   Optional initial data to populate the state.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   A new GraphState instance.
   */
  public function createInitialState(
    ?string $threadId = NULL,
    array $initialData = [],
  ): GraphState {
    $effectiveThreadId = $threadId ?? $this->generateThreadId();

    $state = new GraphState(
      threadId: $effectiveThreadId,
      data: $initialData,
      metadata: [
        "created_at" => time(),
        "workflow_start" => TRUE,
      ],
    );

    $this->logger->debug("StateManager: created initial state for thread @thread", [
      "@thread" => $effectiveThreadId,
    ]);

    return $state;
  }

  /**
   * Applies a state update to the current state.
   *
   * This method uses the configured reducers for each field to merge
   * the update into the existing state, returning a new immutable state.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $currentState
   *   The current state.
   * @param \Drupal\flowdrop_stategraph\DTO\StateUpdate $update
   *   The update to apply.
   * @param int|null $maxIterations
   *   Optional max iterations override (uses config default if NULL).
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   A new GraphState with the update applied.
   *
   * @throws \Drupal\flowdrop_stategraph\Exception\MaxIterationsException
   *   When iteration limit is exceeded.
   */
  public function applyUpdate(
    GraphState $currentState,
    StateUpdate $update,
    ?int $maxIterations = NULL,
  ): GraphState {
    // Apply the update using reducers.
    $newState = $currentState->withUpdate($update, $this->reducers);

    // Check iteration limit.
    $effectiveMaxIterations = $maxIterations ?? $this->getMaxIterations();
    if ($newState->iterationCount > $effectiveMaxIterations) {
      throw new MaxIterationsException(
        maxIterations: $effectiveMaxIterations,
        currentIterations: $newState->iterationCount,
        nodeId: $update->getSourceNodeId() ?? "",
      );
    }

    $this->logger->debug("StateManager: applied update from node @node, iteration @iter", [
      "@node" => $update->getSourceNodeId() ?? "unknown",
      "@iter" => $newState->iterationCount,
    ]);

    return $newState;
  }

  /**
   * Increments the iteration count on the state.
   *
   * This should be called once per graph traversal iteration.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The current state.
   * @param int|null $maxIterations
   *   Optional max iterations override.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   A new state with incremented iteration count.
   *
   * @throws \Drupal\flowdrop_stategraph\Exception\MaxIterationsException
   *   When iteration limit is exceeded.
   */
  public function incrementIteration(
    GraphState $state,
    ?int $maxIterations = NULL,
  ): GraphState {
    $newState = $state->withIncrementedIteration();

    $effectiveMaxIterations = $maxIterations ?? $this->getMaxIterations();
    if ($newState->iterationCount > $effectiveMaxIterations) {
      throw new MaxIterationsException(
        maxIterations: $effectiveMaxIterations,
        currentIterations: $newState->iterationCount,
        nodeId: $state->currentNodeId,
      );
    }

    return $newState;
  }

  /**
   * Creates a checkpoint from the current state.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The current state.
   * @param string $nodeId
   *   The current node ID.
   * @param string|null $parentCheckpointId
   *   Optional parent checkpoint ID.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\Checkpoint
   *   A new Checkpoint instance.
   */
  public function createCheckpoint(
    GraphState $state,
    string $nodeId,
    ?string $parentCheckpointId = NULL,
  ): Checkpoint {
    $checkpoint = Checkpoint::create($state, $nodeId, $parentCheckpointId);

    $this->logger->debug("StateManager: created checkpoint @id at node @node", [
      "@id" => $checkpoint->id,
      "@node" => $nodeId,
    ]);

    return $checkpoint;
  }

  /**
   * Restores state from a checkpoint.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\Checkpoint $checkpoint
   *   The checkpoint to restore from.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   The restored GraphState.
   */
  public function restoreFromCheckpoint(Checkpoint $checkpoint): GraphState {
    $this->logger->debug("StateManager: restoring state from checkpoint @id", [
      "@id" => $checkpoint->id,
    ]);

    return $checkpoint->state;
  }

  /**
   * Marks the state as complete.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The current state.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   A new state marked as complete.
   */
  public function markComplete(GraphState $state): GraphState {
    $completedState = $state->withComplete();

    $this->logger->debug("StateManager: marked state complete for thread @thread", [
      "@thread" => $state->threadId,
    ]);

    return $completedState;
  }

  /**
   * Marks the state with an error.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The current state.
   * @param string $errorMessage
   *   The error message.
   *
   * @return \Drupal\flowdrop_stategraph\DTO\GraphState
   *   A new state with the error set.
   */
  public function markError(GraphState $state, string $errorMessage): GraphState {
    $errorState = $state->withError($errorMessage);

    $this->logger->error("StateManager: marked state error for thread @thread: @error", [
      "@thread" => $state->threadId,
      "@error" => $errorMessage,
    ]);

    return $errorState;
  }

  /**
   * Validates that state contains required fields.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The state to validate.
   *
   * @throws \Drupal\flowdrop_stategraph\Exception\StateValidationException
   *   When validation fails.
   */
  public function validateState(GraphState $state): void {
    $errors = [];

    // Check thread ID is set.
    if ($state->threadId === "") {
      $errors[] = "Thread ID is required";
    }

    // Check for negative iteration count.
    if ($state->iterationCount < 0) {
      $errors[] = "Iteration count cannot be negative";
    }

    if (!empty($errors)) {
      throw new StateValidationException("State validation failed", $errors);
    }
  }

  /**
   * Gets a reducer by ID.
   *
   * @param string $reducerId
   *   The reducer ID.
   *
   * @return \Drupal\flowdrop_stategraph\Reducer\ReducerInterface
   *   The reducer instance.
   *
   * @throws \InvalidArgumentException
   *   When reducer is not found.
   */
  public function getReducer(string $reducerId): ReducerInterface {
    if (!isset($this->reducers[$reducerId])) {
      throw new \InvalidArgumentException(
        "Reducer '{$reducerId}' not found. Available: " . implode(", ", array_keys($this->reducers))
      );
    }

    return $this->reducers[$reducerId];
  }

  /**
   * Gets all available reducers.
   *
   * @return array<string, \Drupal\flowdrop_stategraph\Reducer\ReducerInterface>
   *   Map of reducer ID to instance.
   */
  public function getReducers(): array {
    return $this->reducers;
  }

  /**
   * Gets the configured max iterations limit.
   *
   * @return int
   *   The maximum iterations.
   */
  public function getMaxIterations(): int {
    $config = $this->configFactory->get('flowdrop_stategraph.settings');
    $value = $config->get('max_iterations');
    return $value !== NULL ? (int) $value : 100;
  }

  /**
   * Generates a unique thread ID.
   *
   * @return string
   *   A unique thread ID.
   */
  private function generateThreadId(): string {
    return "thread_" . bin2hex(random_bytes(8));
  }

  /**
   * Extracts state field metadata using reflection.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\GraphState $state
   *   The state instance to inspect.
   *
   * @return array<string, array{reducer: string, description: string, required: bool}>
   *   Array of field metadata keyed by property name.
   */
  public function getStateFieldMetadata(GraphState $state): array {
    $reflection = new \ReflectionClass($state);
    $metadata = [];

    foreach ($reflection->getProperties() as $property) {
      $attributes = $property->getAttributes(StateField::class);

      if (!empty($attributes)) {
        $stateField = $attributes[0]->newInstance();
        $metadata[$property->getName()] = [
          "reducer" => $stateField->reducer,
          "description" => $stateField->description,
          "required" => $stateField->required,
        ];
      }
    }

    return $metadata;
  }

}
