<?php
namespace Actirise\Includes;

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

use ActirisePublic\Includes\AdsTxt;
use ActirisePublic\Includes\PresizedDiv;
use Actirise\Includes\AdFormat;
use Actirise\Includes\EventAnnotation;
use Actirise\Includes\Options;
use Actirise\Includes\Helpers;
use Actirise\Includes\Logger;
use ActiriseAdmin\Includes\HealthCheck;

/**
 * This class allows the management of cron jobs for the proper functioning of our plugin.
 *
 * @link       https://actirise.com
 * @since      2.0.0
 * @package    actirise
 * @subpackage actirise/includes
 * @author     actirise <wordpress@actirise.com>
 *
 * @phpstan-import-type PresizedDivSlot from PresizedDiv
 */
final class Cron {
	/**
	 * Transient key tracking the ads.txt follow-up polling attempt count.
	 */
	const ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT = 'actirise_adstxt_followup_attempt';

	/**
	 * Delay before the next ads.txt follow-up poll, in seconds.
	 */
	const ADSTXT_FOLLOWUP_DELAY = 10;

	/**
	 * Maximum number of ads.txt follow-up polls before giving up.
	 */
	const ADSTXT_FOLLOWUP_MAX_ATTEMPTS = 10;

	/**
	 * Lifetime of the ads.txt follow-up attempt transient, in seconds.
	 */
	const ADSTXT_FOLLOWUP_TRANSIENT_TTL = 1800;

	/**
	 * Schedule cron jobs
	 *
	 * @since    2.0.0
	 * @return void
	 */
	public function schedule() {
		if ( ACTIRISE_CRON === 'true' ) {
			add_filter(
				'cron_schedules',
				function ( $schedules ) {
					$schedules['five_minutes'] = array(
						'interval' => 300,
						'display'  => esc_html__( 'Every Five Minutes', 'actirise' ),
					);
					return $schedules;
				}
			);

			if ( ! wp_next_scheduled( 'actirise_cron_update_adstxt' ) ) {
				wp_schedule_event( time(), 'hourly', 'actirise_cron_update_adstxt' );
				$this->update_adstxt();
			}

			if ( ! wp_next_scheduled( 'actirise_cron_update_presized_div' ) ) {
				wp_schedule_event( time(), 'hourly', 'actirise_cron_update_presized_div' );
				$this->check_presized_div();
			}

			if ( ! wp_next_scheduled( 'actirise_cron_update_fast_cmp' ) ) {
				wp_schedule_event( time(), 'hourly', 'actirise_cron_update_fast_cmp' );
				$this->get_fast_cmp();
			}

			if ( ! wp_next_scheduled( 'actirise_cron_check_adstxt' ) ) {
				wp_schedule_event( time(), 'hourly', 'actirise_cron_check_adstxt' );
				$this->check_adstxt();
			}

			if ( ! wp_next_scheduled( 'actirise_cron_check_slots' ) ) {
				wp_schedule_event( time(), 'hourly', 'actirise_cron_check_slots' );
				$this->check_slots();
			}

			if ( ! wp_next_scheduled( 'actirise_cron_event_annotation' ) ) {
				wp_schedule_event( time(), 'five_minutes', 'actirise_cron_event_annotation' );
				$this->upload_event_annotation();
			}

			if ( Options::get( 'contract-type' ) === 'essential' && ! wp_next_scheduled( 'actirise_cron_check_optimization' ) ) {
				wp_schedule_event( time(), 'five_minutes', 'actirise_cron_check_optimization' );
			}
		}
	}

	/**
	 * Update ads.txt file
	 *
	 * @since    2.0.0
	 * @return void
	 */
	public function update_adstxt() {
		if ( Options::get( 'settings-uuid' ) ) {
			$ads_txt = AdsTxt::get_from_api();

			if ( $ads_txt !== false ) {
				Options::update( 'adstxt-actirise', $ads_txt );
				$this->check_adstxt();

				if ( Options::get( 'adstxt-active', false ) === true ) {
					AdsTxt::update_file();
				}

				Options::update( 'adstxt-lastupdate', time() );
			}

			if ( $ads_txt === false ) {
				Logger::add_log( 'adstxt get_from_api is false', 'include/cron', 'error' );
			}
		}
	}

