<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Service;

use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Psr\Log\LoggerInterface;
use Drupal\flowdrop_interrupt\Event\InterruptCancelledEvent;
use Drupal\flowdrop_interrupt\Event\InterruptCreatedEvent;
use Drupal\flowdrop_interrupt\Event\InterruptExpiredEvent;
use Drupal\flowdrop_interrupt\Event\InterruptResolvedEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Service for managing interrupt requests.
 *
 * Handles the full lifecycle of interrupt entities including creation,
 * resolution, querying, and cleanup.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class InterruptManager implements InterruptManagerInterface {

  /**
   * The interrupt entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected EntityStorageInterface $storage;

  /**
   * Constructs a new InterruptManager.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   The current user.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Lock\LockBackendInterface $lock
   *   The lock backend.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly AccountProxyInterface $currentUser,
    protected readonly LoggerInterface $logger,
    protected readonly EventDispatcherInterface $eventDispatcher,
    protected readonly LockBackendInterface $lock,
  ) {
    $this->storage = $this->entityTypeManager->getStorage("flowdrop_interrupt");
  }

  /**
   * {@inheritdoc}
   */
  public function createInterrupt(
    InterruptType $type,
    string $workflow_id,
    string $node_id,
    string $message,
    array $schema = [],
    array $options = [],
    array $context = [],
    mixed $default_value = NULL,
    ?string $pipeline_id = NULL,
    ?int $session_id = NULL,
    ?int $expires_in = NULL,
  ): FlowDropInterruptInterface {
    /** @var \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt */
    $interrupt = $this->storage->create([
      "workflow_id" => $workflow_id,
      "node_id" => $node_id,
      "type" => $type->value,
      "status" => InterruptStatus::Pending->value,
      "message" => $message,
      "uid" => $this->currentUser->id(),
    ]);

    $interrupt->setSchema($schema);
    $interrupt->setOptions($options);
    $interrupt->setContext($context);
    $interrupt->setDefaultValue($default_value);

    if ($pipeline_id !== NULL) {
      $interrupt->setPipelineId($pipeline_id);
    }

    if ($session_id !== NULL) {
      $interrupt->setSessionId($session_id);
    }

    if ($expires_in !== NULL && $expires_in > 0) {
      $interrupt->setExpires(time() + $expires_in);
    }

    $interrupt->save();

    $this->logger->info("Created @type interrupt @id for workflow @workflow, node @node", [
      "@type" => $type->value,
      "@id" => $interrupt->uuid(),
      "@workflow" => $workflow_id,
      "@node" => $node_id,
    ]);

    // Dispatch event for integrations.
    $this->eventDispatcher->dispatch(
      new InterruptCreatedEvent(
        interrupt: $interrupt,
        type: $type->value,
        workflowId: $workflow_id,
        nodeId: $node_id,
        sessionId: $session_id,
        pipelineId: $pipeline_id,
      ),
      InterruptCreatedEvent::NAME
    );

    return $interrupt;
  }

  /**
   * {@inheritdoc}
   */
  public function resolveInterrupt(
    string $interrupt_id,
    mixed $response_data,
    ?int $user_id = NULL,
  ): ?FlowDropInterruptInterface {
    // Acquire a lock to prevent concurrent resolution of the same interrupt.
    $lockName = "flowdrop_interrupt_resolve_{$interrupt_id}";
    if (!$this->lock->acquire($lockName, 30)) {
      throw new \InvalidArgumentException("Interrupt {$interrupt_id} resolution is already in progress");
    }

    try {
      $interrupt = $this->getInterrupt($interrupt_id);

      if ($interrupt === NULL) {
        $this->logger->warning("Cannot resolve interrupt @id: not found", [
          "@id" => $interrupt_id,
        ]);
        return NULL;
      }

      if (!$interrupt->isPending()) {
        $this->logger->warning("Cannot resolve interrupt @id: status is @status", [
          "@id" => $interrupt_id,
          "@status" => $interrupt->getStatus()->value,
        ]);
        throw new \InvalidArgumentException(
          "Interrupt {$interrupt_id} is not in a resolvable state. Current status: " . $interrupt->getStatus()->value
        );
      }

      // Check expiration.
      if ($interrupt->isExpired()) {
        $interrupt->expire();
        $interrupt->save();
        $this->logger->info("Interrupt @id has expired", ["@id" => $interrupt_id]);
        throw new \InvalidArgumentException("Interrupt {$interrupt_id} has expired");
      }

      // Use provided user ID or current user.
      $resolved_by = $user_id ?? ($this->currentUser->isAuthenticated() ? (int) $this->currentUser->id() : NULL);

      $interrupt->resolve($response_data, $resolved_by);
      $interrupt->save();

      $this->logger->info("Resolved interrupt @id by user @user", [
        "@id" => $interrupt_id,
        "@user" => $resolved_by ?? "anonymous",
      ]);

      // Dispatch event for integrations.
      $this->eventDispatcher->dispatch(
        new InterruptResolvedEvent(
          interrupt: $interrupt,
          responseData: $response_data,
          resolvedBy: $resolved_by,
        ),
        InterruptResolvedEvent::NAME
      );

      return $interrupt;
    }
    finally {
      $this->lock->release($lockName);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getInterrupt(string $interrupt_id): ?FlowDropInterruptInterface {
    $entities = $this->storage->loadByProperties(["uuid" => $interrupt_id]);
    $interrupt = reset($entities);
    return $interrupt instanceof FlowDropInterruptInterface ? $interrupt : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getPendingInterruptsForSession(int $session_id): array {
    $ids = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("session_id", $session_id)
      ->condition("status", InterruptStatus::Pending->value)
      ->sort("created", "ASC")
      ->execute();

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

    $entities = $this->storage->loadMultiple($ids);
    return array_values(array_filter(
      $entities,
      fn($entity) => $entity instanceof FlowDropInterruptInterface
    ));
  }

  /**
   * {@inheritdoc}
   */
  public function getPendingInterruptsForPipeline(string $pipeline_id): array {
    $ids = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("pipeline_id", $pipeline_id)
      ->condition("status", InterruptStatus::Pending->value)
      ->sort("created", "ASC")
      ->execute();

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

    $entities = $this->storage->loadMultiple($ids);
    return array_values(array_filter(
      $entities,
      fn($entity) => $entity instanceof FlowDropInterruptInterface
    ));
  }

  /**
   * {@inheritdoc}
   */
  public function getPendingInterruptsForWorkflow(string $workflow_id): array {
    $ids = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("workflow_id", $workflow_id)
      ->condition("status", InterruptStatus::Pending->value)
      ->sort("created", "ASC")
      ->execute();

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

    $entities = $this->storage->loadMultiple($ids);
    return array_values(array_filter(
      $entities,
      fn($entity) => $entity instanceof FlowDropInterruptInterface
    ));
  }

  /**
   * {@inheritdoc}
   */
  public function hasPendingInterruptsForSession(int $session_id): bool {
    $count = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("session_id", $session_id)
      ->condition("status", InterruptStatus::Pending->value)
      ->count()
      ->execute();

    return $count > 0;
  }

  /**
   * {@inheritdoc}
   */
  public function hasPendingInterruptsForPipeline(string $pipeline_id): bool {
    $count = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("pipeline_id", $pipeline_id)
      ->condition("status", InterruptStatus::Pending->value)
      ->count()
      ->execute();

    return $count > 0;
  }

  /**
   * {@inheritdoc}
   */
  public function cancelInterrupt(string $interrupt_id): bool {
    $interrupt = $this->getInterrupt($interrupt_id);

    if ($interrupt === NULL) {
      $this->logger->warning("Cannot cancel interrupt @id: not found", [
        "@id" => $interrupt_id,
      ]);
      return FALSE;
    }

    if (!$interrupt->isPending()) {
      $this->logger->warning("Cannot cancel interrupt @id: status is @status", [
        "@id" => $interrupt_id,
        "@status" => $interrupt->getStatus()->value,
      ]);
      return FALSE;
    }

    $interrupt->cancel();
    $interrupt->save();

    $this->logger->info("Cancelled interrupt @id", ["@id" => $interrupt_id]);

    // Dispatch event for integrations.
    $this->eventDispatcher->dispatch(
      new InterruptCancelledEvent(
        interrupt: $interrupt,
      ),
      InterruptCancelledEvent::NAME
    );

    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function cancelInterruptsForSession(int $session_id): int {
    $interrupts = $this->getPendingInterruptsForSession($session_id);
    $count = 0;

    foreach ($interrupts as $interrupt) {
      $interrupt->cancel();
      $interrupt->save();
      $count++;
    }

    if ($count > 0) {
      $this->logger->info("Cancelled @count interrupts for session @session", [
        "@count" => $count,
        "@session" => $session_id,
      ]);
    }

    return $count;
  }

  /**
   * {@inheritdoc}
   */
  public function cancelInterruptsForPipeline(string $pipeline_id): int {
    $interrupts = $this->getPendingInterruptsForPipeline($pipeline_id);
    $count = 0;

    foreach ($interrupts as $interrupt) {
      $interrupt->cancel();
      $interrupt->save();
      $count++;
    }

    if ($count > 0) {
      $this->logger->info("Cancelled @count interrupts for pipeline @pipeline", [
        "@count" => $count,
        "@pipeline" => $pipeline_id,
      ]);
    }

    return $count;
  }

  /**
   * {@inheritdoc}
   */
  public function expireOverdueInterrupts(): int {
    $now = time();

    $ids = $this->storage->getQuery()
      // Access bypassed: system cron operation runs without user context.
      ->accessCheck(FALSE)
      ->condition("status", InterruptStatus::Pending->value)
      ->condition("expires", $now, "<")
      ->condition("expires", 0, ">")
      ->execute();

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

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

    foreach ($entities as $entity) {
      if ($entity instanceof FlowDropInterruptInterface) {
        $entity->expire();
        $entity->save();
        $count++;

        // Dispatch event for integrations.
        $this->eventDispatcher->dispatch(
          new InterruptExpiredEvent(
            interrupt: $entity,
          ),
          InterruptExpiredEvent::NAME
        );
      }
    }

    if ($count > 0) {
      $this->logger->info("Expired @count overdue interrupts", ["@count" => $count]);
    }

    return $count;
  }

  /**
   * {@inheritdoc}
   */
  public function cleanupOldInterrupts(int $max_age): int {
    $threshold = time() - $max_age;

    $ids = $this->storage->getQuery()
      // Access bypassed: system cron operation runs without user context.
      ->accessCheck(FALSE)
      ->condition("status", InterruptStatus::Pending->value, "<>")
      ->condition("changed", $threshold, "<")
      ->execute();

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

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

    $this->storage->delete($entities);

    $this->logger->info("Cleaned up @count old interrupts", ["@count" => $count]);

    return $count;
  }

  /**
   * {@inheritdoc}
   */
  public function getResolvedInterruptForNode(string $pipeline_id, string $node_id): ?FlowDropInterruptInterface {
    $ids = $this->storage->getQuery()
      // Access bypassed: internal service query; access enforced at API layer.
      ->accessCheck(FALSE)
      ->condition("pipeline_id", $pipeline_id)
      ->condition("node_id", $node_id)
      ->condition("status", InterruptStatus::Resolved->value)
      ->sort("resolved_at", "DESC")
      ->range(0, 1)
      ->execute();

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

    $id = reset($ids);
    $entity = $this->storage->load($id);

    return $entity instanceof FlowDropInterruptInterface ? $entity : NULL;
  }

}
