<?php
/** Worker for durable final-publication passes. */

defined( 'ABSPATH' ) || exit;

class Costudio_Publication_Worker {

	/** @var array<string,mixed>|null */
	private static $active_job = null;
	/** @var bool */
	private static $shutdown_registered = false;

	/**
	 * Process one publication pass. The publisher already checkpoints images and
	 * post creation, so a killed process can safely resume after lease expiry.
	 *
	 * @return array{claimed:bool,status:string,production_id:int}
	 */
	public static function run_one( string $job_id = '', string $driver = 'web_fallback', string $worker_id = '' ): array {
		$worker_id = '' !== $worker_id ? $worker_id : self::new_worker_id( $driver );
		$lease = 'external_cli' === $driver ? Costudio_Publication_Jobs::CLI_LEASE_SECONDS : Costudio_Publication_Jobs::WEB_LEASE_SECONDS;
		$job = Costudio_Publication_Jobs::claim_next( $worker_id, sanitize_key( $driver ), $lease, $job_id );
		if ( ! $job ) {
			return array( 'claimed' => false, 'status' => 'idle', 'production_id' => 0 );
		}
		$production_id = (int) $job['production_id'];
		self::$active_job = $job;
		self::register_shutdown();
		$production = Costudio_Production::get_by_id( $production_id );
		if ( ! $production ) {
			Costudio_Publication_Jobs::fail( $production_id, 'production_missing', 'Production record is unavailable.', $driver );
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'failed', 'production_id' => $production_id );
		}
		if ( 'paused' === (string) $production->status ) {
			Costudio_Publication_Jobs::requeue( $production_id, 'production_paused', 30 );
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'paused', 'production_id' => $production_id );
		}
		if ( in_array( (string) $production->status, array( 'cancelled', 'deleted' ), true ) ) {
			Costudio_Publication_Jobs::cancel_for_production( $production_id, 'worker_preflight' );
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'cancelled', 'production_id' => $production_id );
		}
		Costudio_Production::update_heartbeat( $production_id );

		try {
			Costudio_Publication_Jobs::stage( $production_id, 'preparing_media', 'publication pass started' );
			$pending = Costudio_Pipeline::execute_publication_job( $production_id );
			if ( $pending ) {
				Costudio_Publication_Jobs::requeue( $production_id, 'chunk_pending', 1 );
				$fresh = Costudio_Publication_Jobs::find( $production_id );
				if ( $fresh ) {
					Costudio_Publication_Jobs::kick_web_fallback( $fresh, 'chunk_pending' );
				}
				self::$active_job = null;
				return array( 'claimed' => true, 'status' => 'requeued', 'production_id' => $production_id );
			}
			Costudio_Publication_Jobs::complete( $production_id, $driver );
			if ( class_exists( 'Costudio_Telemetry' ) ) {
				Costudio_Telemetry::queue( 'publication.job_completed', array(
					'production_id' => $production_id,
					'driver' => sanitize_key( $driver ),
					'claim_count' => (int) ( $job['claim_count'] ?? 0 ),
				) );
			}
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'completed', 'production_id' => $production_id );
		} catch ( Costudio_Publication_Busy_Exception $error ) {
			Costudio_Publication_Jobs::requeue( $production_id, 'ledger_busy', 15 );
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'requeued', 'production_id' => $production_id );
		} catch ( Throwable $error ) {
			$fresh_job = Costudio_Publication_Jobs::find( $production_id );
			if ( $fresh_job && 'cancelled' === (string) ( $fresh_job['job_status'] ?? '' ) ) {
				Costudio_Publication_Jobs::log_event( 'info', 'publication_jobs.late_worker_stopped', $fresh_job, array(
					'reason' => 'cancelled_terminal_won',
				) );
				self::$active_job = null;
				return array( 'claimed' => true, 'status' => 'cancelled', 'production_id' => $production_id );
			}
			$public = __( 'WordPress could not finish the publication. The generated agent content was preserved and the publication can be retried safely.', 'contentosapp-studio' );
			Costudio_Publication_Jobs::fail( $production_id, 'publication_exception', $error->getMessage(), $driver );
			Costudio_Production::set_error( $production_id, $public, 8, 'publishing' );
			Costudio_Runner::pipeline_log_append( $production_id, $public, 'err' );
			self::$active_job = null;
			return array( 'claimed' => true, 'status' => 'failed', 'production_id' => $production_id );
		}
	}

	/** Process a single queued job for the optional long-lived CLI worker. */
	public static function run_next_cli( string $worker_id ): bool {
		$result = self::run_one( '', 'external_cli', $worker_id );
		return ! empty( $result['claimed'] );
	}

	private static function register_shutdown(): void {
		if ( self::$shutdown_registered ) {
			return;
		}
		self::$shutdown_registered = true;
		register_shutdown_function( array( __CLASS__, 'shutdown' ) );
	}

	/** Log fatal shutdowns but leave the lease for safe automatic recovery. */
	public static function shutdown(): void {
		if ( ! is_array( self::$active_job ) ) {
			return;
		}
		$error = error_get_last();
		if ( ! is_array( $error ) || ! in_array( (int) $error['type'], array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR ), true ) ) {
			return;
		}
		Costudio_Publication_Jobs::log_event( 'error', 'publication_jobs.worker_shutdown', self::$active_job, array(
			'error_type' => (int) $error['type'],
			'error_file' => basename( (string) $error['file'] ),
			'error_line' => (int) $error['line'],
			// Never log the PHP message: third-party fatals may contain secrets.
		) );
	}

	private static function new_worker_id( string $driver ): string {
		$host = function_exists( 'gethostname' ) ? (string) gethostname() : 'host';
		$pid  = function_exists( 'getmypid' ) ? (int) getmypid() : 0;
		return sanitize_text_field( $driver . ':' . $host . ':' . $pid . ':' . wp_generate_password( 8, false, false ) );
	}
}
