<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Service;

use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Drupal\flowdrop_node_type\Service\UiSchemaGenerator;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;

/**
 * Builds JSON Schema for trigger configuration forms.
 *
 * Generates dynamic schemas based on event type and current configuration.
 * Used by the API controller to provide schema for the workflow editor.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class TriggerConfigSchemaBuilder {

  /**
   * Editor-form fields that map into the trigger config entity's `conditions`.
   *
   * The in-editor config form (see buildEditorSchema()) presents each condition
   * as a top-level field rather than nesting them under a single `conditions`
   * object, because the FlowDrop form renderer renders a bare object property
   * as a raw JSON editor. Each key here is written to, and read back from,
   * `conditions[$key]` — so this list is the single source of truth shared by
   * the schema builder (which emits the fields) and the workflow presave hook
   * (which flushes them back into the entity and strips them from the node).
   *
   * `event_type` is handled separately (it is a scalar entity field, not a
   * condition), and `orchestrator_settings` is deliberately absent: it never
   * travels through the editor buffer and stays owned by the admin form.
   */
  public const CONDITION_FIELD_KEYS = [
    'entity_types',
    'bundles',
    'roles',
    'form_ids',
    'trigger_on_new',
    'trigger_on_update',
    'cron_schedule',
  ];

  /**
   * Constructs a TriggerConfigSchemaBuilder.
   *
   * @param \Drupal\flowdrop_trigger\Service\EventTypePluginManager $eventTypeManager
   *   The event type plugin manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundleInfo
   *   The bundle info service.
   * @param \Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorPluginManager $orchestratorManager
   *   The orchestrator plugin manager.
   * @param \Drupal\flowdrop_node_type\Service\NodeMetadataResolver $metadataResolver
   *   Resolves node metadata; here it injects the reserved config props
   *   (General/Execution/Ports) so the in-editor form matches every other node.
   * @param \Drupal\flowdrop_node_type\Service\UiSchemaGenerator $uiSchemaGenerator
   *   Derives the grouped config-form uiSchema from a config schema.
   */
  public function __construct(
    protected EventTypePluginManager $eventTypeManager,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected EntityTypeBundleInfoInterface $bundleInfo,
    protected OrchestratorPluginManager $orchestratorManager,
    protected NodeMetadataResolver $metadataResolver,
    protected UiSchemaGenerator $uiSchemaGenerator,
  ) {}

  /**
   * Build schema for a trigger config entity.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $entity
   *   The trigger config entity.
   *
   * @return array<string, mixed>
   *   JSON Schema.
   */
  public function buildSchema(FlowDropTriggerConfigInterface $entity): array {
    return $this->buildSchemaForEventType(
      $entity->getEventType(),
      $entity->getConditions()
    );
  }

  /**
   * Build schema for an event type.
   *
   * @param string $eventType
   *   The event type plugin ID.
   * @param array<string, mixed> $currentConditions
   *   Current condition values (for dependent fields).
   *
   * @return array<string, mixed>
   *   JSON Schema.
   */
  public function buildSchemaForEventType(
    string $eventType,
    array $currentConditions = [],
  ): array {
    $schema = [
      "type" => "object",
      "properties" => [
        "label" => [
          "type" => "string",
          "title" => "Label",
          "description" => "Human-readable name for this trigger",
        ],
        "description" => [
          "type" => "string",
          "title" => "Description",
          "description" => "Optional description of what this trigger does",
        ],
        "event_type" => $this->buildEventTypeField(),
        "conditions" => $this->buildConditionsSchema($eventType, $currentConditions),
        "orchestrator_settings" => $this->buildOrchestratorSettingsSchema($eventType),
        "status" => [
          "type" => "boolean",
          "title" => "Enabled",
          "description" => "Whether this trigger is active",
          "default" => TRUE,
        ],
        "weight" => [
          "type" => "integer",
          "title" => "Weight",
          "description" => "Order of execution when multiple triggers fire",
          "default" => 0,
        ],
      ],
      "required" => ["event_type"],
    ];

    return $schema;
  }

  /**
   * Build the flat schema rendered by the in-editor trigger config form.
   *
   * Unlike buildSchemaForEventType() (the admin-oriented schema, which nests
   * filters under a `conditions` object), this returns only the fields a
   * workflow author edits inline — the event type plus the condition fields
   * relevant to that event type — each as a top-level property. Two reasons:
   * the FlowDrop form renderer renders a bare object property as a raw JSON
   * editor (so nesting would degrade to a JSON blob), and the editor buffer
   * flushes these exact keys back into the entity (see CONDITION_FIELD_KEYS).
   *
   * When an entity is supplied and its event type matches, its stored values
   * are baked in as field `default`s so the form opens populated with the
   * current configuration — the endpoint reads the entity (the source of
   * truth), so there is no drift and pre-existing triggers migrate for free.
   *
   * @param string $eventType
   *   The event type the form is being built for.
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface|null $entity
   *   The existing trigger config, when one exists, to seed defaults from.
   *
   * @return array<string, mixed>
   *   JSON Schema with top-level event_type and condition fields.
   */
  public function buildEditorSchema(
    string $eventType,
    ?FlowDropTriggerConfigInterface $entity = NULL,
  ): array {
    // Only seed condition defaults from an entity configured for THIS event
    // type; a mismatch means the author just switched types and the stored
    // conditions belong to the old one.
    $conditions = ($entity !== NULL && $entity->getEventType() === $eventType)
      ? $entity->getConditions()
      : [];

    $properties = [
      'event_type' => ['default' => $eventType] + $this->buildEventTypeField(),
    ];

    if (str_starts_with($eventType, 'entity.')) {
      $properties['entity_types'] = $this->buildEntityTypesField();
      $properties['bundles'] = $this->buildBundlesField(
        $conditions['entity_types'] ?? []
      );
      if ($eventType === 'entity.presave') {
        $properties['trigger_on_new'] = [
          'type' => 'boolean',
          'title' => 'Trigger on New Entities',
          'description' => 'Whether to trigger when a new entity is created.',
          'default' => TRUE,
        ];
        $properties['trigger_on_update'] = [
          'type' => 'boolean',
          'title' => 'Trigger on Updates',
          'description' => 'Whether to trigger when an existing entity is updated.',
          'default' => TRUE,
        ];
      }
    }

    if (str_starts_with($eventType, 'user.')) {
      $properties['roles'] = $this->buildRolesField();
    }

    if (str_starts_with($eventType, 'form.')) {
      $properties['form_ids'] = $this->buildFormIdsField();
    }

    if (str_starts_with($eventType, 'cron.')) {
      $properties['cron_schedule'] = $this->buildCronScheduleField();
    }

    // Bake the entity's current condition values in as field defaults.
    foreach (self::CONDITION_FIELD_KEYS as $key) {
      if (isset($properties[$key]) && array_key_exists($key, $conditions)) {
        $properties[$key]['default'] = $conditions[$key];
      }
    }

    return [
      'type' => 'object',
      'properties' => $properties,
      'required' => ['event_type'],
    ];
  }

  /**
   * Build the complete in-editor config form: schema plus grouped uiSchema.
   *
   * The buildEditorSchema() output is only the trigger's own domain fields
   * (event type + conditions). On its own that would render a form with none of
   * the reserved props — Title/Description, Max retries, Ports — every other
   * node shows, and no field grouping. This hydrates the narrow schema via the
   * main module (the single source of truth for reserved props) exactly as a
   * static node type is built, then derives the matching uiSchema.
   *
   * The domain fields carry no `x-group`, so they render inline at the top of
   * the form; the injected reserved props collect into the General / Execution
   * / Ports collapsible fieldsets below — identical to a normal node.
   *
   * A trigger is an executable start node, so it gets the executable reserved
   * set (retry budget + ports), hence `executable: TRUE`.
   *
   * @param string $eventType
   *   The event type the form is being built for.
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface|null $entity
   *   The existing trigger config, when one exists, to seed defaults from.
   *
   * @return array{schema: array<string, mixed>, uiSchema: array<string, mixed>|null}
   *   The hydrated config schema and its uiSchema (NULL when nothing groups).
   */
  public function buildEditorForm(
    string $eventType,
    ?FlowDropTriggerConfigInterface $entity = NULL,
  ): array {
    $schema = $this->buildEditorSchema($eventType, $entity);
    $schema = $this->metadataResolver->injectReservedConfigProperties($schema, TRUE);

    return [
      'schema' => $schema,
      'uiSchema' => $this->uiSchemaGenerator->generate($schema),
    ];
  }

  /**
   * Build the cron schedule field from the cron event type's parameter schema.
   *
   * Reuses CronRun::getParameterSchema() so the inline form and the event type
   * plugin stay in lockstep; falls back to a minimal object when the cron event
   * type is unavailable.
   *
   * @return array<string, mixed>
   *   JSON Schema for the cron schedule object.
   */
  protected function buildCronScheduleField(): array {
    if ($this->eventTypeManager->hasDefinition('cron.run')) {
      try {
        $schema = $this->eventTypeManager->createInstance('cron.run')->getParameterSchema();
        if (isset($schema['properties']['cron_schedule']) && is_array($schema['properties']['cron_schedule'])) {
          return $schema['properties']['cron_schedule'];
        }
      }
      catch (\Exception) {
        // Fall through to the minimal schema below.
      }
    }

    return [
      'type' => 'object',
      'title' => 'Schedule Configuration',
      'description' => 'Configure when and how often this trigger should fire.',
      'properties' => [],
    ];
  }

  /**
   * Build UI schema for a trigger config entity.
   *
   * @param \Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface $entity
   *   The trigger config entity.
   *
   * @return array<string, mixed>
   *   UI Schema for form rendering.
   */
  public function buildUiSchema(FlowDropTriggerConfigInterface $entity): array {
    return $this->buildUiSchemaForEventType($entity->getEventType());
  }

  /**
   * Build UI schema for an event type.
   *
   * @param string $eventType
   *   The event type plugin ID.
   *
   * @return array<string, mixed>
   *   UI Schema.
   */
  public function buildUiSchemaForEventType(string $eventType): array {
    $uiSchema = [
      "ui:order" => [
        "label",
        "event_type",
        "conditions",
        "orchestrator_settings",
        "status",
        "weight",
        "description",
      ],
      "label" => [
        "ui:widget" => "text",
        "ui:placeholder" => "Enter a descriptive name",
      ],
      "description" => [
        "ui:widget" => "textarea",
      ],
      "event_type" => [
        "ui:widget" => "select",
      ],
      "conditions" => [
        "entity_types" => [
          "ui:widget" => "checkboxes",
        ],
        "bundles" => [
          "ui:widget" => "checkboxes",
          "ui:dependsOn" => "conditions.entity_types",
        ],
        "roles" => [
          "ui:widget" => "checkboxes",
        ],
      ],
      "orchestrator_settings" => [
        "ui:collapsed" => TRUE,
        "ui:title" => "Advanced Settings",
        "type" => [
          "ui:widget" => "select",
        ],
        "pipeline_mode" => [
          "ui:widget" => "radio",
        ],
        "pipeline_id" => [
          "ui:widget" => "text",
          "ui:dependsOn" => "orchestrator_settings.pipeline_mode",
          "ui:showIf" => ["orchestrator_settings.pipeline_mode" => "reuse"],
        ],
        "retry" => [
          "ui:collapsed" => TRUE,
        ],
      ],
      "weight" => [
        "ui:widget" => "number",
      ],
    ];

    return $uiSchema;
  }

  /**
   * Build event type selection field.
   *
   * @return array<string, mixed>
   *   JSON Schema for event type field.
   */
  protected function buildEventTypeField(): array {
    $definitions = $this->eventTypeManager->getDefinitions();
    $oneOf = [];

    foreach ($definitions as $id => $definition) {
      $oneOf[] = [
        "const" => $id,
        "title" => (string) ($definition["label"] ?? $id),
      ];
    }

    return [
      "type" => "string",
      "title" => "Event Type",
      "description" => "The event that triggers this workflow",
      "oneOf" => $oneOf,
      "default" => "entity.insert",
    ];
  }

  /**
   * Build conditions schema based on event type.
   *
   * @param string $eventType
   *   The event type plugin ID.
   * @param array<string, mixed> $currentConditions
   *   Current condition values.
   *
   * @return array<string, mixed>
   *   JSON Schema for conditions.
   */
  protected function buildConditionsSchema(
    string $eventType,
    array $currentConditions,
  ): array {
    $schema = [
      "type" => "object",
      "title" => "Conditions",
      "description" => "Filter which events trigger this workflow",
      "properties" => [],
    ];

    // Entity-based event types.
    if (str_starts_with($eventType, "entity.")) {
      $schema["properties"]["entity_types"] = $this->buildEntityTypesField();
      $schema["properties"]["bundles"] = $this->buildBundlesField(
        $currentConditions["entity_types"] ?? []
      );
    }

    // User-based event types.
    if (str_starts_with($eventType, "user.")) {
      $schema["properties"]["roles"] = $this->buildRolesField();
    }

    // Form-based event types.
    if (str_starts_with($eventType, "form.")) {
      $schema["properties"]["form_ids"] = $this->buildFormIdsField();
    }

    return $schema;
  }

  /**
   * Build entity types selection field.
   *
   * @return array<string, mixed>
   *   JSON Schema for entity types field.
   */
  protected function buildEntityTypesField(): array {
    $contentEntityTypes = [];
    $entityTypes = $this->entityTypeManager->getDefinitions();

    foreach ($entityTypes as $id => $definition) {
      if ($definition->getGroup() === "content") {
        $contentEntityTypes[$id] = (string) $definition->getLabel();
      }
    }

    // Sort alphabetically by label.
    asort($contentEntityTypes);

    $oneOf = [];
    foreach ($contentEntityTypes as $id => $label) {
      $oneOf[] = ["const" => $id, "title" => $label];
    }

    return [
      "type" => "array",
      "title" => "Entity Types",
      "description" => "Which entity types to trigger on (empty = all)",
      "items" => [
        "type" => "string",
        "oneOf" => $oneOf,
      ],
      "uniqueItems" => TRUE,
      "default" => [],
    ];
  }

  /**
   * Build bundles selection field based on selected entity types.
   *
   * @param array<string> $entityTypes
   *   Selected entity types.
   *
   * @return array<string, mixed>
   *   JSON Schema for bundles field.
   */
  protected function buildBundlesField(array $entityTypes): array {
    $bundles = [];

    foreach ($entityTypes as $entityType) {
      $entityBundles = $this->bundleInfo->getBundleInfo($entityType);
      foreach ($entityBundles as $bundleId => $bundleInfo) {
        $bundles[$bundleId] = sprintf(
          "%s: %s",
          $entityType,
          $bundleInfo["label"] ?? $bundleId
        );
      }
    }

    if (empty($bundles)) {
      // No entity types selected - show placeholder.
      return [
        "type" => "array",
        "title" => "Bundles",
        "description" => "Select entity types first to see available bundles",
        "items" => ["type" => "string"],
        "default" => [],
      ];
    }

    $oneOf = [];
    foreach ($bundles as $id => $label) {
      $oneOf[] = ["const" => $id, "title" => $label];
    }

    return [
      "type" => "array",
      "title" => "Bundles",
      "description" => "Which bundles to trigger on (empty = all)",
      "items" => [
        "type" => "string",
        "oneOf" => $oneOf,
      ],
      "uniqueItems" => TRUE,
      "default" => [],
    ];
  }

  /**
   * Build roles selection field.
   *
   * @return array<string, mixed>
   *   JSON Schema for roles field.
   */
  protected function buildRolesField(): array {
    $roleStorage = $this->entityTypeManager->getStorage("user_role");

    /** @var \Drupal\user\RoleInterface[] $roles */
    $roles = $roleStorage->loadMultiple();

    $roleOptions = [];
    foreach ($roles as $role) {
      $roleId = $role->id();
      if ($roleId !== NULL) {
        $roleOptions[$roleId] = (string) $role->label();
      }
    }

    $oneOf = [];
    foreach ($roleOptions as $id => $label) {
      $oneOf[] = ["const" => $id, "title" => $label];
    }

    return [
      "type" => "array",
      "title" => "Roles",
      "description" => "Trigger only for users with these roles (empty = all)",
      "items" => [
        "type" => "string",
        "oneOf" => $oneOf,
      ],
      "uniqueItems" => TRUE,
      "default" => [],
    ];
  }

  /**
   * Build form IDs selection field.
   *
   * @return array<string, mixed>
   *   JSON Schema for form IDs field.
   */
  protected function buildFormIdsField(): array {
    return [
      "type" => "array",
      "title" => "Form IDs",
      "description" => "Only trigger for these form IDs (empty = all forms)",
      "items" => [
        "type" => "string",
      ],
      "default" => [],
    ];
  }

  /**
   * Build orchestrator settings schema.
   *
   * Dynamically builds the orchestrator enum from registered orchestrators
   * via the OrchestratorPluginManager service.
   *
   * @param string $eventType
   *   The event type plugin ID.
   *
   * @return array<string, mixed>
   *   JSON Schema for orchestrator settings.
   */
  protected function buildOrchestratorSettingsSchema(string $eventType): array {
    // Determine default orchestrator based on event type.
    $defaultOrchestrator = "default";
    if ($this->eventTypeManager->hasDefinition($eventType)) {
      try {
        $plugin = $this->eventTypeManager->createInstance($eventType);
        $defaultOrchestrator = $plugin->getDefaultOrchestrator();
      }
      catch (\Exception) {
        // Use default.
      }
    }

    // Build orchestrator enum dynamically from registered orchestrators.
    $orchestratorOptions = $this->buildOrchestratorOptions();

    return [
      "type" => "object",
      "title" => "Execution Settings",
      "description" => "How the workflow should be executed",
      "properties" => [
        "type" => [
          "type" => "string",
          "title" => "Orchestrator",
          "description" => "How to execute the workflow",
          "oneOf" => $orchestratorOptions,
          "default" => $defaultOrchestrator,
        ],
        "pipeline_mode" => [
          "type" => "string",
          "title" => "Pipeline Mode",
          "description" => "How to handle pipeline creation",
          "oneOf" => [
            ["const" => "new", "title" => "New pipeline each time"],
            ["const" => "reuse", "title" => "Reuse existing pipeline"],
            ["const" => "singleton", "title" => "Singleton (one at a time)"],
          ],
          "default" => "new",
        ],
        "pipeline_id" => [
          "type" => "string",
          "title" => "Pipeline ID",
          "description" => "Pipeline ID for reuse mode",
          "default" => "",
        ],
        "priority" => [
          "type" => "string",
          "title" => "Priority",
          "oneOf" => [
            ["const" => "low", "title" => "Low"],
            ["const" => "normal", "title" => "Normal"],
            ["const" => "high", "title" => "High"],
            ["const" => "critical", "title" => "Critical"],
          ],
          "default" => "normal",
        ],
        "timeout" => [
          "type" => "integer",
          "title" => "Timeout (seconds)",
          "description" => "Maximum execution time",
          "default" => 300,
          "minimum" => 1,
          "maximum" => 86400,
        ],
        "retry" => [
          "type" => "object",
          "title" => "Retry Settings",
          "properties" => [
            "enabled" => [
              "type" => "boolean",
              "title" => "Enable Retry",
              "default" => FALSE,
            ],
            "max_attempts" => [
              "type" => "integer",
              "title" => "Max Attempts",
              "default" => 3,
              "minimum" => 1,
              "maximum" => 10,
            ],
            "delay" => [
              "type" => "integer",
              "title" => "Delay (seconds)",
              "default" => 60,
              "minimum" => 1,
            ],
            "backoff" => [
              "type" => "string",
              "title" => "Backoff Strategy",
              "oneOf" => [
                ["const" => "fixed", "title" => "Fixed"],
                ["const" => "linear", "title" => "Linear"],
                ["const" => "exponential", "title" => "Exponential"],
              ],
              "default" => "exponential",
            ],
          ],
        ],
      ],
    ];
  }

  /**
   * Build orchestrator options from registered orchestrators.
   *
   * Dynamically retrieves all orchestrators registered via the
   * 'flowdrop_runtime.orchestrator' service tag and builds the oneOf
   * array for the JSON Schema.
   *
   * @return array<array{const: string, title: string}>
   *   Array of oneOf options with 'const' and 'title' for each orchestrator.
   */
  protected function buildOrchestratorOptions(): array {
    // Start with "default" option which uses event type's default orchestrator.
    $oneOf = [
      ["const" => "default", "title" => "Default (based on event)"],
    ];

    // Get all registered orchestrators from the plugin manager.
    $definitions = $this->orchestratorManager->getDefinitions();

    foreach ($definitions as $id => $definition) {
      $oneOf[] = [
        "const" => $id,
        "title" => $definition["label"] ?? ucfirst(str_replace("_", " ", $id)),
      ];
    }

    return $oneOf;
  }

  /**
   * Get default values for a new trigger.
   *
   * @param string $eventType
   *   The event type.
   *
   * @return array<string, mixed>
   *   Default values.
   */
  public function getDefaultValues(string $eventType): array {
    $defaultOrchestrator = OrchestratorHelperInterface::ASYNCHRONOUS;

    if ($this->eventTypeManager->hasDefinition($eventType)) {
      try {
        $plugin = $this->eventTypeManager->createInstance($eventType);
        $defaultOrchestrator = $plugin->getDefaultOrchestrator();
      }
      catch (\Exception) {
        // Use default.
      }
    }

    return [
      "label" => "",
      "description" => "",
      "event_type" => $eventType,
      "conditions" => [
        "entity_types" => [],
        "bundles" => [],
        "roles" => [],
        "form_ids" => [],
      ],
      "orchestrator_settings" => [
        "type" => $defaultOrchestrator,
        "pipeline_mode" => "new",
        "pipeline_id" => "",
        "priority" => "normal",
        "timeout" => 300,
        "retry" => [
          "enabled" => FALSE,
          "max_attempts" => 3,
          "delay" => 60,
          "backoff" => "exponential",
        ],
      ],
      "status" => TRUE,
      "weight" => 0,
    ];
  }

}
