<?php

namespace Fotogenic_AI;

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

/**
 * Non-billable, dispatch-free validation and authoritative cost preflight.
 */
final class Workflow_Preflight {
	private Workflow_Draft_Normalizer $normalizer;
	private Workflow_Source_Preflight $source_preflight;
	private Workflow_Configuration_Authority $configuration_authority;
	private Workflow_Accepted_Authority $accepted_authority;
	private \Closure $identity_provider;
	private \Closure $configuration_provider;
	private \Closure $pricing_provider;
	private \Closure $credits_provider;

	public function __construct(
		?Workflow_Draft_Normalizer $normalizer = null,
		?Workflow_Source_Preflight $source_preflight = null,
		?callable $identity_provider = null,
		?callable $pricing_provider = null,
		?callable $credits_provider = null,
		?Workflow_Configuration_Authority $configuration_authority = null,
		?callable $configuration_provider = null,
		?Workflow_Accepted_Authority $accepted_authority = null
	) {
		$this->normalizer               = $normalizer ?? new Workflow_Draft_Normalizer();
		$this->source_preflight         = $source_preflight ?? new Workflow_Source_Preflight();
		$this->configuration_authority = $configuration_authority ?? new Workflow_Configuration_Authority();
		$this->accepted_authority       = $accepted_authority ?? new Workflow_Accepted_Authority();
		$this->identity_provider = \Closure::fromCallable(
			$identity_provider ?? static fn(): bool => Auth_Session::instance()->is_connected()
		);
		$this->configuration_provider = \Closure::fromCallable(
			$configuration_provider ?? static fn() => ( new Service_Configuration() )->get_authoritative_configuration()
		);
		$this->pricing_provider = \Closure::fromCallable(
			$pricing_provider ?? [ $this, 'get_authoritative_pricing' ]
		);
		$this->credits_provider = \Closure::fromCallable(
			$credits_provider ?? static fn() => Credits_Service::instance()->refresh( 0 )
		);
	}

