<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use Exception;
use Limb_Chatbot\Includes\Ai_Providers\AI_Providers;
use Limb_Chatbot\Includes\Repositories\AI_Model_Meta_Repository;
use Limb_Chatbot\Includes\Services\Helper;

/**
 * Class AI_Model
 *
 * Represents an AI model entity with metadata, endpoints, features, and token limits.
 *
 * @package Limb_Chatbot\Includes\Data_Objects
 * @since 1.0.0
 */
class AI_Model extends WPDB_Data_Object {

	/**
	 * Database table name.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	const TABLE_NAME = 'lbaic_ai_models';

	/**
	 * Fillable fields for the model.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	const FILLABLE
		= [
			'id',
			'label',
			'name',
			'ai_provider_id',
			'modalities',
			'endpoints',
			'features',
			'fine_tuned',
			'default',
			'input_token_limit',
			'output_token_limit',
			'description',
			'status'
		];

	/**
	 * Columns storing JSON data.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	const JSON_COLUMNS = [ 'modalities', 'endpoints', 'features' ];

	/**
	 * Status constant for active.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_ACTIVE = 1;

	/**
	 * Status constant for inactive.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	const STATUS_INACTIVE = 2;

	/**
	 * Sub-directory for fine-tuning files.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	const FINE_TUNING_FILES_SUB_DIR = 'fine-tuning/';

	/**
	 * Model name.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $name = null;

	/**
	 * AI provider identifier.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $ai_provider_id = null;

	/**
	 * Modalities supported by the model.
	 *
	 * @var mixed|null Array or object decoded from JSON
	 * @since 1.0.0
	 */
	public $modalities = null;

	/**
	 * API endpoints for the model.
	 *
	 * @var mixed|null Array or object decoded from JSON
	 * @since 1.0.0
	 */
	public $endpoints = null;

	/**
	 * Features supported by the model.
	 *
	 * @var mixed|null Array or object decoded from JSON
	 * @since 1.0.0
	 */
	public $features = null;

	/**
	 * Model status.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $status = null;

	/**
	 * Whether the model is fine-tuned.
	 *
	 * @var bool|null
	 * @since 1.0.0
	 */
	public ?bool $fine_tuned = null;

	/**
	 * Whether the model is the default.
	 *
	 * @var bool|null
	 * @since 1.0.0
	 */
	public ?bool $default = null;

	/**
	 * Model description.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $description = null;

	/**
	 * Input token limit for the model.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $input_token_limit = null;

	/**
	 * Output token limit for the model.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $output_token_limit = null;

	/**
	 * The label of the AI model.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $label = null;

	/**
	 * List of included properties (excluded from JSON).
	 *
	 * @var array
	 * @json_excluded
	 * @since 1.0.0
	 */
	public array $included = [];

	/**
	 * Meta properties related to this model.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $meta_properties = [ 'metas', 'dataset', 'parent_model' ];

	/**
	 * AI_Model constructor.
	 *
	 * Decodes JSON columns if necessary.
	 *
	 * @param  array|null  $instance  Initial data array
	 *
	 * @since 1.0.0
	 */
	public function __construct( $instance = null ) {
		if ( is_array( $instance ) && ! empty( $instance['modalities'] ) && $this->isJson( $instance['modalities'] ) ) {
			$instance['modalities'] = Helper::maybe_json_decode( $instance['modalities'] );
		}
		if ( is_array( $instance ) && ! empty( $instance['endpoints'] ) && $this->isJson( $instance['endpoints'] ) ) {
			$instance['endpoints'] = Helper::maybe_json_decode( $instance['endpoints'] );
		}
		if ( is_array( $instance ) && ! empty( $instance['features'] ) && $this->isJson( $instance['features'] ) ) {
			$instance['features'] = Helper::maybe_json_decode( $instance['features'] );
		}
		parent::__construct( $instance );
	}

	/**
	 * Returns the dataset related to the model.
	 *
	 * @return Dataset|null
	 * @since 1.0.0
	 */
	public function dataset() {
		return Dataset::find( $this->get_meta( 'dataset_id' ) );
	}


	/**
	 * Returns the parent model if any.
	 *
	 * @return AI_Model|null
	 * @since 1.0.0
	 */
	public function parent_model() {
		return self::find( $this->get_meta( 'parent_model_id' ) );
	}

