<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use Exception;
use WP_Post;
use WP_Term;

/**
 * Represents a Dataset data object in the system.
 *
 * @since 1.0.0
 */
class Dataset extends WPDB_Data_Object {

	/**
	 * The name of the database table associated with this model.
	 *
	 * @since 1.0.0
	 */
	const TABLE_NAME = 'lbaic_datasets';

	/**
	 * The list of fillable fields for mass assignment.
	 *
	 * @since 1.0.0
	 */
	const FILLABLE = [
		'name',
		'created_at',
		'type',
		'updated_at',
		'instruction',
		'status',
		'strategy',
		'source',
		'source_type',
		'source_sub_type'
	];
	/**
	 * Dataset type: Knowledge dataset.
	 *
	 * @since 1.0.0
	 */
	const TYPE_INFORMATIONAL_KNOWLEDGE = 1;

	/**
	 * Dataset type: Training dataset.
	 *
	 * @since 1.0.0
	 */
	const TYPE_TRAINING = 2;

	/**
	 * Dataset type: Actionable knowledge dataset.
	 *
	 * @since 1.0.3
	 */
	const TYPE_ACTIONABLE_KNOWLEDGE = 3;

	/**
	 * Source type: Manual entry.
	 *
	 * @since 1.0.0
	 */
	const SOURCE_QA = 'q_a';

	/**
	 * Source type: WordPress post.
	 *
	 * @since 1.0.0
	 */
	const SOURCE_POST = 'post';

	/**
	 * Source type: WordPress term (taxonomy).
	 *
	 * @since 1.0.0
	 */
	const SOURCE_TERM = 'term';

	/**
	 * Source type: File.
	 *
	 * @since 1.0.0
	 */
	const SOURCE_FILE = 'file';

	/**
	 * Dataset status: Created but not started.
	 *
	 * @since 1.0.0
	 */
	const STATUS_PENDING = 0;

	/**
	 * Dataset status: Currently generating content.
	 *
	 * @since 1.0.0
	 */
	const STATUS_GENERATING = 1;

	/**
	 * Dataset status: Content generated, ready for indexing.
	 *
	 * @since 1.0.0
	 */
	const STATUS_GENERATED = 2;

	/**
	 * Dataset status: Currently indexing vectors.
	 *
	 * @since 1.0.0
	 */
	const STATUS_INDEXING = 3;

	/**
	 * Dataset status: Vectors indexed, ready for completion.
	 *
	 * @since 1.0.0
	 */
	const STATUS_INDEXED = 4;

	/**
	 * Dataset status: Failed at some stage.
	 *
	 * @since 1.0.0
	 */
	const STATUS_FAILED = 6;

	/**
	 * The subdirectory where dataset files are stored.
	 *
	 * @since 1.0.0
	 */
	const FILES_SUB_DIR = 'datasets/';

	/**
	 * The subdirectory where knowledge files are stored.
	 *
	 * @since 1.0.0
	 */
	const FILES_KNOWLEDGE_SUB_DIR = 'knowledge/';

	/**
	 * The settings key used to configure dataset file paths.
	 *
	 * @since 1.0.0
	 */
	const FILES_SETTING_KEY = 'lbaic.files.datasets';
	const SOURCE_TEXT = 'text';

	const SOURCE_CPT = 'cpt';

	/**
	 * Source type: URL.
	 *
	 * @since 1.0.0
	 */
	const SOURCE_URL = 'url';


	/**
	 * The dataset type.
	 *
	 * @since 1.0.0
	 *
	 * @var int|null
	 */
	public ?int $type = null;

	/**
	 * The strategy used for the dataset (e.g., 'full', 'increment
	 *
	 * @var string|null
	 *
	 * @since 1.0.0
	 */
	public ?string $source_type = null;

	/**
	 * The source subtype (e.g., post type, taxonomy).
	 *
	 * @var string|null
	 *
	 * @since 1.0.0
	 */
	public ?string $source_sub_type = null;

	/**
	 * The ID of the source entity (e.g., post ID, term ID).
	 *
	 * @var int|string
	 *
	 * @since 1.0.0
	 */
	public $source = null;

	/**
	 * The current status of the dataset.
	 *
	 * @var int|null
	 *
	 * @since 1.0.0
	 */
	public ?int $status;

