<?php

declare(strict_types=1);

namespace Drupal\flowdrop_trigger\Entity;

use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop_trigger\Controller\TriggerConfigListBuilder;
use Drupal\flowdrop_trigger\FlowDropEventTypeInterface;
use Drupal\flowdrop_trigger\FlowDropTriggerConfigInterface;
use Drupal\flowdrop_trigger\Form\TriggerConfigForm;
use Drupal\flowdrop_trigger\Plugin\Validation\Constraint\UniqueWorkflowNodeCombinationConstraint;

/**
 * Defines the FlowDrop Trigger Configuration entity.
 *
 * Stores trigger configuration created when a trigger node is dropped
 * in the workflow editor. The entity ID format is: {workflow_id}__{node_id}
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[ConfigEntityType(
  id: "flowdrop_trigger_config",
  label: new TranslatableMarkup("Trigger Configuration"),
  label_collection: new TranslatableMarkup("Trigger Configurations"),
  label_singular: new TranslatableMarkup("Configure Trigger"),
  label_plural: new TranslatableMarkup("Configure Triggers"),
  label_count: [
    "singular" => "@count trigger configuration",
    "plural" => "@count trigger configurations",
  ],
  handlers: [
    "list_builder" => TriggerConfigListBuilder::class,
    "form" => [
      "default" => TriggerConfigForm::class,
      "add" => TriggerConfigForm::class,
      "edit" => TriggerConfigForm::class,
      "delete" => EntityDeleteForm::class,
    ],
    "route_provider" => [
      "html" => AdminHtmlRouteProvider::class,
    ],
  ],
  config_prefix: "trigger_config",
  admin_permission: "administer flowdrop triggers",
  entity_keys: [
    "id" => "id",
    "label" => "label",
    "status" => "status",
    "weight" => "weight",
  ],
  config_export: [
    "id",
    "label",
    "description",
    "workflow_id",
    "node_id",
    "event_type",
    "conditions",
    "orchestrator_settings",
    "initial_data_mapping",
    "status",
    "weight",
  ],
  links: [
    "collection" => "/admin/flowdrop/triggers",
    "add-form" => "/admin/flowdrop/triggers/add",
    "canonical" => "/admin/flowdrop/triggers/{flowdrop_trigger_config}",
    "edit-form" => "/admin/flowdrop/triggers/{flowdrop_trigger_config}/edit",
    "delete-form" => "/admin/flowdrop/triggers/{flowdrop_trigger_config}/delete",
  ],
  constraints: [
    UniqueWorkflowNodeCombinationConstraint::class,
  ],
)]
class FlowDropTriggerConfig extends ConfigEntityBase implements FlowDropTriggerConfigInterface {

  /**
   * The separator used in entity IDs between workflow_id and node_id.
   */
  public const ID_SEPARATOR = "__";

  /**
   * The trigger config ID (format: {workflow_id}__{node_id}).
   *
   * @var string
   */
  protected string $id = "";

  /**
   * The trigger config label.
   *
   * @var string
   */
  protected string $label = "";

  /**
   * The trigger description.
   *
   * @var string
   */
  protected string $description = "";

  /**
   * The target workflow ID.
   *
   * @var string
   */
  protected string $workflow_id = "";

  /**
   * The node ID within the workflow.
   *
   * @var string
   */
  protected string $node_id = "";

  /**
   * The event type plugin ID.
   *
   * @var string
   */
  protected string $event_type = "entity.insert";

  /**
   * The trigger conditions.
   *
   * @var array<string, mixed>
   */
  protected array $conditions = [];

  /**
   * The orchestrator settings.
   *
   * @var array<string, mixed>
   */
  protected array $orchestrator_settings = [];

  /**
   * The initial data mapping.
   *
   * @var array<string, mixed>
   */
  protected array $initial_data_mapping = [];

  /**
   * The weight for ordering.
   *
   * @var int
   */
  protected int $weight = 0;

  /**
   * {@inheritdoc}
   */
  public function getWorkflowId(): string {
    return $this->workflow_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setWorkflowId(string $workflowId): static {
    $this->workflow_id = $workflowId;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getNodeId(): string {
    return $this->node_id;
  }

  /**
   * {@inheritdoc}
   */
  public function setNodeId(string $nodeId): static {
    $this->node_id = $nodeId;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getEventType(): string {
    return $this->event_type;
  }

  /**
   * {@inheritdoc}
   */
  public function setEventType(string $eventType): static {
    $this->event_type = $eventType;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getConditions(): array {
    return $this->conditions;
  }

  /**
   * {@inheritdoc}
   */
  public function setConditions(array $conditions): static {
    $this->conditions = $conditions;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getOrchestratorSettings(): array {
    return $this->orchestrator_settings;
  }

  /**
   * {@inheritdoc}
   */
  public function setOrchestratorSettings(array $settings): static {
    $this->orchestrator_settings = $settings;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getInitialDataMapping(): array {
    return $this->initial_data_mapping;
  }

  /**
   * {@inheritdoc}
   */
  public function setInitialDataMapping(array $mapping): static {
    $this->initial_data_mapping = $mapping;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getWeight(): int {
    return $this->weight;
  }

  /**
   * {@inheritdoc}
   */
  public function setWeight(int $weight): static {
    $this->weight = $weight;
    return $this;
  }

  /**
   * {@inheritdoc}
   *
   * Declares a config dependency on the workflow this trigger belongs to.
   *
   * The trigger config is meaningless without its workflow (the entity ID is
   * derived from the workflow + node IDs), so it must travel with the workflow
   * on export/staging and be removed when the workflow is removed via config
   * sync. Runtime deletion is additionally guaranteed by
   * WorkflowHooks::workflowPredelete(); the dependency here covers the config
   * management paths (export, sync, uninstall) that hooks do not.
   *
   * calculateDependencies() is called by the config system outside the save
   * path (export, validation, uninstall) and has no injected services, so the
   * core entity_type.manager is the accepted lookup here — mirroring
   * FlowDropWorkflow::calculateDependencies(). Loading the workflow also lets
   * us derive the dependency name via the entity API (no hardcoded config
   * prefix) and silently skip a dangling reference.
   */
  public function calculateDependencies(): static {
    parent::calculateDependencies();

    $workflowId = $this->getWorkflowId();
    if ($workflowId === "") {
      return $this;
    }

    $workflow = \Drupal::entityTypeManager()
      ->getStorage("flowdrop_workflow")
      ->load($workflowId);
    if ($workflow instanceof ConfigEntityInterface) {
      $this->addDependency(
        $workflow->getConfigDependencyKey(),
        $workflow->getConfigDependencyName(),
      );
    }

    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function matches(string $eventType, array $context): bool {
    // Event type must match.
    if ($this->getEventType() !== $eventType) {
      return FALSE;
    }

    // Must be enabled.
    if (!$this->status()) {
      return FALSE;
    }

    // Check conditions match.
    return $this->matchesConditions($context);
  }

  /**
   * Check if conditions match the given context.
   *
   * @param array<string, mixed> $context
   *   The event context.
   *
   * @return bool
   *   TRUE if conditions match.
   */
  protected function matchesConditions(array $context): bool {
    $conditions = $this->getConditions();

    // Entity type filter.
    $entityTypes = $conditions["entity_types"] ?? [];
    if (!empty($entityTypes)) {
      $contextEntityType = $context["entity_type"] ?? "";
      if (!in_array($contextEntityType, $entityTypes, TRUE)) {
        return FALSE;
      }
    }

    // Bundle filter.
    $bundles = $conditions["bundles"] ?? [];
    if (!empty($bundles)) {
      $contextBundle = $context["bundle"] ?? "";
      if (!in_array($contextBundle, $bundles, TRUE)) {
        return FALSE;
      }
    }

    // Role filter (for user events).
    $roles = $conditions["roles"] ?? [];
    if (!empty($roles)) {
      $contextRoles = $context["roles"] ?? [];
      if (empty(array_intersect($roles, $contextRoles))) {
        return FALSE;
      }
    }

    // Form ID filter (for form events).
    $formIds = $conditions["form_ids"] ?? [];
    if (!empty($formIds)) {
      $contextFormId = $context["form_id"] ?? "";
      if (!in_array($contextFormId, $formIds, TRUE)) {
        return FALSE;
      }
    }

    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function getEventTypePlugin(): ?FlowDropEventTypeInterface {
    try {
      /** @var \Drupal\flowdrop_trigger\Service\EventTypePluginManager $manager */
      $manager = \Drupal::service("plugin.manager.flowdrop_event_type");
      return $manager->createInstance($this->getEventType());
    }
    catch (\Exception) {
      return NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  public static function generateId(string $workflowId, string $nodeId): string {
    // Sanitize IDs to ensure valid config entity ID.
    $sanitizedWorkflowId = preg_replace("/[^a-z0-9_]/", "_", strtolower($workflowId));
    $sanitizedNodeId = preg_replace("/[^a-z0-9_]/", "_", strtolower($nodeId));

    return sprintf("%s%s%s", $sanitizedWorkflowId, self::ID_SEPARATOR, $sanitizedNodeId);
  }

  /**
   * {@inheritdoc}
   */
  public static function parseId(string $entityId): ?array {
    $parts = explode(self::ID_SEPARATOR, $entityId, 2);

    if (count($parts) !== 2) {
      return NULL;
    }

    return [
      "workflow_id" => $parts[0],
      "node_id" => $parts[1],
    ];
  }

  /**
   * Get the description.
   *
   * @return string
   *   The description.
   */
  public function getDescription(): string {
    return $this->description;
  }

  /**
   * Set the description.
   *
   * @param string $description
   *   The description.
   *
   * @return static
   *   The entity instance for chaining.
   */
  public function setDescription(string $description): static {
    $this->description = $description;
    return $this;
  }

  /**
   * Check if this is a cron-based trigger.
   *
   * @return bool
   *   TRUE if this is a cron trigger.
   */
  public function isCronTrigger(): bool {
    return str_starts_with($this->getEventType(), "cron.");
  }

  /**
   * Get the cron schedule configuration.
   *
   * @return array<string, mixed>
   *   The cron schedule configuration, or empty array if not configured.
   */
  public function getCronSchedule(): array {
    $conditions = $this->getConditions();
    return $conditions["cron_schedule"] ?? [];
  }

  /**
   * Set the cron schedule configuration.
   *
   * @param array<string, mixed> $schedule
   *   The cron schedule configuration.
   *
   * @return static
   *   The entity instance for chaining.
   */
  public function setCronSchedule(array $schedule): static {
    $conditions = $this->getConditions();
    $conditions["cron_schedule"] = $schedule;
    $this->setConditions($conditions);
    return $this;
  }

  /**
   * Check if the trigger has a schedule configured.
   *
   * @return bool
   *   TRUE if a schedule is configured.
   */
  public function hasSchedule(): bool {
    $schedule = $this->getCronSchedule();
    return !empty($schedule);
  }

  /**
   * Check if this is a one-shot trigger (runs only once or limited times).
   *
   * @return bool
   *   TRUE if this is a one-shot trigger.
   */
  public function isOneShot(): bool {
    $schedule = $this->getCronSchedule();
    return ($schedule["schedule_type"] ?? "recurring") === "one_shot";
  }

  /**
   * Get the maximum number of executions allowed.
   *
   * @return int
   *   Maximum executions (0 = unlimited).
   */
  public function getMaxExecutions(): int {
    $schedule = $this->getCronSchedule();
    return (int) ($schedule["max_executions"] ?? 0);
  }

  /**
   * Get the schedule mode.
   *
   * @return string
   *   Either "simple" or "advanced".
   */
  public function getScheduleMode(): string {
    $schedule = $this->getCronSchedule();
    return $schedule["mode"] ?? "simple";
  }

}