	/**
	 * Check lines in ads.txt file
	 *
	 * @since 3.0.0
	 * @return bool|array<mixed, mixed>
	 */
	public function check_adstxt() {
		if ( ! Helpers::is_user_authenticated() ) {
			return false;
		}

		// Hosted mode: ads.txt is served by Actirise, no monitoring API call needed.
		if ( Options::get( 'hosted-adstxt-enabled', false ) === true ) {
			$this->check_hosted_adstxt_redirect();
			return false;
		}

		$adstxt_status = AdsTxt::check_from_api( 'true' );

		if ( is_array( $adstxt_status ) && isset( $adstxt_status['state'] ) && $adstxt_status['state'] === 'pending' ) {
			$this->schedule_adstxt_followup( 1 );
			return $adstxt_status;
		}

		$this->finalize_adstxt_check( $adstxt_status );

		return $adstxt_status;
	}

	/**
	 * Follow-up cron event for ads.txt monitoring polling.
	 *
	 * Fired once the initial refresh request returned `pending`. Polls the
	 * adstxt monitoring endpoint without the refresh flag and either
	 * reschedules itself (until ADSTXT_FOLLOWUP_MAX_ATTEMPTS is reached) or
	 * finalizes the check when a definitive state is returned.
	 *
	 * @since 4.2.0
	 * @return void
	 */
	public function check_adstxt_followup() {
		if ( ! Helpers::is_user_authenticated() ) {
			delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT );
			return;
		}

