<?php
/**
 * Class ATFPP\AI_Translate\Services\API\Types\Candidate
 *
 * @since 0.1.0
 * @package ai-services
 */

namespace ATFPP\AI_Translate\Services\API\Types;

use ATFPP\AI_Translate\Services\API\Enums\Content_Role;
use Felix_Arntz\ATFPP\WP_OOP_Plugin_Lib\General\Contracts\Arrayable;
use InvalidArgumentException;

/**
 * Class representing a candidate for a content response from a generative AI model.
 *
 * @since 0.1.0
 */
final class Candidate implements Arrayable {

	/**
	 * The content, unless no content is available as part of the candidate.
	 *
	 * @since 0.1.0
	 * @var ?Content
	 */
	private $content;

	/**
	 * Additional data for the candidate, if any.
	 *
	 * @since 0.1.0
	 * @var array<string, mixed>
	 */
	private $additional_data;

	/**
	 * Constructor.
	 *
	 * @since 0.1.0
	 *
	 * @param ?Content             $content         The content, or null to indicate no content is available.
	 * @param array<string, mixed> $additional_data Additional data for the candidate, if any.
	 */
	public function __construct( ?Content $content, array $additional_data = array() ) {
		$this->content = $content;

		// Remove the content from the additional data, if present, to prevent conflicts.
		unset( $additional_data['content'] );
		$this->additional_data = $additional_data;
	}

	/**
	 * Gets the content.
	 *
	 * @since 0.1.0
	 *
	 * @return ?Content The content.
	 */
	public function get_content(): ?Content {
		return $this->content;
	}

	/**
	 * Gets a field value from the additional data.
	 *
	 * @since 0.1.0
	 *
	 * @param string $field The field name.
	 * @return mixed|null The field value, or null if not found.
	 */
	public function get_field_value( string $field ) {
		if ( isset( $this->additional_data[ $field ] ) ) {
			return $this->additional_data[ $field ];
		}

		if ( str_contains( $field, '_' ) ) {
			$camel_case_field = $this->underscore_to_camel_case( $field );
			if ( isset( $this->additional_data[ $camel_case_field ] ) ) {
				return $this->additional_data[ $camel_case_field ];
			}
		}

		/*
		 * A few common special cases.
		 * For instance, "finish_reason" is sometimes called "stop_reason".
		 */
		switch ( $field ) {
			case 'finish_reason':
				return $this->get_field_value( 'stop_reason' );
			case 'finishReason':
				return $this->get_field_value( 'stopReason' );
		}

		return null;
	}

	/**
	 * Gets the additional data.
	 *
	 * @since 0.1.0
	 *
	 * @return array<string, mixed> The additional data.
	 */
	public function get_additional_data(): array {
		return $this->additional_data;
	}

	/**
	 * Returns the array representation.
	 *
	 * @since 0.1.0
	 *
	 * @return mixed[] Array representation.
	 */
	public function to_array(): array {
		return array_merge(
			array(
				'content' => $this->content ? $this->content->to_array() : null,
			),
			$this->additional_data
		);
	}

	/**
	 * Creates a Candidate instance from an array of content data.
	 *
	 * @since 0.1.0
	 *
	 * @param array<string, mixed> $data The content data.
	 * @return Candidate Candidate instance.
	 *
	 * @throws InvalidArgumentException Thrown if the data is missing required fields.
	 */
	public static function from_array( array $data ): Candidate {
		if ( ! isset( $data['content'] ) ) {
			return new Candidate( null, $data );
		}

		/*
		 * Apparently, the API sometimes omits this.
		 * Given candidates are always part of a model response, we can safely assume the role is 'model'.
		 */
		if ( ! isset( $data['content']['role'] ) ) {
			$data['content']['role'] = Content_Role::MODEL;
		}

		$content = Content::from_array( $data['content'] );
		unset( $data['content'] );

		return new Candidate( $content, $data );
	}

	/**
	 * Returns the JSON schema for the expected input.
	 *
	 * @since 0.5.0
	 *
	 * @return array<string, mixed> The JSON schema.
	 */
	public static function get_json_schema(): array {
		return array(
			'type'                 => 'object',
			'properties'           => array(
				'content' => array_merge(
					array(
						'description' => __( 'Candidate content.', 'ai-services' ),
						'readonly'    => true,
					),
					Content::get_json_schema()
				),
			),
			'additionalProperties' => true,
		);
	}

	/**
	 * Transforms a snake_case string to camelCase.
	 *
	 * @since 0.1.0
	 *
	 * @param string $input The snake_case string.
	 * @return string The camelCase string.
	 */
	private function underscore_to_camel_case( string $input ): string {
		return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
	}
}
