<?php

declare(strict_types=1);

namespace Drupal\flowdrop_stategraph\Checkpointer;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\flowdrop_stategraph\DTO\Checkpoint;
use Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpointInterface;
use Drupal\flowdrop_stategraph\Exception\CheckpointException;
use Psr\Log\LoggerInterface;

/**
 * Entity-based checkpointer for production use.
 *
 * This checkpointer stores checkpoints using Drupal's entity system,
 * providing:
 * - Persistent storage across requests.
 * - Database-backed durability.
 * - Integration with Drupal's entity API (views, access control).
 * - JSON field storage for efficient state serialization.
 *
 * @see \Drupal\flowdrop_stategraph\Entity\FlowDropStateCheckpoint
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class EntityCheckpointer implements CheckpointerInterface {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  private readonly EntityTypeManagerInterface $entityTypeManager;

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

  /**
   * Constructs a new EntityCheckpointer.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   */
  public function __construct(
    EntityTypeManagerInterface $entityTypeManager,
    LoggerInterface $logger,
  ) {
    $this->entityTypeManager = $entityTypeManager;
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public function get(string $checkpointId): ?Checkpoint {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      $entities = $storage->loadByProperties(['checkpoint_id' => $checkpointId]);
      $entity = reset($entities);

      if ($entity === FALSE || !($entity instanceof FlowDropStateCheckpointInterface)) {
        return NULL;
      }

      $this->logger->debug("Entity checkpointer: retrieved checkpoint @id", [
        "@id" => $checkpointId,
      ]);

      return $entity->toCheckpointDto();
    }
    catch (\Exception $e) {
      throw new CheckpointException(
        message: "Failed to retrieve checkpoint: " . $e->getMessage(),
        checkpointId: $checkpointId,
        operation: "get",
        previous: $e,
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function put(Checkpoint $checkpoint): string {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      // Check if checkpoint already exists (update case).
      $existingEntities = $storage->loadByProperties([
        'checkpoint_id' => $checkpoint->id,
      ]);
      $existingEntity = reset($existingEntities);

      if ($existingEntity !== FALSE && $existingEntity instanceof FlowDropStateCheckpointInterface) {
        // Update existing entity.
        $existingEntity->setThreadId($checkpoint->threadId);
        $existingEntity->setGraphState($checkpoint->state);
        $existingEntity->setNodeId($checkpoint->nodeId);
        $existingEntity->setMetadata($checkpoint->metadata);
        $existingEntity->save();

        $this->logger->debug("Entity checkpointer: updated checkpoint @id", [
          "@id" => $checkpoint->id,
        ]);
      }
      else {
        // Create new entity.
        // Note: state_data and metadata fields are string_long fields that
        // expect JSON-encoded strings, not arrays. Use Json::encode() for
        // proper storage.
        $entity = $storage->create([
          'checkpoint_id' => $checkpoint->id,
          'thread_id' => $checkpoint->threadId,
          'state_data' => Json::encode($checkpoint->state->toArray()),
          'node_id' => $checkpoint->nodeId,
          'metadata' => Json::encode($checkpoint->metadata),
        ]);

        // Handle parent checkpoint reference.
        if ($checkpoint->parentCheckpointId !== NULL) {
          $parentEntities = $storage->loadByProperties([
            'checkpoint_id' => $checkpoint->parentCheckpointId,
          ]);
          $parentEntity = reset($parentEntities);
          if ($parentEntity !== FALSE && $entity instanceof FieldableEntityInterface) {
            $entity->set('parent_checkpoint', $parentEntity->id());
          }
        }

        $entity->save();

        $this->logger->debug("Entity checkpointer: created checkpoint @id for thread @thread", [
          "@id" => $checkpoint->id,
          "@thread" => $checkpoint->threadId,
        ]);
      }

      return $checkpoint->id;
    }
    catch (\Exception $e) {
      throw CheckpointException::saveFailed($checkpoint->id, $e);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function list(string $threadId, int $limit = 100): array {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      $query = $storage->getQuery()
        ->accessCheck(TRUE)
        ->condition('thread_id', $threadId)
        ->sort('created', 'DESC')
        ->sort('id', 'DESC')
        ->range(0, $limit);

      $ids = $query->execute();

      if (empty($ids)) {
        return [];
      }

      $entities = $storage->loadMultiple($ids);
      $checkpoints = [];

      foreach ($entities as $entity) {
        if ($entity instanceof FlowDropStateCheckpointInterface) {
          $checkpoints[] = $entity->toCheckpointDto();
        }
      }

      return $checkpoints;
    }
    catch (\Exception $e) {
      throw new CheckpointException(
        message: "Failed to list checkpoints: " . $e->getMessage(),
        operation: "list",
        previous: $e,
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getLatest(string $threadId): ?Checkpoint {
    $checkpoints = $this->list($threadId, 1);
    return $checkpoints[0] ?? NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function delete(string $checkpointId): bool {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      $entities = $storage->loadByProperties(['checkpoint_id' => $checkpointId]);
      $entity = reset($entities);

      if ($entity === FALSE) {
        return FALSE;
      }

      $entity->delete();

      $this->logger->debug("Entity checkpointer: deleted checkpoint @id", [
        "@id" => $checkpointId,
      ]);

      return TRUE;
    }
    catch (\Exception $e) {
      throw new CheckpointException(
        message: "Failed to delete checkpoint: " . $e->getMessage(),
        checkpointId: $checkpointId,
        operation: "delete",
        previous: $e,
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteThread(string $threadId): int {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      $query = $storage->getQuery()
        ->accessCheck(TRUE)
        ->condition('thread_id', $threadId);

      $ids = $query->execute();

      if (empty($ids)) {
        return 0;
      }

      $entities = $storage->loadMultiple($ids);
      $count = count($entities);

      $storage->delete($entities);

      $this->logger->debug("Entity checkpointer: deleted @count checkpoints for thread @thread", [
        "@count" => $count,
        "@thread" => $threadId,
      ]);

      return $count;
    }
    catch (\Exception $e) {
      throw new CheckpointException(
        message: "Failed to delete thread checkpoints: " . $e->getMessage(),
        operation: "deleteThread",
        previous: $e,
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function clear(): int {
    try {
      $storage = $this->entityTypeManager->getStorage('state_checkpoint');

      $query = $storage->getQuery()->accessCheck(TRUE);
      $ids = $query->execute();

      if (empty($ids)) {
        return 0;
      }

      $entities = $storage->loadMultiple($ids);
      $count = count($entities);

      $storage->delete($entities);

      $this->logger->info("Entity checkpointer: cleared all @count checkpoints", [
        "@count" => $count,
      ]);

      return $count;
    }
    catch (\Exception $e) {
      throw new CheckpointException(
        message: "Failed to clear checkpoints: " . $e->getMessage(),
        operation: "clear",
        previous: $e,
      );
    }
  }

}
