<?php

namespace Limb_Chatbot\Includes\Services\Jobs\Handlers;

use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\Dataset_Meta;
use Limb_Chatbot\Includes\Data_Objects\Job;
use Limb_Chatbot\Includes\Data_Objects\Task;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Repositories\Vector_Index_Repository;
use Limb_Chatbot\Includes\Services\Dataset_Service;
use Limb_Chatbot\Includes\Services\Job\Abstract_Job_Handler;
use Limb_Chatbot\Includes\Services\Vector_Index_Service;

/**
 * Dataset Delete Job Handler
 *
 * Handles dataset deletion jobs. Migrated from WP_Background_Process system.
 * Deletes datasets and their associated data (entries, metadata, vectors).
 *
 * @since 1.1.0
 */
class Dataset_Delete extends Abstract_Job_Handler {

	/**
	 * Dataset service instance.
	 *
	 * @var Dataset_Service
	 * @since 1.1.0
	 */
	private Dataset_Service $dataset_service;

	/**
	 * Vector index service instance.
	 *
	 * @var Vector_Index_Service
	 * @since 1.1.0
	 */
	private Vector_Index_Service $vector_index_service;

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 */
	public function __construct() {
		parent::__construct();
		$this->dataset_service      = new Dataset_Service();
		$this->vector_index_service = new Vector_Index_Service( new Vector_Index_Repository() );
	}

	/**
	 * Get the job type this handler manages.
	 *
	 * @return string
	 * @since 1.1.0
	 */
	public function get_job_type(): string {
		return Job::TYPE_DATASET_DELETE;
	}

	/**
	 * Non-empty `config.sub_type` selects datasets by `Dataset.source_sub_type`
	 * when `dataset_ids` is omitted, empty, or not the string `all`.
	 *
	 * @param  array  $config  Job configuration.
	 *
	 * @return string Trimmed dataset source sub-type or empty string.
	 */
	private function config_dataset_sub_type( array $config ): string {
		if ( ! isset( $config['sub_type'] ) ) {
			return '';
		}

		return trim( (string) $config['sub_type'] );
	}

	/**
	 * Whether deletion targets all datasets matching `config.sub_type` (source_sub_type).
	 *
	 * @param  array  $config  Job configuration.
	 */
	private function deletes_by_config_dataset_sub_type( array $config ): bool {
		$dataset_ids = $config['dataset_ids'] ?? null;
		if ( $dataset_ids === 'all' ) {
			return false;
		}
		if ( is_array( $dataset_ids ) && count( $dataset_ids ) > 0 ) {
			return false;
		}

		return $this->config_dataset_sub_type( $config ) !== '';
	}

	/**
	 * Dataset IDs for the config sub_type delete mode.
	 *
	 * @param  array  $config  Job configuration.
	 *
	 * @return int[]
	 */
	private function dataset_ids_for_config_sub_type( array $config ): array {
		$sub_type = $this->config_dataset_sub_type( $config );
		if ( $sub_type === '' ) {
			return [];
		}

		return Dataset::where( [ 'source_sub_type' => $sub_type ], -1, -1 )->pluck( 'id' );
	}

	/**
	 * Validate job configuration.
	 *
	 * Validates that dataset IDs are provided and exist in the database.
	 * Extracted from Handler::validate_params()
	 *
	 * @param  array  $config  Job configuration.
	 * @param  string|null  $chatbot_uuid  Chatbot UUID (optional, not used for deletion).
	 *
	 * @return bool True if valid.
	 * @throws Exception If validation fails.
	 * @since 1.1.0
	 */
	public function validate( array $config, ?string $chatbot_uuid = null ): bool {
		if ( $this->deletes_by_config_dataset_sub_type( $config ) ) {
			$ids = $this->dataset_ids_for_config_sub_type( $config );
			if ( empty( $ids ) ) {
				throw new Exception(
					Error_Codes::NOT_FOUND,
					__( 'No datasets found for this sub type.', 'limb-chatbot' )
				);
			}

			return true;
		}

		$dataset_ids = $config['dataset_ids'] ?? [];

		// Validate dataset IDs are provided
		if ( empty( $dataset_ids ) || ( ! is_array( $dataset_ids ) && $dataset_ids != 'all' ) ) {
			throw new Exception(
				Error_Codes::EMPTY_VALUE,
				__( 'Dataset IDs are required for dataset deletion.', 'limb-chatbot' )
			);
		}

		if ( is_array( $dataset_ids ) ) {
			// Validate datasets exist
			$datasets = Dataset::where( [ 'id' => $dataset_ids ] );
			if ( $datasets->is_empty() ) {
				throw new Exception(
					Error_Codes::NOT_FOUND,
					__( 'Datasets not found', 'limb-chatbot' )
				);
			}
		}

		return true;
	}

