<?php
/**
 * Affiliate routing admin service provider.
 *
 * @package Affilun\Presentation\Admin\AffiliateRouting
 */

declare(strict_types=1);

namespace Affilun\Presentation\Admin\AffiliateRouting;

use Affilun\Application\Affiliate\OptionStoreAffiliateRoutingRepository;
use Affilun\Bootstrap\HookRegistrar;
use Affilun\Bootstrap\ServiceProvider;
use Affilun\Bootstrap\ServiceProviderRegistry;
use Affilun\Infrastructure\Persistence\OptionStore;
use Affilun\Infrastructure\Persistence\PersistenceServiceProvider;
use LogicException;
use Throwable;

/**
 * Adds a small admin page for advanced ASP-style affiliate routing templates.
 */
final class AffiliateRoutingAdminServiceProvider implements ServiceProvider {

	private const NONCE_ACTION = 'affilun_save_affiliate_routing';

	private const NONCE_FIELD = '_affilun_affiliate_routing_nonce';

	private const FORM_ACTION = 'affilun_save_affiliate_routing';

	/**
	 * Routing repository.
	 */
	private ?OptionStoreAffiliateRoutingRepository $repository = null;

	/**
	 * Register repository.
	 *
	 * @param ServiceProviderRegistry $registry Registry.
	 *
	 * @throws LogicException When the option store binding is missing or invalid.
	 */
	public function register( ServiceProviderRegistry $registry ): void {
		$key = PersistenceServiceProvider::BINDING_OPTION_STORE;
		if ( ! $registry->has( $key ) ) {
			throw new LogicException( 'AffiliateRoutingAdminServiceProvider requires OptionStore.' );
		}
		$options = $registry->get( $key );
		if ( ! $options instanceof OptionStore ) {
			throw new LogicException( 'AffiliateRoutingAdminServiceProvider expected OptionStore.' );
		}
		$this->repository = new OptionStoreAffiliateRoutingRepository( $options );
	}

	/**
	 * Register hooks.
	 *
	 * @param HookRegistrar $hooks Hooks.
	 */
	public function boot( HookRegistrar $hooks ): void {
		$hooks->addAction( 'admin_post_' . self::FORM_ACTION, array( $this, 'onSavePost' ) );
	}

	/**
	 * Add submenu page.
	 */
	public function onAdminMenu(): void {
		if ( ! function_exists( 'add_submenu_page' ) ) {
			return;
		}

		add_submenu_page(
			'affilun-products',
			'affilun ASPルーティング（上級者向け）',
			'ASPルーティング',
			'manage_options',
			'affilun-affiliate-routing',
			array( $this, 'renderPage' )
		);
	}

	/**
	 * Render page.
	 */
	public function renderPage(): void {
		if ( $this->repository === null ) {
			return;
		}
		$templates = $this->repository->loadRaw();

		echo '<div class="wrap"><h1>' . esc_html( 'ASPルーティング（上級者向け）' ) . '</h1>';
		echo '<p>' . esc_html( '通常は各プロバイダーの設定タブを使用してください。独自ASPや移行用URLテンプレートが必要な場合のみ、プロバイダー別テンプレートを任意設定できます。空欄は通常URLのままです。' ) . '</p>';
		echo '<p>' . esc_html( '利用可能な変数: {url}, {encoded_url}, {provider}, {product_id}' ) . '</p>';
		echo '<form method="post" action="' . esc_url( self::adminPostUrl() ) . '">';
		echo '<input type="hidden" name="action" value="' . esc_attr( self::FORM_ACTION ) . '">';
		if ( function_exists( 'wp_nonce_field' ) ) {
			wp_nonce_field( self::NONCE_ACTION, self::NONCE_FIELD );
		}
		echo '<p><label><input type="checkbox" name="enabled" value="1"';
		if ( $this->repository->enabled() ) {
			echo ' checked="checked"';
		}
		echo '> ' . esc_html( '有効化' ) . '</label></p>';
		foreach ( OptionStoreAffiliateRoutingRepository::PROVIDERS as $provider ) {
			$value = $templates[ $provider ] ?? '';
			echo '<p><label for="affilun-routing-' . esc_attr( $provider ) . '">';
			echo esc_html( $provider ) . '</label><br>';
			echo '<input class="large-text" type="url" id="affilun-routing-' . esc_attr( $provider ) . '"';
			echo ' name="templates[' . esc_attr( $provider ) . ']" value="' . esc_attr( $value ) . '"';
			echo ' placeholder="https://example.test/route?url={encoded_url}"></p>';
		}
		echo '<p><button type="submit" class="button button-primary">' . esc_html( '保存' ) . '</button></p>';
		echo '</form></div>';
	}

