<?php

namespace EasyCriticalCSS;

use DateTime;
use WP_Error;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class API_Request_Handler {
	public static function prepare_generate_request( $url_or_id ) {
		global $wpdb;

		$data = [];
		$url  = $url_or_id;

		// If we have a number, it's a post ID.
		if ( is_numeric( $url_or_id ) ) {
			$data['post_id'] = (int) $url;
			$url             = get_permalink( $data['post_id'] );

			if ( ! $url ) {
				return new WP_Error( 'invalid_post_id', __( 'Invalid post ID provided.', 'easy-critical-css' ) );
			}
		}

		// Is the URL a valid URL format?
		if ( filter_var( $url, FILTER_VALIDATE_URL ) === false ) {
			return new WP_Error( 'invalid_url', __( 'The provided URL is not valid.', 'easy-critical-css' ) );
		}

		// Clean the URL from unwanted params.
		$excluded_url_params = Helpers::get_excluded_url_params();
		$url                 = remove_query_arg( $excluded_url_params, $url );

		// Hashing the URL for quick lookup.
		$url_hash = md5( $url );

		// Short cooldown to prevent immediate re-claim if a generation just completed.
		$cooldown_seconds = 120; // 2 minutes
		$existing_row     = Database::get_row_by_url_hash( $url_hash );
		if ( $existing_row && ! empty( $existing_row['processing_status'] ) && $existing_row['processing_status'] === 'completed' ) {
			if ( ! empty( $existing_row['generated_time'] ) ) {
				$generated_ts = strtotime( $existing_row['generated_time'] );
				if ( $generated_ts !== false && ( time() - $generated_ts ) < $cooldown_seconds ) {
					$remaining = $cooldown_seconds - ( time() - $generated_ts );
					$minutes   = max( 1, ceil( $remaining / 60 ) );
					return new WP_Error(
						'cooldown_active',
						// translators: %d is the remaining waiting time.
						sprintf( __( 'Critical CSS was recently generated. Please try again in %d minute(s).', 'easy-critical-css' ), $minutes )
					);
				}
			}
		}

		// Generate unique handshake and timestamp early to avoid race conditions with atomic DB claim.
		$handshake       = wp_generate_password( 20, false );
		$mysql_timestamp = current_time( 'mysql' );

		// Prepare table and stale threshold.
		$table_name      = esc_sql( Database::get_table_name() );
		$stale_threshold = gmdate( 'Y-m-d H:i:s', time() - ( 30 * MINUTE_IN_SECONDS ) );

		// Atomically claim non-pending existing row by updating it.
		$update_sql = $wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"UPDATE {$table_name}
			 SET handshake = %s, page_url = %s, processing_status = 'pending', requested_time = %s
			 WHERE url_hash = %s AND (processing_status != 'pending' OR requested_time < %s)",
			$handshake,
			esc_url_raw( $url ),
			$mysql_timestamp,
			$url_hash,
			$stale_threshold
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Atomic claim-or-insert operation for race condition prevention requires direct database query without caching. Prepared above.
		$wpdb->query( $update_sql );
		if ( $wpdb->rows_affected <= 0 ) {
			// No existing row could be claimed. Check if non stale, pending row exists and return already_pending.
			$existing_row = Database::get_row_by_url_hash( $url_hash );
			if ( $existing_row && isset( $existing_row['processing_status'] ) && $existing_row['processing_status'] === 'pending' ) {
				$remaining = 0;
				if ( ! empty( $existing_row['requested_time'] ) ) {
					$remaining = Helpers::get_remaining_time( $existing_row['requested_time'] );
				}

				if ( $remaining > 0 ) {
					return new WP_Error(
						'already_pending',
						// translators: %d is the remaining waiting time.
						sprintf( __( 'Critical CSS is already being generated. Please try again in %d minutes.', 'easy-critical-css' ), $remaining )
					);
				}
			}

			// No non-stale pending row to block us, so insert a new row.
			$insert_result = $wpdb->insert( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Atomic claim-or-insert operation for race condition prevention requires direct database query.
				$table_name,
				[
					'page_url'          => esc_url_raw( $url ),
					'url_hash'          => $url_hash,
					'post_id'           => isset( $data['post_id'] ) ? (int) $data['post_id'] : null,
					'handshake'         => $handshake,
					'processing_status' => 'pending',
					'requested_time'    => $mysql_timestamp,
				]
			);

			if ( $insert_result === false ) {
				// If insert failed, play safe and indicate another process likely claimed it.
				return new WP_Error( 'already_pending', __( 'Critical CSS is already being generated.', 'easy-critical-css' ) );
			}
		}

		// Get API-related data for the outgoing request.
		$uid        = Helpers::get_uid();
		$api_key    = Helpers::get_api_key();
		$install_id = Helpers::get_install_id();

		// Construct the response URL dynamically and ensure SSL.
		$response_url = site_url( '/wp-json/easy-critical-css/v1/receive' );
		$scheme       = wp_parse_url( $response_url, PHP_URL_SCHEME );
		if ( $scheme !== 'https' ) {
			return new WP_Error( 'no_ssl', __( 'The response URL must have SSL.', 'easy-critical-css' ) );
		}

		// Convert timestamp to the URL format we need for the skip param.
		$url_timestamp = '';
		$datetime      = DateTime::createFromFormat( 'Y-m-d H:i:s', $mysql_timestamp );
		if ( $datetime instanceof DateTime ) {
			$url_timestamp = $datetime->format( 'YmdHis' );
		}

		$prepared_url = add_query_arg(
			[
				'critical-css'        => 'skip' . $url_timestamp,
				'nocache'             => '',
				'wpr_imagedimensions' => '1',
				'nowprocket'          => '1',
				'no_optimize'         => '1',
				'perfmattersoff'      => '1',
				'two_nooptimize'      => '1',
				'mvt_flags'           => 'disable_critical_css',
			],
			$url
		);

		return [
			'handshake'    => $handshake,
			'install_id'   => $install_id,
			'api_key'      => $api_key,
			'prepared_url' => $prepared_url,
			'response_url' => $response_url,
			'uid'          => $uid,
			'url_hash'     => $url_hash,
		];
	}
}
