<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Form;

use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\Core\Utility\TableSort;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Enum\InterruptStatus;
use Drupal\flowdrop_interrupt\Service\InterruptHandlerPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * The HITL interrupt inbox: a filterable, selectable list of pending requests.
 *
 * Renders each interrupt with links back to its origin (workflow editor,
 * pipeline, playground session) so the operator can see where it came from,
 * and supports dismissing interrupts either by selection or in bulk across
 * the whole active filter.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class InterruptInboxForm extends FormBase {

  /**
   * The private tempstore collection used to hand IDs to the confirm form.
   */
  private const TEMPSTORE_COLLECTION = 'flowdrop_interrupt';

  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly InterruptHandlerPluginManager $handlerManager,
    private readonly DateFormatterInterface $dateFormatter,
    private readonly PrivateTempStoreFactory $tempStoreFactory,
    private readonly Connection $database,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('entity_type.manager'),
      $container->get('plugin.manager.flowdrop_interrupt_handler'),
      $container->get('date.formatter'),
      $container->get('tempstore.private'),
      $container->get('database'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'flowdrop_interrupt_inbox_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array {
    $header = $this->buildHeader();

    $hitlTypeIds = $this->handlerManager->getHitlTypeIds();
    $rows = [];
    $total = 0;

    if ($hitlTypeIds !== []) {
      $storage = $this->entityTypeManager->getStorage('flowdrop_interrupt');

      // Total across all pages, used for the "dismiss all" affordance.
      $total = (int) $this->baseQuery($storage, $hitlTypeIds)->count()->execute();

      // Sorted, paged window for display.
      $ids = $this->baseQuery($storage, $hitlTypeIds, $header)
        ->pager(25)
        ->execute();

      /** @var \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface[] $interrupts */
      $interrupts = $storage->loadMultiple($ids);
      foreach ($interrupts as $interrupt) {
        $rows[$interrupt->uuid()] = $this->buildRow($interrupt);
      }
    }

    $form['interrupts'] = [
      '#type' => 'tableselect',
      '#header' => $header,
      '#options' => $rows,
      '#empty' => $this->t('No pending interrupts match your filters.'),
      '#attributes' => ['class' => ['flowdrop-interrupt-inbox']],
    ];

    $form['actions'] = ['#type' => 'actions'];

    $form['actions']['dismiss_selected'] = [
      '#type' => 'submit',
      '#value' => $this->t('Dismiss selected'),
      '#submit' => ['::submitSelected'],
      '#button_type' => 'danger',
      '#access' => $rows !== [],
    ];

    $form['actions']['dismiss_all'] = [
      '#type' => 'submit',
      '#value' => $this->formatPlural(
        $total,
        'Dismiss the 1 matching interrupt',
        'Dismiss all @count matching interrupts',
      ),
      '#submit' => ['::submitAll'],
      // No tableselect values are required for the "all" path.
      '#limit_validation_errors' => [],
      '#access' => $total > 0,
    ];

    $form['pager'] = ['#type' => 'pager'];
    $form['#cache'] = ['max-age' => 0];

    return $form;
  }

  /**
   * {@inheritdoc}
   *
   * The two bulk actions have dedicated submit handlers; this satisfies the
   * abstract method but is never the active handler.
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {}

  /**
   * Submit handler: dismiss only the rows the operator ticked.
   */
  public function submitSelected(array &$form, FormStateInterface $form_state): void {
    $selected = array_values(array_filter($form_state->getValue('interrupts', [])));

    if ($selected === []) {
      $this->messenger()->addWarning($this->t('Select at least one interrupt to dismiss.'));
      return;
    }

    $this->redirectToConfirm($selected, $form_state);
  }

  /**
   * Submit handler: dismiss every interrupt matching the active filter.
   */
  public function submitAll(array &$form, FormStateInterface $form_state): void {
    $hitlTypeIds = $this->handlerManager->getHitlTypeIds();
    if ($hitlTypeIds === []) {
      return;
    }

    $storage = $this->entityTypeManager->getStorage('flowdrop_interrupt');
    $ids = $this->baseQuery($storage, $hitlTypeIds)->execute();

    /** @var \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface[] $interrupts */
    $interrupts = $storage->loadMultiple($ids);
    $uuids = array_map(static fn(FlowDropInterruptInterface $i): string => $i->uuid(), $interrupts);

    if ($uuids === []) {
      $this->messenger()->addWarning($this->t('There are no matching interrupts to dismiss.'));
      return;
    }

    $this->redirectToConfirm(array_values($uuids), $form_state);
  }

  /**
   * Stashes the target UUIDs in tempstore and redirects to the confirm form.
   *
   * @param array<int, string> $uuids
   *   The interrupt UUIDs to dismiss.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state to set the redirect on.
   */
  private function redirectToConfirm(array $uuids, FormStateInterface $form_state): void {
    $this->tempStoreFactory
      ->get(self::TEMPSTORE_COLLECTION)
      ->set('dismiss_ids', $uuids);

    $form_state->setRedirect('flowdrop_interrupt.bulk_dismiss');
  }

  /**
   * Builds the keyed, sortable table header.
   *
   * @return array<string, mixed>
   *   The header definition keyed by column machine name.
   */
  private function buildHeader(): array {
    return [
      'type' => ['data' => $this->t('Type'), 'field' => 'type'],
      'message' => ['data' => $this->t('Message')],
      'workflow' => ['data' => $this->t('Workflow'), 'field' => 'workflow_id'],
      'node' => ['data' => $this->t('Node')],
      'session' => ['data' => $this->t('Session')],
      'pipeline' => ['data' => $this->t('Pipeline')],
      'created' => ['data' => $this->t('Created'), 'field' => 'created', 'sort' => 'desc'],
      'expires' => ['data' => $this->t('Expires'), 'field' => 'expires'],
      'operations' => ['data' => $this->t('Actions')],
    ];
  }

  /**
   * Builds a single tableselect row keyed by header column.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt
   *   The interrupt entity.
   *
   * @return array<string, mixed>
   *   The row cells keyed by header column machine name.
   */
  private function buildRow(FlowDropInterruptInterface $interrupt): array {
    $expires = $interrupt->getExpires();
    $nodeId = $interrupt->getNodeId();

    return [
      'type' => $interrupt->getType()->label(),
      'message' => $interrupt->getMessage(),
      'workflow' => ['data' => $this->workflowCell($interrupt->getWorkflowId())],
      'node' => $nodeId !== '' ? ['data' => ['#markup' => '<code>' . htmlspecialchars($nodeId, ENT_QUOTES) . '</code>']] : '—',
      'session' => ['data' => $this->sessionCell($interrupt)],
      'pipeline' => ['data' => $this->pipelineCell($interrupt->getPipelineId())],
      'created' => $this->dateFormatter->format($interrupt->getCreatedTime(), 'short'),
      'expires' => $expires ? $this->dateFormatter->format($expires, 'short') : '—',
      'operations' => ['data' => $this->operationsCell($interrupt)],
    ];
  }

  /**
   * Builds the Workflow cell, linking to the editor when the workflow exists.
   *
   * @param string $workflowId
   *   The workflow config entity ID.
   *
   * @return array<string, mixed>
   *   A render array.
   */
  private function workflowCell(string $workflowId): array {
    if ($workflowId === '') {
      return ['#markup' => '—'];
    }

    $workflow = $this->entityTypeManager->getStorage('flowdrop_workflow')->load($workflowId);
    if ($workflow === NULL) {
      // Workflow was deleted; show the raw ID so the trail isn't lost.
      return ['#markup' => '<span title="' . $this->t('Workflow no longer exists') . '">' . htmlspecialchars($workflowId, ENT_QUOTES) . '</span>'];
    }

    return Link::fromTextAndUrl(
      $workflow->label() ?? $workflowId,
      Url::fromRoute('flowdrop.workflow.editor', ['flowdrop_workflow' => $workflowId]),
    )->toRenderable();
  }

  /**
   * Builds the Session cell, linking to the workflow's playground.
   *
   * There is no standalone session view; the playground for the owning
   * workflow is the closest place an operator can inspect the run.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt
   *   The interrupt entity.
   *
   * @return array<string, mixed>
   *   A render array.
   */
  private function sessionCell(FlowDropInterruptInterface $interrupt): array {
    $sessionId = $interrupt->getSessionId();
    if ($sessionId === NULL) {
      return ['#markup' => '—'];
    }

    $label = '#' . $sessionId;

    // Resolve the session's own workflow rather than trusting the interrupt's:
    // a sub-workflow interrupt can run inside a parent workflow's session. Only
    // link when the session still exists and its workflow can be opened, so an
    // orphaned interrupt (session since deleted) doesn't send the operator to
    // an unrelated playground.
    $sessionWorkflow = $this->sessionWorkflowId((int) $sessionId);
    if (
      $sessionWorkflow !== NULL && $sessionWorkflow !== ''
      && $this->entityTypeManager->getStorage('flowdrop_workflow')->load($sessionWorkflow) !== NULL
    ) {
      return Link::fromTextAndUrl(
        $label,
        Url::fromRoute(
          'flowdrop_playground.workflow.session.page',
          [
            'flowdrop_workflow' => $sessionWorkflow,
            'flowdrop_session' => $sessionId,
          ],
        ),
      )->toRenderable();
    }

    return ['#markup' => '<span title="' . $this->t('Session no longer available') . '">' . $label . '</span>'];
  }

  /**
   * Builds the Pipeline cell, linking to the pipeline view when loadable.
   *
   * @param string|null $pipelineId
   *   The pipeline entity ID.
   *
   * @return array<string, mixed>
   *   A render array.
   */
  private function pipelineCell(?string $pipelineId): array {
    if ($pipelineId === NULL || $pipelineId === '') {
      return ['#markup' => '—'];
    }

    $pipeline = $this->entityTypeManager->getStorage('flowdrop_pipeline')->load($pipelineId);
    if ($pipeline === NULL) {
      return ['#markup' => '#' . htmlspecialchars($pipelineId, ENT_QUOTES)];
    }

    return Link::fromTextAndUrl(
      '#' . $pipelineId,
      Url::fromRoute('flowdrop.pipeline.view', ['flowdrop_pipeline' => $pipelineId]),
    )->toRenderable();
  }

  /**
   * Builds the per-row Respond / Dismiss operations.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface $interrupt
   *   The interrupt entity.
   *
   * @return array<string, mixed>
   *   A render array of action links.
   */
  private function operationsCell(FlowDropInterruptInterface $interrupt): array {
    $links = [];

    if ($interrupt->access('resolve')) {
      $links['respond'] = [
        'title' => $this->t('Respond'),
        'url' => Url::fromRoute('flowdrop_interrupt.detail', ['interrupt_id' => $interrupt->uuid()]),
      ];
    }

    if ($interrupt->access('cancel')) {
      $links['dismiss'] = [
        'title' => $this->t('Dismiss'),
        'url' => Url::fromRoute('flowdrop_interrupt.cancel_confirm', ['interrupt_id' => $interrupt->uuid()]),
      ];
    }

    return [
      '#type' => 'operations',
      '#links' => $links,
    ];
  }

  /**
   * Builds the filtered, sorted base entity query for the current user.
   *
   * Applies the inbox scope (current user, pending, HITL types) plus the
   * exposed filters from the request query. When a header is supplied the
   * active table sort is applied; callers that only need a count or the full
   * ID set can omit it.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The interrupt entity storage.
   * @param array<int, string> $hitlTypeIds
   *   The human-facing interrupt type IDs.
   * @param array<string, mixed>|null $header
   *   The table header, for sort resolution. Omit for count/all queries.
   *
   * @return \Drupal\Core\Entity\Query\QueryInterface
   *   The configured query.
   */
  private function baseQuery(EntityStorageInterface $storage, array $hitlTypeIds, ?array $header = NULL): QueryInterface {
    $request = $this->getRequest();

    $query = $storage->getQuery()
      ->accessCheck(TRUE)
      ->condition('uid', (int) $this->currentUser()->id())
      ->condition('status', InterruptStatus::Pending->value)
      ->condition('type', $hitlTypeIds, 'IN');

    $workflow = (string) $request->query->get('workflow', '');
    if ($workflow !== '') {
      $query->condition('workflow_id', $workflow);
    }

    $type = (string) $request->query->get('type', '');
    if ($type !== '' && in_array($type, $hitlTypeIds, TRUE)) {
      $query->condition('type', $type);
    }

    $session = $request->query->get('session', '');
    if ($session !== '' && is_numeric($session)) {
      // `session_id` is an entity_reference field; conditioning on it through
      // the entity query forces resolution of the referenced entity type,
      // which isn't always installed. Resolve matching rows via the base
      // table instead and constrain on the safe integer `id` key.
      $query->condition('id', $this->idsForSession((int) $session) ?: [0], 'IN');
    }

    $search = trim((string) $request->query->get('search', ''));
    if ($search !== '') {
      $query->condition('message', $search, 'CONTAINS');
    }

    if ($header !== NULL) {
      $order = TableSort::getOrder($header, $request);
      $sort = strtoupper(TableSort::getSort($header, $request));
      $query->sort($order['sql'] ?: 'created', $sort ?: 'DESC');
    }

    return $query;
  }

  /**
   * Resolves the interrupt entity IDs attached to a playground session.
   *
   * Reads the base table directly to avoid resolving the `session_id`
   * entity_reference target type, which may not be installed.
   *
   * @param int $sessionId
   *   The playground session ID.
   *
   * @return array<int, int>
   *   The matching interrupt entity IDs.
   */
  private function idsForSession(int $sessionId): array {
    return array_map(
      'intval',
      $this->database->select('flowdrop_interrupt', 'i')
        ->fields('i', ['id'])
        ->condition('i.session_id', $sessionId)
        ->execute()
        ->fetchCol(),
    );
  }

  /**
   * Returns the workflow ID owning a session, or NULL if the session is gone.
   *
   * Reads the base table directly; the session entity type isn't reliably
   * resolvable through the entity API in every install.
   *
   * @param int $sessionId
   *   The playground session ID.
   *
   * @return string|null
   *   The owning workflow ID, or NULL when no such session exists.
   */
  private function sessionWorkflowId(int $sessionId): ?string {
    $workflowId = $this->database->select('flowdrop_session', 's')
      ->fields('s', ['workflow_id'])
      ->condition('s.id', $sessionId)
      ->range(0, 1)
      ->execute()
      ->fetchField();

    return $workflowId === FALSE ? NULL : (string) $workflowId;
  }

}
