<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Controller;

use Cron\CronExpression;
use Drupal\Core\Controller\ControllerBase;
use Drupal\flowdrop\Controller\Api\ApiResponseTrait;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_trigger\Enum\EventCategory;
use Drupal\flowdrop_trigger\Entity\FlowDropTriggerConfig;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_trigger\Service\CronTriggerStateService;
use Drupal\flowdrop_trigger\Service\EventTypePluginManager;
use Drupal\flowdrop_trigger\Service\TriggerConfigSchemaBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

/**
 * API controller for trigger configuration CRUD operations.
 *
 * Provides REST endpoints for the workflow editor to create,
 * read, update, and delete trigger configurations.
 *
 * Endpoints:
 * - POST   /api/flowdrop/triggers              Create new trigger
 * - GET    /api/flowdrop/triggers/{id}         Get trigger with schema
 * - PATCH  /api/flowdrop/triggers/{id}         Update trigger
 * - DELETE /api/flowdrop/triggers/{id}         Delete trigger
 * - GET    /api/flowdrop/triggers/schema       Get schema for new trigger
 * - GET    /api/flowdrop/triggers/event-types  List available event types
 * - GET    /api/flowdrop/triggers/{id}/history Get execution history
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class TriggerConfigApiController extends ControllerBase {

  use ApiResponseTrait;

  /**
   * The event type plugin manager.
   *
   * @var \Drupal\flowdrop_trigger\Service\EventTypePluginManager
   */
  protected EventTypePluginManager $eventTypeManager;

  /**
   * The schema builder service.
   *
   * @var \Drupal\flowdrop_trigger\Service\TriggerConfigSchemaBuilder
   */
  protected TriggerConfigSchemaBuilder $schemaBuilder;

  /**
   * The cron trigger state service.
   *
   * @var \Drupal\flowdrop_trigger\Service\CronTriggerStateService|null
   */
  protected ?CronTriggerStateService $cronStateService;

  /**
   * Constructs a TriggerConfigApiController.
   *
   * @param \Drupal\flowdrop_trigger\Service\EventTypePluginManager $eventTypeManager
   *   The event type plugin manager.
   * @param \Drupal\flowdrop_trigger\Service\TriggerConfigSchemaBuilder $schemaBuilder
   *   The schema builder service.
   * @param \Drupal\flowdrop_trigger\Service\CronTriggerStateService|null $cronStateService
   *   The cron trigger state service.
   */
  public function __construct(
    EventTypePluginManager $eventTypeManager,
    TriggerConfigSchemaBuilder $schemaBuilder,
    ?CronTriggerStateService $cronStateService = NULL,
  ) {
    $this->eventTypeManager = $eventTypeManager;
    $this->schemaBuilder = $schemaBuilder;
    $this->cronStateService = $cronStateService;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get("plugin.manager.flowdrop_event_type"),
      $container->get("flowdrop_trigger.schema_builder"),
      $container->get("flowdrop_trigger.cron_state_service"),
    );
  }

  /**
   * Create a new trigger configuration.
   *
   * POST /api/flowdrop/triggers.
   *
   * Request body:
   * {
   *   "workflow_id": "my_workflow",
   *   "node_id": "node_123",
   *   "event_type": "entity.insert" (optional, defaults to entity.insert)
   * }
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with created entity data and schema.
   */
  public function createTriggerConfig(Request $request): JsonResponse {
    $data = $this->decodeJsonRequest($request);

    if (empty($data["workflow_id"])) {
      throw new BadRequestHttpException("workflow_id is required");
    }

    if (empty($data["node_id"])) {
      throw new BadRequestHttpException("node_id is required");
    }

    $workflowId = (string) $data["workflow_id"];
    $nodeId = (string) $data["node_id"];
    $eventType = (string) ($data["event_type"] ?? "entity.insert");

    // Validate event type exists.
    if (!$this->eventTypeManager->hasDefinition($eventType)) {
      throw new BadRequestHttpException(
        sprintf("Unknown event type: %s", $eventType)
      );
    }

    // Generate deterministic entity ID.
    $entityId = FlowDropTriggerConfig::generateId($workflowId, $nodeId);

    // Check if entity already exists.
    $storage = $this->entityTypeManager()->getStorage("flowdrop_trigger_config");
    $existing = $storage->load($entityId);

    if ($existing instanceof FlowDropTriggerConfigInterface) {
      // Return existing entity instead of creating duplicate.
      return $this->successResponse([
        "id" => $existing->id(),
        "schema" => $this->schemaBuilder->buildSchema($existing),
        "uiSchema" => $this->schemaBuilder->buildUiSchema($existing),
        "values" => $this->entityToValues($existing),
        "created" => FALSE,
      ]);
    }

    // Get default values.
    $defaults = $this->schemaBuilder->getDefaultValues($eventType);

    // Create entity with defaults.
    /** @var \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $entity */
    $entity = $storage->create([
      "id" => $entityId,
      "label" => sprintf("Trigger: %s", $eventType),
      "workflow_id" => $workflowId,
      "node_id" => $nodeId,
      "event_type" => $eventType,
      "conditions" => $defaults["conditions"],
      "orchestrator_settings" => $defaults["orchestrator_settings"],
      "initial_data_mapping" => [],
      "status" => TRUE,
      "weight" => 0,
    ]);

    $entity->save();

    return $this->successResponse([
      "id" => $entity->id(),
      "schema" => $this->schemaBuilder->buildSchema($entity),
      "uiSchema" => $this->schemaBuilder->buildUiSchema($entity),
      "values" => $this->entityToValues($entity),
      "created" => TRUE,
    ], Response::HTTP_CREATED);
  }

  /**
   * Get a trigger configuration with schema.
   *
   * GET /api/flowdrop/triggers/{id}
   *
   * @param string $id
   *   The trigger config ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with entity data and schema.
   */
  public function get(string $id): JsonResponse {
    $entity = $this->loadEntity($id);

    return $this->successResponse([
      "id" => $entity->id(),
      "schema" => $this->schemaBuilder->buildSchema($entity),
      "uiSchema" => $this->schemaBuilder->buildUiSchema($entity),
      "values" => $this->entityToValues($entity),
    ]);
  }

  /**
   * Update a trigger configuration.
   *
   * PATCH /api/flowdrop/triggers/{id}
   *
   * @param string $id
   *   The trigger config ID.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with updated entity data.
   */
  public function update(string $id, Request $request): JsonResponse {
    $entity = $this->loadEntity($id);
    $data = $this->decodeJsonRequest($request);

    // Update allowed fields.
    if (array_key_exists("label", $data)) {
      $entity->set("label", (string) $data["label"]);
    }

    if (array_key_exists("description", $data)) {
      $entity->setDescription((string) $data["description"]);
    }

    if (array_key_exists("event_type", $data)) {
      $eventType = (string) $data["event_type"];
      if (!$this->eventTypeManager->hasDefinition($eventType)) {
        throw new BadRequestHttpException(
          sprintf("Unknown event type: %s", $eventType)
        );
      }
      $entity->setEventType($eventType);
    }

    if (array_key_exists("conditions", $data) && is_array($data["conditions"])) {
      $entity->setConditions($data["conditions"]);
    }

    if (array_key_exists("orchestrator_settings", $data) && is_array($data["orchestrator_settings"])) {
      $entity->setOrchestratorSettings($data["orchestrator_settings"]);
    }

    if (array_key_exists("initial_data_mapping", $data) && is_array($data["initial_data_mapping"])) {
      $entity->setInitialDataMapping($data["initial_data_mapping"]);
    }

    if (array_key_exists("status", $data)) {
      $entity->set("status", (bool) $data["status"]);
    }

    if (array_key_exists("weight", $data)) {
      $entity->setWeight((int) $data["weight"]);
    }

    $entity->save();

    // Rebuild schema if event_type changed.
    return $this->successResponse([
      "id" => $entity->id(),
      "schema" => $this->schemaBuilder->buildSchema($entity),
      "uiSchema" => $this->schemaBuilder->buildUiSchema($entity),
      "values" => $this->entityToValues($entity),
    ]);
  }

  /**
   * Delete a trigger configuration.
   *
   * DELETE /api/flowdrop/triggers/{id}
   *
   * @param string $id
   *   The trigger config ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response confirming deletion.
   */
  public function delete(string $id): JsonResponse {
    $entity = $this->loadEntity($id);
    $entity->delete();

    return $this->successResponse([
      "deleted_id" => $id,
    ]);
  }

  /**
   * Get the in-editor config schema for a trigger node.
   *
   * GET /api/flowdrop/triggers/schema?event_type=…&workflow=…&node=….
   *
   * Backs the node's dynamicSchema config edit: returns the hydrated editor
   * form (see TriggerConfigSchemaBuilder::buildEditorForm()). The config schema
   * is the response `data` so the FlowDrop schema parser reads it there; the
   * companion `uiSchema` (grouping the injected reserved props into the
   * General/Execution/Ports fieldsets) rides alongside as a sibling key. The
   * client prefers this uiSchema over the node's static metadata uiSchema,
   * which references only the static schema's properties.
   *
   * The effective event type is: a valid `event_type` query value (the author
   * just picked one in the form) → else the existing entity's event type →
   * else the default. When the workflow and node identify an existing config,
   * its current values are baked in as form defaults (populate on read).
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with the editor schema as its data payload.
   */
  public function schema(Request $request): JsonResponse {
    $workflowId = $request->query->get("workflow");
    $nodeId = $request->query->get("node");

    // Load the existing config for this node, if any, to seed defaults and
    // resolve the event type when the query does not carry a valid one.
    $entity = NULL;
    if (is_string($workflowId) && $workflowId !== "" && is_string($nodeId) && $nodeId !== "") {
      $id = FlowDropTriggerConfig::generateId($workflowId, $nodeId);
      $loaded = $this->entityTypeManager()->getStorage("flowdrop_trigger_config")->load($id);
      if ($loaded instanceof FlowDropTriggerConfigInterface) {
        $entity = $loaded;
      }
    }

    // Precedence: a valid query event type (author picked one) wins, then the
    // stored entity's, then the default.
    $queryEventType = $request->query->get("event_type");
    if (is_string($queryEventType) && $this->eventTypeManager->hasDefinition($queryEventType)) {
      $eventType = $queryEventType;
    }
    elseif ($entity !== NULL) {
      $eventType = $entity->getEventType();
    }
    else {
      $eventType = "entity.insert";
    }

    $form = $this->schemaBuilder->buildEditorForm($eventType, $entity);

    // Schema at `data` (where the FlowDrop schema parser reads it) with the
    // uiSchema as a sibling key, mirroring successResponse()'s envelope.
    return new JsonResponse([
      'success' => TRUE,
      'data' => $form['schema'],
      'uiSchema' => $form['uiSchema'],
    ]);
  }

  /**
   * List available event types.
   *
   * GET /api/flowdrop/triggers/event-types?category=user.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with event types, optionally filtered by category.
   */
  public function eventTypes(Request $request): JsonResponse {
    $definitions = $this->eventTypeManager->getDefinitions();
    $eventTypes = [];

    // Get optional category filter from query parameter.
    $categoryFilter = $request->query->get("category");
    $categoryFilter = is_string($categoryFilter) ? $categoryFilter : NULL;

    foreach ($definitions as $id => $definition) {
      $category = EventCategory::toString($definition["category"] ?? "other");

      // Filter by category if specified.
      if ($categoryFilter !== NULL && $category !== $categoryFilter) {
        continue;
      }

      $eventTypes[] = [
        "id" => $id,
        "label" => (string) ($definition["label"] ?? $id),
        "description" => (string) ($definition["description"] ?? ""),
        "category" => $category,
        "defaultOrchestrator" => $definition["defaultOrchestrator"] ?? OrchestratorHelperInterface::ASYNCHRONOUS,
      ];
    }

    // Sort by category then label.
    usort($eventTypes, function ($a, $b) {
      $categoryCompare = $a["category"] <=> $b["category"];
      if ($categoryCompare !== 0) {
        return $categoryCompare;
      }
      return $a["label"] <=> $b["label"];
    });

    return $this->successResponse(["event_types" => $eventTypes]);
  }

  /**
   * Load a trigger config entity by ID.
   *
   * @param string $id
   *   The entity ID.
   *
   * @return \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface
   *   The loaded entity.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
   *   If entity not found.
   */
  protected function loadEntity(string $id): FlowDropTriggerConfigInterface {
    $storage = $this->entityTypeManager()->getStorage("flowdrop_trigger_config");
    $entity = $storage->load($id);

    if (!$entity instanceof FlowDropTriggerConfigInterface) {
      throw new NotFoundHttpException(
        sprintf("Trigger configuration not found: %s", $id)
      );
    }

    return $entity;
  }

  /**
   * Convert entity to values array for frontend.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $entity
   *   The entity.
   *
   * @return array<string, mixed>
   *   The values array.
   */
  protected function entityToValues(FlowDropTriggerConfigInterface $entity): array {
    $entityId = $entity->id();

    return [
      "id" => $entityId,
      "label" => $entity->label(),
      "description" => $entity->getDescription(),
      "workflow_id" => $entity->getWorkflowId(),
      "node_id" => $entity->getNodeId(),
      "event_type" => $entity->getEventType(),
      "conditions" => $entity->getConditions(),
      "orchestrator_settings" => $entity->getOrchestratorSettings(),
      "initial_data_mapping" => $entity->getInitialDataMapping(),
      "status" => $entity->status(),
      "weight" => $entity->getWeight(),
      // Admin links for configuration UI.
      "_links" => [
        "admin_edit" => sprintf("/admin/flowdrop/triggers/%s/edit", $entityId),
        "admin_view" => sprintf("/admin/flowdrop/triggers/%s", $entityId),
        "admin_delete" => sprintf("/admin/flowdrop/triggers/%s/delete", $entityId),
      ],
    ];
  }

  /**
   * Get execution history for a cron trigger.
   *
   * GET /api/flowdrop/triggers/{id}/history.
   *
   * Returns the execution state and history for a cron trigger,
   * including last run time, execution count, and recent history.
   *
   * @param string $id
   *   The trigger config ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with execution history.
   */
  public function history(string $id): JsonResponse {
    $entity = $this->loadEntity($id);

    // Check if this is a cron trigger.
    $eventType = $entity->getEventType();
    if (!str_starts_with($eventType, "cron.")) {
      return $this->errorResponse(
        "Execution history is only available for cron triggers",
        Response::HTTP_BAD_REQUEST
      );
    }

    // Get state from state service if available.
    if ($this->cronStateService === NULL) {
      return $this->errorResponse(
        "Cron state service not available",
        Response::HTTP_SERVICE_UNAVAILABLE
      );
    }

    $state = $this->cronStateService->getState($id);
    $schedule = $entity->getConditions()["cron_schedule"] ?? [];
    $maxExecutions = (int) ($schedule["max_executions"] ?? 0);

    // Calculate next run time if possible.
    $nextRun = NULL;
    if (!empty($schedule)) {
      $nextRun = $this->calculateNextRunTime($schedule, $state["last_run"]);
    }

    // Build response with execution state and history.
    return $this->successResponse([
      "trigger_id" => $id,
      "event_type" => $eventType,
      "status" => $entity->status() ? "enabled" : "disabled",
      "execution_state" => [
        "last_run" => $state["last_run"],
        "last_run_formatted" => $state["last_run"] > 0
          ? date("Y-m-d H:i:s", $state["last_run"])
          : NULL,
        "run_count" => $state["run_count"],
        "max_executions" => $maxExecutions,
        "remaining_executions" => $maxExecutions > 0
          ? max(0, $maxExecutions - $state["run_count"])
          : NULL,
        "next_run" => $nextRun,
        "next_run_formatted" => $nextRun !== NULL
          ? date("Y-m-d H:i:s", $nextRun)
          : NULL,
      ],
      "schedule" => $schedule,
      "history" => $state["history"],
    ]);
  }

  /**
   * Reset execution state for a cron trigger.
   *
   * POST /api/flowdrop/triggers/{id}/reset-state.
   *
   * Resets the execution count and history for a cron trigger.
   * This can be used to restart a one-shot trigger or clear history.
   *
   * @param string $id
   *   The trigger config ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response confirming reset.
   */
  public function resetState(string $id): JsonResponse {
    $entity = $this->loadEntity($id);

    // Check if this is a cron trigger.
    $eventType = $entity->getEventType();
    if (!str_starts_with($eventType, "cron.")) {
      return $this->errorResponse(
        "State reset is only available for cron triggers",
        Response::HTTP_BAD_REQUEST
      );
    }

    if ($this->cronStateService === NULL) {
      return $this->errorResponse(
        "Cron state service not available",
        Response::HTTP_SERVICE_UNAVAILABLE
      );
    }

    // Reset the state.
    $this->cronStateService->resetState($id);

    // Re-enable the trigger if it was disabled due to max executions.
    if (!$entity->status()) {
      $entity->set("status", TRUE);
      $entity->save();
    }

    return $this->successResponse([
      "trigger_id" => $id,
      "status" => "enabled",
    ]);
  }

  /**
   * Calculate the next run time based on schedule configuration.
   *
   * @param array<string, mixed> $schedule
   *   The schedule configuration.
   * @param int $lastRun
   *   The last run timestamp.
   *
   * @return int|null
   *   The next run timestamp, or NULL if cannot be determined.
   */
  protected function calculateNextRunTime(array $schedule, int $lastRun): ?int {
    $mode = $schedule["mode"] ?? "simple";

    if ($mode === "advanced") {
      // For cron expressions, use the cron library.
      $cronExpression = $schedule["cron_expression"] ?? "";
      if (empty($cronExpression)) {
        return NULL;
      }

      try {
        $cron = new CronExpression($cronExpression);
        $timezone = $schedule["timezone"] ?? date_default_timezone_get();
        $tz = new \DateTimeZone($timezone);

        if ($lastRun > 0) {
          $lastRunDate = (new \DateTimeImmutable())->setTimestamp($lastRun)->setTimezone($tz);
          $nextRun = $cron->getNextRunDate($lastRunDate, 0, FALSE, $tz->getName());
        }
        else {
          $nextRun = $cron->getNextRunDate("now", 0, FALSE, $tz->getName());
        }

        return $nextRun->getTimestamp();
      }
      catch (\Exception) {
        return NULL;
      }
    }

    // Simple mode: interval-based.
    $interval = $schedule["interval"] ?? "every_hour";

    // Convert interval key to seconds.
    $intervalSeconds = match ($interval) {
      "every_minute" => 60,
      "every_5_minutes" => 300,
      "every_15_minutes" => 900,
      "every_30_minutes" => 1800,
      "every_hour" => 3600,
      "every_2_hours" => 7200,
      "every_6_hours" => 21600,
      "every_12_hours" => 43200,
      "daily" => 86400,
      "weekly" => 604800,
      "custom" => (int) ($schedule["custom_interval"] ?? 3600),
      default => is_numeric($interval) ? (int) $interval : 3600,
    };

    if ($lastRun > 0) {
      return $lastRun + $intervalSeconds;
    }

    // If never run, next run is now.
    return time();
  }

}