	/**
	 * Save posted templates.
	 */
	public function onSavePost(): void {
		if ( $this->repository === null || ! self::canManage() || ! self::verifyNonce() ) {
			self::redirect( 'forbidden' );
			return;
		}

		$postData = self::currentPostData();
		$raw      = $postData['templates'] ?? array();
		$values   = array();
		if ( is_array( $raw ) ) {
			foreach ( OptionStoreAffiliateRoutingRepository::PROVIDERS as $provider ) {
				$value               = $raw[ $provider ] ?? '';
				$values[ $provider ] = is_scalar( $value ) ? (string) $value : '';
			}
		}

		try {
			$this->repository->saveRaw( $values, self::readBool( $postData, 'enabled' ) );
			self::redirect( 'saved' );
		} catch ( Throwable $unused ) {
			unset( $unused );
			self::redirect( 'error' );
		}
	}

	/**
	 * Read a checkbox-style boolean from a form payload.
	 *
	 * @param array<string, mixed> $source Source array.
	 * @param string               $key    Field key.
	 */
	private static function readBool( array $source, string $key ): bool {
		if ( ! array_key_exists( $key, $source ) ) {
			return false;
		}

		$value = $source[ $key ];
		return ! ( $value === false || $value === 0 || $value === '0' || $value === '' || $value === null );
	}

	/**
	 * Capability check.
	 */
	private static function canManage(): bool {
		return function_exists( 'current_user_can' ) && current_user_can( 'manage_options' );
	}

	/**
	 * Nonce check.
	 */
	private static function verifyNonce(): bool {
		$postData = self::currentPostData();
		$value    = $postData[ self::NONCE_FIELD ] ?? '';
		$nonce    = is_scalar( $value ) ? self::sanitizeTextField( (string) $value ) : '';
		return $nonce !== '' && function_exists( 'wp_verify_nonce' )
			&& (bool) wp_verify_nonce( $nonce, self::NONCE_ACTION );
	}

	/**
	 * Return sanitized POST data for the affiliate-routing form.
	 *
	 * @return array<string,mixed>
	 */
	private static function currentPostData(): array {
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified before callers use the payload.
		$raw       = $_POST;
		$unslashed = function_exists( 'wp_unslash' ) ? wp_unslash( $raw ) : $raw;
		if ( ! is_array( $unslashed ) ) {
			return array();
		}

		$result = array();
		foreach ( $unslashed as $key => $value ) {
			if ( ! is_string( $key ) ) {
				continue;
			}
			$result[ $key ] = self::sanitizePostValue( $key, $value );
		}

		return $result;
	}

	/**
	 * Sanitize one POST value recursively.
	 *
	 * @param string $key   Field key.
	 * @param mixed  $value Field value.
	 *
	 * @return mixed
	 */
	private static function sanitizePostValue( string $key, $value ) {
		if ( is_array( $value ) ) {
			$result = array();
			foreach ( $value as $childKey => $childValue ) {
				if ( ! is_string( $childKey ) ) {
					continue;
				}
				$result[ $childKey ] = self::sanitizePostValue( $childKey, $childValue );
			}
			return $result;
		}

		if ( ! is_scalar( $value ) ) {
			return '';
		}

		$string = (string) $value;
		if ( $key === 'action' || $key === 'enabled' || $key === self::NONCE_FIELD ) {
			return self::sanitizeTextField( $string );
		}

		return self::sanitizeRoutingTemplate( $string );
	}

	/**
	 * Sanitize a routing template while preserving URL-template placeholders.
	 *
	 * @param string $value Raw template.
	 */
	private static function sanitizeRoutingTemplate( string $value ): string {
		$value = self::sanitizeTextField( $value );
		return trim( preg_replace( '/[\x00-\x1F\x7F]/', '', $value ) ?? $value );
	}

	/**
	 * Sanitize a single-line text field.
	 *
	 * @param string $value Raw value.
	 */
	private static function sanitizeTextField( string $value ): string {
		if ( function_exists( 'sanitize_text_field' ) ) {
			return (string) sanitize_text_field( $value );
		}

		$value = preg_replace( '/<[^>]*>/', '', $value ) ?? $value;
		return trim( (string) preg_replace( '/\s+/', ' ', $value ) );
	}

	/**
	 * Redirect back to settings page.
	 *
	 * @param string $status Status token.
	 */
	private static function redirect( string $status ): void {
		$url  = self::adminPageUrl();
		$url .= ( str_contains( $url, '?' ) ? '&' : '?' ) . 'affilun_status=' . rawurlencode( $status );
		if ( function_exists( 'wp_safe_redirect' ) ) {
			wp_safe_redirect( $url );
			exit;
		}
	}

	/**
	 * Admin-post URL.
	 */
	private static function adminPostUrl(): string {
		return function_exists( 'admin_url' ) ? (string) admin_url( 'admin-post.php' ) : 'admin-post.php';
	}

	/**
	 * Admin page URL.
	 */
	private static function adminPageUrl(): string {
		return function_exists( 'admin_url' )
			? (string) admin_url( 'options-general.php?page=affilun-settings#affilun-settings-tab-asp' )
			: 'options-general.php?page=affilun-settings#affilun-settings-tab-asp';
	}
}
