<?php

namespace Fotogenic_AI;

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

/**
 * Reconcile one parent queue and admit at most one deterministic Bulk child.
 */
final class Store_Queue_Runner {
	private const MAX_RUN_ITEMS = Workflow_Operational_Limits::MAX_RUN_ITEMS;
	private const LEASE_TTL = 180;
	private static ?Store_Queue_Runner $instance = null;
	private Store_Queue_Repo $repo;
	private Bulk_Repo $bulk_repo;
	private Service_Bulk $bulk;
	private Store_Queue_Snapshot_Builder $builder;
	private Store_Queue_Scheduler $scheduler;
	private Workflow_Draft_Normalizer $normalizer;

	public function __construct(
		?Store_Queue_Repo $repo = null,
		?Bulk_Repo $bulk_repo = null,
		?Service_Bulk $bulk = null,
		?Store_Queue_Snapshot_Builder $builder = null,
		?Store_Queue_Scheduler $scheduler = null,
		?Workflow_Draft_Normalizer $normalizer = null
	) {
		$this->repo       = $repo ?? Store_Queue_Repo::instance();
		$this->bulk_repo  = $bulk_repo ?? Bulk_Repo::instance();
		$this->bulk       = $bulk ?? new Service_Bulk();
		$this->builder    = $builder ?? new Store_Queue_Snapshot_Builder();
		$this->scheduler  = $scheduler ?? Store_Queue_Scheduler::instance();
		$this->normalizer = $normalizer ?? new Workflow_Draft_Normalizer();
	}

