<?php

namespace Redirect404;

use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\Contracts\DataLocationsInterface;
use WP_PowerSuite\Core\FrontendRequestGuard;
use WP_PowerSuite\Core\RequestPathHelper;

class Module extends AbstractModule implements DataLocationsInterface {

	private const MODULE_ID = '404-redirect';
	private const MODULE_VERSION = '1.3.0';
	private const LOOP_COOKIE = 'wpps_404_redirect_visits';
	private const LOOP_MAX_VISITS = 2;

	private ?array $settings_cache = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( '404 Redirect', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Global fallback redirect for unknown URLs instead of a generic 404 page.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'frontend' ];
	}

	protected function registerHooks(): void {
		if ( is_admin() ) {
			return;
		}

		// Priority 999 so Redirect Manager (wp_loaded) runs first - only handle 404s that Redirect Manager didn't match
		add_action( 'template_redirect', [ $this, 'handle404Redirect' ], 999 );
	}

	public function handle404Redirect(): void {
		if ( ! is_404() || FrontendRequestGuard::shouldSkipPublic404Action() ) {
			return;
		}

		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$path        = RequestPathHelper::pathFromRequestUri( $request_uri );

		if ( $path === '' || FrontendRequestGuard::shouldSkip404Path( $path ) ) {
			return;
		}

		$settings      = $this->getSettings();
		$redirect_type = $settings['redirect_type'] ?? 'home';
		$redirect_url  = $this->resolveRedirectUrl( $redirect_type, $settings );

		if ( $redirect_url === '' ) {
			return;
		}

		if ( $this->pathsMatch( $path, $redirect_url ) ) {
			return;
		}

		if ( $this->destinationIsUnsafe( $redirect_url, $path, $redirect_type, $settings ) ) {
			return;
		}

		if ( $this->isRedirectLoopDetected( $path ) ) {
			return;
		}

		$redirect_code = $this->resolveRedirectCode( $settings );

		if ( ! empty( $settings['preserve_query_string'] ) ) {
			$query = RequestPathHelper::queryFromRequestUri( $request_uri );
			if ( $query !== '' ) {
				$redirect_url = RequestPathHelper::appendQueryString( $redirect_url, $query );
			}
		}

		/**
		 * Fires immediately before a global 404 fallback redirect.
		 *
		 * @param string $path          Request path that returned 404 (no query string).
		 * @param string $redirect_url  Destination URL.
		 * @param int    $redirect_code HTTP redirect status code.
		 */
		do_action( 'wpps_404_auto_redirect', $path, $redirect_url, $redirect_code );

		$this->recordRedirectAttempt( $path );

		if ( ! $this->performSafeRedirect( $redirect_url, $redirect_code ) ) {
			return;
		}

		exit;
	}

	/**
	 * Resolve destination URL from module settings.
	 *
	 * @param string               $redirect_type home|page|custom.
	 * @param array<string, mixed> $settings      Module settings.
	 */
	private function resolveRedirectUrl( string $redirect_type, array $settings ): string {
		switch ( $redirect_type ) {
			case 'home':
				return home_url( '/' );
			case 'custom':
				$redirect_url = $settings['custom_url'] ?? '';
				if ( $redirect_url === '' ) {
					return home_url( '/' );
				}
				$redirect_url = esc_url_raw( $redirect_url, [ 'http', 'https' ] );
				if ( $redirect_url === '' || ! filter_var( $redirect_url, FILTER_VALIDATE_URL ) ) {
					return '';
				}
				return $redirect_url;
			case 'page':
				$page_id = isset( $settings['redirect_page_id'] ) ? (int) $settings['redirect_page_id'] : 0;
				if ( $page_id <= 0 ) {
					return home_url( '/' );
				}
				$post = get_post( $page_id );
				if ( ! $post instanceof \WP_Post || $post->post_status !== 'publish' ) {
					return '';
				}
				$redirect_url = get_permalink( $page_id );
				return is_string( $redirect_url ) && $redirect_url !== '' ? $redirect_url : '';
			default:
				return home_url( '/' );
		}
	}

	/**
	 * Catch-all 404 redirects must stay temporary unless explicitly acknowledged (C1).
	 *
	 * @param array<string, mixed> $settings Module settings.
	 */
	private function resolveRedirectCode( array $settings ): int {
		$requested = isset( $settings['redirect_code'] ) ? (string) $settings['redirect_code'] : '302';
		$allow_301 = ! empty( $settings['allow_permanent_404_redirect'] );

		if ( $requested === '301' && $allow_301 ) {
			return 301;
		}

		return 302;
	}

	/**
	 * Whether the destination would repeat the 404 or points at missing content (C3).
	 *
	 * @param string               $redirect_url  Destination URL.
	 * @param string               $request_path  Current request path.
	 * @param string               $redirect_type home|page|custom.
	 * @param array<string, mixed> $settings      Module settings.
	 */
	private function destinationIsUnsafe( string $redirect_url, string $request_path, string $redirect_type, array $settings ): bool {
		if ( $this->pathsMatch( $request_path, $redirect_url ) ) {
			return true;
		}

		if ( $redirect_type === 'page' ) {
			$page_id = isset( $settings['redirect_page_id'] ) ? (int) $settings['redirect_page_id'] : 0;
			if ( $page_id <= 0 ) {
				return true;
			}
			$post = get_post( $page_id );
			if ( ! $post instanceof \WP_Post || $post->post_status !== 'publish' ) {
				return true;
			}
		}

		if ( ! $this->isInternalUrl( $redirect_url ) ) {
			return false;
		}

		$post_id = url_to_postid( $redirect_url );
		if ( $post_id > 0 ) {
			$post = get_post( $post_id );
			return ! $post instanceof \WP_Post || $post->post_status !== 'publish';
		}

		return false;
	}

	private function isInternalUrl( string $url ): bool {
		$site_host = wp_parse_url( home_url(), PHP_URL_HOST );
		$url_host  = wp_parse_url( $url, PHP_URL_HOST );

		if ( ! is_string( $site_host ) || $site_host === '' ) {
			return true;
		}

		if ( ! is_string( $url_host ) || $url_host === '' ) {
			return true;
		}

		return strcasecmp( $url_host, $site_host ) === 0;
	}

	private function pathsMatch( string $request_path, string $redirect_url ): bool {
		$dest_path = wp_parse_url( $redirect_url, PHP_URL_PATH );
		if ( ! is_string( $dest_path ) || $dest_path === '' ) {
			return false;
		}

		$left  = rtrim( $request_path, '/' ) ?: '/';
		$right = rtrim( $dest_path, '/' ) ?: '/';

		return strcasecmp( $left, $right ) === 0;
	}

	private function isRedirectLoopDetected( string $path ): bool {
		$visits = $this->getVisitCounts();
		$count  = $visits[ $path ] ?? 0;

		return $count >= self::LOOP_MAX_VISITS;
	}

	private function recordRedirectAttempt( string $path ): void {
		$visits         = $this->getVisitCounts();
		$visits[ $path ] = ( $visits[ $path ] ?? 0 ) + 1;
		$json           = wp_json_encode( $visits );

		if ( $json !== false && ! headers_sent() ) {
			setcookie( self::LOOP_COOKIE, $json, time() + 300, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
		}
	}

	/**
	 * @return array<string, int>
	 */
	private function getVisitCounts(): array {
		$raw = isset( $_COOKIE[ self::LOOP_COOKIE ] ) ? sanitize_text_field( wp_unslash( $_COOKIE[ self::LOOP_COOKIE ] ) ) : '';
		if ( $raw === '' ) {
			return [];
		}

		$decoded = json_decode( $raw, true );

		return is_array( $decoded ) ? $decoded : [];
	}

	/**
	 * Redirect safely without falling back to wp-admin for external URLs (C2).
	 */
	private function performSafeRedirect( string $url, int $code ): bool {
		$url = esc_url_raw( $url, [ 'http', 'https' ] );
		if ( $url === '' ) {
			return false;
		}

		$host = wp_parse_url( $url, PHP_URL_HOST );
		if ( ! is_string( $host ) || $host === '' ) {
			return false;
		}

		$allow_host = static function ( array $hosts ) use ( $host ): array {
			$hosts[] = $host;

			return array_values( array_unique( $hosts ) );
		};

		add_filter( 'allowed_redirect_hosts', $allow_host );
		$validated = wp_validate_redirect( $url, '' );
		remove_filter( 'allowed_redirect_hosts', $allow_host );

		if ( $validated === '' ) {
			return false;
		}

		add_filter( 'allowed_redirect_hosts', $allow_host );
		wp_safe_redirect( $validated, $code, 'WP PowerSuite 404 Redirect' );
		remove_filter( 'allowed_redirect_hosts', $allow_host );

		return true;
	}

	public static function validateSettingsForSave( array $settings ): ?string {
		$redirect_code = isset( $settings['redirect_code'] ) ? (string) $settings['redirect_code'] : '302';
		if ( $redirect_code === '301' && empty( $settings['allow_permanent_404_redirect'] ) ) {
			return __( 'Check the confirmation box below to save a 301 permanent redirect for this catch-all rule.', 'wp-powersuite' );
		}

		return null;
	}

	/**
	 * Normalize settings for save/API (C1).
	 *
	 * @param array<string, mixed> $settings Raw settings.
	 * @return array<string, mixed>
	 */
	public static function normalizeSettings( array $settings ): array {
		$redirect_type = isset( $settings['redirect_type'] ) ? sanitize_key( (string) $settings['redirect_type'] ) : 'home';
		if ( ! in_array( $redirect_type, [ 'home', 'page', 'custom' ], true ) ) {
			$redirect_type = 'home';
		}

		$redirect_code = isset( $settings['redirect_code'] ) ? (string) $settings['redirect_code'] : '302';
		if ( ! in_array( $redirect_code, [ '301', '302' ], true ) ) {
			$redirect_code = '302';
		}

		$allow_permanent = ! empty( $settings['allow_permanent_404_redirect'] );
		if ( $redirect_code !== '301' ) {
			$allow_permanent = false;
		} elseif ( ! $allow_permanent ) {
			$redirect_code = '302';
		}

		$custom_url = isset( $settings['custom_url'] ) ? esc_url_raw( (string) $settings['custom_url'], [ 'http', 'https' ] ) : '';

		return [
			'redirect_type'                 => $redirect_type,
			'custom_url'                    => $custom_url,
			'redirect_page_id'              => max( 0, (int) ( $settings['redirect_page_id'] ?? 0 ) ),
			'redirect_code'                 => $redirect_code,
			'allow_permanent_404_redirect'  => $allow_permanent,
			'preserve_query_string'         => ! empty( $settings['preserve_query_string'] ),
		];
	}

	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$settings = $this->moduleSettings()->all( [] );
		$defaults = $this->getDefaultSettings();
		$this->settings_cache = self::normalizeSettings( array_merge( $defaults, is_array( $settings ) ? $settings : [] ) );

		return $this->settings_cache;
	}

	public function getDefaultSettings(): array {
		return [
			'redirect_type'                => 'home',
			'custom_url'                   => '',
			'redirect_page_id'             => 0,
			'redirect_code'                => '302',
			'allow_permanent_404_redirect' => false,
			'preserve_query_string'        => true,
		];
	}

	/**
	 * {@inheritdoc}
	 */
	public function getDataLocations(): array {
		return [
			'option_store_keys' => [ self::MODULE_ID ],
		];
	}
}
