<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;

/**
 * Schedule node processor — pauses the workflow until a point in time.
 *
 * Accepts an ISO 8601 datetime, any `strtotime()`-parseable string
 * ("tomorrow 09:00"), or a numeric unix timestamp. The `when` input is
 * connectable, so upstream nodes (e.g. a publish_at field) can feed it.
 * A time already in the past passes straight through without pausing.
 *
 * Timezone: a datetime WITHOUT an explicit offset is resolved in the
 * active PHP timezone — which Drupal sets per-user, so "2026-06-10 09:00"
 * fires at 09:00 in the timezone of whoever launched the run. Prefer
 * ISO 8601 with an explicit offset (or a unix timestamp) for
 * deterministic fire times.
 *
 * Persists a Timer interrupt resolved by the cron sweep once due; resume
 * precision is cron-bound. Operators can skip the remaining wait from the
 * interrupt inbox or the /resume API.
 *
 * Config:
 * - when: The datetime to resume at.
 *
 * Outputs:
 * - scheduled_at / resumed_at / drift_seconds: timing audit trail.
 * - resolved_by_timer: FALSE when a user skipped the wait.
 * - user_id: the skipping user, if any.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "schedule",
  label: new TranslatableMarkup("Schedule"),
  description: "Pauses the workflow until a specific date and time",
  version: "1.0.0"
)]
class ScheduleNode extends AbstractTimerNode {

  /**
   * {@inheritdoc}
   *
   * An empty `when` passes validation: the value may arrive through a
   * connection and only exists at execution time, where computeFireAt()
   * rejects unparseable input.
   */
  public function validateParams(array $params): ValidationResult {
    $when = $params['when'] ?? NULL;
    if ($when === NULL || $when === '') {
      return ValidationResult::success();
    }
    if (!is_string($when) && !is_numeric($when)) {
      return ValidationResult::error("when", "Resume time must be a datetime string or unix timestamp.", "INVALID_TYPE");
    }

    $fireAt = $this->parseWhen((string) $when);
    if ($fireAt === NULL) {
      return ValidationResult::error("when", "Could not parse the resume time. Use ISO 8601 (2026-06-10T09:00:00+02:00) or a unix timestamp.", "INVALID_VALUE");
    }

    $maxDelay = $this->getMaxDelay();
    if ($maxDelay > 0 && $fireAt - time() > $maxDelay) {
      return ValidationResult::error("when", "Resume time exceeds the configured maximum delay of {$maxDelay} seconds.", "OUT_OF_RANGE");
    }

    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  protected function computeFireAt(ParameterBagInterface $params): int {
    $when = trim($params->getString('when'));
    $fireAt = $when === '' ? NULL : $this->parseWhen($when);
    if ($fireAt === NULL) {
      throw new \InvalidArgumentException(sprintf(
        'Schedule node could not parse resume time "%s". Use ISO 8601 or a unix timestamp.',
        $when,
      ));
    }
    return $fireAt;
  }

  /**
   * Minimum accepted numeric timestamp (2001-09-09).
   *
   * A bare year ("2026") or an ID-like connected number is_numeric() too,
   * and would silently parse as a 1970-era timestamp — already past, so
   * the node would pass through without pausing and without any error.
   * Rejecting sub-epoch-floor numerics surfaces the mistake as a
   * validation/runtime error instead of a silent no-op.
   */
  protected const MIN_NUMERIC_TIMESTAMP = 1000000000;

  /**
   * Parses a datetime input into a unix timestamp.
   *
   * @param string $when
   *   Numeric unix timestamp or `strtotime()`-parseable datetime.
   *
   * @return int|null
   *   The timestamp, or NULL when unparseable.
   */
  protected function parseWhen(string $when): ?int {
    $when = trim($when);
    if ($when === '') {
      return NULL;
    }
    if (is_numeric($when)) {
      $timestamp = (int) $when;
      return $timestamp >= self::MIN_NUMERIC_TIMESTAMP ? $timestamp : NULL;
    }
    $timestamp = strtotime($when);
    return $timestamp !== FALSE ? $timestamp : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "when" => [
          "type" => "string",
          "title" => "Resume at",
          "description" => "When to resume: ISO 8601 datetime, unix timestamp, or natural phrase like 'tomorrow 09:00'. Connect an upstream value or configure a fixed time. Past times pass through without pausing. Datetimes without an explicit offset use the launching user's timezone — prefer an explicit offset for deterministic times.",
        ],
        "__interrupt_id__" => $this->interruptIdParameterSchema(),
      ],
      "required" => ["when"],
    ];
  }

}
