<?php

namespace Fotogenic_AI;

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

/**
 * Validates the stable direct-account response surface before durable storage.
 */
final class Workflow_Upstream_Response_Parser {
	private const OPERATIONS = [
		'product_images'     => 'generate_images',
		'campaign_set'       => 'campaign_set',
		'increase_resolution' => 'increase_resolution',
	];

	private const RECONCILIATION_STATES = [ 'not_found', 'accepted', 'processing', 'completed', 'failed', 'unknown' ];
	private const START_STATES = [ 'processing' ];
	private const REPLAY_STATES = [ 'processing', 'completed', 'failed' ];
	private const JOB_STATES = [ 'processing', 'completed', 'failed' ];

	/** @return string|\WP_Error */
	public static function operation_for_workflow( string $workflow ) {
		$workflow = sanitize_key( $workflow );
		if ( ! isset( self::OPERATIONS[ $workflow ] ) ) {
			return self::error( 'invalid_workflow' );
		}

		return self::OPERATIONS[ $workflow ];
	}

	/** @return array<string,mixed>|\WP_Error */
	public function parse_start( string $workflow, array $payload ) {
		$verified = $this->inspect_start_admission( $workflow, $payload );
		if ( is_wp_error( $verified ) ) {
			return $verified;
		}
		$replayed = $verified['replayed'];
		$state = $verified['state'];
		$job_ids = $verified['admission_evidence']['job_ids'];
		if (
			! $this->has_valid_start_job_count( $workflow, $job_ids )
			|| ( $replayed && ! $this->job_statuses_match_aggregate( (array) $payload['jobs'], (string) $state ) )
		) {
			return self::error( 'invalid_start_response', $verified );
		}

		$parsed = [
			'job_ids'               => $job_ids,
			'accepted_output_count' => count( $job_ids ),
			'credits_debited'        => $verified['admission_evidence']['credits_debited'],
			'upstream'               => $verified['upstream'],
		];
		if ( 'campaign_set' === $workflow ) {
			$campaign = $this->parse_campaign_start( $payload, $job_ids );
			if ( is_wp_error( $campaign ) ) {
				return self::error( 'invalid_campaign_start_response', $verified );
			}
			$parsed['accepted_output_count'] = $campaign['accepted_output_count'];
			$parsed['accepted_outputs'] = $campaign['accepted_outputs'];
		}

		return $parsed;
	}

	/** @return array{replayed:bool,state:string,upstream:array<string,string>,admission_evidence:array{job_ids:array<int,string>,accepted_output_count:?int,credits_debited:float}}|\WP_Error */
	private function inspect_start_admission( string $workflow, array $payload ) {
		$operation = self::operation_for_workflow( $workflow );
		if ( is_wp_error( $operation ) ) {
			return $operation;
		}
		if ( ! $this->is_boolean( $payload['ok'] ?? null ) || true !== $payload['ok'] || ! $this->is_boolean( $payload['accepted'] ?? null ) || true !== $payload['accepted'] || ! $this->is_boolean( $payload['replayed'] ?? null ) ) {
			return self::error( 'invalid_start_response' );
		}
		$replayed = true === $payload['replayed'];
		$state = $payload['state'] ?? null;
		$allowed_states = $replayed ? self::REPLAY_STATES : self::START_STATES;
		if ( ! $this->matches_operation( $payload['operation'] ?? null, $operation ) || ! $this->matches_enum( $state, $allowed_states ) ) {
			return self::error( 'invalid_start_response' );
		}
		$identity = $this->parse_identity( $payload );
		if ( is_wp_error( $identity ) ) {
			return $identity;
		}
		$allowed_job_states = $replayed ? self::JOB_STATES : [ 'processing' ];
		$job_ids = $this->parse_jobs( $payload['jobs'] ?? null, $allowed_job_states );
		if ( is_wp_error( $job_ids ) || ! $this->is_billing( $payload['billing'] ?? null ) ) {
			return is_wp_error( $job_ids ) ? $job_ids : self::error( 'invalid_start_response' );
		}
		$accepted_count = count( $job_ids );
		if ( 'campaign_set' === $workflow ) {
			$accepted_count = $payload['acceptedOutputCount'] ?? null;
		}
		if ( ! is_int( $accepted_count ) || $accepted_count < 1 || $accepted_count > 6 ) {
			$accepted_count = null;
		}

		return [
			'replayed'             => $replayed,
			'state'                => (string) $state,
			'upstream'             => [
				'request_id'            => $identity['request_id'],
				'generation_request_id' => $identity['generation_request_id'],
				'reconcile_state'       => (string) $state,
			],
			'admission_evidence'   => [
				'job_ids'               => $job_ids,
				'accepted_output_count' => $accepted_count,
				'credits_debited'        => (float) $payload['billing']['creditsDebited'],
			],
		];
	}