	/**
	 * Get the current status of the dataset.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_status(): ?int {
		return $this->status;
	}


	/**
	 * Get the source type of the entry
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_source_type(): ?string {
		return $this->source_type;
	}

	/**
	 * Set the source type of the entry
	 *
	 * @param  string|null  $source_type
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_source_type( ?string $source_type ): void {
		$this->source_type = $source_type;
	}

	/**
	 * Get the source subtype of the entry
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_source_sub_type(): ?string {
		return $this->source_sub_type;
	}

	/**
	 * Set the source subtype of the entry
	 *
	 * @param  string|null  $source_sub_type
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_source_sub_type( ?string $source_sub_type ): void {
		$this->source_sub_type = $source_sub_type;
	}

	/**
	 * Get the source id of the entry
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_source() {
		return $this->source;
	}

	/**
	 * Set the source id of the entry
	 *
	 * @param  int|string  $source
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_source( $source ): void {
		$this->source = $source;
	}

	/**
	 * Dataset name.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	public string $name;

	/**
	 * Dataset instruction or description.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $instruction;

	/**
	 * Last update timestamp.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	public string $updated_at;

	/**
	 * Creation timestamp.
	 *
	 * @var string
	 * @since 1.0.0
	 */
	public string $created_at;

	/**
	 * Excluded from JSON serialization.
	 *
	 * @var array
	 * @json_excluded
	 * @since 1.0.0
	 */
	public array $included = [];

	/**
	 * Metadata properties loaded on demand.
	 *
	 * @var array
	 * @json_excluded
	 * @since 1.0.0
	 */
	public array $meta_properties = [ 'dataset_entries', 'metas', 'storage', 'source_object', 'source_url', 'dataset_entries_count', 'ai_provider_id', 'dimension' ];

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

	/**
	 * Set the dataset name.
	 *
	 * @param string $name The name of the dataset.
	 * @return void
	 * @since 1.0.0
	 */
	public function set_name( $name ): void {
		$this->name = $name;
	}

	/**
	 * Get the count of dataset entries.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_entries_count(): ?int {
		return Dataset_entry::count( [ 'dataset_id' => $this->get_id() ] );
	}

	/**
	 * Include the dataset entries count
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function dataset_entries_count(): ?int {
		return $this->get_entries_count();
	}

	/**
	 * Get all dataset entries associated with this dataset.
	 *
	 * @return array|null
	 * @since 1.0.0
	 */
	public function dataset_entries(): ?array {
		return Dataset_Entry::where( [ 'dataset_id' => $this->get_id() ] )->get();
	}

	/**
	 * Get all metadata records for this dataset.
	 *
	 * @return array|null
	 * @since 1.0.0
	 */
	public function metas(): ?array {
		return Dataset_Meta::where( [ 'dataset_id' => $this->get_id() ] )->get();
	}

	/**
	 * Get the instruction or description for the dataset.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_instruction(): ?string {
		return $this->instruction;
	}

	/**
	 * Set the instruction or description for the dataset.
	 *
	 * @param string|null $instruction Instruction text.
	 * @return void
	 * @since 1.0.0
	 */
	public function set_instruction( ?string $instruction ): void {
		$this->instruction = $instruction;
	}

	/**
	 * Get a specific meta object by key for this dataset.
	 *
	 * @param string $meta_key The meta key to search for.
	 * @return Dataset_Meta|null
	 * @since 1.0.0
	 */
	public function get_meta( string $meta_key ): ?Dataset_Meta {
		return Dataset_Meta::where( [ 'dataset_id' => $this->get_id(), 'meta_key' => $meta_key ] )->first();
	}

	public function get_meta_value( string $meta_key ) {
		$meta = Dataset_Meta::where( [ 'dataset_id' => $this->get_id(), 'meta_key' => $meta_key ] )->first();

		return $meta instanceof Dataset_Meta ? $meta->get_meta_value() : null;
	}

	/**
	 * Set the dataset type.
	 *
	 * @since 1.0.0
	 *
	 * @param int|string $type Dataset type (e.g., self::TYPE_INFORMATIONAL_KNOWLEDGE).
	 * @return void
	 */
	public function set_type($type): void {
		$this->type = $type;
	}

