<?php
/**
 * Costudio_Production_Queue — local single-flight queue.
 *
 * Keeps one active Contentosapp Studio runner per WordPress installation. Additional
 * productions remain in the existing `pending` state and are dispatched when
 * the active runner releases the local slot.
 *
 * @package Contentosapp_Studio
 */

defined( 'ABSPATH' ) || exit;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery,PluginCheck.Security.DirectDB.UnescapedDBParameter -- Queue queries on plugin-owned tables; $table = $wpdb->prefix . constant; user-supplied values use prepare() placeholders.

final class Costudio_Production_Queue {

	const LOCK_OPTION    = 'costudio_queue_dispatch_lock';
	const LOCK_STALE_SEC = 300;
	const ACTIVE_RUNNER_LOCK_STALE_SEC = 900;
	const WATCHDOG_HOOK  = 'costudio_queue_dispatch_watchdog';
	const WATCHDOG_SCHEDULE = 'costudio_queue_every_five_minutes';

	private function __construct() {
	}

	public static function register_hooks(): void {
		if ( ! self::production_table_exists() ) {
			wp_clear_scheduled_hook( self::WATCHDOG_HOOK );
			return;
		}

		add_filter( 'cron_schedules', array( __CLASS__, 'add_cron_schedules' ) );
		add_action( self::WATCHDOG_HOOK, array( __CLASS__, 'run_watchdog' ) );
		add_action( 'costudio_production_completed', array( __CLASS__, 'maybe_dispatch_next_from_hook' ), 20 );
		add_action( 'costudio_production_failed', array( __CLASS__, 'maybe_dispatch_next_from_hook' ), 20 );

		if ( ! wp_next_scheduled( self::WATCHDOG_HOOK ) ) {
			wp_schedule_event( time() + MINUTE_IN_SECONDS, self::WATCHDOG_SCHEDULE, self::WATCHDOG_HOOK );
		}
	}

	/**
	 * @param array<string,array<string,mixed>> $schedules
	 * @return array<string,array<string,mixed>>
	 */
	public static function add_cron_schedules( array $schedules ): array {
		$schedules[ self::WATCHDOG_SCHEDULE ] = array(
			'interval' => 5 * MINUTE_IN_SECONDS,
			'display'  => __( 'Every 5 minutes (Contentosapp Studio queue)', 'contentosapp-studio' ),
		);

		return $schedules;
	}

	public static function run_watchdog(): void {
		if ( ! self::production_table_exists() ) {
			wp_clear_scheduled_hook( self::WATCHDOG_HOOK );
			return;
		}

		self::maybe_dispatch_next( 'queue_watchdog' );
	}

	public static function maybe_dispatch_next_from_hook(): void {
		self::maybe_dispatch_next( 'production_terminal_hook' );
	}

	/**
	 * Submit a newly-created production to the local queue.
	 *
	 * @return array<string,mixed>
	 */
	public static function submit( int $production_id ): array {
		if ( class_exists( 'Costudio_Runner' ) ) {
			Costudio_Runner::pipeline_log_append(
				$production_id,
				__( 'Pedido recebido. Produção adicionada à fila local.', 'contentosapp-studio' ),
				'info'
			);
		}

		$result     = self::maybe_dispatch_next( 'submit_' . $production_id );
		$production = Costudio_Production::get_by_id( $production_id );
		$status     = $production ? (string) $production->status : 'unknown';
		$queued     = 'pending' === $status;

		if ( $queued && class_exists( 'Costudio_Runner' ) ) {
			Costudio_Runner::pipeline_log_append(
				$production_id,
				sprintf(
					/* translators: %d: production position in the local queue. */
					__( 'Outra produção está em execução. Esta produção aguardará na fila local (posição %d).', 'contentosapp-studio' ),
					self::queue_position( $production_id )
				),
				'info'
			);
		}

		return array(
			'queued'        => $queued,
			'status'        => $status,
			'position'      => self::queue_position( $production_id ),
			'dispatch'      => $result,
		);
	}

	/**
	 * Queue an existing production that should resume/retry later.
	 *
	 * @return array<string,mixed>|WP_Error
	 */
	public static function queue_existing( int $production_id, string $source = 'manual' ) {
		$production = Costudio_Production::get_by_id( $production_id );
		if ( ! $production ) {
			return new WP_Error( 'not_found', __( 'Production not found.', 'contentosapp-studio' ) );
		}

		$current_agent = max( 1, (int) ( $production->current_agent ?? 1 ) );
		Costudio_Production::mark_pending( $production_id, $current_agent );

		if ( class_exists( 'Costudio_Runner' ) ) {
			Costudio_Runner::pipeline_log_append(
				$production_id,
				sprintf(
					/* translators: %02d: two-digit agent number. */
					__( 'Produção aguardando slot local para retomar no Agente %02d.', 'contentosapp-studio' ),
					$current_agent
				),
				'info',
				$current_agent
			);
		}

		return self::submit( $production_id );
	}

