<?php
/**
 * Scheduled link-health batch runner.
 *
 * @package Affilun\Application\LinkHealth
 */

declare(strict_types=1);

namespace Affilun\Application\LinkHealth;

use Affilun\Application\Cloud\CloudLinkHealthClient;
use Affilun\Application\Settings\SettingsRepository;
use Affilun\Domain\Clock;
use Affilun\Domain\Sync\RetryPolicy;
use Affilun\Infrastructure\Persistence\OptionStore;
use Throwable;

/**
 * Executes one small Cloud-gated scheduled link-health batch.
 */
final class ScheduledLinkHealthBatchRunner {

	/**
	 * Option key storing cursor/retry state.
	 */
	public const OPTION_STATE = 'affilun_link_health_cron_state';

	/**
	 * Default number of URLs submitted per tick.
	 */
	public const DEFAULT_SUBMIT_LIMIT = 200;

	/**
	 * Default number of product rows scanned per tick.
	 */
	public const DEFAULT_SCAN_PAGE_SIZE = 500;

	/**
	 * Default freshness TTL before a product URL is checked again.
	 */
	public const DEFAULT_FRESH_TTL_SECONDS = 86400;

	/**
	 * Cooldown applied after the retry policy reaches a terminal failure.
	 */
	public const DEFAULT_TERMINAL_FAILURE_COOLDOWN_SECONDS = 21600;

	/**
	 * @param SettingsRepository                 $settings  Settings repository.
	 * @param OptionStore                        $options   Option store.
	 * @param SavedProductLinkHealthUrlCollector $collector URL collector.
	 * @param ProductLinkHealthStatusStore       $statusStore Status store.
	 * @param CloudLinkHealthClient              $client    Cloud client.
	 * @param Clock                              $clock     Clock.
	 * @param RetryPolicy                        $retry     Retry policy.
	 * @param int                                $submitLimit Submit limit.
	 * @param int                                $scanPageSize Scan page size.
	 * @param int                                $freshTtlSeconds Freshness TTL.
	 * @param int                                $terminalCooldownSeconds Terminal cooldown.
	 */
	public function __construct(
		private SettingsRepository $settings,
		private OptionStore $options,
		private SavedProductLinkHealthUrlCollector $collector,
		private ProductLinkHealthStatusStore $statusStore,
		private CloudLinkHealthClient $client,
		private Clock $clock,
		private RetryPolicy $retry,
		private int $submitLimit = self::DEFAULT_SUBMIT_LIMIT,
		private int $scanPageSize = self::DEFAULT_SCAN_PAGE_SIZE,
		private int $freshTtlSeconds = self::DEFAULT_FRESH_TTL_SECONDS,
		private int $terminalCooldownSeconds = self::DEFAULT_TERMINAL_FAILURE_COOLDOWN_SECONDS
	) {}

	/**
	 * Run one scheduled link-health tick.
	 */
	public function tick(): ScheduledLinkHealthTickResult {
		$now   = $this->clock->now();
		$cloud = $this->settings->load()->cloud();
		if ( ! $cloud->isConfigured() ) {
			return ScheduledLinkHealthTickResult::skipped( 'cloud_not_configured' );
		}

		$state = $this->loadState();
		if ( ! $state->canRun( $now ) ) {
			return ScheduledLinkHealthTickResult::skipped(
				'backoff',
				$state->attempts(),
				$state->nextAvailableAt()
			);
		}

		$batch = $this->collector->collect(
			$state->nextPage(),
			$this->scanPageSize,
			$this->submitLimit,
			$now,
			$this->freshTtlSeconds
		);

		if ( $batch->isEmpty() ) {
			$this->saveState( $state->recordSuccess( $batch->nextPage() ) );
			return ScheduledLinkHealthTickResult::success( $batch->scannedProducts(), 0, 0 );
		}

		try {
			$results   = $this->client->check(
				$cloud,
				$batch->urls(),
				$now->getTimestamp(),
				bin2hex( random_bytes( 12 ) ),
				$batch->contexts()
			);
			$persisted = $this->statusStore->persistResults( $results, $batch->contexts(), $now );
			$this->saveState( $state->recordSuccess( $batch->nextPage() ) );
			return ScheduledLinkHealthTickResult::success(
				$batch->scannedProducts(),
				count( $batch->urls() ),
				$persisted
			);
		} catch ( Throwable $e ) {
			$newAttempts    = $state->attempts() + 1;
			$storedAttempts = $newAttempts;
			if ( $this->retry->hasNextAttempt( $newAttempts ) ) {
				$waitSeconds = $this->retry->backoffSeconds( $newAttempts );
			} else {
				$waitSeconds    = $this->terminalCooldownSeconds;
				$storedAttempts = 0;
			}
			$nextAvailableAt = $now->setTimestamp( $now->getTimestamp() + $waitSeconds );
			$nextState       = $state->recordRetry( $storedAttempts, $nextAvailableAt, $e->getMessage() );
			$this->saveState( $nextState );
			return ScheduledLinkHealthTickResult::retryScheduled(
				$nextState->attempts(),
				$nextAvailableAt
			);
		}
	}

	/**
	 * Load persisted runner state.
	 */
	private function loadState(): ScheduledLinkHealthState {
		$stored = $this->options->get( self::OPTION_STATE, null );
		if ( ! is_array( $stored ) ) {
			return ScheduledLinkHealthState::defaults();
		}

		return ScheduledLinkHealthState::fromArray( $stored );
	}

	/**
	 * Persist runner state.
	 *
	 * @param ScheduledLinkHealthState $state State.
	 */
	private function saveState( ScheduledLinkHealthState $state ): void {
		$this->options->update( self::OPTION_STATE, $state->toArray() );
	}
}
