<?php

namespace PublicPreviewForDrafts;

use WP_Post;
use WP_Query;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID      = 'public-preview-for-drafts';
	private const MODULE_VERSION = '1.0.4';

	/** @var list<string>|null */
	private ?array $enabled_types_cache = null;

	/** @var array<string, mixed>|null */
	private ?array $settings_cache = null;

	private bool $classic_preview_rendered = false;

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

	public function getName(): string {
		return __( 'Public Preview for Drafts', 'wp-powersuite' );
	}

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

	public function getDescription(): string {
		return __( 'Share expiring preview URLs for drafts—no login for viewers, optional search-engine hints in settings.', 'wp-powersuite' );
	}

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

	public function getDependencies(): array {
		return [];
	}

	private function isLicenseActive(): bool {
		return function_exists( 'wp_powersuite_is_license_valid' ) && wp_powersuite_is_license_valid();
	}

	protected function registerHooks(): void {
		if ( ! $this->isLicenseActive() ) {
			return;
		}

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

		add_action( 'rest_api_init', [ $this, 'registerRestRoutes' ] );

		if ( is_admin() ) {
			add_filter( 'post_row_actions', [ $this, 'addActionRowPreviewLink' ], 10, 2 );
			add_filter( 'page_row_actions', [ $this, 'addActionRowPreviewLink' ], 10, 2 );
			foreach ( $this->getEnabledPostTypes() as $post_type ) {
				if ( post_type_exists( $post_type ) && ! in_array( $post_type, [ 'post', 'page' ], true ) ) {
					add_filter( "{$post_type}_row_actions", [ $this, 'addActionRowPreviewLink' ], 10, 2 );
				}
			}
			add_action( 'post_submitbox_misc_actions', [ $this, 'addSubmitboxPreviewButton' ] );
			add_action( 'enqueue_block_editor_assets', [ $this, 'enqueueBlockEditorAssets' ] );
			add_action( 'admin_print_footer_scripts-post.php', [ $this, 'printClassicCopyScript' ], 99 );
			add_action( 'admin_print_footer_scripts-post-new.php', [ $this, 'printClassicCopyScript' ], 99 );
			return;
		}

		add_filter( 'query_vars', [ $this, 'addQueryVar' ] );
		add_action( 'pre_get_posts', [ $this, 'allowDraftInMainQueryForPublicPreview' ], 1 );
		add_action( 'parse_query', [ $this, 'showPublicPreview' ], 1 );
	}

	public static function getDefaultSettings(): array {
		return [
			'enabled_post_types' => [ 'post', 'page' ],
			'max_days'           => 3,
		];
	}

	/**
	 * @param array<string, mixed> $raw
	 * @return array{enabled_post_types: list<string>, max_days: int}
	 */
	public static function normalizeSettings( array $raw ): array {
		$settings = wp_parse_args( $raw, self::getDefaultSettings() );

		$types = $settings['enabled_post_types'] ?? [];
		$clean = [];
		if ( is_array( $types ) ) {
			foreach ( $types as $type ) {
				$key = sanitize_key( (string) $type );
				if ( $key !== '' && post_type_exists( $key ) && ! in_array( $key, $clean, true ) ) {
					$clean[] = $key;
				}
			}
		} elseif ( ! array_key_exists( 'enabled_post_types', $raw ) ) {
			$clean = self::getDefaultSettings()['enabled_post_types'];
		}

		return [
			'enabled_post_types' => $clean,
			'max_days'           => max( 1, min( 30, absint( $settings['max_days'] ?? 3 ) ) ),
		];
	}

	public function registerRestRoutes(): void {
		register_rest_route(
			'wp-powersuite/v1',
			'/public-preview-link/(?P<post_id>\d+)',
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'restGetPreviewLink' ],
				'permission_callback' => function ( WP_REST_Request $request ) {
					$id = (int) $request['post_id'];
					return $id > 0 && current_user_can( 'edit_post', $id );
				},
				'args'                => [
					'post_id' => [
						'required'          => true,
						'validate_callback' => static function ( $v ) {
							return (int) $v > 0;
						},
					],
				],
			]
		);
	}

	public function restGetPreviewLink( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id   = (int) $request['post_id'];
		$post = get_post( $id );
		if ( ! $post instanceof WP_Post ) {
			return new WP_Error( 'wpps_pp_not_found', __( 'Post not found.', 'wp-powersuite' ), [ 'status' => 404 ] );
		}
		if ( 'draft' !== $post->post_status ) {
			return new WP_Error(
				'wpps_pp_not_draft',
				__( 'Public preview is only available for drafts.', 'wp-powersuite' ),
				[ 'status' => 400 ]
			);
		}
		if ( ! $this->isPostTypeEnabled( $post->post_type ) ) {
			return new WP_Error(
				'wpps_pp_type',
				__( 'This post type is not enabled for public preview.', 'wp-powersuite' ),
				[ 'status' => 403 ]
			);
		}

		return new WP_REST_Response( [ 'url' => $this->buildPreviewLink( $post ) ] );
	}

	public function enqueueBlockEditorAssets(): void {
		if ( $this->getEnabledPostTypes() === [] ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && $screen->base === 'post' && ! empty( $screen->post_type ) && ! $this->isPostTypeEnabled( $screen->post_type ) ) {
			return;
		}

		$path = __DIR__ . '/assets/public-preview-editor.js';
		if ( ! is_readable( $path ) ) {
			return;
		}

		wp_enqueue_script(
			'wpps-public-preview-editor',
			plugins_url( 'assets/public-preview-editor.js', __FILE__ ),
			[ 'wp-element', 'wp-components', 'wp-data', 'wp-plugins', 'wp-edit-post', 'wp-api-fetch' ],
			(string) filemtime( $path ),
			true
		);

		wp_localize_script(
			'wpps-public-preview-editor',
			'wppsPublicPreview',
			[
				'enabledTypes' => $this->getEnabledPostTypes(),
				'strings'      => [
					'title'    => __( 'Public preview', 'wp-powersuite' ),
					'copy'     => __( 'Copy link', 'wp-powersuite' ),
					'refresh'  => __( 'Refresh link', 'wp-powersuite' ),
					'error'    => __( 'Could not load preview link.', 'wp-powersuite' ),
					'help'     => __( 'Share this URL to preview the draft without logging in. Links expire after the number of days set in module settings.', 'wp-powersuite' ),
					'urlLabel' => __( 'Public preview URL', 'wp-powersuite' ),
				],
			]
		);
	}

	public function addQueryVar( array $vars ): array {
		$vars[] = 'pp';
		if ( ! in_array( 'preview', $vars, true ) ) {
			$vars[] = 'preview';
		}
		return $vars;
	}

	/**
	 * Let the main query load draft posts when preview=true and pp=nonce are valid.
	 */
	public function allowDraftInMainQueryForPublicPreview( WP_Query $query ): void {
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		$pp = $query->get( 'pp' );
		if ( $pp === '' || $pp === false || $pp === null ) {
			return;
		}

		if ( ! $query->is_preview() && ! $query->get( 'preview' ) ) {
			return;
		}

		$post_id = $this->resolvePublicPreviewRequestPostId( $query );
		if ( $post_id <= 0 ) {
			return;
		}

		if ( ! is_string( $pp ) || ! $this->verifyNonce( $pp, 'public_preview_for_draft_' . $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );
		if ( ! $post instanceof WP_Post || 'draft' !== $post->post_status || ! $this->isPostTypeEnabled( $post->post_type ) ) {
			return;
		}

		$query->set(
			'post_status',
			[
				'publish',
				'pending',
				'draft',
				'future',
				'private',
			]
		);
	}

	private function resolvePublicPreviewRequestPostId( WP_Query $query ): int {
		$p = (int) $query->get( 'p' );
		if ( $p > 0 ) {
			return $p;
		}

		$page_id = (int) $query->get( 'page_id' );
		if ( $page_id > 0 ) {
			return $page_id;
		}

		return 0;
	}

	public function showPublicPreview( WP_Query $query ): void {
		if ( ! $query->is_main_query() || $query->get( 'pp' ) === '' || $query->get( 'pp' ) === false || $query->get( 'pp' ) === null ) {
			return;
		}

		$has_preview = $query->is_preview() || $query->get( 'preview' );
		if ( ! $has_preview ) {
			return;
		}

		if ( ! $query->is_singular() && ! $query->is_preview() ) {
			return;
		}

		if ( ! headers_sent() ) {
			nocache_headers();
			header( 'X-Robots-Tag: noindex, nofollow' );
		}

		if ( function_exists( 'wp_robots_no_robots' ) ) {
			add_filter( 'wp_robots', 'wp_robots_no_robots' );
		} else {
			add_action( 'wp_head', 'wp_no_robots' );
		}

		add_filter( 'posts_results', [ $this, 'setPostToPublish' ], 10, 2 );
	}

	public function setPostToPublish( array $posts, WP_Query $query ): array {
		unset( $query );
		remove_filter( 'posts_results', [ $this, 'setPostToPublish' ], 10 );

		if ( $posts === [] ) {
			return $posts;
		}

		if ( ! $this->isPostTypeEnabled( $posts[0]->post_type ) ) {
			return $posts;
		}

		$post_id = (int) $posts[0]->ID;
		$this->maybeRedirectToPublishedPost( $post_id );

		$nonce = get_query_var( 'pp' );
		if ( $this->isPreviewRateLimited( $post_id ) ) {
			wp_die( esc_html__( 'Too many invalid preview attempts. Please try again later.', 'wp-powersuite' ), '', [ 'response' => 429 ] );
		}
		if ( ! is_string( $nonce ) || ! $this->verifyNonce( $nonce, 'public_preview_for_draft_' . $post_id ) ) {
			$this->recordPreviewFailure( $post_id );
			wp_die( esc_html__( 'This link has expired or is invalid.', 'wp-powersuite' ), '', [ 'response' => 403 ] );
		}

		if ( 'draft' === $posts[0]->post_status ) {
			$posts[0]->post_status = 'publish';
			add_filter( 'comments_open', '__return_false' );
			add_filter( 'pings_open', '__return_false' );
			add_filter( 'wp_link_pages_link', [ $this, 'filterWpLinkPagesLink' ], 10, 2 );
		}

		return $posts;
	}

	public function filterWpLinkPagesLink( string $link, int $page_number ): string {
		$post = get_post();
		if ( ! $post instanceof WP_Post ) {
			return $link;
		}

		$preview_link = add_query_arg( 'page', $page_number, $this->buildPreviewLink( $post ) );

		return preg_replace( '~href=(["\'])(.+?)\1~', 'href=$1' . esc_url( $preview_link ) . '$1', $link ) ?? $link;
	}

	private function maybeRedirectToPublishedPost( int $post_id ): void {
		$status = get_post_status( $post_id );
		if ( in_array( $status, [ 'publish', 'private' ], true ) ) {
			wp_safe_redirect( get_permalink( $post_id ), 301 );
			exit;
		}
	}

	public function addActionRowPreviewLink( array $actions, WP_Post $post ): array {
		if ( 'draft' !== $post->post_status || ! $this->isPostTypeEnabled( $post->post_type ) ) {
			return $actions;
		}

		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return $actions;
		}

		$preview_link              = $this->buildPreviewLink( $post );
		$actions['public-preview'] = '<a href="' . esc_url( $preview_link ) . '" title="' . esc_attr__( 'Public preview of this draft', 'wp-powersuite' ) . '">' . esc_html__( 'Public preview', 'wp-powersuite' ) . '</a>';

		return $actions;
	}

	public function addSubmitboxPreviewButton(): void {
		global $post;

		if ( ! $post instanceof WP_Post || 'draft' !== $post->post_status || ! $this->isPostTypeEnabled( $post->post_type ) ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return;
		}

		$this->classic_preview_rendered = true;

		$preview_link = $this->buildPreviewLink( $post );
		$input_id     = 'wpps-public-preview-url-' . (int) $post->ID;
		?>
		<div class="misc-pub-section wpps-public-preview-classic">
			<strong><?php esc_html_e( 'Public preview', 'wp-powersuite' ); ?></strong>
			<p class="description" style="margin:6px 0 8px;">
				<?php esc_html_e( 'Share this URL so others can view the draft without logging in.', 'wp-powersuite' ); ?>
			</p>
			<label for="<?php echo esc_attr( $input_id ); ?>" class="screen-reader-text"><?php esc_html_e( 'Public preview URL', 'wp-powersuite' ); ?></label>
			<input type="text" id="<?php echo esc_attr( $input_id ); ?>" readonly class="widefat code" style="font-size:12px;margin-bottom:8px;"
				value="<?php echo esc_attr( $preview_link ); ?>" onclick="this.select();" />
			<div style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
				<button type="button" class="button button-small wpps-public-preview-copy" data-target="<?php echo esc_attr( $input_id ); ?>"><?php esc_html_e( 'Copy link', 'wp-powersuite' ); ?></button>
				<a href="<?php echo esc_url( $preview_link ); ?>" class="button button-small" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Open', 'wp-powersuite' ); ?></a>
			</div>
		</div>
		<?php
	}

	public function printClassicCopyScript(): void {
		if ( ! $this->classic_preview_rendered ) {
			return;
		}
		$this->classic_preview_rendered = false;
		?>
		<script>
		(function(){
			document.body.addEventListener('click', function(e){
				var btn = e.target.closest('.wpps-public-preview-copy');
				if (!btn) return;
				var el = document.getElementById(btn.getAttribute('data-target'));
				if (!el) return;
				el.select();
				if (el.setSelectionRange) el.setSelectionRange(0, 99999);
				var ok = <?php echo wp_json_encode( __( 'Copied!', 'wp-powersuite' ) ); ?>;
				var label = btn.textContent;
				if (navigator.clipboard && navigator.clipboard.writeText) {
					navigator.clipboard.writeText(el.value).then(function(){ btn.textContent = ok; setTimeout(function(){ btn.textContent = label; }, 2000); });
				} else {
					try { document.execCommand('copy'); } catch (e) {}
					btn.textContent = ok;
					setTimeout(function(){ btn.textContent = label; }, 2000);
				}
			});
		})();
		</script>
		<?php
	}

	private function buildPreviewLink( WP_Post $post ): string {
		if ( 'page' === $post->post_type ) {
			$args = [ 'page_id' => $post->ID ];
		} elseif ( 'post' === $post->post_type ) {
			$args = [ 'p' => $post->ID ];
		} else {
			$args = [
				'p'         => $post->ID,
				'post_type' => $post->post_type,
			];
		}

		$args['preview'] = 'true';
		$args['pp']      = $this->createNonce( 'public_preview_for_draft_' . $post->ID );

		return add_query_arg( $args, home_url( '/' ) );
	}

	private function nonceTick(): int {
		$settings   = $this->getSettings();
		$max_days   = max( 1, min( 30, (int) ( $settings['max_days'] ?? 3 ) ) );
		$nonce_life = $max_days * DAY_IN_SECONDS;
		$half       = max( 1, (int) ( $nonce_life / 2 ) );

		return (int) ceil( time() / $half );
	}

	private function createNonce( string $action ): string {
		$i = $this->nonceTick();
		return substr( wp_hash( $i . $action, 'nonce' ), 0, 32 );
	}

	private function verifyNonce( string $nonce, string $action ): bool {
		if ( strlen( $nonce ) !== 32 ) {
			return false;
		}
		$i = $this->nonceTick();
		$a = substr( wp_hash( $i . $action, 'nonce' ), 0, 32 );
		if ( hash_equals( $a, $nonce ) ) {
			return true;
		}
		$b = substr( wp_hash( ( $i - 1 ) . $action, 'nonce' ), 0, 32 );
		return hash_equals( $b, $nonce );
	}

	private function previewRateLimitKey( int $post_id ): string {
		$ip = isset( $_SERVER['REMOTE_ADDR'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) ) : 'unknown';
		return 'wpps_pp_fail_' . md5( $post_id . '|' . $ip );
	}

	private function isPreviewRateLimited( int $post_id ): bool {
		return (int) get_transient( $this->previewRateLimitKey( $post_id ) ) >= 10;
	}

	private function recordPreviewFailure( int $post_id ): void {
		$key      = $this->previewRateLimitKey( $post_id );
		$attempts = (int) get_transient( $key );
		set_transient( $key, $attempts + 1, HOUR_IN_SECONDS );
	}

	/** @return list<string> */
	private function getEnabledPostTypes(): array {
		if ( $this->enabled_types_cache !== null ) {
			return $this->enabled_types_cache;
		}

		$this->enabled_types_cache = self::normalizeSettings( $this->getSettings() )['enabled_post_types'];

		return $this->enabled_types_cache;
	}

	private function isPostTypeEnabled( string $post_type ): bool {
		return in_array( $post_type, $this->getEnabledPostTypes(), true );
	}

	/** @return array{enabled_post_types: list<string>, max_days: int} */
	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}

		$raw                   = $this->moduleSettings()->all( [] );
		$this->settings_cache  = self::normalizeSettings( is_array( $raw ) ? $raw : [] );

		return $this->settings_cache;
	}
}
