<?php

declare(strict_types=1);

namespace Drupal\flowdrop_pipeline\Drush\Commands;

use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface;
use Drush\Attributes as CLI;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands;

/**
 * Drush commands for inspecting pipeline runs.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class PipelineTraceCommands extends DrushCommands {

  use AutowireTrait;

  /**
   * Constructs a PipelineTraceCommands object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct();
  }

  /**
   * Dump the job trail of a pipeline.
   *
   * Prints one row per job with node ID, status, timing, error, and output
   * data. The table view truncates data columns for readability; use
   * --full to disable truncation, or --format=json for the complete
   * structured trace including input data and job metadata.
   */
  #[CLI\Command(name: "flowdrop:pipeline:trace", aliases: ["fd:pipeline:trace"])]
  #[CLI\Argument(name: "pipelineId", description: "The pipeline entity ID.")]
  #[CLI\Option(name: "full", description: "Do not truncate data columns in the table view.")]
  #[CLI\Usage(name: "flowdrop:pipeline:trace 42", description: "Show the job trail of pipeline 42.")]
  #[CLI\Usage(name: "flowdrop:pipeline:trace 42 --full", description: "Show the job trail without truncating data columns.")]
  #[CLI\Usage(name: "flowdrop:pipeline:trace 42 --format=json", description: "Dump the complete trace (input, output, metadata) as JSON.")]
  #[CLI\FieldLabels(labels: [
    "job" => "Job",
    "node" => "Node",
    "status" => "Status",
    "time" => "Time",
    "retries" => "Retries",
    "error" => "Error",
    "output" => "Output",
    "input" => "Input",
    "metadata" => "Metadata",
  ])]
  #[CLI\DefaultTableFields(fields: ["job", "node", "status", "time", "error", "output"])]
  public function trace(string $pipelineId, array $options = ["format" => "table", "full" => FALSE]): RowsOfFields {
    $pipeline = $this->entityTypeManager
      ->getStorage("flowdrop_pipeline")
      ->load($pipelineId);

    if (!$pipeline instanceof FlowDropPipelineRuntimeInterface) {
      throw new \InvalidArgumentException("Pipeline not found: {$pipelineId}");
    }

    // The summary goes to the human view only; structured formats stay
    // machine-readable.
    if ($options["format"] === "table") {
      $this->printSummary($pipeline);
    }

    $isTable = $options["format"] === "table";
    $truncate = $isTable && !$options["full"];

    $jobs = $pipeline->getJobs();
    usort($jobs, static fn ($a, $b) => (int) $a->id() <=> (int) $b->id());

    $rows = [];
    foreach ($jobs as $job) {
      $rows[] = [
        "job" => (string) $job->id(),
        "node" => $job->getNodeId(),
        "status" => $job->getStatus(),
        "time" => $this->formatJobTime($job->getMetadata(), $job->getStarted(), $job->getCompleted()),
        "retries" => (string) $job->getRetryCount(),
        "error" => $truncate ? $this->truncate($job->getErrorMessage()) : $job->getErrorMessage(),
        "output" => $this->formatData($job->getOutputData(), $isTable, $truncate),
        "input" => $this->formatData($job->getInputData(), $isTable, $truncate),
        "metadata" => $this->formatData($job->getMetadata(), $isTable, $truncate),
      ];
    }

    return new RowsOfFields($rows);
  }

  /**
   * Prints the pipeline summary block for the human-readable view.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineRuntimeInterface $pipeline
   *   The pipeline.
   */
  private function printSummary(FlowDropPipelineRuntimeInterface $pipeline): void {
    $counts = $pipeline->calculateJobCounts();
    $countSummary = implode(", ", array_map(
      static fn (string $status, int $count): string => "{$status}: {$count}",
      array_keys($counts),
      array_values($counts),
    ));

    $this->io()->definitionList(
      ["Pipeline" => (string) $pipeline->id()],
      ["Workflow" => $pipeline->getWorkflowId()],
      ["Status" => $pipeline->getStatus()],
      ["Started" => $this->formatTimestamp($pipeline->getStarted())],
      ["Completed" => $this->formatTimestamp($pipeline->getCompleted())],
      ["Error" => $pipeline->getErrorMessage() !== "" ? $pipeline->getErrorMessage() : "-"],
      ["Jobs" => $countSummary !== "" ? $countSummary : "-"],
    );
  }

  /**
   * Formats job execution time from metadata or start/completed stamps.
   *
   * @param array<string, mixed> $metadata
   *   The job metadata.
   * @param int $started
   *   The started timestamp.
   * @param int $completed
   *   The completed timestamp.
   *
   * @return string
   *   A human-readable duration, or "-" when unknown.
   */
  private function formatJobTime(array $metadata, int $started, int $completed): string {
    if (isset($metadata["execution_time_us"])) {
      return round(((int) $metadata["execution_time_us"]) / 1000000, 3) . "s";
    }
    if ($started > 0 && $completed >= $started) {
      return ($completed - $started) . "s";
    }
    return "-";
  }

  /**
   * Formats a unix timestamp for the summary block.
   *
   * @param int $timestamp
   *   The timestamp.
   *
   * @return string
   *   A formatted date, or "-" when unset.
   */
  private function formatTimestamp(int $timestamp): string {
    return $timestamp > 0 ? date("Y-m-d H:i:s", $timestamp) : "-";
  }

  /**
   * Formats a data array for output.
   *
   * Structured formats (json, yaml) receive the raw array; the table view
   * gets a JSON string, truncated unless --full was passed.
   *
   * @param array<string, mixed> $data
   *   The data to format.
   * @param bool $asString
   *   Whether to render as a string (table view).
   * @param bool $truncate
   *   Whether to truncate the rendered string.
   *
   * @return array<string, mixed>|string
   *   The formatted data.
   */
  private function formatData(array $data, bool $asString, bool $truncate): array|string {
    if (!$asString) {
      return $data;
    }
    $encoded = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: "{}";
    return $truncate ? $this->truncate($encoded) : $encoded;
  }

  /**
   * Truncates a string for the table view.
   *
   * @param string $value
   *   The value to truncate.
   *
   * @return string
   *   The truncated value.
   */
  private function truncate(string $value): string {
    return mb_strimwidth($value, 0, 100, "…");
  }

}
