<?php

namespace AIOSEO\Plugin\Pro\SeoAnalysis\ActionScheduler;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use AIOSEO\Plugin\Common\Models\Post as AioseoPost;
use AIOSEO\Plugin\Pro\Models\Issue;

/**
 * Handles the action scheduler for Posts.
 *
 * @since 4.8.6
 */
class Post {
	/**
	 * The action.
	 *
	 * @since 4.8.6
	 *
	 * @var string
	 */
	protected $action = 'aioseo_seo_analysis_posts_scan';

	/**
	 * The number of items to analyze per run.
	 *
	 * @since 4.8.6
	 *
	 * @var integer
	 */
	protected $perRun = 5;

	/**
	 * Class constructor.
	 *
	 * @since   4.8.6
	 * @version 4.9.4.2 Change admin_init to init to allow frontend scheduling.
	 */
	public function __construct() {
		if ( ! aioseo()->sensitiveOptions->hasValue( 'licenseKey' ) ) {
			return;
		}

		// After completion, the scan won't reschedule until someone is in the admin. That's fine for most cases since posts being created/updated happens in the admin.
		add_action( 'admin_init', [ $this, 'scheduleScan' ] );
		add_action( $this->action, [ $this, 'scanPosts' ] );
	}

	/**
	 * Schedule the recurring action to scan posts.
	 *
	 * @since 4.8.6
	 *
	 * @return void
	 */
	public function scheduleScan() {
		// If we're in idle mode (no posts to scan), unschedule and don't reschedule yet.
		if ( aioseo()->core->cache->get( 'as_seo_analysis_post_idle' ) ) {
			aioseo()->actionScheduler->unschedule( $this->action );

			return;
		}

		$inteval = apply_filters( 'aioseo_seo_analyzer_scan_interval', MINUTE_IN_SECONDS );
		aioseo()->actionScheduler->scheduleRecurrent( $this->action, 0, $inteval );
	}

	/**
	 * Get the number of items to analyze per page.
	 *
	 * @since 4.8.6
	 *
	 * @return int
	 */
	protected function getPerRun() {
		return (int) apply_filters( 'aioseo_seo_analyzer_scan_items_per_run', $this->perRun );
	}

	/**
	 * Handles the analysis for X posts and store the results.
	 *
	 * @since   4.8.6
	 * @version 4.9.4.2 Add runtime lock to prevent concurrent execution.
	 *
	 * @return void
	 */
	public function scanPosts() {
		// Runtime lock: Prevent concurrent execution of this action.
		$lockKey = 'as_seo_analysis_post_running';
		if ( aioseo()->core->cache->get( $lockKey ) ) {
			return;
		}

		// Set lock with a safety timeout in case the action fails mid-execution.
		aioseo()->core->cache->update( $lockKey, true, 2 * MINUTE_IN_SECONDS );

		$postsToAnalyze = $this->getEnqueuedPosts();
		if ( empty( $postsToAnalyze ) ) {
			// No posts to analyze - set idle cache. The schedule method on the next init will unschedule.
			aioseo()->core->cache->update( 'as_seo_analysis_post_idle', true, HOUR_IN_SECONDS );
			aioseo()->core->cache->delete( $lockKey );

			return;
		}

		foreach ( $postsToAnalyze as $postId ) {
			Issue::deleteAll( $postId, 'post' );

			$scan                     = aioseo()->seoAnalysis->analyzePost( $postId );
			list( $basic, $advanced ) = array_values( $scan['results'] );

			$data     = array_merge( $basic, $advanced );
			$postType = get_post_type( $postId );
			foreach ( $data as $issue ) {
				$model = new Issue( [
					'object_id'      => $postId,
					'object_type'    => 'post',
					'object_subtype' => $postType,
					'code'           => $issue->code,
					'metadata'       => $issue->metadata
				] );

				$model->save();
			}

			// Update the scan date.
			$post = AioseoPost::getPost( $postId );
			$post->seo_analyzer_scan_date = gmdate( 'Y-m-d H:i:s' );
			$post->save();
		}

		aioseo()->core->cache->delete( $lockKey );
	}

	/**
	 * Get the posts that need to be analyzed.
	 *
	 * @since 4.8.6
	 *
	 * @return array Posts object
	 */
	private function getEnqueuedPosts() {
		$settings           = aioseo()->dynamicOptions->seoAnalysis->all();
		$publicPostTypes    = aioseo()->helpers->getScannablePostTypes();
		$publicPostStatuses = aioseo()->helpers->getPublicPostStatuses( true );

		$postTypes = $publicPostTypes;
		if ( 1 !== (int) $settings['postTypes']['all'] && ! empty( $settings['postTypes']['included'] ) ) {
			$postTypes = array_intersect( $postTypes, $settings['postTypes']['included'] );
		}

		$orderByCasesPostTypes = [];
		foreach ( $postTypes as $value ) {
			$count                   = count( $orderByCasesPostTypes ) + 1;
			$orderByCasesPostTypes[] = "WHEN p.post_type = '$value' THEN $count";
		}

		$postStatuses = aioseo()->helpers->getPublicPostStatuses( true );
		if ( 1 !== (int) $settings['postStatuses']['all'] && ! empty( $settings['postStatuses']['included'] ) ) {
			$postStatuses = array_intersect( $postStatuses, $settings['postStatuses']['included'] );
		}

		$orderByCasesPostStatuses = [];
		foreach ( $postStatuses as $value ) {
			$count                      = count( $orderByCasesPostStatuses ) + 1;
			$orderByCasesPostStatuses[] = "WHEN p.post_status = '$value' THEN $count";
		}

		$select  = [ 'p.ID' ];
		$orderBy = [];

		if ( ! empty( $orderByCasesPostTypes ) ) {
			$select[]  = '(CASE ' . implode( ' ', $orderByCasesPostTypes ) . ' END) as post_type_order';
			$orderBy[] = 'post_type_order';
		}

		if ( ! empty( $orderByCasesPostStatuses ) ) {
			$select[]  = '(CASE ' . implode( ' ', $orderByCasesPostStatuses ) . ' END) as post_status_order';
			$orderBy[] = 'post_status_order';
		}

		$rescanThreshold = (int) apply_filters( 'aioseo_seo_analyzer_rescan_threshold', 6 * MONTH_IN_SECONDS );
		$cutoffDate      = gmdate( 'Y-m-d H:i:s', time() - $rescanThreshold );

		$query = aioseo()->core->db->start( 'posts as p' )
			->select( implode( ', ', $select ) )
			->leftJoin( 'aioseo_posts as ap', 'ap.post_id = p.ID' )
			->whereIn( 'p.post_type', $publicPostTypes )
			->whereIn( 'p.post_status', $publicPostStatuses )
			->whereRaw( "(ap.seo_analyzer_scan_date IS NULL OR ap.seo_analyzer_scan_date < '$cutoffDate')" );

		$orderBy[] = 'ap.seo_analyzer_scan_date';
		$query->orderBy( implode( ', ', $orderBy ) . ' ASC' );

		$posts = $query
			->limit( $this->getPerRun() )
			->run()
			->result();

		// We just need the IDs.
		foreach ( $posts as &$post ) {
			$post = $post->ID;
		}

		return $posts;
	}
}