	/** @return array<string,mixed>|\WP_Error */
	public function parse_reconciliation( string $workflow, array $payload ) {
		$operation = self::operation_for_workflow( $workflow );
		if ( is_wp_error( $operation ) ) {
			return $operation;
		}
		if ( ! $this->is_boolean( $payload['ok'] ?? null ) || true !== $payload['ok'] || ! $this->is_nonempty_string( $payload['status'] ?? null ) ) {
			return self::error( 'invalid_reconciliation_response' );
		}

		$state = $payload['status'];
		if ( ! $this->matches_enum( $state, self::RECONCILIATION_STATES ) ) {
			return self::error( 'invalid_reconciliation_response' );
		}
		if ( 'not_found' === $state ) {
			if ( null !== ( $payload['requestId'] ?? null ) || null !== ( $payload['generationRequestId'] ?? null ) || ! isset( $payload['jobs'] ) || ! is_array( $payload['jobs'] ) || [] !== $payload['jobs'] || null !== ( $payload['billing'] ?? null ) ) {
				return self::error( 'invalid_reconciliation_response' );
			}
			return [
				'state'           => $state,
				'job_ids'         => [],
				'credits_debited' => null,
				'upstream'        => [
					'request_id'            => '',
					'generation_request_id' => '',
					'reconcile_state'       => $state,
				],
			];
		}

		if ( ! $this->matches_operation( $payload['operation'] ?? null, $operation ) || ! $this->is_billing( $payload['billing'] ?? null ) ) {
			return self::error( 'invalid_reconciliation_response' );
		}
		$identity = $this->parse_identity( $payload );
		$job_ids = $this->parse_jobs( $payload['jobs'] ?? null, $this->allowed_reconciliation_job_statuses( $state ) );
		if (
			is_wp_error( $identity )
			|| is_wp_error( $job_ids )
			|| ! $this->has_valid_reconciliation_job_count( $workflow, $state, $job_ids )
			|| ( ! empty( $job_ids ) && ! $this->job_statuses_match_aggregate( (array) $payload['jobs'], $state ) )
		) {
			return self::error( 'invalid_reconciliation_response' );
		}

		return [
			'state'           => $state,
			'job_ids'         => $job_ids,
			'credits_debited' => (float) $payload['billing']['creditsDebited'],
			'upstream'        => [
				'request_id'            => $identity['request_id'],
				'generation_request_id' => $identity['generation_request_id'],
				'reconcile_state'       => $state,
			],
		];
	}

	/** @return array<string,string>|\WP_Error */
	private function parse_identity( array $payload ) {
		$request_id = $payload['requestId'] ?? null;
		$generation_request_id = $payload['generationRequestId'] ?? null;
		if ( ! $this->is_nonempty_string( $request_id ) || ! $this->is_nonempty_string( $generation_request_id ) ) {
			return self::error( 'invalid_upstream_identity' );
		}

		return [
			'request_id'            => trim( $request_id ),
			'generation_request_id' => trim( $generation_request_id ),
		];
	}

	/** @return array<int,string>|\WP_Error */
	private function parse_jobs( $jobs, array $allowed_statuses ) {
		if ( ! is_array( $jobs ) || ! $this->is_list( $jobs ) ) {
			return self::error( 'invalid_upstream_jobs' );
		}
		$job_ids = [];
		foreach ( $jobs as $job ) {
			if ( ! is_array( $job ) || ! $this->is_nonempty_string( $job['jobId'] ?? null ) || ! $this->matches_enum( $job['status'] ?? null, $allowed_statuses ) ) {
				return self::error( 'invalid_upstream_jobs' );
			}
			$job_id = trim( $job['jobId'] );
			if ( isset( $job_ids[ $job_id ] ) ) {
				return self::error( 'invalid_upstream_jobs' );
			}
			$job_ids[ $job_id ] = true;
		}

		return array_keys( $job_ids );
	}

	private function has_valid_start_job_count( string $workflow, array $job_ids ): bool {
		$count = count( $job_ids );
		if ( 'product_images' === $workflow ) {
			return $count >= 1 && $count <= 6;
		}

		return 1 === $count;
	}

	private function has_valid_reconciliation_job_count( string $workflow, string $state, array $job_ids ): bool {
		if ( in_array( $state, [ 'accepted', 'unknown' ], true ) ) {
			return [] === $job_ids;
		}
		if ( 'failed' === $state && [] === $job_ids ) {
			return true;
		}

		return $this->has_valid_start_job_count( $workflow, $job_ids );
	}

	/** @return array<int,string> */
	private function allowed_reconciliation_job_statuses( string $state ): array {
		if ( 'processing' === $state ) {
			return self::JOB_STATES;
		}
		if ( 'completed' === $state ) {
			return [ 'completed' ];
		}
		if ( 'failed' === $state ) {
			return [ 'failed' ];
		}
		return [];
	}

