<?php
/**
 * Link-health cron service provider.
 *
 * @package Affilun\Bootstrap
 */

declare(strict_types=1);

namespace Affilun\Bootstrap;

use Affilun\Application\Cloud\CloudLinkHealthClient;
use Affilun\Application\LinkHealth\ProductLinkHealthStatusStore;
use Affilun\Application\LinkHealth\SavedProductLinkHealthUrlCollector;
use Affilun\Application\LinkHealth\ScheduledLinkHealthBatchRunner;
use Affilun\Application\Settings\OptionStoreSettingsRepository;
use Affilun\Domain\Sync\RetryPolicy;
use Affilun\Infrastructure\Clock\SystemClock;
use Affilun\Infrastructure\Persistence\OptionStore;
use Affilun\Infrastructure\Persistence\PersistenceServiceProvider;
use Affilun\Infrastructure\Persistence\ProductMetaRepository;
use Affilun\Infrastructure\Persistence\ProductRepository;
use LogicException;
use Throwable;

/**
 * Wires scheduled saved-product link-health batches to WP-Cron.
 */
final class LinkHealthCronServiceProvider implements ServiceProvider {

	/**
	 * WP-Cron action name for scheduled saved-product link-health batches.
	 */
	public const HOOK_LINK_HEALTH_BATCH = 'affilun_cron_link_health_batch';

	/**
	 * Custom cron interval key for the scheduled link-health batch.
	 */
	public const INTERVAL_LINK_HEALTH_DAILY = 'affilun_link_health_daily';

	/**
	 * Registry binding for the scheduled link-health runner.
	 */
	public const BINDING_LINK_HEALTH_BATCH_RUNNER = 'affilun.link_health.batch_runner';

	/**
	 * Resolved runner cached after register().
	 */
	private ?ScheduledLinkHealthBatchRunner $runner = null;

	/**
	 * Compose and bind scheduled link-health collaborators.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$options     = $this->resolveOptionStore( $registry );
		$products    = $this->resolveProductRepository( $registry );
		$productMeta = $this->resolveProductMetaRepository( $registry );

		$statusStore = new ProductLinkHealthStatusStore( $productMeta );
		$collector   = new SavedProductLinkHealthUrlCollector( $products, $statusStore );
		$runner      = new ScheduledLinkHealthBatchRunner(
			new OptionStoreSettingsRepository( $options ),
			$options,
			$collector,
			$statusStore,
			new CloudLinkHealthClient(),
			new SystemClock(),
			RetryPolicy::forJobType( RetryPolicy::JOB_LINK_HEALTH_BATCH )
		);

		$this->runner = $runner;
		$registry->bind( self::BINDING_LINK_HEALTH_BATCH_RUNNER, $runner );
	}

	/**
	 * Wire WordPress hooks.
	 *
	 * @param HookRegistrar $hooks Shared hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$hooks->addFilter(
			'cron_schedules',
			static function ( $schedules ): array {
				if ( ! is_array( $schedules ) ) {
					$schedules = array();
				}

				if ( ! array_key_exists( self::INTERVAL_LINK_HEALTH_DAILY, $schedules ) ) {
					$schedules[ self::INTERVAL_LINK_HEALTH_DAILY ] = array(
						'interval' => 86400,
						'display'  => 'affilun: daily link-health batch',
					);
				}

				return $schedules;
			}
		);

		$hooks->addAction(
			'affilun_activate',
			function (): void {
				$this->onActivate();
			}
		);
		$hooks->addAction(
			'init',
			function (): void {
				$this->ensureScheduled();
			}
		);

		$runner = $this->runner;
		$hooks->addAction(
			self::HOOK_LINK_HEALTH_BATCH,
			static function () use ( $runner ): void {
				if ( $runner === null ) {
					return;
				}
				try {
					$runner->tick();
				} catch ( Throwable $e ) {
					if ( function_exists( 'error_log' ) ) {
						// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron failure surface; intentional.
						error_log( 'AFFILUN ScheduledLinkHealthBatchRunner tick failed: ' . $e->getMessage() );
					}
				}
			}
		);
	}

	/**
	 * Activation entry point: schedule daily link-health batches.
	 */
	public function onActivate(): void {
		if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) {
			return;
		}

