<?php
/**
 * Amazon discount feature service provider.
 *
 * @package Affilun\Bootstrap
 */

declare(strict_types=1);

namespace Affilun\Bootstrap;

use Affilun\Application\AmazonDiscount\AmazonDiscountBrand;
use Affilun\Application\AmazonDiscount\AmazonDiscountBrandSettingsRepository;
use Affilun\Application\AmazonDiscount\AmazonDiscountCatalog;
use Affilun\Application\AmazonDiscount\AmazonDiscountCategory;
use Affilun\Application\AmazonDiscount\AmazonDiscountRepository;
use Affilun\Application\AmazonDiscount\AmazonDiscountSyncService;
use Affilun\Application\Provider\ProviderRegistry;
use Affilun\Application\Render\ProductCardRenderer;
use Affilun\Application\Render\RenderServiceProvider;
use Affilun\Application\Render\ViewModel\ProductCardViewModelFactory;
use Affilun\Application\Settings\OptionStoreSettingsRepository;
use Affilun\Application\Settings\SettingsRepository;
use Affilun\Infrastructure\Persistence\OptionStore;
use Affilun\Infrastructure\Persistence\OptionStoreAmazonDiscountBrandSettingsRepository;
use Affilun\Infrastructure\Persistence\PersistenceServiceProvider;
use Affilun\Presentation\Admin\AdminCapability;
use Affilun\Presentation\Admin\Page\AmazonDiscountConfigFormToken;
use Affilun\Presentation\Admin\Page\AmazonDiscountShortcodeCardRenderer;
use Affilun\Presentation\Admin\Page\AmazonDiscountSyncFormToken;
use Affilun\Presentation\Rest\ProviderServiceProvider;
use Affilun\Presentation\Shortcode\AmazonDiscountShortcodeRenderer;
use LogicException;
use Throwable;

/**
 * Wires local storage, daily Creators API sync, shortcode and admin preview.
 */
final class AmazonDiscountServiceProvider implements ServiceProvider {

	public const BINDING_SYNC_SERVICE        = 'affilun.amazon_discounts.sync_service';
	public const BINDING_SHORTCODE_RENDERER  = 'affilun.amazon_discounts.shortcode_renderer';
	public const BINDING_ADMIN_CARD_RENDERER = 'affilun.amazon_discounts.admin_card_renderer';
	public const HOOK_DAILY_SYNC             = 'affilun_cron_amazon_discount_sync';
	public const HOOK_MANUAL_SYNC            = 'affilun_cron_amazon_discount_manual_sync';

	/**
	 * Sync service when Amazon credentials are configured.
	 */
	private ?AmazonDiscountSyncService $syncService = null;

	/**
	 * Priority-brand settings used by the configurator and sync service.
	 */
	private ?AmazonDiscountBrandSettingsRepository $brandSettings = null;

	/**
	 * Discount snapshots used when removed brands are pruned.
	 */
	private ?AmazonDiscountRepository $repository = null;

