<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\flowdrop\Service\Bundle\BundleVerifier;
use Drupal\flowdrop\Service\Bundle\VerificationResult;
use Drupal\flowdrop_node_type\Service\NodeTypeGenerationResult;
use Drupal\flowdrop_node_type\Service\NodeTypeGeneratorInterface;
use Drupal\flowdrop_workflow\DTO\BundleImportResult;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;

/**
 * Imports a signed workflow bundle into a site.
 *
 * Orchestrates, in order: verify signature → validate structure (R1/R4/R5) →
 * build the capability manifest → generate node types for installed-but-
 * unprovisioned plugins → save the workflow. Each gate short-circuits, so the
 * returned BundleImportResult always carries the verdict that stopped (or
 * completed) the import. A dry run stops just before generation/save.
 *
 * Trust enforcement is split: this service decides nothing about permissions;
 * the caller passes $allowUntrusted (computed from the untrusted-import
 * permission plus an explicit confirm), and an untrusted bundle is blocked
 * unless that flag is set.
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
final class BundleImporter {

  /**
   * Machine-name pattern for an imported workflow id (untrusted input).
   */
  private const ID_PATTERN = '/^[a-z0-9_]+$/';

  /**
   * Maximum length of an imported workflow id.
   */
  private const ID_MAX_LENGTH = 64;

  public function __construct(
    private readonly BundleVerifier $verifier,
    private readonly WorkflowValidatorInterface $validator,
    private readonly WorkflowCapabilityManifestBuilder $manifestBuilder,
    private readonly NodeTypeGeneratorInterface $nodeTypeGenerator,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly AccountInterface $currentUser,
  ) {}

  /**
   * Imports or previews a bundle.
   *
   * @param array $bundle
   *   The decoded bundle (payload + signature envelope).
   * @param bool $dryRun
   *   When TRUE, run every gate but write nothing.
   * @param bool $allowUntrusted
   *   When TRUE, proceed even if the signature is untrusted/unsigned.
   *
   * @return \Drupal\flowdrop_workflow\DTO\BundleImportResult
   *   The import outcome.
   */
  public function import(array $bundle, bool $dryRun, bool $allowUntrusted): BundleImportResult {
    // Reject an envelope this release can't interpret before any processing.
    // The format is outside the signed payload, so this is a version gate
    // only, never a trust decision.
    if ((string) ($bundle['format'] ?? '') !== BundleVerifier::FORMAT) {
      return new BundleImportResult(BundleImportResult::UNSUPPORTED_FORMAT, VerificationResult::unsigned());
    }

    $verification = $this->verifier->verify($bundle);
    if (!$verification->isTrusted() && !$allowUntrusted) {
      return new BundleImportResult(BundleImportResult::UNTRUSTED_BLOCKED, $verification);
    }

    $payload = $bundle['payload'] ?? [];
    $flow = is_array($payload['flow'] ?? NULL) ? $payload['flow'] : [];
    $nodes = is_array($flow['nodes'] ?? NULL) ? $flow['nodes'] : [];
    $edges = is_array($flow['edges'] ?? NULL) ? $flow['edges'] : [];
    $inputPorts = is_array($flow['input_ports'] ?? NULL) ? $flow['input_ports'] : [];
    $outputPorts = is_array($flow['output_ports'] ?? NULL) ? $flow['output_ports'] : [];

    // The manifest is the import-time authority on capability presence: a node
    // referencing an uninstalled processor is an actionable "install module X",
    // not a generic structural error. Check it before validation so a missing
    // plugin is reported as such rather than as a validation failure (R1).
    $manifest = $this->manifestBuilder->build($nodes);
    if (!$manifest->canApply()) {
      return new BundleImportResult(BundleImportResult::MISSING_CAPABILITY, $verification, NULL, $manifest);
    }

    $validation = $this->validator->validate($nodes, $edges, $inputPorts, $outputPorts);
    if (!$validation->isValid()) {
      return new BundleImportResult(BundleImportResult::INVALID, $verification, $validation, $manifest);
    }

    // The flow id (untrusted) becomes the config entity id when present; reject
    // anything that isn't a valid machine name before it reaches storage.
    $flowId = (string) ($flow['id'] ?? '');
    if ($flowId !== '' && (strlen($flowId) > self::ID_MAX_LENGTH || preg_match(self::ID_PATTERN, $flowId) !== 1)) {
      return new BundleImportResult(BundleImportResult::INVALID, $verification, $validation, $manifest);
    }

    // A bundle's id collides with an existing workflow: never overwrite.
    $storage = $this->entityTypeManager->getStorage('flowdrop_workflow');
    if ($flowId !== '' && $storage->load($flowId) !== NULL) {
      return new BundleImportResult(BundleImportResult::CONFLICT, $verification, $validation, $manifest);
    }

    if ($dryRun) {
      return new BundleImportResult(BundleImportResult::PREFLIGHT_OK, $verification, $validation, $manifest);
    }

    // Generate node types for installed-but-unprovisioned plugins (additive;
    // existing types are left untouched).
    $generated = [];
    foreach ($nodes as $node) {
      $result = $this->nodeTypeGenerator->generate($node);
      if ($result->status === NodeTypeGenerationResult::CREATED) {
        $generated[] = $result->nodeTypeId;
      }
    }

    $workflow = $storage->create([
      'id' => $flowId !== '' ? $flowId : 'workflow_' . bin2hex(random_bytes(8)),
      'label' => (string) ($flow['label'] ?? $flow['name'] ?? 'Imported workflow'),
      'description' => (string) ($flow['description'] ?? ''),
      'nodes' => $nodes,
      'edges' => $edges,
      'input_ports' => $inputPorts,
      'output_ports' => $outputPorts,
      'metadata' => is_array($flow['metadata'] ?? NULL) ? $flow['metadata'] : [],
      'uid' => $this->currentUser->id(),
    ]);

    if (!$workflow instanceof WorkflowDefinitionInterface) {
      // Should not happen for a valid flow; surface as invalid rather than
      // a fatal so the caller gets a structured response.
      return new BundleImportResult(BundleImportResult::INVALID, $verification, $validation, $manifest);
    }

    $workflow->save();

    return new BundleImportResult(
      BundleImportResult::APPLIED,
      $verification,
      $validation,
      $manifest,
      (string) $workflow->id(),
      array_values(array_unique(array_filter($generated))),
    );
  }

}