		$this->ensureScheduled();
	}

	/**
	 * Ensure existing installs also get the link-health cron event.
	 */
	public function ensureScheduled(): void {
		if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) {
			return;
		}

		$timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH );
		if ( ! $timestamp ) {
			$this->scheduleNextBatch();
			return;
		}

		if (
			function_exists( 'wp_get_schedule' )
			&& function_exists( 'wp_unschedule_event' )
			&& wp_get_schedule( self::HOOK_LINK_HEALTH_BATCH ) !== self::INTERVAL_LINK_HEALTH_DAILY
		) {
			$this->unscheduleBatches();
			$this->scheduleNextBatch();
		}
	}

	/**
	 * Schedule the next daily link-health batch.
	 */
	private function scheduleNextBatch(): void {
		wp_schedule_event(
			time() + 300,
			self::INTERVAL_LINK_HEALTH_DAILY,
			self::HOOK_LINK_HEALTH_BATCH
		);
	}

	/**
	 * Deactivation entry point: unschedule link-health batches.
	 */
	public function onDeactivate(): void {
		if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_unschedule_event' ) ) {
			return;
		}

		$this->unscheduleBatches();
	}

	/**
	 * Unschedule all pending link-health batches.
	 */
	private function unscheduleBatches(): void {
		$timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH );
		while ( false !== $timestamp && null !== $timestamp ) {
			wp_unschedule_event( (int) $timestamp, self::HOOK_LINK_HEALTH_BATCH );
			$timestamp = wp_next_scheduled( self::HOOK_LINK_HEALTH_BATCH );
		}
	}

	/**
	 * Runner bound after register(), or null before register().
	 */
	public function runner(): ?ScheduledLinkHealthBatchRunner {
		return $this->runner;
	}

	/**
	 * Resolve option store from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	private function resolveOptionStore( ServiceProviderRegistry $registry ): OptionStore {
		$key = PersistenceServiceProvider::BINDING_OPTION_STORE;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'LinkHealthCronServiceProvider requires OptionStore binding.' );
		}

		$value = $registry->get( $key );
		if ( ! $value instanceof OptionStore ) {
			$message = sprintf(
				'LinkHealthCronServiceProvider expected OptionStore under "%s"; got %s.',
				$key,
				is_object( $value ) ? get_class( $value ) : gettype( $value )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $value;
	}

	/**
	 * Resolve product repository from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	private function resolveProductRepository( ServiceProviderRegistry $registry ): ProductRepository {
		$key = PersistenceServiceProvider::BINDING_PRODUCT_REPOSITORY;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'LinkHealthCronServiceProvider requires ProductRepository binding.' );
		}

		$value = $registry->get( $key );
		if ( ! $value instanceof ProductRepository ) {
			$message = sprintf(
				'LinkHealthCronServiceProvider expected ProductRepository under "%s"; got %s.',
				$key,
				is_object( $value ) ? get_class( $value ) : gettype( $value )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $value;
	}

	/**
	 * Resolve product-meta repository from the registry.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 */
	private function resolveProductMetaRepository( ServiceProviderRegistry $registry ): ProductMetaRepository {
		$key = PersistenceServiceProvider::BINDING_PRODUCT_META_REPOSITORY;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'LinkHealthCronServiceProvider requires ProductMetaRepository binding.' );
		}

		$value = $registry->get( $key );
		if ( ! $value instanceof ProductMetaRepository ) {
			$message = sprintf(
				'LinkHealthCronServiceProvider expected ProductMetaRepository under "%s"; got %s.',
				$key,
				is_object( $value ) ? get_class( $value ) : gettype( $value )
			);
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML.
			throw new LogicException( $message );
		}

		return $value;
	}
}