	/**
	 * Bind discount feature collaborators.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$repository          = $this->resolveRepository( $registry );
		$factory             = $this->resolveViewModelFactory( $registry );
		$renderer            = $this->resolveProductCardRenderer( $registry );
		$settings            = $this->resolveSettingsRepository( $registry );
		$brands              = $this->resolveBrandSettingsRepository( $registry );
		$catalog             = $this->resolveOptionalCatalog( $registry );
		$this->brandSettings = $brands;
		$this->repository    = $repository;

		if ( $catalog !== null ) {
			$this->syncService = new AmazonDiscountSyncService(
				$catalog,
				$repository,
				null,
				$brands->load()
			);
			$registry->bind( self::BINDING_SYNC_SERVICE, $this->syncService );
		}

		$shortcode = new AmazonDiscountShortcodeRenderer( $repository, $factory, $renderer, $settings );
		$adminCard = new AmazonDiscountShortcodeCardRenderer(
			$repository,
			$shortcode,
			$this->syncService !== null,
			$brands
		);

		$registry->bind( self::BINDING_SHORTCODE_RENDERER, $shortcode );
		$registry->bind( self::BINDING_ADMIN_CARD_RENDERER, $adminCard );
	}

	/**
	 * Register scheduling and cron callbacks.
	 *
	 * @param HookRegistrar $hooks Hook registrar.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$hooks->addAction( 'affilun_activate', array( $this, 'ensureScheduled' ) );
		$hooks->addAction( 'init', array( $this, 'ensureScheduled' ) );
		$hooks->addAction( self::HOOK_DAILY_SYNC, array( $this, 'runSync' ) );
		$hooks->addAction( self::HOOK_MANUAL_SYNC, array( $this, 'runSync' ) );
		$hooks->addAction(
			'admin_post_' . AmazonDiscountSyncFormToken::FORM_ACTION,
			array( $this, 'handleManualSyncRequest' )
		);
		$hooks->addAction(
			'admin_post_' . AmazonDiscountConfigFormToken::FORM_ACTION,
			array( $this, 'handleConfigRequest' )
		);
	}

	/**
	 * Ensure the daily synchronization event exists.
	 */
	public function ensureScheduled(): void {
		if ( ! function_exists( 'wp_next_scheduled' ) || ! function_exists( 'wp_schedule_event' ) ) {
			return;
		}
		if ( ! wp_next_scheduled( self::HOOK_DAILY_SYNC ) ) {
			wp_schedule_event( time() + 300, 'daily', self::HOOK_DAILY_SYNC );
		}
	}

	/**
	 * Run one complete discount synchronization.
	 */
	public function runSync(): void {
		if ( $this->syncService === null ) {
			return;
		}

		try {
			$result = $this->syncService->sync();
			if ( $result->errorsByCategory() !== array() && function_exists( 'error_log' ) ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron partial-failure surface.
				error_log( 'AFFILUN Amazon discount sync partial failure: ' . wp_json_encode( $result->errorsByCategory() ) );
			}
		} catch ( Throwable $error ) {
			if ( function_exists( 'error_log' ) ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Cron failure surface.
				error_log( 'AFFILUN Amazon discount sync failed: ' . $error->getMessage() );
			}
		}
	}

