<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

use Drupal\ffmpeg_media\Enum\ConversionStatus;

/**
 * Immutable value object representing conversion progress.
 *
 * Uses PHP 8.5 clone() with associative array for immutable transformations.
 */
final class ProgressData {

  /**
   * Constructs a ProgressData object.
   *
   * @param int $mediaId
   *   The media entity ID.
   * @param \Drupal\ffmpeg_media\Enum\ConversionStatus $status
   *   Current conversion status.
   * @param int $percent
   *   Progress percentage (0-100).
   * @param string|null $currentStep
   *   Current step description.
   * @param string|null $currentOutput
   *   Current output format being generated.
   * @param string|null $error
   *   Error message if failed.
   * @param int|null $startedAt
   *   Unix timestamp when conversion started.
   * @param int|null $completedAt
   *   Unix timestamp when conversion completed.
   * @param int $updatedAt
   *   Unix timestamp of last progress update.
   * @param string|null $correlationId
   *   UUID correlation ID for log tracing across the pipeline.
   * @param float|null $elapsedSeconds
   *   FFmpeg encoding position in seconds (from FFmpeg output, not wall clock).
   *   Used when total duration is unknown and percentage cannot be computed.
   * @param string $fieldName
   *   The entity field name this progress is tracked for.
   *
   * @throws \InvalidArgumentException
   *   If percent is out of range.
   */
  public function __construct(
    public readonly int $mediaId,
    public readonly ConversionStatus $status,
    public readonly int $percent,
    public readonly ?string $currentStep = NULL,
    public readonly ?string $currentOutput = NULL,
    public readonly ?string $error = NULL,
    public readonly ?int $startedAt = NULL,
    public readonly ?int $completedAt = NULL,
    public readonly int $updatedAt = 0,
    public readonly ?string $correlationId = NULL,
    public readonly ?float $elapsedSeconds = NULL,
    public readonly string $fieldName = '',
  ) {
    if ($percent < 0 || $percent > 100) {
      throw new \InvalidArgumentException('Percent must be between 0 and 100.');
    }
  }

  // phpcs:disable -- PHP 8.4 property hooks not yet supported by PHPCS sniffs.
  /**
   * Whether conversion is complete.
   */
  public bool $isComplete {
    get => $this->status === ConversionStatus::Completed;
  }

  /**
   * Whether conversion has failed.
   */
  public bool $hasFailed {
    get => $this->status === ConversionStatus::Failed;
  }

  /**
   * Whether conversion is in progress.
   */
  public bool $isInProgress {
    get => $this->status === ConversionStatus::Converting;
  }

  /**
   * Whether conversion is pending.
   */
  public bool $isPending {
    get => $this->status === ConversionStatus::Pending;
  }

  /**
   * Elapsed time since start (seconds), or NULL if not started.
   */
  public ?int $elapsedTime {
    get => $this->calculateElapsedTime();
  }

  /**
   * Estimated remaining time (seconds), or NULL if cannot estimate.
   */
  public ?int $estimatedRemainingTime {
    get => $this->calculateEstimatedRemainingTime();
  }
  // phpcs:enable

  /**
   * Calculates elapsed time since start.
   */
  private function calculateElapsedTime(): ?int {
    if ($this->startedAt === NULL) {
      return NULL;
    }

    $endTime = $this->completedAt ?? time();
    return $endTime - $this->startedAt;
  }

  /**
   * Calculates estimated remaining time.
   */
  private function calculateEstimatedRemainingTime(): ?int {
    if ($this->percent <= 0 || $this->percent >= 100) {
      return NULL;
    }

    $elapsed = $this->elapsedTime;
    if ($elapsed === NULL || $elapsed <= 0) {
      return NULL;
    }

    $totalEstimated = ($elapsed / $this->percent) * 100;
    return (int) max(0, $totalEstimated - $elapsed);
  }

