<?php
/** Secure serialization for durable local BYOK requests and results. */

defined( 'ABSPATH' ) || exit;

class Costudio_BYOK_Codec_Exception extends RuntimeException {}

class Costudio_BYOK_Job_Codec {

	public const SCHEMA_VERSION = 1;
	private const MAX_PROMPT_BYTES = 2_000_000;
	private const MAX_CONTEXT_BYTES = 4_000_000;
	private const MAX_RESULT_BYTES = 8_000_000;

	/**
	 * Normalize, authenticate and encrypt a request snapshot.
	 *
	 * @return array{payload:array,ciphertext:string,request_hash:string,credential_fingerprint:string,execution_profile_hash:string}
	 */
	public static function encode_request( array $request ): array {
		$payload = self::normalize_request( $request );
		$json    = self::canonical_json( $payload );
		$cipher  = Costudio_Standalone_Crypto::encrypt( $json );
		if ( 0 !== strpos( $cipher, 'FOSc2:' ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_encryption_unavailable' );
		}

		$profile = array(
			'execution_kind'  => $payload['execution_kind'],
			'provider'        => $payload['provider'],
			'model'           => $payload['model'],
			'temperature'     => $payload['temperature'],
			'thinking_budget' => $payload['thinking_budget'],
			'deadline_seconds'=> $payload['deadline_seconds'],
		);

		return array(
			'payload'                => $payload,
			'ciphertext'             => $cipher,
			'request_hash'           => self::keyed_hash( $json ),
			'credential_fingerprint' => self::keyed_hash( $payload['api_key'] ),
			'execution_profile_hash' => self::keyed_hash( self::canonical_json( $profile ) ),
		);
	}

	public static function decode_request( string $ciphertext ): array {
		$data = self::decrypt_json( $ciphertext );
		return self::normalize_request( $data );
	}

	public static function encode_result( array $result ): string {
		$text = (string) ( $result['text'] ?? '' );
		if ( '' === trim( $text ) || strlen( $text ) > self::MAX_RESULT_BYTES ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_result_invalid' );
		}
		$payload = array(
			'schema_version' => self::SCHEMA_VERSION,
			'text'           => $text,
			'provider'       => sanitize_key( (string) ( $result['provider'] ?? '' ) ),
			'model'          => sanitize_text_field( (string) ( $result['model'] ?? '' ) ),
			'usage'          => self::normalize_usage( (array) ( $result['usage'] ?? array() ) ),
			'completed_at'   => current_time( 'mysql', true ),
		);
		$cipher = Costudio_Standalone_Crypto::encrypt( self::canonical_json( $payload ) );
		if ( 0 !== strpos( $cipher, 'FOSc2:' ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_encryption_unavailable' );
		}
		return $cipher;
	}

	public static function decode_result( string $ciphertext ): array {
		$data = self::decrypt_json( $ciphertext );
		if ( (int) ( $data['schema_version'] ?? 0 ) !== self::SCHEMA_VERSION || empty( $data['text'] ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_result_invalid' );
		}
		$data['usage'] = self::normalize_usage( (array) ( $data['usage'] ?? array() ) );
		return $data;
	}

	public static function normalize_usage( array $usage ): array {
		return array(
			'input_tokens'                => max( 0, (int) ( $usage['input_tokens'] ?? 0 ) ),
			'output_tokens'               => max( 0, (int) ( $usage['output_tokens'] ?? 0 ) ),
			'cache_creation_input_tokens' => max( 0, (int) ( $usage['cache_creation_input_tokens'] ?? 0 ) ),
			'cache_read_input_tokens'     => max( 0, (int) ( $usage['cache_read_input_tokens'] ?? 0 ) ),
		);
	}

	private static function normalize_request( array $request ): array {
		$execution_kind = sanitize_key( (string) ( $request['execution_kind'] ?? 'provider_request' ) );
		if ( ! in_array( $execution_kind, array( 'provider_request', 'pipeline_agent' ), true ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_execution_kind_invalid' );
		}
		$provider = sanitize_key( (string) ( $request['provider'] ?? '' ) );
		if ( ! in_array( $provider, array( 'claude', 'gemini', 'openai' ), true ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_provider_invalid' );
		}
		$model   = sanitize_text_field( (string) ( $request['model'] ?? '' ) );
		$api_key = trim( (string) ( $request['api_key'] ?? '' ) );
		$prompt  = (string) ( $request['prompt'] ?? '' );
		if ( '' === $model || '' === $api_key || strlen( $api_key ) > 1024 ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_credentials_invalid' );
		}
		if ( strlen( $prompt ) > self::MAX_PROMPT_BYTES || ( 'provider_request' === $execution_kind && '' === trim( $prompt ) ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_prompt_invalid' );
		}

		$context = array();
		$context_bytes = 0;
		foreach ( (array) ( $request['context'] ?? array() ) as $turn ) {
			if ( ! is_array( $turn ) ) {
				throw new Costudio_BYOK_Codec_Exception( 'byok_context_invalid' );
			}
			$role = (string) ( $turn['role'] ?? 'user' );
			$role = in_array( $role, array( 'model', 'assistant' ), true ) ? 'model' : 'user';
			$content = (string) ( $turn['content'] ?? '' );
			$context_bytes += strlen( $content );
			if ( $context_bytes > self::MAX_CONTEXT_BYTES ) {
				throw new Costudio_BYOK_Codec_Exception( 'byok_context_too_large' );
			}
			$context[] = array( 'role' => $role, 'content' => $content );
		}

		$temperature = array_key_exists( 'temperature', $request ) && null !== $request['temperature']
			? (float) $request['temperature']
			: null;
		if ( null !== $temperature && ( $temperature < 0.0 || $temperature > 2.0 ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_temperature_invalid' );
		}

		return array(
			'schema_version'  => self::SCHEMA_VERSION,
			'execution_kind'  => $execution_kind,
			'provider'        => $provider,
			'model'           => $model,
			'api_key'         => $api_key,
			'prompt'          => $prompt,
			'context'         => $context,
			'temperature'     => $temperature,
			'thinking_budget' => max( -1, min( 64000, (int) ( $request['thinking_budget'] ?? -1 ) ) ),
			'deadline_seconds'=> max( 30, min( 540, (int) ( $request['deadline_seconds'] ?? 540 ) ) ),
			'has_grounding'   => ! empty( $request['has_grounding'] ),
		);
	}

	private static function decrypt_json( string $ciphertext ): array {
		if ( 0 !== strpos( $ciphertext, 'FOSc2:' ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_ciphertext_invalid' );
		}
		$json = Costudio_Standalone_Crypto::decrypt( $ciphertext );
		if ( $json === $ciphertext ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_decryption_failed' );
		}
		$data = json_decode( $json, true );
		if ( ! is_array( $data ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_payload_invalid' );
		}
		return $data;
	}

	private static function keyed_hash( string $value ): string {
		return hash_hmac( 'sha256', $value, wp_salt( 'auth' ) );
	}

	private static function canonical_json( array $value ): string {
		$value = self::sort_recursive( $value );
		$json  = wp_json_encode( $value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
		if ( ! is_string( $json ) ) {
			throw new Costudio_BYOK_Codec_Exception( 'byok_payload_encode_failed' );
		}
		return $json;
	}

	private static function sort_recursive( array $value ): array {
		$is_list = empty( $value ) || array_keys( $value ) === range( 0, count( $value ) - 1 );
		if ( ! $is_list ) {
			ksort( $value );
		}
		foreach ( $value as $key => $item ) {
			if ( is_array( $item ) ) {
				$value[ $key ] = self::sort_recursive( $item );
			}
		}
		return $value;
	}
}