	/**
	 * Validate the admin request and queue a due-now background sync.
	 */
	public function handleManualSyncRequest(): void {
		if (
			! function_exists( 'current_user_can' )
			|| ! current_user_can( AdminCapability::CAP_RUN_SYNC )
		) {
			$this->redirectManualSyncStatus( 'forbidden' );
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized by sanitizeText(), then verified immediately below.
		$rawNonce = $_POST[ AmazonDiscountSyncFormToken::NONCE_FIELD ] ?? '';
		$nonce    = is_scalar( $rawNonce ) ? $this->sanitizeText( (string) $rawNonce ) : '';
		if ( $nonce === '' ) {
			$this->redirectManualSyncStatus( 'invalid_nonce' );
			return;
		}
		if ( ! function_exists( 'wp_verify_nonce' ) ) {
			$this->redirectManualSyncStatus( 'invalid_nonce' );
			return;
		}
		if ( ! wp_verify_nonce( $nonce, AmazonDiscountSyncFormToken::NONCE_ACTION ) ) {
			$this->redirectManualSyncStatus( 'invalid_nonce' );
			return;
		}

		if ( $this->syncService === null ) {
			$this->redirectManualSyncStatus( 'unavailable' );
			return;
		}

		if ( ! function_exists( 'wp_schedule_single_event' ) ) {
			$this->redirectManualSyncStatus( 'error' );
			return;
		}

		$alreadyQueued = function_exists( 'wp_next_scheduled' )
			&& wp_next_scheduled( self::HOOK_MANUAL_SYNC ) !== false;
		if ( ! $alreadyQueued && ! wp_schedule_single_event( time(), self::HOOK_MANUAL_SYNC ) ) {
			$this->redirectManualSyncStatus( 'error' );
			return;
		}
		if ( function_exists( 'spawn_cron' ) ) {
			spawn_cron( time() );
		}

		$this->redirectManualSyncStatus( 'queued' );
	}

	/**
	 * Save priority brands and redirect to the updated shortcode preview.
	 */
	public function handleConfigRequest(): void {
		if (
			! function_exists( 'current_user_can' )
			|| ! current_user_can( AdminCapability::CAP_RUN_SYNC )
		) {
			$this->redirectConfigRequest( array() );
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized below, then verified.
		$rawNonce = $_POST[ AmazonDiscountConfigFormToken::NONCE_FIELD ] ?? '';
		$nonce    = is_scalar( $rawNonce ) ? $this->sanitizeText( (string) $rawNonce ) : '';
		if ( $nonce === '' ) {
			$this->redirectConfigRequest( array() );
			return;
		}
		if ( ! function_exists( 'wp_verify_nonce' ) ) {
			$this->redirectConfigRequest( array() );
			return;
		}
		if ( ! wp_verify_nonce( $nonce, AmazonDiscountConfigFormToken::NONCE_ACTION ) ) {
			$this->redirectConfigRequest( array() );
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; normalized through closed value objects below.
		$rawBrands      = $_POST['affilun_discount_brands'] ?? '';
		$brands         = AmazonDiscountBrand::parse(
			is_string( $rawBrands ) && function_exists( 'wp_unslash' )
				? wp_unslash( $rawBrands )
				: $rawBrands
		);
		$previousBrands = $this->brandSettings !== null ? $this->brandSettings->load() : array();
		if ( $this->brandSettings !== null ) {
			$this->brandSettings->save( $brands );
		}
		if ( $this->repository !== null ) {
			foreach ( array_diff( $previousBrands, $brands ) as $removedBrand ) {
				$this->repository->deleteBrand( $removedBrand );
			}
		}

		$rawCategories = $_POST['affilun_discount_categories'] ?? array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; unslashed next.
		$rawCategories = function_exists( 'wp_unslash' ) ? wp_unslash( $rawCategories ) : $rawCategories;
		$categories    = is_array( $rawCategories )
			? AmazonDiscountCategory::normalize( array_values( $rawCategories ) )
			: array();
		if ( $categories === array() ) {
			$categories = AmazonDiscountCategory::DEFAULT_SELECTION;
		}

		$rawSort  = $_POST['affilun_discount_sort'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; sanitizeText unslashes and sanitizes next.
		$sort     = AmazonDiscountShortcodeRenderer::sortFromAttributes(
			array( 'sort' => is_scalar( $rawSort ) ? $this->sanitizeText( (string) $rawSort ) : '' )
		);
		$rawCount = $_POST['affilun_discount_per_category'] ?? ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Nonce verified above; sanitizeText unslashes and sanitizes next.
		$count    = AmazonDiscountShortcodeRenderer::perCategoryFromAttributes(
			array( 'per_category' => is_scalar( $rawCount ) ? $this->sanitizeText( (string) $rawCount ) : '' )
		);

		$this->redirectConfigRequest(
			array(
				'affilun_discount_brands'       => implode( ',', $brands ),
				'affilun_discount_categories'   => $categories,
				'affilun_discount_sort'         => $sort,
				'affilun_discount_per_category' => $count,
			)
		);
	}

	/**
	 * Sanitize a submitted scalar without requiring WordPress in unit tests.
	 *
	 * @param string $value Submitted value.
	 */
	private function sanitizeText( string $value ): string {
		$value = function_exists( 'wp_unslash' ) ? wp_unslash( $value ) : $value;
		return function_exists( 'sanitize_text_field' )
			? sanitize_text_field( $value )
			: trim( $value );
	}

	/**
	 * Redirect back to the products page with a status token.
	 *
	 * @param string $status Closed status token.
	 */
	private function redirectManualSyncStatus( string $status ): void {
		$path = 'admin.php?page=affilun-products&'
			. AmazonDiscountSyncFormToken::STATUS_QUERY
			. '='
			. rawurlencode( $status );
		$url  = function_exists( 'admin_url' ) ? admin_url( $path ) : $path;
		if ( function_exists( 'wp_safe_redirect' ) ) {
			wp_safe_redirect( $url );
			exit;
		}
	}

	/**
	 * Redirect back to the products page with normalized preview values.
	 *
	 * @param array<string,mixed> $query Preview query.
	 */
	private function redirectConfigRequest( array $query ): void {
		$params = array_merge( array( 'page' => 'affilun-products' ), $query );
		$path   = 'admin.php?' . http_build_query( $params, '', '&', PHP_QUERY_RFC3986 );
		$url    = function_exists( 'admin_url' ) ? admin_url( $path ) : $path;
		if ( function_exists( 'wp_safe_redirect' ) ) {
			wp_safe_redirect( $url );
			exit;
		}
	}

	/**
	 * Resolve the discount repository.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveRepository( ServiceProviderRegistry $registry ): AmazonDiscountRepository {
		$key = PersistenceServiceProvider::BINDING_AMAZON_DISCOUNT_REPOSITORY;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'AmazonDiscountServiceProvider requires AmazonDiscountRepository.' );
		}
		$value = $registry->get( $key );
		if ( ! $value instanceof AmazonDiscountRepository ) {
			throw new LogicException( 'AmazonDiscountServiceProvider expected AmazonDiscountRepository.' );
		}
		return $value;
	}

	/**
	 * Resolve the standard card view-model factory.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveViewModelFactory( ServiceProviderRegistry $registry ): ProductCardViewModelFactory {
		$key   = RenderServiceProvider::BINDING_PRODUCT_CARD_VIEW_MODEL_FACTORY;
		$value = $registry->has( $key ) ? $registry->get( $key ) : null;
		if ( ! $value instanceof ProductCardViewModelFactory ) {
			throw new LogicException( 'AmazonDiscountServiceProvider requires ProductCardViewModelFactory.' );
		}
		return $value;
	}

	/**
	 * Resolve the standard card renderer.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveProductCardRenderer( ServiceProviderRegistry $registry ): ProductCardRenderer {
		$key   = RenderServiceProvider::BINDING_PRODUCT_CARD_RENDERER;
		$value = $registry->has( $key ) ? $registry->get( $key ) : null;
		if ( ! $value instanceof ProductCardRenderer ) {
			throw new LogicException( 'AmazonDiscountServiceProvider requires ProductCardRenderer.' );
		}
		return $value;
	}

	/**
	 * Resolve the same option-backed settings used by normal product cards.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveSettingsRepository( ServiceProviderRegistry $registry ): SettingsRepository {
		$key   = PersistenceServiceProvider::BINDING_OPTION_STORE;
		$value = $registry->has( $key ) ? $registry->get( $key ) : null;
		if ( ! $value instanceof OptionStore ) {
			throw new LogicException( 'AmazonDiscountServiceProvider requires OptionStore.' );
		}
		return new OptionStoreSettingsRepository( $value );
	}

	/**
	 * Resolve option-backed priority-brand settings.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveBrandSettingsRepository(
		ServiceProviderRegistry $registry
	): AmazonDiscountBrandSettingsRepository {
		$key   = PersistenceServiceProvider::BINDING_OPTION_STORE;
		$value = $registry->has( $key ) ? $registry->get( $key ) : null;
		if ( ! $value instanceof OptionStore ) {
			throw new LogicException( 'AmazonDiscountServiceProvider requires OptionStore.' );
		}
		return new OptionStoreAmazonDiscountBrandSettingsRepository( $value );
	}

	/**
	 * Resolve Amazon discount discovery when credentials are available.
	 *
	 * @param ServiceProviderRegistry $registry Service registry.
	 */
	private function resolveOptionalCatalog( ServiceProviderRegistry $registry ): ?AmazonDiscountCatalog {
		$key   = ProviderServiceProvider::BINDING_PROVIDER_REGISTRY;
		$value = $registry->has( $key ) ? $registry->get( $key ) : null;
		if ( ! $value instanceof ProviderRegistry || ! $value->has( 'amazon' ) ) {
			return null;
		}
		$provider = $value->get( 'amazon' );
		return $provider instanceof AmazonDiscountCatalog ? $provider : null;
	}
}