	/**
	 * Get the dataset type.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null Dataset type or null if not set.
	 */
	public function get_type() {
		return $this->type;
	}

	/**
	 * Mark the dataset as not synced.
	 *
	 * Updates the dataset meta to indicate that it is out of sync.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 *
	 */
	public function mark_not_synced(): void {
		$this->update_meta(Dataset_Meta::SYNCED_KEY, Dataset_Meta::SYNCED_NO_VALUE);
	}

	/**
	 * Update or create a meta key-value pair for this dataset.
	 *
	 * @param  string  $key  The meta key to update.
	 * @param  mixed  $value  The meta value to set.
	 *
	 * @return void
	 * @throws Exception
	 * @since 1.0.0
	 *
	 */
	public function update_meta( $key, $value ) {
		Dataset_Meta::update(
			[
				'dataset_id' => $this->id,
				'meta_key'   => $key,
			],
			[
				'meta_value' => $value,
			]
		);
	}

	/**
	 * Check whether the dataset is synced.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if synced, false otherwise.
	 */
	public function is_synced(): bool {
		return $this->get_meta(Dataset_Meta::SYNCED_KEY)->get_meta_value() === Dataset_Meta::SYNCED_YES_VALUE;
	}

	/**
	 * Mark the dataset as synced by updating its meta.
	 *
	 * @return void
	 * @throws Exception
	 */
	public function mark_synced() {
		$this->update_meta(Dataset_Meta::SYNCED_KEY, Dataset_Meta::SYNCED_YES_VALUE);
	}

	/**
	 * Retrieve the storage (vector index) associated with this dataset.
	 *
	 * This method first checks if the dataset has a stored vector index ID.
	 * If not, it tries to find a related vector through dataset entries.
	 *
	 * @return Vector_Index|null The vector index if found, or null.
	 */
	public function storage() {
		$vector_index_meta = $this->get_meta( 'vector_index_id' );
		if ( $vector_index_meta instanceof Dataset_Meta && ! empty( $vector_index_meta->get_meta_value() ) ) {
			$vector_index = Vector_Index::find( $vector_index_meta->get_meta_value() );
			if ( $vector_index instanceof Vector_Index ) {
				return $vector_index;
			}
		}
		$dataset_entry = Dataset_Entry::where( [ 'dataset_id' => $this->get_id() ], 1, 1 )->first();
		if ( ! empty( $dataset_entry ) && $dataset_entry instanceof Dataset_Entry ) {
			$vector = $dataset_entry->vector();
			if ( $vector instanceof Vector ) {
				$vector_index = $vector->vector_index();

				return $vector_index ? $vector_index[0] : null;
			}
		}

		return null;
	}

	/**
	 * Get the source object for this dataset.
	 *
	 * Depending on the dataset source type, it may return a WP_Post, WP_Term, or null.
	 *
	 * @return WP_Post|WP_Term|null|File
	 */
	public function source_object() {
		if ( in_array( $this->get_source_type(), [ self::SOURCE_POST, self::SOURCE_CPT ] ) ) {
			return get_post( $this->get_source() );
		}
		if ( $this->get_source_type() === self::SOURCE_TERM ) {
			return get_term( $this->get_source() );
		}
		if ( $this->get_source_type() === self::SOURCE_FILE ) {
			return File::find_by_uuid( $this->get_source() ) ?? null;
		}

		return null;
	}

	/**
	 * Get the URL of the source object for this dataset.
	 *
	 * If the source object is a WP_Post, it returns the permalink.
	 * If it's a WP_Term, it returns the term link.
	 * For other source types, it returns null.
	 *
	 * @return string|null The URL of the source object, or null if not applicable.
	 */
	public function source_url() {
		if ( in_array( $this->get_source_type(), [ self::SOURCE_POST, self::SOURCE_CPT ] ) ) {
			return get_permalink( $this->get_source() );
		}
		if ( $this->get_source_type() === self::SOURCE_TERM ) {
			return get_term_link( $this->get_source() );
		}
		if ( $this->get_source_type() === self::SOURCE_FILE ) {
			$file = File::find_by_uuid( $this->get_source() );
			if ( $file instanceof File ) {
				return $file->get_url();
			}
		}
		if ( $this->get_source_type() === self::SOURCE_URL ) {
			return $this->get_source();
		}

		return null;
	}

