<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Entity;

use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop_workflow\FlowDropWorkflowAccessControlHandler;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowListBuilder;
use Drupal\flowdrop_workflow\Form\FlowDropWorkflowDuplicateForm;
use Drupal\flowdrop\Utility\NodeAnchor;
use Drupal\flowdrop_workflow\Form\FlowDropWorkflowForm;

/**
 * Defines the flowdrop workflow entity type.
 */
#[ConfigEntityType(
  id: 'flowdrop_workflow',
  label: new TranslatableMarkup('FlowDrop Workflow'),
  label_collection: new TranslatableMarkup('FlowDrop Workflows'),
  label_singular: new TranslatableMarkup('workflow'),
  label_plural: new TranslatableMarkup('workflows'),
  config_prefix: 'flowdrop_workflow',
  entity_keys: [
    'id' => 'id',
    'label' => 'label',
    'uuid' => 'uuid',
  ],
  handlers: [
    'access' => FlowDropWorkflowAccessControlHandler::class,
    'list_builder' => FlowDropWorkflowListBuilder::class,
    'form' => [
      'add' => FlowDropWorkflowForm::class,
      'edit' => FlowDropWorkflowForm::class,
      'duplicate' => FlowDropWorkflowDuplicateForm::class,
      'delete' => EntityDeleteForm::class,
    ],
    'route_provider' => [
      'html' => 'Drupal\Core\Entity\Routing\AdminHtmlRouteProvider',
    ],
  ],
  links: [
    'add-form' => '/admin/flowdrop/workflows/add',
    'edit-form' => '/admin/flowdrop/workflows/{flowdrop_workflow}/edit',
    'delete-form' => '/admin/flowdrop/workflows/{flowdrop_workflow}/delete',
    'collection' => '/admin/flowdrop/workflows',
  ],
  admin_permission: 'administer flowdrop_workflow',
  label_count: [
    'singular' => '@count workflow',
    'plural' => '@count workflows',
  ],
  config_export: [
    'id',
    'label',
    'description',
    'nodes',
    'edges',
    'metadata',
    'created',
    'changed',
    'uid',
    'input_ports',
    'output_ports',
    'parameter_schema',
    'output_schema',
    'schema_version',
  ],
)]
final class FlowDropWorkflow extends ConfigEntityBase implements FlowDropWorkflowInterface {

  /**
   * The workflow label.
   *
   * @var string
   */
  protected string $label = '';

  /**
   * The workflow description.
   *
   * @var string
   */
  protected string $description = '';

  /**
   * The workflow nodes.
   *
   * @var array
   */
  protected array $nodes = [];

  /**
   * The workflow edges.
   *
   * @var array
   */
  protected array $edges = [];

  /**
   * The workflow metadata.
   *
   * @var array
   */
  protected array $metadata = [];

  /**
   * The creation timestamp.
   *
   * @var int
   */
  protected int $created = 0;

  /**
   * The last modified timestamp.
   *
   * @var int
   */
  protected int $changed = 0;

  /**
   * The user ID who created the workflow.
   *
   * @var int
   */
  protected int $uid = 0;

  /**
   * Input port exposure map.
   *
   * @var list<array{name: string, node_id: string, port: string}>|null
   */
  protected ?array $input_ports = NULL;

  /**
   * Output port exposure map.
   *
   * @var list<array{name: string, node_id: string, port: string}>|null
   */
  protected ?array $output_ports = NULL;

  /**
   * Parameter schema snapshot (FlowDrop Schema contract subset).
   *
   * @var array<string, mixed>|null
   */
  protected ?array $parameter_schema = NULL;

  /**
   * Output schema snapshot (FlowDrop Schema contract subset).
   *
   * @var array<string, mixed>|null
   */
  protected ?array $output_schema = NULL;