	/**
	 * Find model by name and optionally by AI provider ID.
	 *
	 * @param  string  $name
	 * @param  string|null  $ai_provider_id
	 *
	 * @return AI_Model|null
	 * @since 1.0.0
	 */
	public static function find_by_name( $name, $ai_provider_id = null ): ?AI_Model {
		$args = [ 'name' => $name ];
		if ( ! empty( $ai_provider_id ) ) {
			$args['ai_provider_id'] = $ai_provider_id;
		}

		return self::where( $args )->first();
	}

	/**
	 * Get API endpoints.
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 */
	public function get_endpoints() {
		return $this->endpoints;
	}

	/**
	 * Set API endpoints.
	 *
	 * @param  mixed  $endpoints
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_endpoints( $endpoints ): void {
		$this->endpoints = $endpoints;
	}

	/**
	 * Get model features.
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 */
	public function get_features() {
		return $this->features;
	}

	/**
	 * Set model features.
	 *
	 * @param  mixed  $features
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_features( $features ): void {
		$this->features = $features;
	}

	/**
	 * Update a meta key-value pair.
	 *
	 * @param  string  $key
	 * @param  mixed  $value
	 *
	 * @return AI_Model_Meta
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function update_meta( $key, $value ) {
		return AI_Model_Meta::update( [ 'ai_model_id' => $this->id, 'meta_key' => $key ], [ 'meta_value' => $value ] );
	}

	/**
	 * Get model name.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_name(): string {
		return $this->name;
	}

	public function set_name( string $name ): void {
		$this->name = $name;
	}

	/**
	 * Get AI provider ID.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_ai_provider_id(): string {
		return $this->ai_provider_id;
	}

	/**
	 * Set AI provider ID.
	 *
	 * @param  string|null  $ai_provider_id
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_ai_provider_id( ?string $ai_provider_id ): void {
		$this->ai_provider_id = $ai_provider_id;
	}

	/**
	 * Get fine-tuned status.
	 *
	 * @return bool|null
	 * @since 1.0.0
	 */
	public function get_fine_tuned(): ?bool {
		return $this->fine_tuned;
	}

	/**
	 * Set fine-tuned status.
	 *
	 * @param  bool|null  $fine_tuned
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_fine_tuned( $fine_tuned ): void {
		$this->fine_tuned = $fine_tuned;
	}

	/**
	 * Get meta value by key.
	 *
	 * @param  string  $key
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 */
	public function get_meta( string $key ) {
		$metas = ( new AI_Model_Meta_Repository )->get_items( [ 'meta_key' => $key, 'ai_model_id' => $this->id ] );

		return ! empty( $metas ) ? $metas[0]->get_meta_value() : null;
	}

	/**
	 * Add a new meta key-value pair.
	 *
	 * @param  string  $key
	 * @param  mixed  $value
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function add_meta( $key, $value ) {
		AI_Model_Meta::create( [ 'ai_model_id' => $this->id, 'meta_key' => $key, 'meta_value' => $value ] );
	}

	/**
	 * Delete a meta by key.
	 *
	 * @param  string  $string
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function delete_meta( string $string ) {
		AI_Model_Meta::delete( [ 'ai_model_id' => $this->id, 'meta_key' => $string ] );
	}

	/**
	 * Get metas filtered by criteria.
	 *
	 * @param  array  $filters
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public function metas( $filters ) {
		return ( new AI_Model_Meta_Repository() )->get_items( array_merge( [ 'ai_model_id' => $this->id ], $filters ) );
	}

	/**
	 * Get output token limit.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_output_token_limit(): ?int {
		return $this->output_token_limit;
	}

	/**
	 * Set output token limit.
	 *
	 * @param  int|null  $output_token_limit
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_output_token_limit( ?int $output_token_limit ): void {
		$this->output_token_limit = $output_token_limit;
	}

	/**
	 * Checks if the model supports dynamic embedding dimension.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function supports_dynamic_dimension() {
		return ! empty( $this->get_endpoints()['embeddings'] ) && ! empty( $this->get_endpoints()['embeddings']['dynamic_dimension'] );
	}

	/**
	 * Get modalities supported by the model.
	 *
	 * @return mixed|null
	 * @since 1.0.0
	 */
	public function get_modalities() {
		return $this->modalities;
	}