	/**
	 * Validate and price one decoded universal draft without admitting a start.
	 *
	 * @param mixed $raw_draft Raw decoded JSON object.
	 * @param mixed $accepted_authority Accepted authority proof.
	 * @return array|\WP_Error
	 */
	public function evaluate( $raw_draft, $accepted_authority = null ) {
		$draft = $this->normalizer->normalize( $raw_draft );
		if ( is_wp_error( $draft ) ) {
			return $draft;
		}

		if ( 'query' === ( $draft['selection']['strategy'] ?? '' ) ) {
			return new \WP_Error(
				'fotogenic_ai_bulk_snapshot_required',
				__( 'Query-backed Bulk selection requires an immutable server snapshot before Review.', 'fotogenic-ai' ),
				[
					'status' => 409,
					'state'  => 'requires_snapshot',
				]
			);
		}

		$identity = ( $this->identity_provider )();
		if ( is_wp_error( $identity ) ) {
			return $identity;
		}
		if ( true !== $identity ) {
			return new \WP_Error(
				'fotogenic_ai_missing_auth_session',
				__( 'Connect Fotogenic AI before reviewing this workflow.', 'fotogenic-ai' ),
				[ 'status' => 401 ]
			);
		}

		$sources = $this->source_preflight->validate( $draft );
		if ( is_wp_error( $sources ) ) {
			return $sources;
		}
		$total_items = (int) ( $sources['totalItems'] ?? 0 );
		$source_fingerprint = (string) ( $sources['sourceFingerprint'] ?? '' );
		if ( $total_items < 1 || 1 !== preg_match( '/^[a-f0-9]{64}$/', $source_fingerprint ) ) {
			return $this->invalid_source_state();
		}

		$workflow      = (string) $draft['workflow'];
		$configuration = null;
		if ( in_array( $workflow, [ 'product_images', 'campaign_set', 'increase_resolution' ], true ) ) {
			$configuration = ( $this->configuration_provider )();
			if ( is_wp_error( $configuration ) ) {
				return $configuration;
			}
			$configuration_valid = $this->configuration_authority->validate( $draft, $configuration );
			if ( is_wp_error( $configuration_valid ) ) {
				return $configuration_valid;
			}
		}

		$pricing  = ( $this->pricing_provider )( $workflow, (array) $draft['recipe'], $configuration );
		if ( is_wp_error( $pricing ) ) {
			return $pricing;
		}
		if ( ! is_array( $pricing ) || true !== ( $pricing['fresh'] ?? false ) ) {
			return $this->pricing_unavailable();
		}

		$output_counts = $this->get_output_counts( $draft, $pricing );
		if ( is_wp_error( $output_counts ) ) {
			return $output_counts;
		}
		$total_outputs = array_sum( $output_counts );
		if ( count( $output_counts ) !== $total_items ) {
			return $this->pricing_unavailable();
		}
		if ( 'campaign_set' === $workflow && $total_outputs > Workflow_Operational_Limits::MAX_RUN_OUTPUTS ) {
			return new \WP_Error(
				'fotogenic_ai_campaign_output_limit',
				sprintf(
					/* translators: %d: Maximum outputs allowed in one Campaign Set Bulk review. */
					__( 'Campaign Set supports no more than %d outputs in one Bulk review.', 'fotogenic-ai' ),
					Workflow_Operational_Limits::MAX_RUN_OUTPUTS
				),
				[
					'status'  => 400,
					'maximum' => Workflow_Operational_Limits::MAX_RUN_OUTPUTS,
				]
			);
		}

		$credits_per_output = $this->get_credits_per_output( $workflow, $pricing );
		if ( is_wp_error( $credits_per_output ) ) {
			return $credits_per_output;
		}
		$total_credits = $this->credit_amount( $credits_per_output * $total_outputs );

		$credits = ( $this->credits_provider )();
		if ( is_wp_error( $credits ) ) {
			return $this->credits_unavailable();
		}
		if (
			! is_array( $credits )
			|| true !== ( $credits['fresh'] ?? false )
			|| ! array_key_exists( 'balance', $credits )
			|| ! is_numeric( $credits['balance'] )
			|| ! is_finite( (float) $credits['balance'] )
			|| (float) $credits['balance'] < 0
		) {
			return $this->credits_unavailable();
		}

		$balance = $this->credit_amount( (float) $credits['balance'] );
		$fingerprint = $this->normalizer->fingerprint( $draft );
		if ( is_wp_error( $fingerprint ) ) {
			return $fingerprint;
		}
		$pricing_evidence = [
			'workflow'         => $workflow,
			'creditsPerOutput' => $this->credit_amount( $credits_per_output ),
			'quality'          => 'product_images' === $workflow ? (string) ( $draft['recipe']['quality'] ?? '' ) : '',
		];
		$credits_evidence = $this->get_credits_evidence( $credits, $balance );
		$context_fingerprint       = $this->accepted_authority->context_fingerprint();
		$configuration_fingerprint = $this->accepted_authority->evidence_fingerprint( $configuration );
		$pricing_fingerprint       = $this->accepted_authority->evidence_fingerprint( $pricing_evidence );
		$credits_fingerprint       = $this->accepted_authority->evidence_fingerprint( $credits_evidence );
		foreach ( [ $context_fingerprint, $configuration_fingerprint, $pricing_fingerprint, $credits_fingerprint ] as $evidence_fingerprint ) {
			if ( is_wp_error( $evidence_fingerprint ) ) {
				return $evidence_fingerprint;
			}
		}
		$balance_after = $this->credit_amount( $balance - $total_credits );
		$authority_evidence = [
			'draftFingerprint'         => $fingerprint,
			'sourceFingerprint'        => $source_fingerprint,
			'contextFingerprint'       => $context_fingerprint,
			'configurationFingerprint' => $configuration_fingerprint,
			'pricingFingerprint'       => $pricing_fingerprint,
			'creditsFingerprint'       => $credits_fingerprint,
			'totalItems'               => $total_items,
			'totalOutputs'             => $total_outputs,
			'totalCredits'             => $total_credits,
			'balance'                  => $balance,
			'balanceAfter'             => $balance_after,
		];
		$authority = null === $accepted_authority
			? $this->accepted_authority->issue( $authority_evidence )
			: $this->accepted_authority->verify( $accepted_authority, $authority_evidence );
		if ( is_wp_error( $authority ) ) {
			return $authority;
		}

		return [
			'draftFingerprint' => $fingerprint,
			'sourceFingerprint'=> $source_fingerprint,
			'totalItems'       => $total_items,
			'totalOutputs'     => $total_outputs,
			'totalCredits'     => $total_credits,
			'balance'          => $balance,
			'balanceAfter'     => $balance_after,
			'pricing'          => $pricing_evidence,
			'authorityEvidence'=> [
				'contextFingerprint'       => $context_fingerprint,
				'configurationFingerprint' => $configuration_fingerprint,
				'pricingFingerprint'       => $pricing_fingerprint,
				'creditsFingerprint'       => $credits_fingerprint,
			],
			'authority'        => $authority,
		];
	}

	/**
	 * @return array|\WP_Error
	 */
	public function get_authoritative_pricing( string $workflow, array $recipe = [], ?array $configuration = null ) {
		if ( 'product_images' === $workflow ) {
			return Price_Service::instance()->get_generate_images_pricing( (string) ( $recipe['quality'] ?? '' ) );
		}
		if ( 'campaign_set' === $workflow ) {
			return Price_Service::instance()->get_campaign_set_pricing( true, $configuration );
		}
		if ( 'increase_resolution' === $workflow ) {
			return Price_Service::instance()->get_enhance_pricing( true );
		}

		return $this->pricing_unavailable();
	}