	public static function instance(): Store_Queue_Runner {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	public function register(): void {
		add_action( Store_Queue_Scheduler::HOOK_TICK, [ $this, 'handle_tick' ], 10, 2 );
	}

	public function handle_tick( string $queue_uuid, int $user_id ): void {
		if ( ! Background_Runtime_State::is_enabled() || $user_id <= 0 || ! wp_is_uuid( $queue_uuid ) ) {
			return;
		}
		$user = get_user_by( 'id', $user_id );
		if ( ! $user instanceof \WP_User ) {
			return;
		}
		wp_set_current_user( $user_id );
		$queue = $this->repo->get_queue_by_uuid( $queue_uuid, $user_id );
		if ( ! is_array( $queue ) ) {
			return;
		}

		if ( 'preparing' === (string) $queue['status'] ) {
			$prepared = $this->builder->prepare_tick( $queue_uuid, $user_id );
			if ( is_wp_error( $prepared ) ) {
				return;
			}
			if ( 'preparing' === (string) ( $prepared['status'] ?? '' ) ) {
				$this->schedule_or_pause( $prepared, $user_id, 2 );
			} elseif ( 'reviewing' === (string) ( $prepared['status'] ?? '' ) ) {
				$review = ( new Store_Queue_Preflight() )->review( $prepared, $user_id );
				if ( is_wp_error( $review ) ) {
					$this->pause_queue( $prepared, $user_id, 'review_unavailable', 'The prepared Product queue could not refresh Review.' );
				}
			}
			return;
		}
		if ( ! in_array( (string) $queue['status'], [ 'running', 'paused', 'stopping' ], true ) ) {
			return;
		}

		$owner = 'store-queue-' . substr( wp_generate_uuid4(), 0, 24 );
		if ( ! $this->repo->acquire_lease( (int) $queue['id'], $user_id, $owner, self::LEASE_TTL ) ) {
			$this->scheduler->schedule_tick( $queue_uuid, $user_id, 5 );
			return;
		}
		try {
			$queue = $this->repo->get_queue_by_uuid( $queue_uuid, $user_id );
			if ( ! is_array( $queue ) ) {
				return;
			}
			$batch = $this->repo->get_current_batch( (int) $queue['id'] );
			if ( is_array( $batch ) ) {
				$state = $this->reconcile_batch( $queue, $batch, $user_id, $owner );
				if ( 'active' === $state ) {
					$this->schedule_or_pause( $queue, $user_id, 4 );
					return;
				}
				if ( 'paused' === $state || 'error' === $state ) {
					return;
				}
				$queue = $this->repo->get_queue_by_uuid( $queue_uuid, $user_id ) ?? $queue;
			}

			if ( 'stopping' === (string) $queue['status'] ) {
				$this->finalize_parent( $queue, $user_id, true );
				return;
			}
			if ( 'paused' === (string) $queue['status'] ) {
				return;
			}
			if ( $this->finalize_parent( $queue, $user_id, false ) ) {
				return;
			}
			$this->admit_next_child( $queue, $user_id, $owner );
		} catch ( \Throwable $error ) {
			$this->pause_queue( $queue, $user_id, 'worker_exception', 'Store Queue worker failed safely.' );
			Logger::instance()->warn( 'store_queue_worker_exception' );
		} finally {
			$this->repo->release_lease( (int) $queue['id'], $user_id, $owner );
		}
	}

	private function reconcile_batch( array $queue, array $batch, int $user_id, string $owner ): string {
		$bulk_run = $this->bulk_repo->get_run_by_uuid( (string) $batch['bulk_run_uuid'], $user_id );
		if ( ! is_array( $bulk_run ) ) {
			$reserved = $this->repo->page_items( (int) $queue['id'], $user_id, 1, self::MAX_RUN_ITEMS, 'reserved' );
			if ( empty( $reserved['items'] ) ) {
				$this->pause_queue( $queue, $user_id, 'child_mapping_incomplete', 'The child queue mapping requires recovery.' );
				return 'error';
			}
			return $this->admit_child( $queue, $batch, $reserved['items'], $user_id, $owner ) ? 'active' : 'paused';
		}

		$bulk_items = $this->bulk_repo->list_items_by_statuses(
			(int) $bulk_run['id'],
			$user_id,
			[ 'pending', 'starting', 'generating', 'completed', 'partial', 'failed', 'skipped', 'uncertain' ],
			self::MAX_RUN_ITEMS,
			'id',
			'ASC'
		);
		$this->repair_mapping( (int) $queue['id'], (int) $batch['id'], $user_id, $bulk_items );
		$this->repo->project_batch_items( (int) $queue['id'], (int) $batch['id'], $bulk_items );
		$run_status = sanitize_key( (string) ( $bulk_run['status'] ?? '' ) );
		if ( ! in_array( $run_status, [ 'completed', 'stopped', 'failed' ], true ) ) {
			$this->repo->update_batch_status( (int) $queue['id'], (int) $batch['id'], 'running' );
			return 'active';
		}

		$item_statuses = array_values( array_unique( array_map( static fn( array $item ): string => sanitize_key( (string) ( $item['status'] ?? '' ) ), $bulk_items ) ) );
		$batch_status = in_array( 'uncertain', $item_statuses, true )
			? 'uncertain'
			: ( array_intersect( $item_statuses, [ 'partial', 'failed', 'skipped' ] ) ? 'partial' : ( 'stopped' === $run_status ? 'stopped' : 'completed' ) );
		$this->repo->update_batch_status( (int) $queue['id'], (int) $batch['id'], $batch_status, '', [ 'finished_at' => current_time( 'mysql', true ) ] );
		return 'terminal';
	}

	private function admit_next_child( array $queue, int $user_id, string $owner ): void {
		$consent = $this->decode_snapshot( $queue['consent_snapshot'] ?? '' );
		$credits_per_item = (float) ( $consent['creditsPerItem'] ?? 0 );
		if ( $credits_per_item <= 0 ) {
			$this->pause_queue( $queue, $user_id, 'review_required', 'Queue pricing consent is unavailable.' );
			return;
		}
		$credits = Credits_Service::instance()->refresh( 0 );
		if ( is_wp_error( $credits ) || true !== ( $credits['fresh'] ?? false ) || ! is_numeric( $credits['balance'] ?? null ) ) {
			$this->pause_queue( $queue, $user_id, 'credits_unavailable', 'Current credits could not be confirmed.' );
			return;
		}
		$affordable = (int) floor( (float) $credits['balance'] / $credits_per_item );
		if ( $affordable < 1 ) {
			$this->pause_queue( $queue, $user_id, 'insufficient_credits', 'Add credits to continue this Product queue.' );
			return;
		}
		$items = $this->repo->claim_ready_items( (int) $queue['id'], $user_id, min( self::MAX_RUN_ITEMS, $affordable ), $owner );
		if ( empty( $items ) ) {
			$this->finalize_parent( $queue, $user_id, false );
			return;
		}

		$batch_ordinal = (int) $queue['next_batch_ordinal'];
		$child_uuid    = Workflow_Item_Identity::deterministic_uuid(
			'fotogenic-ai-store-queue-child',
			(string) $queue['queue_uuid'],
			(string) $queue['revision'],
			(string) $batch_ordinal
		);
		$batch_result = $this->repo->create_batch(
			(int) $queue['id'],
			$user_id,
			[
				'batch_ordinal'             => $batch_ordinal,
				'bulk_run_uuid'             => $child_uuid,
				'selected_count'            => count( $items ),
				'pricing_fingerprint'       => $consent['pricingFingerprint'] ?? '',
				'configuration_fingerprint' => $consent['configurationFingerprint'] ?? '',
			]
		);
		if ( true !== ( $batch_result['ok'] ?? false ) || ! is_array( $batch_result['batch'] ?? null ) ) {
			$this->repo->release_reserved_items( (int) $queue['id'], $owner );
			$this->pause_queue( $queue, $user_id, 'batch_persistence_failed', 'The next Product batch could not be reserved.' );
			return;
		}
		$this->admit_child( $queue, $batch_result['batch'], $items, $user_id, $owner );
	}

	private function admit_child( array $queue, array $batch, array $items, int $user_id, string $owner ): bool {
		$draft = $this->decode_snapshot( $queue['draft_snapshot'] ?? '' );
		$consent = $this->decode_snapshot( $queue['consent_snapshot'] ?? '' );
		if ( empty( $draft ) || empty( $consent ) ) {
			$this->pause_queue( $queue, $user_id, 'review_required', 'Queue consent requires Review.' );
			return false;
		}
		$subject_ids = [];
		$sources     = [];
		foreach ( $items as $item ) {
			$subject_id = (int) $item['subject_id'];
			$refreshed  = $this->builder->refresh_item( $draft, $subject_id );
			if ( is_wp_error( $refreshed ) || 'ready' !== (string) ( $refreshed['status'] ?? '' )
				|| ! hash_equals( (string) $item['source_fingerprint'], (string) ( $refreshed['source_fingerprint'] ?? '' ) ) ) {
				$this->repo->mark_item_attention( (int) $queue['id'], (int) $item['id'], 'source_changed' );
				$this->repo->release_reserved_items( (int) $queue['id'], $owner );
				$this->pause_queue( $queue, $user_id, 'review_required', 'A queued Product source changed before generation.' );
				return false;
			}
			$subject_ids[] = $subject_id;
			$sources[]     = $this->decode_snapshot( $refreshed['source_snapshot'] ?? [] );
		}
		$draft['selection'] = [ 'strategy' => 'explicit', 'selectedIds' => $subject_ids, 'excludedIds' => [], 'query' => [] ];
		$draft_object       = $this->normalizer->rehydrate_normalized( $draft );
		if ( is_wp_error( $draft_object ) ) {
			$this->pause_queue( $queue, $user_id, 'review_required', 'The queued Product draft requires Review.' );
			return false;
		}
		$preflight = ( new Workflow_Preflight() )->evaluate( $draft_object );
		if ( is_wp_error( $preflight ) ) {
			$this->repo->release_reserved_items( (int) $queue['id'], $owner );
			$reason = 'fotogenic_ai_insufficient_credits' === $preflight->get_error_code() ? 'insufficient_credits' : 'review_required';
			$this->pause_queue( $queue, $user_id, $reason, 'The next Product batch requires Review.' );
			return false;
		}
		foreach ( [ 'contextFingerprint', 'configurationFingerprint', 'pricingFingerprint' ] as $key ) {
			if ( ! hash_equals( (string) ( $consent[ $key ] ?? '' ), (string) ( $preflight['authorityEvidence'][ $key ] ?? '' ) ) ) {
				$this->repo->release_reserved_items( (int) $queue['id'], $owner );
				$this->pause_queue( $queue, $user_id, 'review_required', 'Pricing, configuration, or account context changed.' );
				return false;
			}
		}
		$snapshot_fingerprint = $this->bulk->build_exact_snapshot_fingerprint( $draft, $sources, $this->normalizer );
		if ( is_wp_error( $snapshot_fingerprint ) ) {
			$this->pause_queue( $queue, $user_id, 'child_snapshot_failed', 'The next Product batch could not be frozen.' );
			return false;
		}
		$started = $this->bulk->start_exact_queue_child( (string) $batch['bulk_run_uuid'], $draft_object, $snapshot_fingerprint, $preflight['authority'] ?? null );
		if ( is_wp_error( $started ) ) {
			$this->pause_queue( $queue, $user_id, 'child_admission_failed', 'The next Product batch was not admitted.' );
			return false;
		}

		$bulk_run = $this->bulk_repo->get_run_by_uuid( (string) $batch['bulk_run_uuid'], $user_id );
		$bulk_items = is_array( $bulk_run )
			? $this->bulk_repo->list_items_by_statuses( (int) $bulk_run['id'], $user_id, [ 'pending', 'starting', 'generating', 'completed', 'partial', 'failed', 'skipped', 'uncertain' ], self::MAX_RUN_ITEMS, 'id', 'ASC' )
			: [];
		if ( ! is_array( $bulk_run ) || ! $this->repair_mapping( (int) $queue['id'], (int) $batch['id'], $user_id, $bulk_items ) ) {
			$this->pause_queue( $queue, $user_id, 'child_mapping_incomplete', 'The admitted child requires mapping recovery.' );
			return false;
		}
		$this->repo->update_batch_status( (int) $queue['id'], (int) $batch['id'], 'running', '', [ 'started_at' => current_time( 'mysql', true ) ] );
		$this->repo->update_queue_cas(
			(int) $queue['id'],
			$user_id,
			(int) $queue['revision'],
			[ 'status' => 'running', 'status_reason' => '', 'next_batch_ordinal' => (int) $batch['batch_ordinal'] + 1, 'last_heartbeat_at' => current_time( 'mysql', true ) ]
		);
		$this->schedule_or_pause( $queue, $user_id, 4 );
		return true;
	}

	private function repair_mapping( int $queue_id, int $batch_id, int $user_id, array $bulk_items ): bool {
		if ( empty( $bulk_items ) ) {
			return false;
		}
		$reserved = $this->repo->page_items( $queue_id, $user_id, 1, self::MAX_RUN_ITEMS, 'reserved' );
		if ( empty( $reserved['items'] ) ) {
			return true;
		}
		$by_subject = [];
		foreach ( $bulk_items as $bulk_item ) {
			$by_subject[ (int) $bulk_item['subject_id'] ] = (int) $bulk_item['id'];
		}
		$mappings = [];
		foreach ( $reserved['items'] as $item ) {
			$bulk_item_id = $by_subject[ (int) $item['subject_id'] ] ?? 0;
			if ( $bulk_item_id <= 0 ) {
				return false;
			}
			$mappings[] = [ 'queueItemId' => (int) $item['id'], 'bulkItemId' => $bulk_item_id ];
		}
		return $this->repo->map_batch_items( $queue_id, $batch_id, $mappings );
	}

	private function finalize_parent( array $queue, int $user_id, bool $stopping ): bool {
		$counts = $this->repo->aggregate_counts( (int) $queue['id'], $user_id );
		$active = (int) $counts['ready'] + (int) $counts['reserved'] + (int) $counts['admitted'];
		if ( $active > 0 ) {
			return false;
		}
		$status = $stopping
			? 'stopped'
			: ( (int) $counts['uncertain'] > 0 ? 'uncertain' : ( (int) $counts['failed'] + (int) $counts['partial'] + (int) $counts['skipped'] + (int) $counts['ineligible'] + (int) $counts['review_required'] > 0 ? 'partial' : 'completed' ) );
		$this->repo->update_queue_cas(
			(int) $queue['id'],
			$user_id,
			(int) $queue['revision'],
			[
				'status'            => $status,
				'status_reason'     => '',
				'completed_count'   => (int) $counts['completed'],
				'partial_count'     => (int) $counts['partial'],
				'failed_count'      => (int) $counts['failed'],
				'skipped_count'     => (int) $counts['skipped'],
				'attention_count'   => (int) $counts['review_required'] + (int) $counts['uncertain'],
				'finished_at'       => current_time( 'mysql', true ),
			]
		);
		$this->scheduler->unschedule_tick( (string) $queue['queue_uuid'], $user_id );
		return true;
	}

	private function schedule_or_pause( array $queue, int $user_id, int $delay ): void {
		if ( ! $this->scheduler->schedule_tick( (string) $queue['queue_uuid'], $user_id, $delay ) ) {
			$this->pause_queue( $queue, $user_id, 'scheduler_unavailable', 'Store Queue could not schedule its next recovery tick.' );
		}
	}

	private function pause_queue( array $queue, int $user_id, string $reason, string $message ): void {
		$this->repo->update_queue_cas(
			(int) $queue['id'],
			$user_id,
			(int) $queue['revision'],
			[ 'status' => 'paused', 'status_reason' => $reason, 'last_error' => $message ]
		);
	}

	/** @return array<string, mixed> */
	private function decode_snapshot( $value ): array {
		if ( is_array( $value ) ) {
			return $value;
		}
		$decoded = is_string( $value ) ? json_decode( $value, true ) : null;
		return is_array( $decoded ) ? $decoded : [];
	}
}