	/**
	 * Dispatch the oldest pending production when the local slot is free.
	 *
	 * @return array<string,mixed>
	 */
	public static function maybe_dispatch_next( string $source = 'unknown' ): array {
		if ( ! self::production_table_exists() ) {
			return array( 'dispatched' => false, 'reason' => 'production_table_missing' );
		}

		if ( ! self::acquire_lock() ) {
			return array( 'dispatched' => false, 'reason' => 'queue_locked' );
		}

		try {
			if ( self::has_active_runner_lock() ) {
				return array( 'dispatched' => false, 'reason' => 'runner_lock_active' );
			}

			if ( self::has_running_production() ) {
				return array( 'dispatched' => false, 'reason' => 'production_running' );
			}

			$next = self::next_pending_production();
			if ( ! $next ) {
				return array( 'dispatched' => false, 'reason' => 'no_pending' );
			}

			self::dispatch( $next, $source );

			return array(
				'dispatched'    => true,
				'production_id' => (int) $next->id,
				'source'        => $source,
			);
		} finally {
			self::release_lock();
		}
	}

	public static function queue_position( int $production_id ): int {
		global $wpdb;

		if ( ! self::production_table_exists() ) {
			return 0;
		}

		$table = $wpdb->prefix . Costudio_Production::TABLE;
		$row   = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, created_at FROM {$table} WHERE id = %d AND status = 'pending'",
				$production_id
			)
		);

		if ( ! $row ) {
			return 0;
		}

		return 1 + (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$table}
				 WHERE status = 'pending'
				   AND (created_at < %s OR (created_at = %s AND id < %d))",
				$row->created_at,
				$row->created_at,
				$production_id
			)
		);
	}

	private static function dispatch( Costudio_Production $production, string $source ): void {
		$production_id = (int) $production->id;
		$current_agent = max( 1, (int) ( $production->current_agent ?? 1 ) );

		Costudio_Production::mark_pipeline_started( $production_id, $current_agent );

		if ( class_exists( 'Costudio_Runner' ) ) {
			Costudio_Runner::pipeline_log_append(
				$production_id,
				sprintf(
					/* translators: %02d: two-digit agent number. */
					__( 'Slot local liberado. A iniciar processamento no Agente %02d.', 'contentosapp-studio' ),
					$current_agent
				),
				'info',
				$current_agent
			);

			$secret_token = wp_generate_password( 32, false );
			Costudio_Runner::prime_background_token( $production_id, $secret_token, 'queue_' . $source );
			Costudio_Runner::kick_off_background( $production_id, $secret_token );
		}
	}

	private static function next_pending_production(): ?Costudio_Production {
		global $wpdb;

		if ( ! self::production_table_exists() ) {
			return null;
		}

		$table = $wpdb->prefix . Costudio_Production::TABLE;
		$id    = (int) $wpdb->get_var(
			"SELECT id FROM {$table}
			 WHERE status = 'pending'
			 ORDER BY created_at ASC, id ASC
			 LIMIT 1"
		);

		return $id > 0 ? Costudio_Production::get_by_id( $id ) : null;
	}

	private static function has_running_production(): bool {
		global $wpdb;

		if ( ! self::production_table_exists() ) {
			return false;
		}

		$table = $wpdb->prefix . Costudio_Production::TABLE;
		return (bool) $wpdb->get_var( "SELECT id FROM {$table} WHERE status = 'running' LIMIT 1" );
	}

	private static function production_table_exists(): bool {
		global $wpdb;

		$table = $wpdb->prefix . Costudio_Production::TABLE;
		return $table === $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) );
	}

	private static function has_active_runner_lock(): bool {
		global $wpdb;

		$prefix = 'costudio_runner_lock_';
		$rows   = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s LIMIT 50",
				$wpdb->esc_like( $prefix ) . '%'
			),
			ARRAY_A
		);

		$now = time();
		foreach ( (array) $rows as $row ) {
			$value = (string) ( $row['option_value'] ?? '' );
			$parts = explode( '|', $value, 2 );
			$ts    = isset( $parts[1] ) ? (int) $parts[1] : 0;

			if ( $ts > 0 && ( $now - $ts ) < self::ACTIVE_RUNNER_LOCK_STALE_SEC ) {
				return true;
			}

			if ( ! empty( $row['option_name'] ) ) {
				$wpdb->query(
					$wpdb->prepare(
						"DELETE FROM {$wpdb->options} WHERE option_name = %s AND option_value = %s",
						(string) $row['option_name'],
						$value
					)
				);
				wp_cache_delete( (string) $row['option_name'], 'options' );
			}
		}

		return false;
	}

	private static function acquire_lock(): bool {
		global $wpdb;

		$value = wp_generate_password( 12, false ) . '|' . time();
		$ok    = $wpdb->query(
			$wpdb->prepare(
				"INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')",
				self::LOCK_OPTION,
				$value
			)
		);

		if ( 1 === (int) $ok ) {
			wp_cache_delete( self::LOCK_OPTION, 'options' );
			return true;
		}

		$existing = (string) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT option_value FROM {$wpdb->options} WHERE option_name = %s",
				self::LOCK_OPTION
			)
		);
		$parts = explode( '|', $existing, 2 );
		$ts    = isset( $parts[1] ) ? (int) $parts[1] : 0;

		if ( $ts > 0 && ( time() - $ts ) <= self::LOCK_STALE_SEC ) {
			return false;
		}

		$updated = $wpdb->query(
			$wpdb->prepare(
				"UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND option_value = %s",
				$value,
				self::LOCK_OPTION,
				$existing
			)
		);
		wp_cache_delete( self::LOCK_OPTION, 'options' );

		return 1 === (int) $updated;
	}

	private static function release_lock(): void {
		delete_option( self::LOCK_OPTION );
	}
}
