<?php

namespace Limb_Chatbot\Includes\Services\Knowledge;

use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\Dataset_Entry;
use Limb_Chatbot\Includes\Data_Objects\Task;
use Limb_Chatbot\Includes\Data_Objects\Vector;
use Limb_Chatbot\Includes\Data_Objects\Vector_Index;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Repositories\Vector_Repository;
use Limb_Chatbot\Includes\Services\Vector_Service;

/**
 * Indexing Service
 *
 * Handles indexing of dataset entries into vector storage.
 * Used by job handlers to process child tasks for indexing.
 *
 * @since 1.1.0
 */
class Indexing_Service {

	/**
	 * Vector service instance.
	 *
	 * @var Vector_Service
	 * @since 1.1.0
	 */
	private Vector_Service $vector_service;

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 */
	public function __construct() {
		$this->vector_service = new Vector_Service( new Vector_Repository() );
	}

	/**
	 * Index a dataset entry from a task payload.
	 *
	 * Processes a child task that contains dataset_entry_id and indexing configuration.
	 *
	 * @param  Task  $task  Task containing the entry ID and indexing config.
	 *
	 * @return bool True on success, false on failure.
	 * @throws Exception If indexing fails.
	 * @since 1.1.0
	 */
	public function index_entry( Task $task ): bool {
		$payload = $task->get_payload();
		$dataset_entry_id = $payload['dataset_entry_id'] ?? null;

		if ( empty( $dataset_entry_id ) ) {
			return false;
		}

		// Get dataset entry
		$dataset_entry = Dataset_Entry::find( $dataset_entry_id );

		if ( ! $dataset_entry instanceof Dataset_Entry ) {
			// Entry not found, skip
			return true;
		}

		// Skip if vector already exists
		$existing_vector = $dataset_entry->vector();
		if ( $existing_vector instanceof Vector ) {
			return true;
		}

		$dataset = $dataset_entry->dataset();
		if ( ! $dataset instanceof Dataset ) {
			return false;
		}

		// Set dataset status to indexing if not already
		if ( $dataset->get_status() === Dataset::STATUS_GENERATED ) {
			$dataset->set_status( Dataset::STATUS_INDEXING );
			$dataset->save();
		}

		// Build payload for vector creation (needs entry data)
		$vector_payload = [
			'dataset_entry_id' => $dataset_entry->get_id(),
			'config_id'        => $payload['config_id'] ?? $dataset->get_meta_value( 'index_config_id' ),
			'ai_model_id'      => $payload['ai_model_id'] ?? $dataset->get_meta_value( 'index_ai_model_id' ),
			'vector_index_id'  => $payload['vector_index_id'] ?? $dataset->get_meta_value( 'vector_index_id' ),
			'dimension'        => $payload['dimension'] ?? $dataset->get_meta_value( 'dimension' ),
		];

		// Create vector (generates embedding)
		$vector = $this->vector_service->create( $vector_payload );

		// Handle empty vector values (failed embedding)
		if ( empty( $vector->get_values() ) ) {
			// Mark vector as inactive and log failed embedding
			$vector->set_status( Vector::STATUS_INACTIVE );
			$vector->save();
			return true; // Not a failure, just skipped
		}

		// Proceed with upsert to vector index
		$vector_index_id = $vector_payload['vector_index_id'] ?? null;
		if ( empty( $vector_index_id ) ) {
			throw new Exception(
				Error_Codes::MISSING_VALUE,
				__( 'Vector Index ID not found.', 'limb-chatbot' )
			);
		}

		$vector_index = Vector_Index::find( $vector_index_id );
		if ( ! $vector_index ) {
			throw new Exception(
				Error_Codes::MISSING_VALUE,
				__( 'Vector Index not found.', 'limb-chatbot' )
			);
		}

		$this->vector_service->upsert( $vector, $vector_index );

		// Check if all entries for this dataset are indexed and update status
		$this->check_and_update_dataset_status( $dataset );

		return true;
	}

	/**
	 * Index a dataset entry directly without a task.
	 *
	 * Used for synchronous indexing when not using background processes.
	 *
	 * @param  Dataset_Entry  $dataset_entry  Dataset entry to index.
	 * @param  array  $config  Optional indexing configuration. If not provided, uses dataset meta.
	 *
	 * @return bool True on success, false on failure.
	 * @throws Exception If indexing fails.
	 * @since 1.0.0
	 */
	public function index_entry_direct( Dataset_Entry $dataset_entry, array $config = [] ): bool {
		// Skip if vector already exists
		$existing_vector = $dataset_entry->vector();
		if ( $existing_vector instanceof Vector ) {
			return true;
		}

		$dataset = $dataset_entry->dataset();
		if ( ! $dataset instanceof Dataset ) {
			return false;
		}

		// Set dataset status to indexing if not already
		if ( $dataset->get_status() === Dataset::STATUS_GENERATED ) {
			$dataset->set_status( Dataset::STATUS_INDEXING );
			$dataset->save();
		}

		// Build payload for vector creation
		$vector_payload = [
			'dataset_entry_id' => $dataset_entry->get_id(),
			'config_id'        => $config['config_id'] ?? $dataset->get_meta_value( 'index_config_id' ),
			'ai_model_id'      => $config['ai_model_id'] ?? $dataset->get_meta_value( 'index_ai_model_id' ),
			'vector_index_id'  => $config['vector_index_id'] ?? $dataset->get_meta_value( 'vector_index_id' ),
			'dimension'        => $config['dimension'] ?? $dataset->get_meta_value( 'dimension' ),
		];

		// Only proceed if we have the required configuration
		if ( empty( $vector_payload['config_id'] ) || empty( $vector_payload['ai_model_id'] ) || 
			 empty( $vector_payload['vector_index_id'] ) || empty( $vector_payload['dimension'] ) ) {
			return true; // No indexing config, skip silently
		}

		// Create vector (generates embedding)
		$vector = $this->vector_service->create( $vector_payload );

		// Handle empty vector values (failed embedding)
		if ( empty( $vector->get_values() ) ) {
			// Mark vector as inactive and log failed embedding
			$vector->set_status( Vector::STATUS_INACTIVE );
			$vector->save();
			return true; // Not a failure, just skipped
		}

		$vector_index = Vector_Index::find( $vector_payload['vector_index_id'] );
		if ( ! $vector_index ) {
			throw new Exception(
				Error_Codes::MISSING_VALUE,
				__( 'Vector Index not found.', 'limb-chatbot' )
			);
		}

		$this->vector_service->upsert( $vector, $vector_index );

		// Check if all entries for this dataset are indexed and update status
		$this->check_and_update_dataset_status( $dataset );

		return true;
	}

	/**
	 * Check and update dataset status if all entries are indexed.
	 *
	 * @param  Dataset  $dataset  Dataset to check.
	 *
	 * @return void
	 * @since 1.1.0
	 */
	private function check_and_update_dataset_status( Dataset $dataset ): void {
		// Check if all entries have vectors
		$entries = $dataset->dataset_entries();
		$all_indexed = true;

		foreach ( $entries as $entry ) {
			if ( ! $entry->vector() instanceof Vector ) {
				$all_indexed = false;
				break;
			}
		}

		// Update status if all entries are indexed
		if ( $all_indexed && $dataset->get_status() === Dataset::STATUS_INDEXING ) {
			$dataset->set_status( Dataset::STATUS_INDEXED );
			$dataset->save();
		}
	}
}