	/**
	 * Validate the server-only pricing handoff created by a verified preflight.
	 *
	 * @return array{creditsPerOutput:float,outputsPerItem:int,quality:string}|\WP_Error
	 */
	public static function normalize_trusted_pricing( array $pricing, string $workflow, int $outputs_per_item, string $quality = '' ) {
		if (
			[ 'workflow', 'creditsPerOutput', 'quality' ] !== array_keys( $pricing )
			|| $workflow !== (string) ( $pricing['workflow'] ?? '' )
			|| ! is_numeric( $pricing['creditsPerOutput'] ?? null )
			|| ! is_finite( (float) $pricing['creditsPerOutput'] )
			|| (float) $pricing['creditsPerOutput'] < 0
			|| $outputs_per_item < 1
			|| $quality !== (string) ( $pricing['quality'] ?? '' )
		) {
			return new \WP_Error(
				'fotogenic_ai_verified_pricing_invalid',
				__( 'Verified workflow pricing could not be handed off safely.', 'fotogenic-ai' ),
				[ 'status' => 500 ]
			);
		}

		return [
			'creditsPerOutput' => (float) $pricing['creditsPerOutput'],
			'outputsPerItem'   => $outputs_per_item,
			'quality'          => $quality,
		];
	}

	/**
	 * Resolve every immutable run item's output count before accepting Review.
	 *
	 * Campaign groups own their own recipe/output setting. Other workflows keep
	 * one canonical recipe count applied to each exact item.
	 *
	 * @return int[]|\WP_Error
	 */
	private function get_output_counts( array $draft, array $pricing ) {
		$workflow = (string) ( $draft['workflow'] ?? '' );
		if ( 'campaign_set' === $workflow ) {
			$groups = $draft['sourcePolicy']['groups'] ?? null;
			if ( ! is_array( $groups ) || empty( $groups ) ) {
				return $this->pricing_unavailable();
			}
			$output_counts = [];
			foreach ( $groups as $group ) {
				$recipe = is_array( $group['recipe'] ?? null ) ? $group['recipe'] : [];
				$count = $this->get_outputs_per_item( $workflow, $recipe, $pricing );
				if ( is_wp_error( $count ) ) {
					return $count;
				}
				$output_counts[] = $count;
			}

			return $output_counts;
		}

		$count = $this->get_outputs_per_item( $workflow, (array) ( $draft['recipe'] ?? [] ), $pricing );
		if ( is_wp_error( $count ) ) {
			return $count;
		}
		$total_items = count( (array) ( $draft['selection']['selectedIds'] ?? [] ) );

		return array_fill( 0, $total_items, $count );
	}

	/**
	 * @param array $recipe  Normalized recipe.
	 * @param array $pricing Authoritative pricing record.
	 * @return int|\WP_Error
	 */
	private function get_outputs_per_item( string $workflow, array $recipe, array $pricing ) {
		$count = 'increase_resolution' === $workflow ? 1 : ( $recipe['count'] ?? null );
		if ( ! is_int( $count ) || $count < 1 ) {
			return $this->pricing_unavailable();
		}
		if ( 'campaign_set' === $workflow ) {
			$minimum = $pricing['min_count'] ?? null;
			$maximum = $pricing['max_count'] ?? null;
			if ( ! is_int( $minimum ) || ! is_int( $maximum ) || $count < $minimum || $count > $maximum ) {
				return new \WP_Error(
					'fotogenic_ai_invalid_campaign_count',
					__( 'The Campaign Set count is not available for the current account.', 'fotogenic-ai' ),
					[ 'status' => 400 ]
				);
			}
		}

		return $count;
	}

	/**
	 * @return float|\WP_Error
	 */
	private function get_credits_per_output( string $workflow, array $pricing ) {
		$key   = 'increase_resolution' === $workflow ? 'credits' : 'credits_per_image';
		$value = $pricing[ $key ] ?? null;
		if ( ! is_numeric( $value ) || ! is_finite( (float) $value ) || (float) $value < 0 ) {
			return $this->pricing_unavailable();
		}

		return (float) $value;
	}

	private function credit_amount( float $amount ): float {
		return round( $amount, 6 );
	}

	private function get_credits_evidence( array $credits, float $balance ): array {
		$evidence = [ 'balance' => $balance ];
		foreach (
			[
				'topupBalance',
				'subscriptionBalance',
				'billingSubscriptionStatus',
					'activePlan',
					'hasPlan',
					'freeTrial',
			] as $field
		) {
			if ( array_key_exists( $field, $credits ) ) {
				$evidence[ $field ] = $credits[ $field ];
			}
		}

		return $evidence;
	}

	private function pricing_unavailable(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_pricing_unavailable',
			__( 'Current workflow pricing is unavailable. Please refresh and try again.', 'fotogenic-ai' ),
			[ 'status' => 503 ]
		);
	}

	private function credits_unavailable(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_credits_unavailable',
			__( 'The current credit balance could not be confirmed.', 'fotogenic-ai' ),
			[ 'status' => 503 ]
		);
	}

	private function invalid_source_state(): \WP_Error {
		return new \WP_Error(
			'fotogenic_ai_invalid_source_state',
			__( 'The selected sources could not be verified.', 'fotogenic-ai' ),
			[ 'status' => 400 ]
		);
	}
}