	/**
	 * Set modalities.
	 *
	 * @param  mixed  $modalities
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_modalities( $modalities ): void {
		$this->modalities = $modalities;
	}

	/**
	 * Get supported embedding dimension.
	 * dimensions=[] means supporting any value, with max_dimension limitation (if applied).
	 *
	 * @return array|null
	 * @since 1.0.0
	 */
	public function get_supported_dimensions(): ?array {
		return ! empty( $this->get_endpoints()['embeddings'] ) && isset( $this->get_endpoints()['embeddings']['dimensions'] ) ? $this->get_endpoints()['embeddings']['dimensions'] : null;
	}

	/**
	 * Checks whether dimension is supported.
	 * If model has dimensions=[] it means any dimension is supported, up to max_dimension limitation (if applied).
	 *
	 * @param  int  $dimension
	 *
	 * @return bool
	 */
	public function is_supported_dimension( int $dimension ): bool {
		$dimensions = $this->get_supported_dimensions();
		if ( isset( $dimensions ) && ! count( $dimensions ) ) {
			if ( isset( $this->get_endpoints()['embeddings']['max_dimension'] ) ) {
				return $dimension <= $this->get_endpoints()['embeddings']['max_dimension'];
			}

			return true;
		} elseif ( isset( $dimensions ) && in_array( $dimension, $dimensions ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if model has a given modality.
	 *
	 * @param  string  $string
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function has_modality( string $string ): bool {
		return in_array( $string, array_keys( $this->get_modalities() ) ) && $this->get_modalities()[ $string ];
	}

	/**
	 * Check if model has a given feature.
	 *
	 * @param  string  $string
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function has_feature( string $string ): bool {
		return in_array( $string, array_keys( $this->get_features() ) ) && $this->get_features()[ $string ];
	}

	/**
	 * Get input token limit.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_input_token_limit(): ?int {
		return $this->input_token_limit;
	}

	/**
	 * Set input token limit.
	 *
	 * @param  int|null  $input_token_limit
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_input_token_limit( ?int $input_token_limit ): void {
		$this->input_token_limit = $input_token_limit;
	}

	/**
	 * Get fine-tuning file path for the model.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public function get_fine_tuning_file(): string {
		$target_dir  = Helper::get_wp_uploaded_file_dir( Limb_Chatbot()->get_files_dir() . self::FINE_TUNING_FILES_SUB_DIR );
		$ai_provider = AI_Providers::instance()->get_ai_provider( $this->get_ai_provider_id() );

		return $target_dir . ( 'model_' . $this->get_id() ) . $ai_provider::FINE_TUNING_FILE_EXTENSION;
	}

	/**
	 * Apply JSON column filters for database queries.
	 *
	 * @param  array  $where
	 *
	 * @return array
	 * @since 1.0.0
	 */
	protected static function filter_where( $where ) {
		foreach ( AI_Model::JSON_COLUMNS as $json_col ) {
			if ( ! empty( $where[ $json_col ] ) && is_array( $where[ $json_col ] ) ) {
				foreach ( $where[ $json_col ] as $flag ) {
					// TODO later we can adjust this in a way to support fetching models which support 2 or more endpoints same time 
					// Check if JSON key exists and is truthy (handles both boolean true and object/array values)
					// Using a special 'NOT_FALSE_OR_NULL' operator that will be handled in the database strategy
					$json_filters[] = [ 'col' => $json_col, 'path' => '$."' . $flag . '"', 'op' => 'NOT_FALSE_OR_NULL', 'val' => null, ];
				}
				unset( $where[ $json_col ] );
			}
		}
		if ( ! empty( $json_filters ) ) {
			$where['json_params'] = $json_filters;
		}

		return parent::filter_where( $where );
	}

	/**
	 * Get model label
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_label() {
		return $this->label;
	}

	/**
	 * Set model label.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_label( $label ) {
		$this->label = $label;
	}

	public function has_endpoint($endpoint) {
		return in_array($endpoint, array_keys($this->get_endpoints())) && $this->get_endpoints()[$endpoint];
	}
}