  /**
   * Schema version (semver string).
   *
   * @var string
   */
  protected string $schema_version = '0.0.0';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  /**
   * {@inheritdoc}
   */
  public function getGraphData(): array {
    return [
      "nodes" => $this->getNodes(),
      "edges" => $this->getEdges(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getInputPorts(): array {
    return $this->input_ports ?? [];
  }

  /**
   * {@inheritdoc}
   */
  public function setInputPorts(?array $ports): static {
    $this->input_ports = $ports;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputPorts(): array {
    return $this->output_ports ?? [];
  }

  /**
   * {@inheritdoc}
   */
  public function setOutputPorts(?array $ports): static {
    $this->output_ports = $ports;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): ?array {
    return $this->parameter_schema;
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): ?array {
    return $this->output_schema;
  }

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

  /**
   * {@inheritdoc}
   */
  public function setParameterSchema(?array $schema): static {
    $this->parameter_schema = $schema;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setOutputSchema(?array $schema): static {
    $this->output_schema = $schema;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function setSchemaVersion(string $version): static {
    $this->schema_version = $version;
    return $this;
  }

  /**
   * {@inheritdoc}
   *
   * Stamps timestamps only. The schema-snapshot rebuild lives in
   * {@see \Drupal\flowdrop_workflow\Service\WorkflowSchemaSnapshotterInterface}
   * and is wired up by the `flowdrop_workflow_presave` hook so the entity
   * stays free of service lookups.
   *
   * During config sync the imported `created`/`changed` values are
   * authoritative and must be preserved verbatim; re-stamping them would make
   * every config import re-stamp `changed`, producing a phantom diff on the
   * next export even when nothing substantive changed.
   */
  public function preSave(EntityStorageInterface $storage): void {
    // Slim each node to its irreducible per-instance shape. The authoritative
    // definition is the node type entity + plugin schema; the editor-produced
    // data.metadata cache (executor_plugin, name, ports, config defaults) and
    // the constant React Flow node-level `type` are re-injected on read
    // (NodeMetadataResolver) and re-resolved at execution, so persisting them
    // only invites drift and lets a tampered payload steer behaviour. Transient
    // xyflow canvas state (selected/dragging/deletable) is dropped too; the
    // editor re-supplies it on load. Skipped during config sync: imported
    // config is authoritative and must round-trip verbatim; legacy fat nodes
    // slim on their next user save.
    //
    // This MUST run before parent::preSave(): the parent invokes
    // calculateDependencies(), which reads each node's node_type_id anchor via
    // NodeAnchor (node_type_id only, no legacy fallback). Slimming first
    // renames a legacy `metadata.id` to `node_type_id`, so dependency
    // calculation sees the migrated anchor, not an un-migrated node.
    if (!$this->isSyncing()) {
      $this->setNodes(self::slimNodesForStorage($this->getNodes()));
    }

    parent::preSave($storage);

    if ($this->isSyncing()) {
      return;
    }

    $now = time();
    if ($this->isNew()) {
      $this->setCreated($now);
    }
    $this->setChanged($now);
  }

  /**
   * Reduces stored nodes to their irreducible per-instance shape.
   *
   * Two independent passes:
   * 1. Transient xyflow canvas state (`selected`, `dragging`, `deletable`)
   *    plus `data.nodeId` are dropped from every node. `selected`/`dragging`
   *    only reflect the editor's pointer state at the instant of save;
   *    `deletable` is a constant the editor stamps, while the real
   *    lock/read-only policy is applied at runtime rather than read from this
   *    flag; `data.nodeId` only ever duplicated the top-level node `id`. The
   *    editor re-supplies all of these on load. `measured` is deliberately
   *    kept: it is a useful layout cache (edge routing, fitView, auto-layout
   *    fallback) that xyflow would otherwise recompute on load.
   * 2. For standard, enrichable nodes, only the node-type anchor
   *    (data.metadata.node_type_id) and genuine user input (data.config,
   *    data.label, data.extensions, position, measured) are kept; the constant
   *    node-level `type` and every derived data.metadata.* field are dropped
   *    and re-injected on read.
   *
   * Nodes without a resolvable node_type_id anchor skip pass 2 (nothing to
   * re-resolve from on read) but still get pass 1. Idempotent.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The stored nodes.
   *
   * @return array<int, array<string, mixed>>
   *   The slimmed nodes.
   */
  private static function slimNodesForStorage(array $nodes): array {
    foreach ($nodes as $index => $node) {
      if (!is_array($node)) {
        continue;
      }
      // Pass 1: drop transient canvas state from every node plus data.nodeId,
      // which only ever duplicated the top-level node id. The editor owns and
      // re-supplies all of these; persisting them only adds churn at rest.
      unset($node['selected'], $node['dragging'], $node['deletable']);
      if (is_array($node['data'] ?? NULL)) {
        unset($node['data']['nodeId']);
      }

      $metadata = $node['data']['metadata'] ?? NULL;
      // Pass 2 applies only to standard, enrichable nodes — those anchored by
      // a node-type id that enrich-on-read can rebuild the derived fields from.
      // A node without that anchor cannot be re-enriched, so dropping its
      // fields would lose them irrecoverably; leave the rest of it verbatim.
      // Tolerate a legacy `metadata.id` anchor on read so an old-format config
      // import slims (and renames to node_type_id) on its next save — the only
      // back-compat seam; runtime code reads node_type_id exclusively.
      $typeId = is_array($metadata)
        ? ($metadata['node_type_id'] ?? $metadata['id'] ?? NULL)
        : NULL;
      if (!is_string($typeId)) {
        $nodes[$index] = $node;
        continue;
      }
      // The constant React Flow component type is re-injected on read.
      unset($node['type']);
      // Keep only the node-type anchor; everything else is derived.
      $node['data']['metadata'] = ['node_type_id' => $typeId];
      $nodes[$index] = $node;
    }
    return $nodes;
  }

  /**
   * {@inheritdoc}
   */
  public function setSchemaRebuildForced(bool $forced): static {
    $this->schemaRebuildForced = $forced;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function isSchemaRebuildForced(): bool {
    return $this->schemaRebuildForced;
  }

  /**
   * Transient runtime flag — not persisted on the config entity.
   *
   * @internal
   */
  protected bool $schemaRebuildForced = FALSE;

  /**
   * {@inheritdoc}
   */
  public function calculateDependencies(): static {
    parent::calculateDependencies();

    $nodes = $this->getNodes();
    if ($nodes === []) {
      return $this;
    }

    // A workflow depends on the node-type config entities its nodes reference
    // (the persisted `metadata.node_type_id` anchor — the single source of
    // truth). Declare that relationship honestly as a config dependency and
    // let the dependency graph carry the rest: each node type records a
    // `module` dependency on its executor plugin's provider, so the
    // workflow's module dependency is transitive — uninstalling that module
    // cascades through the node type to the workflow rather than vanishing
    // out from under it.
    //
    // This is the only seam where the entity touches a service: there is no
    // constructor injection for calculateDependencies(), which the config
    // system calls outside the save path (export, validation, uninstall).
    // The core entity_type.manager is the accepted lookup here; loading the
    // referenced node types also lets us derive the dependency name via the
    // entity API (no hardcoded config prefix) and silently skip dangling
    // references.
    $ids = [];
    foreach ($nodes as $node) {
      $nodeTypeId = NodeAnchor::read($node);
      if ($nodeTypeId !== '') {
        $ids[$nodeTypeId] = $nodeTypeId;
      }
    }
    if ($ids === []) {
      return $this;
    }

    $nodeTypes = \Drupal::entityTypeManager()
      ->getStorage('flowdrop_node_type')
      ->loadMultiple(array_keys($ids));
    foreach ($nodeTypes as $nodeType) {
      $this->addDependency(
        $nodeType->getConfigDependencyKey(),
        $nodeType->getConfigDependencyName(),
      );
    }

    return $this;
  }

}