  /**
   * Creates ProgressData from a database row.
   *
   * @param array<string, int|string|null> $row
   *   Database row from ffmpeg_media_progress table.
   *
   * @return self
   *   New ProgressData instance.
   */
  #[\NoDiscard]
  public static function fromDatabaseRow(array $row): self {
    return new self(
      mediaId: (int) $row['media_id'],
      status: ConversionStatus::from((string) $row['status']),
      percent: (int) $row['progress_percent'],
      currentStep: $row['current_step'] ?? NULL,
      currentOutput: $row['current_output'] ?? NULL,
      error: $row['error_message'] ?? NULL,
      startedAt: isset($row['started']) ? (int) $row['started'] : NULL,
      completedAt: isset($row['completed']) ? (int) $row['completed'] : NULL,
      updatedAt: (int) ($row['updated'] ?? 0),
      correlationId: $row['correlation_id'] ?? NULL,
      elapsedSeconds: isset($row['elapsed_seconds']) ? (float) $row['elapsed_seconds'] : NULL,
      fieldName: (string) ($row['field_name'] ?? ''),
    );
  }

  /**
   * Creates initial pending progress.
   *
   * @param int $mediaId
   *   The media entity ID.
   *
   * @return self
   *   New ProgressData instance in pending state.
   */
  #[\NoDiscard]
  public static function pending(int $mediaId): self {
    return new self(
      mediaId: $mediaId,
      status: ConversionStatus::Pending,
      percent: 0,
      updatedAt: time(),
    );
  }

  /**
   * Creates a new instance with elapsed encoding time.
   *
   * @param float $seconds
   *   FFmpeg encoding position in seconds.
   * @param string|null $step
   *   Current step description.
   * @param string|null $output
   *   Current output format.
   *
   * @return self
   *   New instance with elapsed time.
   */
  #[\NoDiscard]
  public function withElapsedTime(float $seconds, ?string $step = NULL, ?string $output = NULL): self {
    return clone($this, [
      'status' => ConversionStatus::Converting,
      'currentStep' => $step ?? $this->currentStep,
      'currentOutput' => $output ?? $this->currentOutput,
      'elapsedSeconds' => $seconds,
      'updatedAt' => time(),
    ]);
  }

  /**
   * Creates a new instance with updated progress.
   *
   * @param int $percent
   *   New progress percentage.
   * @param string|null $step
   *   Current step description.
   * @param string|null $output
   *   Current output format.
   *
   * @return self
   *   New instance with updated progress.
   */
  #[\NoDiscard]
  public function withProgress(int $percent, ?string $step = NULL, ?string $output = NULL): self {
    $percent = max(0, min(100, $percent));

    return clone($this, [
      'status' => ConversionStatus::Converting,
      'percent' => $percent,
      'currentStep' => $step ?? $this->currentStep,
      'currentOutput' => $output ?? $this->currentOutput,
      'updatedAt' => time(),
    ]);
  }

  /**
   * Creates a new instance marked as completed.
   *
   * @return self
   *   New instance in completed state.
   */
  #[\NoDiscard]
  public function asCompleted(): self {
    return clone($this, [
      'status' => ConversionStatus::Completed,
      'percent' => 100,
      'error' => NULL,
      'completedAt' => time(),
      'updatedAt' => time(),
    ]);
  }

  /**
   * Creates a new instance marked as failed.
   *
   * @param string $error
   *   Error message.
   *
   * @return self
   *   New instance in failed state.
   */
  #[\NoDiscard]
  public function asFailed(string $error): self {
    return clone($this, [
      'status' => ConversionStatus::Failed,
      'error' => $error,
      'completedAt' => time(),
      'updatedAt' => time(),
    ]);
  }

  /**
   * Converts progress to array for JSON response.
   *
   * @return array<string, bool|float|int|string|null>
   *   Associative array representation.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'media_id' => $this->mediaId,
      'status' => $this->status->value,
      'percent' => $this->percent,
      'current_step' => $this->currentStep,
      'current_output' => $this->currentOutput,
      'error' => $this->error,
      'started_at' => $this->startedAt,
      'completed_at' => $this->completedAt,
      'updated_at' => $this->updatedAt,
      'elapsed_time' => $this->elapsedTime,
      'estimated_remaining' => $this->estimatedRemainingTime,
      'elapsed_seconds' => $this->elapsedSeconds,
      'correlation_id' => $this->correlationId,
    ];
  }

}