	/**
	 * Get total number of datasets to delete.
	 *
	 * Calculates total without actually fetching datasets.
	 * Essential for large deletion operations to prevent timeouts.
	 *
	 * @param  array  $config  Job configuration.
	 *
	 * @return int Total task count.
	 * @throws Exception If calculation fails.
	 * @since 1.1.0
	 */
	public function get_total( array $config, Job $job): int {
		$dataset_ids = $config['dataset_ids'] ?? [];
		if ( $dataset_ids == 'all' ) {
			$type  = $job->get_sub_type() == 'informational' ? Dataset::TYPE_INFORMATIONAL_KNOWLEDGE : Dataset::TYPE_ACTIONABLE_KNOWLEDGE;
			$total = Dataset::count( [ 'type' => $type ] );
		} elseif ( $this->deletes_by_config_dataset_sub_type( $config ) ) {
			$total = Dataset::count( [ 'source_sub_type' => $this->config_dataset_sub_type( $config ) ] );
		} else {
			if ( empty( $dataset_ids ) || ! is_array( $dataset_ids ) ) {
				throw new Exception(
					Error_Codes::EMPTY_VALUE,
					__( 'There is no dataset to delete', 'limb-chatbot' )
				);
			}

			$total = count( $dataset_ids );
		}

		if ( empty( $total ) ) {
			throw new Exception(
				Error_Codes::EMPTY_VALUE,
				__( 'Total is empty', 'limb-chatbot' )
			);
		}

		return (int) $total;
	}

	/**
	 * Generate a batch of tasks for dataset deletion.
	 *
	 * Creates one task per dataset ID. Each task will delete a single dataset.
	 *
	 * @param  Job  $job  Job instance.
	 * @param  array  $config  Job configuration.
	 * @param  int  $offset  Starting offset for this batch.
	 * @param  int  $limit  Maximum number of tasks to generate.
	 *
	 * @return int Number of tasks actually created.
	 * @since 1.1.0
	 */
	public function generate_task_batch( Job $job, array $config, int $offset, int $limit ): int {
		$dataset_ids = $config['dataset_ids'] ?? [];
		if ( $dataset_ids == 'all' ) {
			$type        = $job->get_sub_type() == 'informational' ? Dataset::TYPE_INFORMATIONAL_KNOWLEDGE : Dataset::TYPE_ACTIONABLE_KNOWLEDGE;
			$dataset_ids = Dataset::where( [ 'type' => $type ], -1, -1 )->pluck( 'id' );
		} elseif ( $this->deletes_by_config_dataset_sub_type( $config ) ) {
			$dataset_ids = $this->dataset_ids_for_config_sub_type( $config );
		}
		if ( empty( $dataset_ids ) || ! is_array( $dataset_ids ) ) {
			return 0;
		}

		// Get the slice of dataset IDs for this batch
		$batch_ids = array_slice( $dataset_ids, $offset, $limit );

		$task_count = 0;

		foreach ( $batch_ids as $dataset_id ) {
			// Create task payload with dataset ID
			$payload = [
				'dataset_id' => (int) $dataset_id,
			];

			// Create task
			if ( $this->create_task( $job->get_id(), $payload ) ) {
				$task_count++;
			}
		}

		return $task_count;
	}

	/**
	 * Process a single task (delete a dataset).
	 *
	 * Deletes a dataset and all its associated data:
	 * - Checks if dataset is last in storage and removes vector index if so
	 * - Clears dataset entries using Dataset_Service
	 * - Deletes dataset metadata
	 * - Deletes the dataset itself
	 *
	 * Extracted from Process::process()
	 *
	 * @param  Task  $task  Task to process.
	 *
	 * @return bool True on success, false on failure.
	 * @throws Exception If processing fails.
	 * @since 1.1.0
	 */
	public function process_task( Task $task ): bool {
		$payload = $task->get_payload();
		$dataset_id = $payload['dataset_id'] ?? null;

		if ( empty( $dataset_id ) ) {
			// Invalid payload, skip
			return true;
		}

		// Find the dataset
		$dataset = Dataset::find( $dataset_id );

		if ( ! $dataset instanceof Dataset ) {
			// Dataset not found, skip (may have been deleted already)
			return true;
		}

		// Check if dataset is last in storage and remove vector index if so
		$vector_index_id = $dataset->is_last_in_storage();
		if ( $vector_index_id ) {
			$this->vector_index_service->remove( $vector_index_id );
		}

		// Clear dataset entries (deletes all entries and their vectors)
		$this->dataset_service->clear( $dataset );

		// Delete dataset metadata
		Dataset_Meta::delete( [ 'dataset_id' => $dataset_id ] );

		// Delete the dataset itself
		Dataset::delete( [ 'id' => $dataset_id ] );

		return true;
	}

	/**
	 * Determine if an exception is critical.
	 *
	 * Uses parent's default implementation which checks common critical codes.
	 *
	 * @param  Exception  $exception  Exception that occurred.
	 *
	 * @return bool True if critical.
	 * @since 1.1.0
	 */
	public function is_critical_error( Exception $exception ): bool {
		// Use parent's default implementation
		return parent::is_critical_error( $exception );
	}
}
