<?php
namespace ContentExpiration;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID           = 'content-expiration';
	private const MODULE_VERSION      = '2.1.3';
	private const META_KEY            = 'wpps_expiration_date';
	private const LEGACY_META_KEY     = '_wpps_expiration_date';
	private const NONCE_ACTION        = 'wpps_content_expiration';
	private const AJAX_ACTION         = 'wpps_save_content_expiration';
	private const CRON_HOOK           = 'wpps_content_expiration_check';
	private const CRON_TRANSIENT      = 'wpps_expiration_cron_checked';
	private const ADMIN_SCAN_LOCK     = 'wpps_ce_admin_scan';
	private const RUN_LOCK            = 'wpps_ce_run_lock';
	private const ADMIN_SCAN_INTERVAL = 300;
	private const EXPIRED_BATCH_LIMIT = 100;

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

	private bool $meta_registered           = false;
	private bool $list_ui_booted            = false;
	private bool $editor_ui_booted          = false;
	private bool $save_handlers_registered  = false;

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

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

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

	public function getDescription(): string {
		return __( 'Auto-unpublish posts after a date you set—set-and-forget for time-limited offers and listings.', 'wp-powersuite' );
	}

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

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

	protected function registerHooks(): void {
		add_action( 'wp_ajax_' . self::AJAX_ACTION, [ $this, 'ajaxSaveExpiration' ] );
		add_action( self::CRON_HOOK, [ $this, 'checkExpiredPosts' ] );
		add_action( 'updated_post_meta', [ $this, 'onExpirationMetaChanged' ], 10, 4 );
		add_action( 'added_post_meta', [ $this, 'onExpirationMetaChanged' ], 10, 4 );
		add_action( 'load-edit.php', [ $this, 'bootListTableUi' ] );
		add_action( 'load-post.php', [ $this, 'bootEditorUi' ] );
		add_action( 'load-post-new.php', [ $this, 'bootEditorUi' ] );
		$this->scheduleCron();

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

		if ( did_action( 'init' ) ) {
			$this->registerPostMeta();
			$this->registerSaveHandlers();
		} else {
			add_action( 'init', [ $this, 'registerPostMeta' ], 20 );
			add_action( 'init', [ $this, 'registerSaveHandlers' ], 21 );
		}
	}

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

		foreach ( $this->getEnabledPostTypes() as $post_type ) {
			add_action( "save_post_{$post_type}", [ $this, 'savePostExpiration' ], 10, 2 );
			add_filter( $this->listColumnsFilter( $post_type ), [ $this, 'addListColumn' ] );
			add_action( $this->listColumnAction( $post_type ), [ $this, 'renderListColumn' ], 10, 2 );
		}

		add_action( 'bulk_edit_posts', [ $this, 'saveFromBulkEdit' ], 10, 2 );
	}

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

		foreach ( $this->getEnabledPostTypes() 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 );
						$post_id = (int) $post_id;

						return $post_id > 0 && current_user_can( 'edit_post', $post_id );
					},
					'sanitize_callback' => [ $this, 'sanitizeExpirationMeta' ],
				]
			);
		}
	}

	public function bootListTableUi(): void {
		if ( $this->list_ui_booted ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || ! $this->isEnabledType( (string) $screen->post_type ) ) {
			return;
		}

		$this->list_ui_booted = true;

		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueListAssets' ] );
		add_action( 'load-edit.php', [ $this, 'maybeCheckExpiredPostsInAdmin' ] );
		add_action( 'quick_edit_custom_box', [ $this, 'renderQuickEdit' ], 10, 2 );
		add_action( 'bulk_edit_custom_box', [ $this, 'renderQuickEdit' ], 10, 2 );
	}

	public function bootEditorUi(): void {
		if ( $this->editor_ui_booted ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || ! $this->isEnabledType( (string) $screen->post_type ) ) {
			return;
		}

		$this->editor_ui_booted = true;

		add_action( 'add_meta_boxes', [ $this, 'registerMetaBox' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueueEditorAssets' ] );
		add_action( 'load-post.php', [ $this, 'maybeCheckExpiredPostsInAdmin' ] );
	}

	public function ajaxSaveExpiration(): void {
		check_ajax_referer( self::NONCE_ACTION, 'nonce' );

		$post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
		$raw     = isset( $_POST['expiration'] ) ? sanitize_text_field( wp_unslash( $_POST['expiration'] ) ) : '';

		if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid request.', 'wp-powersuite' ) ], 400 );
		}

		$post_type = get_post_type( $post_id );
		if ( ! is_string( $post_type ) || ! $this->isEnabledType( $post_type ) ) {
			wp_send_json_error( [ 'message' => __( 'This post type does not support expiration.', 'wp-powersuite' ) ], 403 );
		}

		if ( $raw !== '' && $this->normalizeExpirationDate( $raw ) === '' ) {
			wp_send_json_error( [ 'message' => __( 'Invalid expiration date.', 'wp-powersuite' ) ], 400 );
		}

		$was_published = get_post_status( $post_id ) === 'publish';
		$this->persistExpiration( $post_id, $raw );

		$stored = $this->getPostExpiration( $post_id );
		$ts     = $stored !== '' ? $this->parseExpirationToTimestamp( $stored ) : null;

		wp_send_json_success(
			[
				'display'     => $ts ? wp_date( 'M j, Y g:i a', $ts ) : '',
				'input'       => $stored !== '' ? $this->formatForDatetimeLocal( $stored ) : '',
				'unpublished' => $was_published && get_post_status( $post_id ) !== 'publish',
			]
		);
	}

	/**
	 * @param int      $post_id Post ID.
	 * @param \WP_Post $post    Post object.
	 */
	public function savePostExpiration( int $post_id, \WP_Post $post ): void {
		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 ) || ! $this->isEnabledType( $post->post_type ) ) {
			return;
		}

		if ( isset( $_POST['action'] ) && $_POST['action'] === 'inline-save' ) {
			if ( ! array_key_exists( 'wpps_expiration_date', $_POST ) ) {
				return;
			}

			if ( ! $this->verifyInlineEditNonce( $_POST ) ) {
				return;
			}

			$this->persistExpiration(
				$post_id,
				sanitize_text_field( wp_unslash( (string) $_POST['wpps_expiration_date'] ) )
			);
			return;
		}

		if ( isset( $_REQUEST['bulk_edit'] ) && ! empty( $_REQUEST['wpps_expiration_update'] ) ) {
			if ( ! array_key_exists( 'wpps_expiration_date', $_REQUEST ) ) {
				return;
			}

			if ( ! $this->verifyInlineEditNonce( $_REQUEST ) ) {
				return;
			}

			$this->persistExpiration(
				$post_id,
				sanitize_text_field( wp_unslash( (string) $_REQUEST['wpps_expiration_date'] ) )
			);
			return;
		}

		if ( ! isset( $_POST['wpps_expiration_date'], $_POST['wpps_content_expiration_nonce'] ) ) {
			return;
		}

		if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['wpps_content_expiration_nonce'] ) ), self::NONCE_ACTION ) ) {
			return;
		}

		$this->persistExpiration(
			$post_id,
			sanitize_text_field( wp_unslash( $_POST['wpps_expiration_date'] ) )
		);
	}

	/**
	 * @param mixed  $meta_id  Meta ID.
	 * @param int    $post_id  Post ID.
	 * @param string $meta_key Meta key.
	 * @param mixed  $value    Meta value.
	 */
	public function onExpirationMetaChanged( $meta_id, int $post_id, string $meta_key, $value ): void {
		unset( $meta_id );

		if ( $meta_key !== self::META_KEY && $meta_key !== self::LEGACY_META_KEY ) {
			return;
		}

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

		$post_type = get_post_type( $post_id );
		if ( ! is_string( $post_type ) || ! $this->isEnabledType( $post_type ) ) {
			return;
		}

		$this->maybeUnpublishIfExpired( $post_id, $value );
	}

	/**
	 * @param mixed $value Raw meta value.
	 */
	public function sanitizeExpirationMeta( $value ): string {
		if ( ! is_string( $value ) ) {
			return '';
		}

		$value = trim( $value );

		return $value === '' ? '' : $this->normalizeExpirationDate( $value );
	}

	public function registerMetaBox(): void {
		foreach ( $this->getEnabledPostTypes() as $post_type ) {
			add_meta_box(
				'wpps-content-expiration',
				__( 'Content Expiration', 'wp-powersuite' ),
				[ $this, 'renderMetaBox' ],
				$post_type,
				'side',
				'high',
				[ '__block_editor_compatible_meta_box' => true ]
			);
		}
	}

	public function renderMetaBox( \WP_Post $post ): void {
		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return;
		}

		$stored        = $this->getPostExpiration( $post->ID );
		$display_value = $stored !== '' ? $this->formatForDatetimeLocal( $stored ) : '';
		$timestamp     = $stored !== '' ? $this->parseExpirationToTimestamp( $stored ) : null;
		$display_label = $timestamp ? wp_date( 'M j, Y g:i a', $timestamp ) : '';

		wp_nonce_field( self::NONCE_ACTION, 'wpps_content_expiration_nonce' );
		?>
		<div class="wpps-content-expiration">
			<label class="wpps-content-expiration__label" for="wpps_expiration_date">
				<?php esc_html_e( 'Expiration Date', 'wp-powersuite' ); ?>
			</label>
			<input
				type="datetime-local"
				id="wpps_expiration_date"
				name="wpps_expiration_date"
				class="wpps-content-expiration__input"
				value="<?php echo esc_attr( $display_value ); ?>"
			>
			<div class="wpps-content-expiration__actions">
				<button type="button" class="button button-primary wpps-content-expiration__save">
					<?php esc_html_e( 'Save expiration', 'wp-powersuite' ); ?>
				</button>
				<button type="button" class="button button-link-delete wpps-content-expiration__clear">
					<?php esc_html_e( 'Clear', 'wp-powersuite' ); ?>
				</button>
			</div>
			<p class="wpps-content-expiration__status description" <?php echo $display_label === '' ? 'hidden' : ''; ?>>
				<?php if ( $display_label !== '' ) : ?>
					<?php
					printf(
						/* translators: %s: formatted expiration date */
						esc_html__( 'Saved: %s', 'wp-powersuite' ),
						esc_html( $display_label )
					);
					?>
				<?php endif; ?>
			</p>
			<p class="wpps-content-expiration__hint description">
				<?php
				printf(
					/* translators: 1: timezone, 2: current time */
					esc_html__( 'Uses site timezone %1$s. Current time: %2$s.', 'wp-powersuite' ),
					esc_html( wp_timezone_string() ),
					esc_html( current_time( 'Y-m-d H:i:s' ) )
				);
				?>
			</p>
		</div>
		<?php
	}

	public function addListColumn( array $columns ): array {
		$columns['wpps_expiration'] = __( 'Expiration', 'wp-powersuite' );

		return $columns;
	}

	public function renderListColumn( string $column, int $post_id ): void {
		if ( $column !== 'wpps_expiration' ) {
			return;
		}

		$stored = $this->getPostExpiration( $post_id );
		if ( $stored === '' ) {
			echo '<span class="wpps-expiration-empty" data-expiration="">—</span>';
			return;
		}

		$timestamp = $this->parseExpirationToTimestamp( $stored );
		$display   = $timestamp ? wp_date( 'M j, Y g:i a', $timestamp ) : esc_html( $stored );
		$input     = $timestamp ? $this->formatForDatetimeLocal( $stored ) : '';

		echo '<span class="wpps-expiration-value" data-expiration="' . esc_attr( $input ) . '">' . esc_html( (string) $display ) . '</span>';
	}

	public function renderQuickEdit( string $column_name, string $post_type ): void {
		if ( ! in_array( $column_name, [ 'title', 'wpps_expiration' ], true ) || ! $this->isEnabledType( $post_type ) ) {
			return;
		}

		static $quick_rendered = false;
		static $bulk_rendered  = false;

		$is_bulk = current_filter() === 'bulk_edit_custom_box';
		if ( $is_bulk ) {
			if ( $bulk_rendered ) {
				return;
			}
			$bulk_rendered = true;
		} elseif ( $quick_rendered ) {
			return;
		} else {
			$quick_rendered = true;
		}

		wp_nonce_field( 'wpps_expiration_inline_edit', 'wpps_expiration_inline_nonce' );
		?>
		<fieldset class="inline-edit-col-right inline-edit-wpps-expiration">
			<div class="inline-edit-group">
				<?php if ( $is_bulk ) : ?>
					<label class="alignleft">
						<input type="checkbox" name="wpps_expiration_update" value="1">
						<span class="checkbox-label"><?php esc_html_e( 'Update expiration date', 'wp-powersuite' ); ?></span>
					</label>
				<?php endif; ?>
				<label class="alignleft" <?php echo $is_bulk ? 'style="clear:both;margin-top:6px;"' : ''; ?>>
					<span class="title"><?php esc_html_e( 'Expiration Date', 'wp-powersuite' ); ?></span>
					<input type="datetime-local" name="wpps_expiration_date" class="wpps-expiration-datetime" value="">
				</label>
				<?php if ( $is_bulk ) : ?>
					<p class="description" style="margin:4px 0 0;clear:both;"><?php esc_html_e( 'Leave empty to clear. Check "Update" to apply.', 'wp-powersuite' ); ?></p>
				<?php endif; ?>
			</div>
		</fieldset>
		<?php
	}

	public function saveFromBulkEdit( array $updated, array $shared_post_data ): void {
		if ( $updated === [] || empty( $shared_post_data['wpps_expiration_update'] ) ) {
			return;
		}

		if ( ! $this->verifyInlineEditNonce( $shared_post_data ) ) {
			return;
		}

		$value = isset( $shared_post_data['wpps_expiration_date'] )
			? sanitize_text_field( wp_unslash( (string) $shared_post_data['wpps_expiration_date'] ) )
			: '';

		foreach ( $updated as $post_id ) {
			$post_id = (int) $post_id;
			if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
				continue;
			}

			$ptype = get_post_type( $post_id );
			if ( is_string( $ptype ) && $this->isEnabledType( $ptype ) ) {
				$this->persistExpiration( $post_id, $value );
			}
		}
	}

	/**
	 * @param array<string, mixed> $request
	 */
	private function verifyInlineEditNonce( array $request ): bool {
		$nonce = isset( $request['wpps_expiration_inline_nonce'] )
			? sanitize_text_field( wp_unslash( (string) $request['wpps_expiration_inline_nonce'] ) )
			: '';

		return $nonce !== '' && wp_verify_nonce( $nonce, 'wpps_expiration_inline_edit' );
	}

	private function persistExpiration( int $post_id, string $value ): void {
		$value = trim( $value );
		if ( $value === '' ) {
			delete_post_meta( $post_id, self::META_KEY );
			delete_post_meta( $post_id, self::LEGACY_META_KEY );
			return;
		}

		$normalized = $this->normalizeExpirationDate( $value );
		if ( $normalized === '' ) {
			delete_post_meta( $post_id, self::META_KEY );
			delete_post_meta( $post_id, self::LEGACY_META_KEY );
			return;
		}

		update_post_meta( $post_id, self::META_KEY, $normalized );
		delete_post_meta( $post_id, self::LEGACY_META_KEY );
		$this->maybeUnpublishIfExpired( $post_id, $normalized );
	}

	private function getPostExpiration( int $post_id ): string {
		$current = get_post_meta( $post_id, self::META_KEY, true );
		if ( is_string( $current ) && $current !== '' ) {
			return $current;
		}

		$legacy = get_post_meta( $post_id, self::LEGACY_META_KEY, true );
		if ( ! is_string( $legacy ) || $legacy === '' ) {
			return '';
		}

		update_post_meta( $post_id, self::META_KEY, $legacy );
		delete_post_meta( $post_id, self::LEGACY_META_KEY );

		return $legacy;
	}

	public function checkSinglePostExpiration(): void {
		if ( ! is_singular() ) {
			return;
		}

		$post = get_queried_object();
		if ( ! $post instanceof \WP_Post || $post->post_status !== 'publish' || ! $this->isEnabledType( $post->post_type ) ) {
			return;
		}

		$expiration = $this->getPostExpiration( $post->ID );
		if ( $expiration === '' || ! $this->isExpirationPast( $expiration ) ) {
			return;
		}

		if ( ! $this->unpublishExpiredPost( $post->ID ) ) {
			return;
		}

		wp_safe_redirect( home_url( '/' ) );
		exit;
	}

	public function maybeCheckExpiredPostsInAdmin(): void {
		if ( get_transient( self::ADMIN_SCAN_LOCK ) ) {
			return;
		}

		set_transient( self::ADMIN_SCAN_LOCK, 1, self::ADMIN_SCAN_INTERVAL );
		$this->checkExpiredPosts();
	}

	public function checkExpiredPosts(): void {
		if ( get_transient( self::RUN_LOCK ) ) {
			return;
		}

		set_transient( self::RUN_LOCK, 1, MINUTE_IN_SECONDS );

		try {
			$this->processExpiredPostsBatch();
		} finally {
			delete_transient( self::RUN_LOCK );
		}
	}

	private function processExpiredPostsBatch(): void {
		global $wpdb;

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

		$current  = current_time( 'Y-m-d H:i:s' );
		$type_sql = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$expired = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT pm.post_id
				FROM {$wpdb->postmeta} pm
				INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
				WHERE pm.meta_key IN (%s, %s)
				AND pm.meta_value != ''
				AND (
					pm.meta_value <= %s
					OR REPLACE(pm.meta_value, 'T', ' ') <= %s
				)
				AND p.post_status = 'publish'
				AND p.post_type IN ({$type_sql})
				LIMIT %d",
				array_merge(
					[ self::META_KEY, self::LEGACY_META_KEY, $current, $current ],
					$post_types,
					[ self::EXPIRED_BATCH_LIMIT ]
				)
			)
		);

		foreach ( (array) $expired as $row ) {
			$this->unpublishExpiredPost( (int) $row->post_id );
		}
	}

	private function maybeUnpublishIfExpired( int $post_id, string $expiration_date ): void {
		if ( $this->isExpirationPast( $expiration_date ) ) {
			$this->unpublishExpiredPost( $post_id );
		}
	}

	private function unpublishExpiredPost( int $post_id ): bool {
		$post = get_post( $post_id );
		if ( ! $post instanceof \WP_Post || $post->post_status !== 'publish' || ! $this->isEnabledType( $post->post_type ) ) {
			return false;
		}

		if ( current_user_can( 'edit_post', $post_id ) ) {
			$result = wp_update_post(
				[
					'ID'          => $post_id,
					'post_status' => 'draft',
				],
				true
			);

			return ! is_wp_error( $result ) && (int) $result > 0;
		}

		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$updated = $wpdb->update(
			$wpdb->posts,
			[ 'post_status' => 'draft' ],
			[
				'ID'          => $post_id,
				'post_status' => 'publish',
			],
			[ '%s' ],
			[ '%d', '%s' ]
		);

		if ( $updated === false || $updated === 0 ) {
			return false;
		}

		clean_post_cache( $post_id );
		$updated_post = get_post( $post_id );
		if ( $updated_post instanceof \WP_Post ) {
			wp_transition_post_status( 'draft', 'publish', $updated_post );
		}

		return true;
	}

	public function enqueueEditorAssets( string $hook ): void {
		if ( ! in_array( $hook, [ 'post.php', 'post-new.php' ], true ) ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || ! $this->isEnabledType( (string) $screen->post_type ) ) {
			return;
		}

		$post_id = 0;
		if ( $hook === 'post.php' ) {
			$post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0;
			if ( $post_id <= 0 ) {
				return;
			}
		}

		$this->enqueueAssetBundle( $post_id );
	}

	public function enqueueListAssets( string $hook ): void {
		if ( $hook !== 'edit.php' ) {
			return;
		}

		$screen = get_current_screen();
		if ( ! $screen || ! $this->isEnabledType( (string) $screen->post_type ) ) {
			return;
		}

		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		if ( $base === '' ) {
			return;
		}

		wp_enqueue_script(
			'wpps-content-expiration-inline',
			$base . 'modules/content-expiration/assets/content-expiration-inline.js',
			[ 'jquery', 'inline-edit-post' ],
			$this->assetVersion( 'content-expiration-inline.js' ),
			true
		);
		wp_enqueue_style(
			'wpps-content-expiration',
			$base . 'modules/content-expiration/assets/content-expiration.css',
			[],
			$this->assetVersion( 'content-expiration.css' )
		);
	}

	private function enqueueAssetBundle( int $post_id ): void {
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		if ( $base === '' ) {
			return;
		}

		wp_enqueue_style(
			'wpps-content-expiration',
			$base . 'modules/content-expiration/assets/content-expiration.css',
			[],
			$this->assetVersion( 'content-expiration.css' )
		);
		wp_enqueue_script(
			'wpps-content-expiration-editor',
			$base . 'modules/content-expiration/assets/content-expiration-editor.js',
			[ 'jquery', 'wp-data' ],
			$this->assetVersion( 'content-expiration-editor.js' ),
			true
		);
		wp_localize_script(
			'wpps-content-expiration-editor',
			'wppsContentExpiration',
			[
				'metaKey' => self::META_KEY,
				'postId'  => $post_id,
				'ajaxUrl' => admin_url( 'admin-ajax.php' ),
				'action'  => self::AJAX_ACTION,
				'nonce'   => wp_create_nonce( self::NONCE_ACTION ),
				'i18n'    => [
					'save'        => __( 'Save expiration', 'wp-powersuite' ),
					'saving'      => __( 'Saving…', 'wp-powersuite' ),
					'clear'       => __( 'Clear', 'wp-powersuite' ),
					'saved'       => __( 'Saved: %s', 'wp-powersuite' ),
					'error'       => __( 'Could not save expiration date.', 'wp-powersuite' ),
					'cleared'     => __( 'Expiration cleared.', 'wp-powersuite' ),
					'unpublished' => __( 'This post was unpublished because the expiration date has passed.', 'wp-powersuite' ),
				],
			]
		);
	}

	private function scheduleCron(): void {
		if ( get_transient( self::CRON_TRANSIENT ) ) {
			return;
		}

		if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
			wp_schedule_event( time(), 'hourly', self::CRON_HOOK );
		}

		set_transient( self::CRON_TRANSIENT, 1, DAY_IN_SECONDS );
	}

	/**
	 * @return list<string>
	 */
	private function getEnabledPostTypes(): array {
		if ( ! did_action( 'init' ) ) {
			return self::normalizeSettings( $this->moduleSettings()->all( [] ) )['post_types'];
		}

		if ( $this->enabled_types_cache !== null ) {
			return $this->enabled_types_cache;
		}

		$types = self::normalizeSettings( $this->moduleSettings()->all( [] ) )['post_types'];
		$out   = [];

		foreach ( $types as $slug ) {
			if ( post_type_exists( $slug ) ) {
				$out[] = $slug;
			}
		}

		$this->enabled_types_cache = $out !== [] ? $out : [ 'post', 'page' ];

		return $this->enabled_types_cache;
	}

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

	private function normalizeExpirationDate( string $value ): string {
		$timestamp = $this->parseExpirationToTimestamp( $value );

		return $timestamp ? wp_date( 'Y-m-d H:i:s', $timestamp ) : '';
	}

	private function parseExpirationToTimestamp( string $expiration_date ): ?int {
		$normalized = $this->normalizeStoredExpirationDate( $expiration_date );
		if ( $normalized === '' ) {
			return null;
		}

		$timezone = wp_timezone();
		$datetime = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $normalized, $timezone );
		if ( ! $datetime ) {
			$datetime = \DateTimeImmutable::createFromFormat( 'Y-m-d H:i', $normalized, $timezone );
		}

		return $datetime ? $datetime->getTimestamp() : null;
	}

	private function normalizeStoredExpirationDate( string $expiration_date ): string {
		$normalized = trim( str_replace( 'T', ' ', $expiration_date ) );
		if ( $normalized === '' ) {
			return '';
		}

		if ( strlen( $normalized ) === 16 ) {
			$normalized .= ':00';
		}

		return $normalized;
	}

	private function formatForDatetimeLocal( string $expiration_date ): string {
		$timestamp = $this->parseExpirationToTimestamp( $expiration_date );

		return $timestamp ? wp_date( 'Y-m-d\TH:i', $timestamp ) : '';
	}

	private function isExpirationPast( string $expiration_date ): bool {
		$timestamp = $this->parseExpirationToTimestamp( $expiration_date );

		return $timestamp !== null && $timestamp <= current_time( 'timestamp' );
	}

	private function listColumnsFilter( string $post_type ): string {
		return $post_type === 'page' ? 'manage_pages_columns' : ( $post_type === 'post' ? 'manage_posts_columns' : "manage_{$post_type}_posts_columns" );
	}

	private function listColumnAction( string $post_type ): string {
		return $post_type === 'page' ? 'manage_pages_custom_column' : ( $post_type === 'post' ? 'manage_posts_custom_column' : "manage_{$post_type}_posts_custom_column" );
	}

	private function assetVersion( string $filename ): string {
		$path = __DIR__ . '/assets/' . $filename;

		return is_readable( $path ) ? (string) filemtime( $path ) : self::MODULE_VERSION;
	}

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

	/**
	 * @param array<string, mixed> $raw
	 * @return array{post_types: list<string>}
	 */
	public static function normalizeSettings( array $raw ): array {
		$defaults = self::getDefaultSettings();
		$allowed  = did_action( 'init' ) ? array_flip( self::getAllowedSettingsPostTypes() ) : array_flip( $defaults['post_types'] );
		$clean    = [];

		if ( isset( $raw['post_types'] ) && is_array( $raw['post_types'] ) ) {
			foreach ( $raw['post_types'] as $slug ) {
				$key = sanitize_key( (string) $slug );
				if ( $key !== '' && isset( $allowed[ $key ] ) ) {
					$clean[] = $key;
				}
			}
		}

		if ( $clean === [] ) {
			$clean = $defaults['post_types'];
		}

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

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

	/**
	 * @return list<string>
	 */
	public static function getAllowedSettingsPostTypes(): array {
		return array_values(
			array_diff(
				get_post_types( [ 'public' => true ], 'names' ),
				self::getSettingsExcludedPostTypes()
			)
		);
	}
}
