<?php

namespace Fotogenic_AI;

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

/**
 * Durable terminal credit reconciliation for every billable workflow run.
 */
final class Workflow_Credit_Settlement {
	public const SCHEMA_VERSION = 1;
	private const MAX_ADMITTED_OUTPUTS = 6;
	private const MAX_RETRY_SECONDS = 300;
	private const STATUSES = [ 'pending', 'settling', 'settled' ];

	public static function register(): void {
		add_action( 'fotogenic_ai_run_finalized', [ self::class, 'schedule_terminal_settlement' ], 40, 2 );
	}

	public static function pending_state( int $user_id, array $metadata ): array {
		$cached_status = Credits_Service::instance()->get_status( $user_id );
		$balance = is_array( $cached_status )
			&& true === ( $cached_status['fresh'] ?? false )
			&& is_numeric( $cached_status['balance'] ?? null )
			? max( 0.0, (float) $cached_status['balance'] )
			: null;
		$now = time();

		return [
			'schemaVersion'   => self::SCHEMA_VERSION,
			'status'          => 'pending',
			'outcome'         => 'awaiting_terminal',
			'balanceBefore'   => $balance,
			'balanceAfter'    => null,
			'imagesCount'     => max( 0, (int) ( $metadata['images_count'] ?? $metadata['expected_output_count'] ?? 0 ) ),
			'requiredCredits' => max( 0.0, (float) ( $metadata['estimated_cost'] ?? 0 ) ),
			'acceptedImagesCount' => null,
			'creditsDebited'      => null,
			'attempts'        => 0,
			'nextAttemptAt'   => 0,
			'lastErrorCode'   => '',
			'createdAt'       => $now,
			'updatedAt'       => $now,
			'settledAt'       => 0,
		];
	}

	public static function normalize_state( $raw ): array {
		if ( is_string( $raw ) ) {
			$decoded = json_decode( $raw, true );
			$raw = is_array( $decoded ) ? $decoded : [];
		}
		if ( ! is_array( $raw ) || self::SCHEMA_VERSION !== (int) ( $raw['schemaVersion'] ?? 0 ) ) {
			return [];
		}

		$status = sanitize_key( (string) ( $raw['status'] ?? '' ) );
		if ( ! in_array( $status, self::STATUSES, true ) ) {
			return [];
		}
		$balance_before = $raw['balanceBefore'] ?? null;
		$balance_after = $raw['balanceAfter'] ?? null;
		$balance_before = is_numeric( $balance_before ) ? max( 0.0, (float) $balance_before ) : null;
		$balance_after = is_numeric( $balance_after ) ? max( 0.0, (float) $balance_after ) : null;
		$accepted_count = $raw['acceptedImagesCount'] ?? null;
		$credits_debited = $raw['creditsDebited'] ?? null;
		if (
			( null !== $accepted_count && ( ! is_int( $accepted_count ) || $accepted_count < 1 || $accepted_count > self::MAX_ADMITTED_OUTPUTS ) )
			|| ( null !== $credits_debited && ( ! is_int( $credits_debited ) && ! is_float( $credits_debited ) ) )
			|| ( null !== $credits_debited && ( ! is_finite( (float) $credits_debited ) || (float) $credits_debited < 0 ) )
		) {
			return [];
		}

		return [
			'schemaVersion'   => self::SCHEMA_VERSION,
			'status'          => $status,
			'outcome'         => substr( sanitize_key( (string) ( $raw['outcome'] ?? '' ) ), 0, 64 ),
			'balanceBefore'   => $balance_before,
			'balanceAfter'    => $balance_after,
			'imagesCount'     => max( 0, (int) ( $raw['imagesCount'] ?? 0 ) ),
			'requiredCredits' => max( 0.0, (float) ( $raw['requiredCredits'] ?? 0 ) ),
			'acceptedImagesCount' => null === $accepted_count ? null : $accepted_count,
			'creditsDebited'      => null === $credits_debited ? null : (float) $credits_debited,
			'attempts'        => max( 0, (int) ( $raw['attempts'] ?? 0 ) ),
			'nextAttemptAt'   => max( 0, (int) ( $raw['nextAttemptAt'] ?? 0 ) ),
			'lastErrorCode'   => substr( sanitize_key( (string) ( $raw['lastErrorCode'] ?? '' ) ), 0, 64 ),
			'createdAt'       => max( 0, (int) ( $raw['createdAt'] ?? 0 ) ),
			'updatedAt'       => max( 0, (int) ( $raw['updatedAt'] ?? 0 ) ),
			'settledAt'       => max( 0, (int) ( $raw['settledAt'] ?? 0 ) ),
		];
	}

