<?php
namespace ExternalPermalinks;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID      = 'external-permalinks';
	private const MODULE_VERSION = '1.0.4';
	private const META_KEY       = '_links_to';
	private const MAX_URL_LENGTH = 2048;

	/** @var list<string> */
	private array $enabled_post_types = [];

	/** @var array<int, string|null> */
	private array $external_url_cache = [];

	private bool $hooks_booted       = false;
	private bool $meta_registered    = false;

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

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

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

	public function getDescription(): string {
		return __( 'Content entries that link out to external URLs—handy for curated resources and partner pages.', 'wp-powersuite' );
	}

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

	/** @return list<string> */
	public static function getDisallowedPostTypes(): array {
		return [
			'attachment',
			'revision',
			'nav_menu_item',
			'custom_css',
			'customize_changeset',
			'oembed_cache',
			'user_request',
			'wp_block',
			'wp_template',
			'wp_template_part',
			'wp_navigation',
		];
	}

	/** @return list<string> */
	public static function getAllowedPostTypes(): array {
		if ( ! did_action( 'init' ) ) {
			return self::getDefaultSettings()['enabled_post_types'];
		}

		return array_values(
			array_diff(
				get_post_types( [ 'public' => true, 'show_ui' => true ], 'names' ),
				self::getDisallowedPostTypes()
			)
		);
	}

	protected function registerHooks(): void {
		if ( did_action( 'init' ) ) {
			$this->bootModule();
		} else {
			add_action( 'init', [ $this, 'bootModule' ], 20 );
		}
	}

	public function bootModule(): void {
		if ( $this->hooks_booted ) {
			return;
		}

		$this->enabled_post_types = $this->getEnabledPostTypes();
		if ( $this->enabled_post_types === [] ) {
			return;
		}

		$this->hooks_booted = true;

		$this->registerPostMeta();

		if ( in_array( 'post', $this->enabled_post_types, true ) ) {
			add_filter( 'post_link', [ $this, 'filterPermalink' ], 10, 2 );
		}
		if ( in_array( 'page', $this->enabled_post_types, true ) ) {
			add_filter( 'page_link', [ $this, 'filterPermalink' ], 10, 2 );
		}
		if ( array_diff( $this->enabled_post_types, [ 'post', 'page' ] ) !== [] ) {
			add_filter( 'post_type_link', [ $this, 'filterPermalink' ], 10, 2 );
		}

		if ( is_admin() ) {
			add_action( 'add_meta_boxes', [ $this, 'addExternalPermalinkMetaBox' ], 10, 2 );
			foreach ( $this->enabled_post_types as $post_type ) {
				add_action( "save_post_{$post_type}", [ $this, 'saveExternalPermalink' ], 10, 2 );
			}
		}

		if ( ! is_admin() ) {
			add_action( 'template_redirect', [ $this, 'redirectToExternalPermalink' ], 1 );
		}
	}

	public function registerPostMeta(): void {
		if ( $this->meta_registered ) {
			return;
		}
		$this->meta_registered = true;

		foreach ( $this->enabled_post_types as $post_type ) {
			register_post_meta(
				$post_type,
				self::META_KEY,
				[
					'type'              => 'string',
					'single'            => true,
					'show_in_rest'      => true,
					'auth_callback'     => static function ( $allowed, $meta_key, $post_id ): bool {
						unset( $allowed, $meta_key );

						return (int) $post_id > 0 && current_user_can( 'edit_post', (int) $post_id );
					},
					'sanitize_callback' => [ self::class, 'sanitizeExternalUrl' ],
				]
			);
		}
	}

	public function addExternalPermalinkMetaBox( string $post_type, \WP_Post $post ): void {
		unset( $post );
		if ( ! in_array( $post_type, $this->enabled_post_types, true ) ) {
			return;
		}

		add_meta_box(
			'wpps-external-permalink',
			__( 'External Permalink', 'wp-powersuite' ),
			[ $this, 'outputExternalPermalinkMetaBox' ],
			$post_type,
			'normal',
			'high',
			[ '__block_editor_compatible_meta_box' => true ]
		);
	}

	public function outputExternalPermalinkMetaBox( \WP_Post $post ): void {
		$external_permalink = $this->getExternalUrl( $post->ID ) ?? '';
		wp_nonce_field( 'external_permalink_' . $post->ID, 'external_permalink_nonce', false, true );
		?>
		<div class="wpps-external-permalink-metabox">
			<input
				name="<?php echo esc_attr( self::META_KEY ); ?>"
				class="large-text"
				id="wpps_external_permalink"
				type="url"
				value="<?php echo esc_attr( $external_permalink ); ?>"
				placeholder="https://"
			/>
			<p class="description">
				<?php esc_html_e( 'Leave empty to use the normal WordPress URL. When set, permalinks point to this URL and visitors are sent there with a permanent (301) redirect.', 'wp-powersuite' ); ?>
			</p>
		</div>
		<?php
	}

	/**
	 * @param int      $post_id Post ID.
	 * @param \WP_Post $post    Post object.
	 */
	public function saveExternalPermalink( int $post_id, \WP_Post $post ): void {
		unset( $post );

		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$post_type = get_post_type( $post_id );
		if ( ! is_string( $post_type ) || ! in_array( $post_type, $this->enabled_post_types, true ) ) {
			return;
		}

		$raw = $this->readExternalUrlFromPostRequest();
		if ( $raw === null ) {
			return;
		}

		if ( ! isset( $_POST['external_permalink_nonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['external_permalink_nonce'] ) ), 'external_permalink_' . $post_id ) ) {
			return;
		}

		$this->persistExternalUrl( $post_id, $raw );
	}

	/**
	 * @param string         $permalink Generated permalink.
	 * @param int|\WP_Post   $post      Post ID or object.
	 */
	public function filterPermalink( string $permalink, $post ): string {
		$post = get_post( $post );
		if ( ! $post instanceof \WP_Post || ! in_array( $post->post_type, $this->enabled_post_types, true ) ) {
			return $permalink;
		}

		$external = $this->getExternalUrl( $post->ID );

		return $external ?? $permalink;
	}

	public function redirectToExternalPermalink(): void {
		if ( ! is_singular() || is_preview() || is_customize_preview() ) {
			return;
		}

		$post = get_queried_object();
		if ( ! $post instanceof \WP_Post || ! in_array( $post->post_type, $this->enabled_post_types, true ) ) {
			return;
		}

		$external = $this->getExternalUrl( $post->ID );
		if ( $external === null ) {
			return;
		}

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

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

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

		add_filter( 'allowed_redirect_hosts', $allow_external_host );
		wp_safe_redirect( $external, 301, 'WP PowerSuite External Permalinks' );
		remove_filter( 'allowed_redirect_hosts', $allow_external_host );
		exit;
	}

	private function getExternalUrl( int $post_id ): ?string {
		if ( array_key_exists( $post_id, $this->external_url_cache ) ) {
			$cached = $this->external_url_cache[ $post_id ];

			return $cached === null ? null : $cached;
		}

		$raw = get_post_meta( $post_id, self::META_KEY, true );
		$url = is_string( $raw ) ? self::sanitizeExternalUrl( $raw ) : '';
		$this->external_url_cache[ $post_id ] = $url !== '' ? $url : null;

		return $this->external_url_cache[ $post_id ];
	}

	private function persistExternalUrl( int $post_id, string $raw ): void {
		$external_permalink = self::sanitizeExternalUrl( $raw );

		if ( $external_permalink !== '' ) {
			update_post_meta( $post_id, self::META_KEY, $external_permalink );
		} else {
			delete_post_meta( $post_id, self::META_KEY );
		}

		unset( $this->external_url_cache[ $post_id ] );
	}

	private function readExternalUrlFromPostRequest(): ?string {
		if ( isset( $_POST[ self::META_KEY ] ) ) {
			return trim( (string) wp_unslash( $_POST[ self::META_KEY ] ) );
		}

		// Backward compatibility with older metabox field name.
		if ( isset( $_POST['external_permalink'] ) ) {
			return trim( (string) wp_unslash( $_POST['external_permalink'] ) );
		}

		return null;
	}

	public static function sanitizeExternalUrl( string $url ): string {
		$url = trim( $url );
		if ( $url === '' ) {
			return '';
		}

		if ( function_exists( 'mb_substr' ) ) {
			$url = mb_substr( $url, 0, self::MAX_URL_LENGTH );
		} else {
			$url = substr( $url, 0, self::MAX_URL_LENGTH );
		}

		$url = esc_url_raw( $url );
		if ( $url === '' ) {
			return '';
		}

		$scheme = wp_parse_url( $url, PHP_URL_SCHEME );

		return is_string( $scheme ) && in_array( strtolower( $scheme ), [ 'http', 'https' ], true ) ? $url : '';
	}

	/** @return list<string> */
	private function getEnabledPostTypes(): array {
		$settings = self::normalizeSettings( $this->moduleSettings()->all( [] ) );

		return $settings['enabled_post_types'];
	}

	/**
	 * @return array{enabled_post_types: list<string>}
	 */
	public static function getDefaultSettings(): array {
		return [
			'enabled_post_types' => [ 'post', 'page' ],
		];
	}

	/**
	 * @param array<string, mixed> $raw
	 * @return array{enabled_post_types: list<string>}
	 */
	public static function normalizeSettings( array $raw ): array {
		$allow = array_flip( self::getAllowedPostTypes() );
		$clean = [];

		if ( isset( $raw['enabled_post_types'] ) && is_array( $raw['enabled_post_types'] ) ) {
			foreach ( $raw['enabled_post_types'] as $slug ) {
				$key = sanitize_key( (string) $slug );
				if ( $key !== '' && isset( $allow[ $key ] ) && post_type_exists( $key ) ) {
					$clean[] = $key;
				}
			}
		} elseif ( ! array_key_exists( 'enabled_post_types', $raw ) ) {
			$clean = self::getDefaultSettings()['enabled_post_types'];
		}

		return [
			'enabled_post_types' => array_values( array_unique( $clean ) ),
		];
	}
}