	/**
	 * Set the current status of the dataset.
	 *
	 * @param int|null $status Status code to set, or null.
	 * @return void
	 */
	public function set_status( ?int $status ) {
		$this->status = $status;
	}

	/**
	 * Check whether the dataset has at least one entry.
	 *
	 * @return bool True if the dataset has entries, false otherwise.
	 */
	public function has_entries() {
		return ! Dataset_Entry::where( [ 'dataset_id' => $this->get_id() ], 1, 1 )->is_empty();
	}

	/**
	 * Retrieve the AI model associated with this dataset.
	 *
	 * @return AI_Model|null The AI model instance, or null if not set.
	 */
	public function get_model(): ?AI_Model {
		$ai_model_id = $this->get_meta( 'ai_model_id' );
		if ( $ai_model_id instanceof Dataset_Meta && ! empty( $ai_model_id->get_meta_value() ) ) {
			return AI_Model::find( $ai_model_id->get_meta_value() );
		}

		return null;
	}

	/**
	 * Retrieve the configuration associated with this dataset.
	 *
	 * @return Config|null The configuration instance, or null if not set.
	 */
	public function get_config(): ?Config {
		$config = $this->get_meta( 'config_id' );
		if ( $config instanceof Dataset_Meta && ! empty( $config->get_meta_value() ) ) {
			return Config::find( $config->get_meta_value() );
		}

		return null;
	}

	/**
	 * Retrieve the errors stored in the dataset meta.
	 *
	 * Returns an array of errors if present, otherwise an empty array.
	 *
	 * @return array Array of errors.
	 */
	public function get_errors() {
		$errors = $this->get_meta( 'errors' );
		if ( $errors instanceof Dataset_Meta ) {
			$error_value = $errors->get_meta_value();
			if ( is_string( $error_value ) && ! empty( $error_value ) ) {
				$errors = json_decode( $error_value, true ) ?: [];
			} elseif ( is_array( $error_value ) ) {
				$errors = $error_value;
			} else {
				$errors = [];
			}
		} else {
			$errors = [];
		}

		return $errors;
	}

	public function is_last_in_storage() {
		$vector_index_id = $this->get_meta( 'vector_index_id' );
		if ( ! $vector_index_id instanceof Dataset_Meta ) {
			return false;
		}
		$vector_index_id = $vector_index_id->get_meta_value();
		$dataset_metas = Dataset_Meta::where( [
			'meta_key'   => 'vector_index_id',
			'meta_value' => $vector_index_id,
		] );
		if ( $dataset_metas->is_empty() ) {
			return $vector_index_id;
		}
		foreach ( $dataset_metas->get() as $item ) {
			if ( ! $item instanceof Dataset_Meta ) {
				continue;
			}
			if ( $item->get_dataset_id() != $this->get_id() ) {
				return false;
			}
		}

		return $vector_index_id;
	}

	public function ai_provider_id() {
		$ai_model = $this->get_meta_value( 'index_ai_model_id' );
		if ( empty( $ai_model ) ) {
			return null;
		}

		return AI_Model::find( $ai_model )->get_ai_provider_id();
	}

	public function dimension() {
		// First, check if dimension is stored directly in dataset metas
		$dimension_from_meta = $this->get_meta_value( 'dimension' );

		if ( ! empty( $dimension_from_meta ) ) {
			return (int) $dimension_from_meta;
		}

		// Fallback: try to get from AI model
		$ai_model_id = $this->get_meta_value( 'index_ai_model_id' );

		if ( empty( $ai_model_id ) ) {
			return null;
		}

		$ai_model = AI_Model::find( $ai_model_id );

		if ( empty( $ai_model ) ) {
			return null;
		}

		// Get dimension from model meta
		$dimension_from_model = $ai_model->get_meta( 'default_dimension' );

		return $dimension_from_model ? (int) $dimension_from_model : null;
	}

	public function chatbot() {
		$chatbot_uuid = $this->get_meta_value( 'chatbot_uuid' );
		if ( empty( $chatbot_uuid ) ) {
			return Chatbot::make();
		}

		return Chatbot::find_by_uuid( $chatbot_uuid ) ?? null;
	}
}