	public static function encode_state( $raw ): string {
		$state = self::normalize_state( $raw );
		return [] === $state ? '' : (string) wp_json_encode( $state, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
	}

	/**
	 * Bind authoritative upstream admission billing without overwriting the
	 * distinct reviewed estimate retained in imagesCount/requiredCredits.
	 */
	/** @return array|\WP_Error */
	public static function record_admission( Logger $logger, string $run_id, $raw_state, ?int $accepted_count, ?float $credits_debited ) {
		if (
			'' === $run_id
			|| ( null === $accepted_count && null === $credits_debited )
			|| ( null !== $accepted_count && ( $accepted_count < 1 || $accepted_count > self::MAX_ADMITTED_OUTPUTS ) )
			|| ( null !== $credits_debited && ( ! is_finite( $credits_debited ) || $credits_debited < 0 ) )
		) {
			return self::admission_error( 'fotogenic_ai_admission_billing_invalid' );
		}
		$row = $logger->get_run_row( $run_id );
		$state = self::normalize_state( $row['credit_settlement_state'] ?? null );
		if ( [] === $state ) {
			$state = self::normalize_state( $raw_state );
		}
		if ( [] === $state ) {
			return self::admission_error( 'fotogenic_ai_admission_billing_unavailable' );
		}
		if (
			( null !== $accepted_count && null !== $state['acceptedImagesCount'] && $accepted_count !== $state['acceptedImagesCount'] )
			|| ( null !== $credits_debited && null !== $state['creditsDebited'] && abs( $credits_debited - $state['creditsDebited'] ) > 0.000001 )
		) {
			return self::admission_error( 'fotogenic_ai_admission_billing_conflict' );
		}
		if ( null !== $accepted_count ) {
			$state['acceptedImagesCount'] = $accepted_count;
		}
		if ( null !== $credits_debited ) {
			$state['creditsDebited'] = $credits_debited;
		}
		$state['updatedAt'] = max( time(), (int) $state['updatedAt'] );
		$stored = self::persist_state( $logger, $run_id, $state );
		if (
			[] === $stored
			|| ( null !== $accepted_count && $accepted_count !== ( $stored['acceptedImagesCount'] ?? null ) )
			|| ( null !== $credits_debited && ( ! isset( $stored['creditsDebited'] ) || abs( $credits_debited - (float) $stored['creditsDebited'] ) > 0.000001 ) )
		) {
			return self::admission_error( 'fotogenic_ai_admission_billing_persist_failed' );
		}
		return $stored;
	}

	private static function admission_error( string $code ): \WP_Error {
		return new \WP_Error(
			$code,
			__( 'The upstream admission billing could not be durably verified.', 'fotogenic-ai' ),
			[ 'status' => 502 ]
		);
	}

	public static function cancel_before_dispatch( Logger $logger, string $run_id, $raw_state ): bool {
		$state = self::normalize_state( $raw_state );
		if ( '' === $run_id || [] === $state ) {
			return false;
		}
		$now = time();
		$state = array_merge(
			$state,
			[
				'status'        => 'settled',
				'outcome'       => 'not_dispatched',
				'nextAttemptAt' => 0,
				'lastErrorCode' => '',
				'updatedAt'     => $now,
				'settledAt'     => $now,
			]
		);
		$stored = self::persist_state( $logger, $run_id, $state );
		return 'settled' === ( $stored['status'] ?? '' ) && 'not_dispatched' === ( $stored['outcome'] ?? '' );
	}

	/**
	 * @return array{settled: bool, retryIn?: int}
	 */
	public static function settle_terminal_run( Logger $logger, array $row, bool $force = false ): array {
		$run_id = trim( (string) ( $row['run_id'] ?? '' ) );
		$user_id = (int) ( $row['user_id'] ?? 0 );
		$state = self::normalize_state( $row['credit_settlement_state'] ?? null );
		if ( '' === $run_id || $user_id <= 0 || [] === $state || 'settled' === $state['status'] ) {
			return [ 'settled' => true ];
		}

		$now = time();
		if ( ! $force && $state['nextAttemptAt'] > $now ) {
			return [ 'settled' => false, 'retryIn' => max( 1, $state['nextAttemptAt'] - $now ) ];
		}

		$attempts = $state['attempts'] + 1;
		$state = array_merge(
			$state,
			[
				'status'        => 'settling',
				'attempts'      => $attempts,
				'nextAttemptAt' => 0,
				'updatedAt'     => $now,
			]
		);
		if ( [] === self::persist_state( $logger, $run_id, $state ) ) {
			return [ 'settled' => false, 'retryIn' => 15 ];
		}

		$after = Credits_Service::instance()->refresh( $user_id );
		if ( is_wp_error( $after ) ) {
			$retry_in = min( self::MAX_RETRY_SECONDS, 15 * ( 2 ** min( 4, $attempts - 1 ) ) );
			$state = array_merge(
				$state,
				[
					'status'        => 'pending',
					'nextAttemptAt' => $now + $retry_in,
					'lastErrorCode' => substr( sanitize_key( (string) $after->get_error_code() ), 0, 64 ),
					'updatedAt'     => $now,
				]
			);
			self::persist_state( $logger, $run_id, $state );
			return [ 'settled' => false, 'retryIn' => $retry_in ];
		}

		$after_balance = is_array( $after ) && is_numeric( $after['balance'] ?? null )
			? max( 0.0, (float) $after['balance'] )
			: null;
		$state = array_merge(
			$state,
			[
				'status'          => 'settled',
				'outcome'         => 'terminal_refreshed',
				'balanceAfter'    => $after_balance,
				'nextAttemptAt'   => 0,
				'lastErrorCode'   => '',
				'updatedAt'       => $now,
				'settledAt'       => $now,
			]
		);
		if ( [] === self::persist_state( $logger, $run_id, $state ) ) {
			return [ 'settled' => false, 'retryIn' => 15 ];
		}
		if ( $logger->enabled() ) {
			$logger->info(
				'credits_generation',
				[
					'run_id'           => $run_id,
					'run_target'       => (string) ( $row['run_target'] ?? '' ),
					'subject_id'       => (int) ( $row['subject_id'] ?? 0 ),
					'final_status'     => (string) ( $row['final_status'] ?? '' ),
					'images_count'     => $state['imagesCount'],
					'required_credits' => $state['requiredCredits'],
					'balance_before'   => $state['balanceBefore'],
					'balance_after'    => $state['balanceAfter'],
				]
			);
		}
		return [ 'settled' => true ];
	}

	public static function schedule_terminal_settlement( string $run_id, string $final_status ): void {
		if ( ! in_array( $final_status, [ 'completed', 'partial', 'failed', 'timeout', 'stale', 'stopped', 'unresolved' ], true ) ) {
			return;
		}
		$row = Logger::instance()->get_run_row( $run_id );
		$state = self::normalize_state( $row['credit_settlement_state'] ?? null );
		$user_id = (int) ( $row['user_id'] ?? 0 );
		if ( $user_id > 0 && [] !== $state && 'settled' !== $state['status'] ) {
			Background_Scheduler::instance()->schedule_finalize( $run_id, $user_id, 5 );
		}
	}

	private static function persist_state( Logger $logger, string $run_id, array $state ): array {
		$expected = self::encode_state( $state );
		if ( '' === $expected ) {
			return [];
		}
		$logger->update_run( $run_id, [ 'credit_settlement_state' => $state ] );
		$stored = self::normalize_state( $logger->get_run_row( $run_id )['credit_settlement_state'] ?? null );
		if ( '' === self::encode_state( $stored ) || ! hash_equals( $expected, self::encode_state( $stored ) ) ) {
			return [];
		}
		return $stored;
	}
}
