<?php

declare(strict_types=1);

namespace Drupal\flowdrop_pipeline\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\flowdrop\Controller\Api\ApiResponseTrait;
use Drupal\flowdrop_job\Constants\JobStatus;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Constants\PausedReason;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;

/**
 * API Controller for FlowDrop Pipeline operations.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class PipelineApiController extends ControllerBase {

  use ApiResponseTrait;

  /**
   * Get pipelines for a workflow.
   *
   * @param string $workflow_id
   *   The workflow ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with pipelines data.
   */
  public function getWorkflowPipelines(string $workflow_id): JsonResponse {
    try {
      // Load workflow entity.
      $workflow_storage = $this->entityTypeManager()->getStorage('flowdrop_workflow');
      $workflow = $workflow_storage->load($workflow_id);

      if (!$workflow) {
        return $this->errorResponse("Workflow with ID {$workflow_id} does not exist.", Response::HTTP_NOT_FOUND);
      }

      // Load pipelines for this workflow.
      $pipeline_storage = $this->entityTypeManager()->getStorage('flowdrop_pipeline');
      $query = $pipeline_storage->getQuery()
        ->condition('workflow_id', $workflow_id)
        ->accessCheck(TRUE);
      $pipeline_ids = $query->execute();

      $pipelines = [];
      if (!empty($pipeline_ids)) {
        $pipeline_entities = $pipeline_storage->loadMultiple($pipeline_ids);
        foreach ($pipeline_entities as $pipeline) {
          if (!$pipeline instanceof FlowDropPipelineInterface) {
            continue;
          }
          $pipelines[] = $this->formatPipelineData($pipeline);
        }
      }

      return $this->successResponse([
        'workflow_id' => $workflow_id,
        'workflow_name' => $workflow->label(),
        'pipelines' => $pipelines,
        'count' => count($pipelines),
      ]);

    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_pipeline')->error('Error fetching workflow pipelines: @message', [
        '@message' => $e->getMessage(),
      ]);

      return $this->errorResponse('Failed to fetch pipelines for workflow.', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Get a specific pipeline.
   *
   * @param string $pipeline_id
   *   The pipeline ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with pipeline data.
   */
  public function getPipeline(string $pipeline_id): JsonResponse {
    try {
      $pipeline_storage = $this->entityTypeManager()->getStorage('flowdrop_pipeline');
      $pipeline = $pipeline_storage->load($pipeline_id);

      if (!$pipeline instanceof FlowDropPipelineInterface) {
        return $this->errorResponse("Pipeline with ID {$pipeline_id} does not exist.", Response::HTTP_NOT_FOUND);
      }

      if (!$pipeline->access('view')) {
        return $this->errorResponse('Access denied', Response::HTTP_FORBIDDEN);
      }

      return $this->successResponse($this->formatPipelineData($pipeline));

    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_pipeline')->error('Error fetching pipeline: @message', [
        '@message' => $e->getMessage(),
      ]);

      return $this->errorResponse('Failed to fetch pipeline.', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Get pipeline execution status (lightweight, poll-friendly).
   *
   * Returns only the lifecycle status, key timestamps, and the pending
   * interrupt (if any) — deliberately omits the jobs payload that
   * `getPipeline()` includes. Clients polling at sub-second intervals
   * should hit this endpoint; fetch the full pipeline (`getPipeline()`)
   * on completion or when opening a detail view.
   *
   * @param string $pipeline_id
   *   The pipeline ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with pipeline status data.
   */
  public function getPipelineStatus(string $pipeline_id): JsonResponse {
    try {
      $pipeline_storage = $this->entityTypeManager()->getStorage('flowdrop_pipeline');
      $pipeline = $pipeline_storage->load($pipeline_id);

      if (!$pipeline instanceof FlowDropPipelineInterface) {
        return $this->errorResponse("Pipeline with ID {$pipeline_id} does not exist.", Response::HTTP_NOT_FOUND);
      }

      if (!$pipeline->access('view')) {
        return $this->errorResponse('Access denied', Response::HTTP_FORBIDDEN);
      }

      $created = $pipeline->get('created')->value;
      $completed = $pipeline->get('completed')->value ?? NULL;

      // Surface the pending interrupt so a client seeing status='paused'
      // can resolve it without a second roundtrip to the interrupt API.
      // Null when no HITL is pending — the common case for in-flight runs.
      // NOTE: stamped only by the playground HITL path, so a budget pause
      // carries NULL here — clients key "continue" affordances on
      // status='paused' + pausedReason instead.
      $pendingInterrupt = NULL;
      $interruptId = $pipeline->getPendingInterruptId();
      if ($interruptId !== NULL && $interruptId !== '') {
        $pendingInterrupt = [
          'id' => $interruptId,
          'type' => $pipeline->getPendingInterruptType(),
        ];
      }

      return $this->successResponse([
        'id' => $pipeline->id(),
        'status' => $pipeline->getStatus(),
        'createdAt' => $created ? date('c', (int) $created) : NULL,
        'lastExecuted' => $completed ? date('c', (int) $completed) : NULL,
        'pendingInterrupt' => $pendingInterrupt,
        // Why the run is paused when an execution budget stopped it
        // ('time_budget' / 'max_iterations'); NULL otherwise. Resumable
        // via POST /flowdrop/api/pipelines/{id}/resume.
        'pausedReason' => $pipeline->getExecutionContext()[PausedReason::CONTEXT_KEY] ?? NULL,
      ]);

    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_pipeline')->error('Error fetching pipeline status: @message', [
        '@message' => $e->getMessage(),
      ]);

      return $this->errorResponse('Failed to fetch pipeline status.', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Get pipeline execution logs.
   *
   * @param string $pipeline_id
   *   The pipeline ID.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with pipeline logs.
   */
  public function getPipelineLogs(string $pipeline_id): JsonResponse {
    try {
      $pipeline_storage = $this->entityTypeManager()->getStorage('flowdrop_pipeline');
      $pipeline = $pipeline_storage->load($pipeline_id);

      if (!$pipeline instanceof FlowDropPipelineInterface) {
        return $this->errorResponse("Pipeline with ID {$pipeline_id} does not exist.", Response::HTTP_NOT_FOUND);
      }

      if (!$pipeline->access('view')) {
        return $this->errorResponse('Access denied', Response::HTTP_FORBIDDEN);
      }

      return $this->successResponse($this->buildLogsResponse($pipeline_id, $pipeline));

    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_pipeline')->error('Error fetching pipeline logs: @message', [
        '@message' => $e->getMessage(),
      ]);

      return $this->errorResponse('Failed to fetch pipeline logs.', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Format pipeline data for API response.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline entity.
   *
   * @return array
   *   Formatted pipeline data.
   */
  protected function formatPipelineData(FlowDropPipelineInterface $pipeline): array {
    // Get creation date.
    $created_at = $pipeline->get('created')->value;
    $created_date = $created_at ? date('c', (int) $created_at) : date('c');

    // Get completed date if available (this is the closest to "last executed")
    $completed = $pipeline->get('completed')->value ?? NULL;
    $last_executed_date = $completed ? date('c', (int) $completed) : NULL;

    // Get associated jobs for this pipeline.
    $jobs_data = $this->getPipelineJobs($pipeline->id());

    // Calculate execution count from jobs.
    $execution_count = count($jobs_data['jobs']);

    return [
      'id' => $pipeline->id(),
      'name' => $pipeline->label(),
      'description' => $pipeline->get('description')->value ?? '',
      'status' => $pipeline->getStatus(),
      'createdAt' => $created_date,
      'lastExecuted' => $last_executed_date,
      'executionCount' => (int) $execution_count,
      'execution_data' => [
        'input' => $pipeline->getInputData(),
        'output' => $pipeline->getOutputData(),
        'context' => $pipeline->getExecutionContext(),
      ],
      'node_statuses' => $jobs_data['node_statuses'],
      'jobs' => $jobs_data['jobs'],
      'job_status_summary' => $jobs_data['status_summary'],
      'timestamp' => date('c'),
    ];
  }

  /**
   * Get jobs for a specific pipeline.
   *
   * @param string $pipeline_id
   *   The pipeline ID.
   *
   * @return array
   *   Array containing jobs, node_statuses, and status_summary.
   */
  protected function getPipelineJobs(string $pipeline_id): array {
    try {
      // Load the pipeline entity to get its jobs directly.
      $pipeline_storage = $this->entityTypeManager()->getStorage('flowdrop_pipeline');
      $pipeline = $pipeline_storage->load($pipeline_id);

      if (!$pipeline instanceof FlowDropPipelineInterface) {
        return [
          'jobs' => [],
          'node_statuses' => [],
          'status_summary' => [
            'total' => 0,
            JobStatus::IDLE => 0,
            JobStatus::PENDING => 0,
            JobStatus::RUNNING => 0,
            JobStatus::COMPLETED => 0,
            JobStatus::FAILED => 0,
            JobStatus::CANCELLED => 0,
            JobStatus::SKIPPED => 0,
          ],
        ];
      }

      // Get jobs directly from the pipeline entity.
      $pipeline_jobs = $pipeline->getJobs();

      $jobs = [];
      $statusSummary = [
        'total' => 0,
        JobStatus::IDLE => 0,
        JobStatus::PENDING => 0,
        JobStatus::RUNNING => 0,
        JobStatus::COMPLETED => 0,
        JobStatus::FAILED => 0,
        JobStatus::CANCELLED => 0,
        JobStatus::SKIPPED => 0,
        JobStatus::INTERRUPTED => 0,
      ];
      $nodeStatuses = [];
      $jobsByNode = [];

      foreach ($pipeline_jobs as $job) {
        $status = $job->getStatus();
        $statusSummary['total']++;
        $statusSummary[$status]++;

        // Group jobs per node; loop iterations create multiple jobs for the
        // same node, so the representative status is resolved afterwards.
        $nodeId = $job->getNodeId();
        if ($nodeId) {
          $jobsByNode[$nodeId][] = $job;
        }

        // Format job data.
        $jobs[] = $this->formatJobData($job);
      }

      foreach ($jobsByNode as $nodeId => $nodeJobs) {
        $nodeStatuses[$nodeId] = $this->resolveNodeStatus($nodeJobs);
      }

      return [
        'jobs' => $jobs,
        'node_statuses' => $nodeStatuses,
        'status_summary' => $statusSummary,
      ];

    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_pipeline')->error('Error fetching pipeline jobs: @message', [
        '@message' => $e->getMessage(),
      ]);

      return [
        'jobs' => [],
        'node_statuses' => [],
        'status_summary' => [
          'total' => 0,
          JobStatus::IDLE => 0,
          JobStatus::PENDING => 0,
          JobStatus::RUNNING => 0,
          JobStatus::COMPLETED => 0,
          JobStatus::FAILED => 0,
          JobStatus::CANCELLED => 0,
          JobStatus::SKIPPED => 0,
        ],
      ];
    }
  }

  /**
   * Resolves a single representative status for a node from its jobs.
   *
   * Loop-orchestrated workflows create one job per iteration for the same
   * node — e.g. two completed runs plus a never-started next-iteration job
   * that is swept to "skipped" when the loop exits. The latest job (by
   * creation order) provides the status so the graph reflects where the run
   * ended up, while timing comes from the most recent job that actually
   * started. `executions` and `status_counts` carry the full per-iteration
   * picture so consumers can flag multi-execution nodes instead of
   * presenting the collapsed status as the whole story.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface[] $nodeJobs
   *   All jobs belonging to one node, in pipeline order.
   *
   * @return array
   *   The node status data, including execution count and per-status counts.
   */
  protected function resolveNodeStatus(array $nodeJobs): array {
    $latest = NULL;
    $latestCreated = -1;
    $lastRun = NULL;
    $lastRunStarted = -1;
    $statusCounts = [];
    $executions = 0;

    foreach ($nodeJobs as $job) {
      $status = $job->getStatus();
      $statusCounts[$status] = ($statusCounts[$status] ?? 0) + 1;

      // ">=" so the later job in pipeline order wins same-second ties.
      $created = (int) ($job->get('created')->value ?? 0);
      if ($created >= $latestCreated) {
        $latestCreated = $created;
        $latest = $job;
      }

      $started = $job->get('started')->value ? (int) $job->get('started')->value : NULL;
      if ($started !== NULL) {
        $executions++;
        if ($started >= $lastRunStarted) {
          $lastRunStarted = $started;
          $lastRun = $job;
        }
      }
    }

    $started = $lastRun?->get('started')->value ? (int) $lastRun->get('started')->value : NULL;
    $completed = $lastRun?->get('completed')->value ? (int) $lastRun->get('completed')->value : NULL;
    $execution_time = ($started !== NULL && $completed !== NULL) ? ($completed - $started) * 1000 : NULL;

    return [
      'status' => $latest->getStatus(),
      'last_executed' => $started ? date('c', $started) : NULL,
      'execution_time' => $execution_time,
      'execution_time_us' => $lastRun !== NULL ? $this->resolveExecutionTimeUs($lastRun) : NULL,
      'error' => ($lastRun ?? $latest)->get('error_message')->value ?? NULL,
      'executions' => $executions,
      'status_counts' => $statusCounts,
    ];
  }

  /**
   * Format job data for API response.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The job entity.
   *
   * @return array
   *   Formatted job data.
   */
  protected function formatJobData(FlowDropJobInterface $job): array {
    // Get creation date.
    $created_at = $job->get('created')->value;
    $created_date = $created_at ? date('c', (int) $created_at) : date('c');

    // Get started date if available.
    $started = $job->get('started')->value ?? NULL;
    $started_date = $started ? date('c', (int) $started) : NULL;

    // Get completed date if available.
    $completed = $job->get('completed')->value ?? NULL;
    $completed_date = $completed ? date('c', (int) $completed) : NULL;

    return [
      'id' => $job->id(),
      'label' => $job->label(),
      'status' => $job->getStatus(),
      'priority' => $job->getPriority(),
      'node_id' => $job->getNodeId(),
      'created_at' => $created_date,
      'started' => $started_date,
      'completed' => $completed_date,
      'execution_time_us' => $this->resolveExecutionTimeUs($job),
      'retry_count' => (int) ($job->get('retry_count')->value ?? 0),
      'max_retries' => (int) ($job->get('max_retries')->value ?? 0),
      'error_message' => $job->get('error_message')->value ?? NULL,
      'input_data' => $job->getInputData(),
      'output_data' => $job->getOutputData(),
      'metadata' => $job->getMetadata(),
      'timestamp' => date('c'),
    ];
  }

  /**
   * Resolves a job's execution time in microseconds.
   *
   * Prefers the precise `execution_time_us` the orchestrators stamp into job
   * metadata; falls back to the second-granularity started/completed
   * timestamps for jobs that predate the metadata stamp.
   *
   * @param \Drupal\flowdrop_job\FlowDropJobInterface $job
   *   The job entity.
   *
   * @return int|null
   *   Execution time in microseconds, or NULL when the job never completed.
   */
  protected function resolveExecutionTimeUs(FlowDropJobInterface $job): ?int {
    $metadata_us = $job->getMetadataValue('execution_time_us');
    if (is_numeric($metadata_us)) {
      return (int) $metadata_us;
    }

    $started = $job->get('started')->value ? (int) $job->get('started')->value : NULL;
    $completed = $job->get('completed')->value ? (int) $job->get('completed')->value : NULL;
    return ($started !== NULL && $completed !== NULL) ? ($completed - $started) * 1000000 : NULL;
  }

  /**
   * Builds the logs response array for a pipeline.
   *
   * @param string $pipeline_id
   *   The pipeline entity ID used in the request.
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline entity.
   *
   * @return array
   *   Logs response data.
   */
  protected function buildLogsResponse(string $pipeline_id, FlowDropPipelineInterface $pipeline): array {
    $logs_data = $pipeline->get('execution_logs')->value ?? [];

    if (is_string($logs_data)) {
      $logs_data = json_decode($logs_data, TRUE) ?? [];
    }

    if (!is_array($logs_data)) {
      $logs_data = [];
    }

    return [
      'pipeline_id' => $pipeline_id,
      'logs' => $logs_data,
      'count' => count($logs_data),
      'timestamp' => date('c'),
    ];
  }

}