		// Hosted mode may have been enabled mid-chain; abort the API polling.
		if ( Options::get( 'hosted-adstxt-enabled', false ) === true ) {
			delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT );
			return;
		}

		/** @var mixed $stored_attempt */
		$stored_attempt = get_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT );
		$attempt        = is_numeric( $stored_attempt ) ? (int) $stored_attempt : 0;

		$adstxt_status = AdsTxt::check_from_api( 'false' );

		$still_pending = is_array( $adstxt_status )
			&& isset( $adstxt_status['state'] )
			&& $adstxt_status['state'] === 'pending';

		if ( $still_pending && $attempt < self::ADSTXT_FOLLOWUP_MAX_ATTEMPTS ) {
			$this->schedule_adstxt_followup( $attempt + 1 );
			return;
		}

		delete_transient( self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT );

		if ( $still_pending ) {
			Logger::add_log( 'check_adstxt_followup max attempts reached, state still pending', 'include/cron', 'error' );
			return;
		}

		$this->finalize_adstxt_check( $adstxt_status );
	}

	/**
	 * Schedule a single follow-up event to re-poll the ads.txt monitoring
	 * endpoint without the refresh flag.
	 *
	 * @since 4.2.0
	 * @param int $attempt The 1-based attempt counter to persist for the next run.
	 * @return void
	 */
	private function schedule_adstxt_followup( $attempt ) {
		set_transient(
			self::ADSTXT_FOLLOWUP_ATTEMPT_TRANSIENT,
			$attempt,
			self::ADSTXT_FOLLOWUP_TRANSIENT_TTL
		);
		wp_schedule_single_event(
			time() + self::ADSTXT_FOLLOWUP_DELAY,
			'actirise_cron_check_adstxt_followup'
		);
	}

	/**
	 * Persist the ads.txt check result and run the hosted redirect check.
	 *
	 * @since 4.2.0
	 * @param bool|array<mixed, mixed> $adstxt_status Final status returned by the API.
	 * @return void
	 */
	private function finalize_adstxt_check( $adstxt_status ) {
		if ( is_array( $adstxt_status ) && isset( $adstxt_status['state'] ) ) {
			if ( $adstxt_status['state'] === 'ok' ) {
				Options::update( 'adstxt-update', false );
			} else {
				Options::update( 'adstxt-update', true );
			}
		}
	}

	/**
	 * Check hosted ads.txt redirect
	 *
	 * @since    3.3.5
	 * @return void
	 */
	public function check_hosted_adstxt_redirect() {
		$is_hosted = Options::get( 'hosted-adstxt-enabled', false ) === true;

		if ( ! $is_hosted ) {
			// Not in hosted mode, nothing to check
			Options::delete( 'hosted-adstxt-redirect-error' );
			return;
		}

		$uuid = Options::get( 'website-uuid' );
		if ( ! $uuid ) {
			return;
		}

		$site_ads_txt_url = get_site_url() . '/ads.txt';
		$flashbid_url     = 'https://gateway.actirise.com/services/adstxt/' . $uuid;

		$response = wp_remote_get(
			$site_ads_txt_url,
			array(
				'timeout'     => 10,
				'redirection' => 0, // Don't follow redirects
  			'sslverify' => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			// Network error, mark as error
			Options::update(
				'hosted-adstxt-redirect-error',
				array(
					'error_type'   => 'manual_required',
					'flashbid_url' => $flashbid_url,
				)
			);
			return;
		}

		$status_code = wp_remote_retrieve_response_code( $response );

		if ( $status_code === 302 || $status_code === 301 ) {
			$redirect_location = wp_remote_retrieve_header( $response, 'location' );

			// Ignore WordPress trailing slash redirects (ads.txt -> ads.txt/)
			$is_trailing_slash_redirect = ( $redirect_location === trailingslashit( $site_ads_txt_url ) );

			if ( $redirect_location === $flashbid_url ) {
				// Redirect is correct (don't check destination status), clear error
				Options::delete( 'hosted-adstxt-redirect-error' );
			} elseif ( $is_trailing_slash_redirect ) {
				// WordPress trailing slash redirect - not an error, clear any existing error
				Options::delete( 'hosted-adstxt-redirect-error' );
			} else {
				// Wrong redirect URL
				Options::update(
					'hosted-adstxt-redirect-error',
					array(
						'error_type'   => 'wrong_url',
						'flashbid_url' => $flashbid_url,
					)
				);
			}
		} elseif ( $status_code === 200 ) {
			// File is being served directly instead of redirected
			// Check if it's a physical file or generated by another plugin/system
			$path_ads_txt = ABSPATH . 'ads.txt';
			$wp_fs        = Helpers::get_wp_fs();
			$file_exists  = ! is_wp_error( $wp_fs ) && $wp_fs->exists( $path_ads_txt );

			if ( $file_exists ) {
				// Physical file exists
				Options::update(
					'hosted-adstxt-redirect-error',
					array(
						'error_type'   => 'file_exists',
						'flashbid_url' => $flashbid_url,
					)
				);
			} else {
				// File is generated by third-party plugin/system
				Options::update(
					'hosted-adstxt-redirect-error',
					array(
						'error_type'   => 'third_party',
						'flashbid_url' => $flashbid_url,
					)
				);
			}
		} else {
			// Other error
			Options::update(
				'hosted-adstxt-redirect-error',
				array(
					'error_type'   => 'manual_required',
					'flashbid_url' => $flashbid_url,
				)
			);
		}
	}

	/**
	 * Check for presized div
	 *
	 * @since    2.0.0
	 * @return void
	 */
	public function check_presized_div() {
		if ( ! Helpers::is_user_authenticated() ) {
			return;
		}

		if ( Options::get( 'settings-uuid' ) ) {
			/** @var array<PresizedDivSlot> | bool $presized_div */
			$presized_div = PresizedDiv::get_from_api();

			if ( $presized_div !== false ) {
				/** @var mixed $presized_div_active */
				$presized_div_active = Options::get( 'presizeddiv-selected', array() );
				/** @var mixed $presized_div_notif */
				$presized_div_notif = Options::get( 'presizeddiv-notif', array() );

				if ( ! is_array( $presized_div_active ) ) {
					$presized_div_active = array();
				}
				/** @var array<array{slotName: string, active: bool}> $presized_div_active */

				if ( ! is_array( $presized_div_notif ) ) {
					$presized_div_notif = array();
				}

				/** @var array<PresizedDivSlot> $presized_div */
				foreach ( $presized_div as $presized_div_item ) {
					/** @var string $slot_name */
					$slot_name = $presized_div_item['slotName'];
					if ( ! in_array( $slot_name, array_column( $presized_div_active, 'slotName' ), true ) ) {
						$presized_div_active[] = array(
							'slotName' => $slot_name,
							'active'   => false,
						);

						/** @var array<\stdclass> $presized_div_notif */
						$presized_div_notif[] = $slot_name;
					}
				}

				/** @var array{slotName: string, active: bool} $presized_div_active_item */
				foreach ( $presized_div_active as $key => $presized_div_active_item ) {
					$slot_name = $presized_div_active_item['slotName'];
					if ( ! in_array( $slot_name, array_column( $presized_div, 'slotName' ), true ) ) {
						unset( $presized_div_active[ $key ] );
					}
				}

				Options::update( 'presizeddiv-selected', $presized_div_active );
				Options::update( 'presizeddiv-actirise', $presized_div );
				Options::update( 'presizeddiv-notif', $presized_div_notif );

				Options::update( 'presizeddiv-lastupdate', time() );
			} else {
				Options::update( 'presizeddiv-actirise', array() );
			}
		}
	}

	/**
	 * Get FastCmp stub light
	 *
	 * @since    2.3.0
	 * @return void
	 */
	public function get_fast_cmp() {
		$this->get_fastcmp_stublight();
		$this->get_fastcmp_vendors();

		if ( Options::get( 'fastcmp-uuid', '' ) === '' ) {
			$this->get_fastcmp_uuid();
		}

		Options::update( 'fastcmp-lastupdate', time() );
	}

	/**
	 * Set auto update
	 *
	 * @since    2.4.0
	 * @param bool $enabled
	 * @return void
	 */
	public function set_auto_update( $enabled ) {
		if ( ACTIRISE_CRON !== 'true' ) {
			return;
		}

		if ( $enabled === Helpers::has_auto_update() ) {
			return;
		}

		/** @var string $plugin_file */
		$plugin_file = plugin_basename( ACTIRISE_FILE );
		/** @var array<string> $auto_updates */
		$auto_updates = get_site_option( 'auto_update_plugins', array() );

		if ( $enabled ) {
			$plugins = array_unique( array_merge( $auto_updates, array( plugin_basename( ACTIRISE_FILE ) ) ) );
		} else {
			$plugins = array_values( array_diff( $auto_updates, array( plugin_basename( ACTIRISE_FILE ) ) ) );
		}

		update_site_option( 'auto_update_plugins', $plugins );
	}

	/**
	 * Check if the slots XPath are present and valid in website pages
	 *
	 * @since    3.2.0
	 * @return void
	 */
	public function check_slots() {
		if ( ! Helpers::is_user_authenticated() ) {
			return;
		}

		if ( Options::get( 'contract-type' ) !== 'essential' ) {
			return;
		}

		$ad_format = new AdFormat();

		$ad_format->validate_xpath_and_notify();
	}

	/**
	 * Uploads event annotations via cron task.
	 *
	 * This method is called by the cron system to trigger the event
	 * annotation upload process. It delegates the actual work to the
	 * EventAnnotation class.
	 *
	 * @return void
	 *
	 * @since 3.2.0
	 */
	public function upload_event_annotation() {
		EventAnnotation::send_events();
	}

	/**
	 * Get FastCmp stub light
	 *
	 * @since    2.2.0
	 * @return void
	 */
	private function get_fastcmp_stublight() {
		$stub_light_url = 'https://static.fastcmp.com/fast-cmp-stub-light.js';

		$response = wp_remote_request(
			$stub_light_url,
			array(
				'method'    => 'GET',
				'timeout'   => 10,
  			'sslverify' => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			Logger::add_log( 'get_fastcmp_stublight error ' . $response->get_error_code(), 'include/cron', 'error' );
			return;
		}

		/** @var string|boolean $stub_light */
		$stub_light = wp_remote_retrieve_body( $response );

		if ( $stub_light === false ) {
			return;
		}

		Options::update( 'fastcmp-stub-light', $stub_light );
	}

	/**
	 * Get FastCmp vendors
	 *
	 * @since    2.2.0
	 * @return void
	 */
	private function get_fastcmp_vendors() {
		$vendors_api_url = 'https://eu.fastcmp.com/wp/vendor-list';

		$response = wp_remote_request(
			$vendors_api_url,
			array(
				'method'    => 'GET',
				'timeout'   => 10,
  			'sslverify' => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			return;
		}

		/** @var string|boolean $vendors */
		$vendors = wp_remote_retrieve_body( $response );

		if ( $vendors === false ) {
			return;
		}

		/** @var string $vendors */
		$vendors_json = json_decode( $vendors, true );

		if ( $vendors_json === null ) {
			return;
		}

		Options::update( 'fastcmp-vendors', $vendors_json );
	}

	/**
	 * Get FastCmp uuid
	 *
	 * @since    2.2.0
	 * @return void
	 */
	private function get_fastcmp_uuid() {
		$fast_cmp_url = 'https://eu.fastcmp.com/wp/domain-uid?domain=';

		$domain = rawurlencode( Helpers::get_server_details()['host'] );
		$domain = str_replace( 'www.', '', $domain );

		$response = wp_remote_request(
			$fast_cmp_url . $domain,
			array(
				'method'    => 'GET',
				'timeout'   => 10,
  			'sslverify' => true,
			)
		);

		if ( is_wp_error( $response ) ) {
			return;
		}

		/** @var string|boolean $uuid */
		$uuid = wp_remote_retrieve_body( $response );

		if ( $uuid === false ) {
			return;
		}

		/** @var string $uuid */
		$uuid_json = json_decode( $uuid, true );

		/** @var null|array{domain?: string, domainUid?: string, error?: string} $uuid_json */
		if ( $uuid_json === null ) {
			return;
		}

		/** @var array{domain?: string, domainUid?: string, error?: string} $uuid_json */
		if ( isset( $uuid_json['error'] ) || ! isset( $uuid_json['domainUid'] ) ) {
			return;
		}

		Options::update( 'fastcmp-uuid', $uuid_json['domainUid'] );
	}

	/**
	 * Check optimization checklist and cache health issues.
	 *
	 * @since 4.1.0
	 * @return void
	 */
	public function check_optimization() {
		$health_check = new HealthCheck();
		$issues       = $health_check->get_issues();
		Options::update( 'health-check-cache', $issues );
	}

	/**
	 * Checks if a scheduled task needs to be executed based on transient expiration.
	 *
	 * This method uses WordPress transients to determine if a task should run.
	 * If the transient doesn't exist (expired or never set), it creates a new one
	 * with the specified duration and executes the callback function.
	 *
	 * @param string   $transient_name The name of the transient to check.
	 * @param callable $callback The function to execute if transient has expired.
	 * @param int      $duration Duration in seconds for the transient (default: 3600).
	 *
	 * @return bool True if the task was executed, false if it was skipped.
	 *
	 * @since 3.2.0
	 */
	public function check_scheduled_task_with_transient( $transient_name, callable $callback, $duration = 3600 ) {
		if ( false === get_transient( $transient_name ) ) {
				set_transient( $transient_name, true, $duration );
				call_user_func( $callback );

				return true;
		}

		return false;
	}
}
