<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Drush\Commands;

use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_workflow\DTO\BundleImportResult;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Drupal\flowdrop_workflow\Service\BundleExporter;
use Drupal\flowdrop_workflow\Service\BundleImporter;
use Drupal\flowdrop_workflow\Service\WorkflowDriftDetectorInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Drush\Attributes as CLI;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;
use Drush\Exceptions\CommandFailedException;

/**
 * Drush commands for workflow bundle export, import, and drift diagnostics.
 *
 * These wrap the same services the REST API uses (BundleExporter,
 * BundleImporter) plus the WorkflowDriftDetector, giving ops and CI a
 * headless path that emits and consumes the identical bundle envelope.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class WorkflowBundleCommands extends DrushCommands {

  use AutowireTrait;

  /**
   * Constructs a WorkflowBundleCommands object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\flowdrop_workflow\Service\BundleExporter $bundleExporter
   *   The bundle exporter.
   * @param \Drupal\flowdrop_workflow\Service\BundleImporter $bundleImporter
   *   The bundle importer.
   * @param \Drupal\flowdrop_workflow\Service\WorkflowDriftDetectorInterface $driftDetector
   *   The drift detector.
   */
  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly BundleExporter $bundleExporter,
    private readonly BundleImporter $bundleImporter,
    private readonly WorkflowDriftDetectorInterface $driftDetector,
  ) {
    parent::__construct();
  }

  /**
   * Export a workflow as a portable, unsigned bundle.
   *
   * Emits the same envelope as the REST export endpoint
   * (POST /api/flowdrop/workflows/{id}/export). The bundle is always unsigned;
   * signing is a publisher operation performed off-site with a private key.
   * Prints the JSON to stdout unless --output is given.
   */
  #[CLI\Command(name: "flowdrop:workflow:export", aliases: ["fd:wf:export"])]
  #[CLI\Argument(name: "id", description: "The workflow entity ID to export.")]
  #[CLI\Option(name: "output", description: "Write the bundle to this file instead of stdout.")]
  #[CLI\Usage(name: "flowdrop:workflow:export my_flow", description: "Print the bundle JSON to stdout.")]
  #[CLI\Usage(name: "flowdrop:workflow:export my_flow --output=my_flow.bundle.json", description: "Write the bundle to a file.")]
  public function export(string $id, array $options = ["output" => NULL]): void {
    $workflow = $this->entityTypeManager->getStorage("flowdrop_workflow")->load($id);
    if (!$workflow instanceof WorkflowDefinitionInterface) {
      throw new \InvalidArgumentException("Workflow not found: {$id}");
    }

    $bundle = $this->bundleExporter->export($workflow);
    $json = json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);

    $output = $options["output"];
    if (is_string($output) && $output !== "") {
      if (file_put_contents($output, $json) === FALSE) {
        throw new \RuntimeException("Could not write bundle to {$output}");
      }
      $this->io()->success("Exported workflow '{$id}' to {$output}");
      return;
    }

    $this->output()->writeln($json);
  }

  /**
   * Import a workflow bundle from a file.
   *
   * Runs the same gate chain as the REST import endpoint: verify signature →
   * capability manifest → validate → conflict check → generate node types →
   * save. Untrusted/unsigned bundles are blocked unless --allow-untrusted is
   * passed; on the CLI that flag is the trust gate (shell access is already
   * privileged), mirroring the permission + confirm flag the API requires.
   */
  #[CLI\Command(name: "flowdrop:workflow:import", aliases: ["fd:wf:import"])]
  #[CLI\Argument(name: "file", description: "Path to the bundle JSON file.")]
  #[CLI\Option(name: "dry-run", description: "Run every gate but write nothing (preflight only).")]
  #[CLI\Option(name: "allow-untrusted", description: "Proceed even if the bundle is untrusted or unsigned.")]
  #[CLI\Usage(name: "flowdrop:workflow:import my_flow.bundle.json", description: "Import a signed bundle from a trusted publisher.")]
  #[CLI\Usage(name: "flowdrop:workflow:import my_flow.bundle.json --dry-run", description: "Preflight the bundle without writing anything.")]
  #[CLI\Usage(name: "flowdrop:workflow:import my_flow.bundle.json --allow-untrusted", description: "Import an unsigned or untrusted bundle.")]
  public function import(string $file, array $options = ["dry-run" => FALSE, "allow-untrusted" => FALSE]): void {
    if (!is_file($file) || !is_readable($file)) {
      throw new \InvalidArgumentException("Bundle file not found or unreadable: {$file}");
    }
    $raw = (string) file_get_contents($file);
    try {
      $bundle = json_decode($raw, TRUE, 512, JSON_THROW_ON_ERROR);
    }
    catch (\JsonException $e) {
      throw new \InvalidArgumentException("Bundle file is not valid JSON: {$e->getMessage()}");
    }
    if (!is_array($bundle)) {
      throw new \InvalidArgumentException("Bundle file does not contain a JSON object.");
    }

    $dryRun = (bool) $options["dry-run"];
    $allowUntrusted = (bool) $options["allow-untrusted"];

    $result = $this->bundleImporter->import($bundle, $dryRun, $allowUntrusted);

    $this->reportImport($result);
  }

  /**
   * Report drift between saved workflows and live plugin schemas.
   *
   * For each workflow (or the one named), compares its stored schema snapshot
   * against the live schema rebuilt from its exposure-map ports. Drift means a
   * processor plugin's parameter/output schema changed after the workflow was
   * saved. By default this reports and exits zero (so --format=json stays
   * consumable); pass --fail-on-drift to exit non-zero when drift is found, so
   * CI can gate the build on it.
   */
  #[CLI\Command(name: "flowdrop:workflow:diagnose", aliases: ["fd:wf:diagnose"])]
  #[CLI\Argument(name: "id", description: "Limit to a single workflow ID. Omit to scan all.")]
  #[CLI\Option(name: "fail-on-drift", description: "Exit non-zero (and skip structured output) when any drift is found.")]
  #[CLI\Usage(name: "flowdrop:workflow:diagnose", description: "Scan every workflow for schema drift.")]
  #[CLI\Usage(name: "flowdrop:workflow:diagnose my_flow", description: "Scan a single workflow.")]
  #[CLI\Usage(name: "flowdrop:workflow:diagnose --format=json", description: "Emit drift findings as JSON.")]
  #[CLI\Usage(name: "flowdrop:workflow:diagnose --fail-on-drift", description: "Exit non-zero on drift, to gate CI.")]
  #[CLI\FieldLabels(labels: [
    "workflow" => "Workflow",
    "side" => "Side",
    "name" => "Exposed port",
    "node_id" => "Node",
    "port" => "Source port",
  ])]
  #[CLI\DefaultTableFields(fields: ["workflow", "side", "name", "node_id", "port"])]
  public function diagnose(?string $id = NULL, array $options = ["format" => "table", "fail-on-drift" => FALSE]): RowsOfFields {
    $storage = $this->entityTypeManager->getStorage("flowdrop_workflow");
    if ($id !== NULL && $id !== "") {
      $workflow = $storage->load($id);
      if (!$workflow instanceof FlowDropWorkflowInterface) {
        throw new \InvalidArgumentException("Workflow not found: {$id}");
      }
      $workflows = [$workflow];
    }
    else {
      $workflows = array_filter(
        $storage->loadMultiple(),
        static fn ($w): bool => $w instanceof FlowDropWorkflowInterface,
      );
    }

    $rows = [];
    foreach ($workflows as $workflow) {
      foreach ($this->driftDetector->detectDrift($workflow) as $entry) {
        $rows[] = [
          "workflow" => (string) $workflow->id(),
          "side" => $entry["side"],
          "name" => $entry["name"],
          "node_id" => $entry["node_id"],
          "port" => $entry["port"],
        ];
      }
    }

    // CI gate: render the findings to the human view, then fail the command so
    // the non-zero exit can break a build. Throwing forgoes the structured
    // formatter, which is why this is opt-in rather than the default.
    if ($options["fail-on-drift"] && $rows !== []) {
      $this->io()->table(["Workflow", "Side", "Exposed port", "Node", "Source port"], $rows);
      throw new CommandFailedException(sprintf("%d drifted exposure(s) across %d workflow(s).", count($rows), count($workflows)));
    }

    if ($options["format"] === "table") {
      if ($rows === []) {
        $this->io()->success(sprintf("No schema drift across %d workflow(s).", count($workflows)));
      }
      else {
        $this->io()->warning(sprintf("%d drifted exposure(s) across %d workflow(s). Re-open and save each flow to refresh its snapshot.", count($rows), count($workflows)));
      }
    }

    return new RowsOfFields($rows);
  }

  /**
   * Prints the import outcome and throws on a non-success status.
   *
   * @param \Drupal\flowdrop_workflow\DTO\BundleImportResult $result
   *   The import outcome.
   */
  private function reportImport(BundleImportResult $result): void {
    $verification = $result->getVerification();
    $this->io()->definitionList(
      ["Trust" => $verification->getStatus() . ($verification->getKeyId() !== NULL ? " ({$verification->getKeyId()})" : "")],
      ["Status" => $result->getStatus()],
    );

    switch ($result->getStatus()) {
      case BundleImportResult::APPLIED:
        $generated = $result->getGeneratedNodeTypes();
        $this->io()->success(sprintf(
          "Imported workflow '%s'.%s",
          (string) $result->getWorkflowId(),
          $generated === [] ? "" : " Generated node types: " . implode(", ", $generated) . ".",
        ));
        return;

      case BundleImportResult::PREFLIGHT_OK:
        $this->io()->success("Preflight passed; nothing was written (dry run).");
        return;

      case BundleImportResult::UNTRUSTED_BLOCKED:
        throw new \RuntimeException("Bundle is untrusted or unsigned. Add the publisher under Trusted Publishers, or re-run with --allow-untrusted.");

      case BundleImportResult::MISSING_CAPABILITY:
        $modules = $result->getManifest()?->missingModules() ?? [];
        throw new \RuntimeException("Bundle needs uninstalled processors. Install module(s): " . (implode(", ", $modules) ?: "unknown") . ".");

      case BundleImportResult::CONFLICT:
        throw new \RuntimeException("A workflow with this ID already exists; import never overwrites.");

      case BundleImportResult::INVALID:
        $errors = $result->getValidation()?->getErrorMessages() ?? [];
        throw new \RuntimeException("Bundle failed validation: " . (implode("; ", $errors) ?: "unknown error") . ".");

      default:
        throw new \RuntimeException("Import did not complete: {$result->getStatus()}.");
    }
  }

}