	private function job_statuses_match_aggregate( array $jobs, string $state ): bool {
		$statuses = [];
		foreach ( $jobs as $job ) {
			if ( ! is_array( $job ) || ! is_string( $job['status'] ?? null ) ) {
				return false;
			}
			$statuses[] = $job['status'];
		}
		if ( empty( $statuses ) ) {
			return false;
		}
		if ( 'completed' === $state ) {
			return [] === array_diff( $statuses, [ 'completed' ] );
		}
		if ( 'failed' === $state ) {
			return [] === array_diff( $statuses, [ 'failed' ] );
		}
		if ( 'processing' !== $state ) {
			return false;
		}

		return in_array( 'processing', $statuses, true )
			|| ( in_array( 'completed', $statuses, true ) && in_array( 'failed', $statuses, true ) );
	}

	/** @return array{accepted_output_count:int,accepted_outputs:array<int,array{output_id:string,output_index:int,job_id:string,status:string}>}|\WP_Error */
	private function parse_campaign_start( array $payload, array $job_ids ) {
		if ( 'multiple_assets_per_job' !== ( $payload['outputModel'] ?? null ) || ! $this->is_nonnegative_integer( $payload['requestedOutputCount'] ?? null ) || ! $this->is_nonnegative_integer( $payload['acceptedOutputCount'] ?? null ) || ! $this->is_nonnegative_integer( $payload['rejectedOutputCount'] ?? null ) || ! $this->is_nonnegative_number( $payload['chargedCredits'] ?? null ) || ! isset( $payload['outputs'] ) || ! is_array( $payload['outputs'] ) ) {
			return self::error( 'invalid_campaign_start_response' );
		}
		$requested_count = (int) $payload['requestedOutputCount'];
		$accepted_count = (int) $payload['acceptedOutputCount'];
		$billing = $payload['billing'] ?? null;
		if (
			$requested_count < 2
			|| $requested_count > 6
			|| $accepted_count !== $requested_count
			|| 0 !== $payload['rejectedOutputCount']
			|| count( $payload['outputs'] ) !== $accepted_count
			|| $requested_count !== $accepted_count + (int) $payload['rejectedOutputCount']
			|| ! is_array( $billing )
			|| (float) $payload['chargedCredits'] !== (float) ( $billing['creditsDebited'] ?? null )
		) {
			return self::error( 'invalid_campaign_start_response' );
		}
		$output_ids = [];
		$accepted_outputs = [];
		foreach ( $payload['outputs'] as $index => $output ) {
			if ( ! is_array( $output ) || ! $this->is_nonempty_string( $output['outputId'] ?? null ) || $index !== ( $output['outputIndex'] ?? null ) || ! $this->is_nonempty_string( $output['jobId'] ?? null ) || ! $this->matches_enum( $output['status'] ?? null, [ 'processing' ] ) || ! in_array( trim( $output['jobId'] ), $job_ids, true ) ) {
				return self::error( 'invalid_campaign_start_response' );
			}
			$output_id = trim( $output['outputId'] );
			if ( isset( $output_ids[ $output_id ] ) ) {
				return self::error( 'invalid_campaign_start_response' );
			}
			$output_ids[ $output_id ] = true;
			$accepted_outputs[] = [
				'output_id'    => $output_id,
				'output_index' => $index,
				'job_id'       => trim( $output['jobId'] ),
				'status'       => $output['status'],
			];
		}

		return [ 'accepted_output_count' => $accepted_count, 'accepted_outputs' => $accepted_outputs ];
	}

	private function matches_operation( $value, string $operation ): bool {
		return is_string( $value ) && hash_equals( $operation, $value );
	}

	private function matches_enum( $value, array $allowed ): bool {
		if ( ! is_string( $value ) ) {
			return false;
		}
		foreach ( $allowed as $candidate ) {
			if ( hash_equals( $candidate, $value ) ) {
				return true;
			}
		}
		return false;
	}

	private function is_list( array $values ): bool {
		$index = 0;
		foreach ( array_keys( $values ) as $key ) {
			if ( $key !== $index ) {
				return false;
			}
			$index++;
		}
		return true;
	}

	private function is_billing( $billing ): bool {
		return is_array( $billing )
			&& $this->is_nonnegative_number( $billing['creditsDebited'] ?? null )
			&& $this->is_nonnegative_number( $billing['creditsRefunded'] ?? null )
			&& 'credits' === ( $billing['currency'] ?? null );
	}

	private function is_boolean( $value ): bool {
		return is_bool( $value );
	}

	private function is_nonnegative_integer( $value ): bool {
		return is_int( $value ) && $value >= 0;
	}

	private function is_nonnegative_number( $value ): bool {
		return ( is_int( $value ) || is_float( $value ) )
			&& is_finite( (float) $value )
			&& (float) $value >= 0;
	}

	private function is_nonempty_string( $value ): bool {
		return is_string( $value ) && '' !== trim( $value ) && strlen( trim( $value ) ) <= 255;
	}

	private static function error( string $code, array $verified_admission = [] ): \WP_Error {
		$data = [ 'status' => 502 ];
		if ( ! empty( $verified_admission ) ) {
			$data['verified_admission'] = $verified_admission;
		}
		return new \WP_Error(
			'fotogenic_ai_' . $code,
			__( 'The upstream generation response could not be verified.', 'fotogenic-ai' ),
			$data
		);
	}
}
