<?php

namespace Limb_Chatbot\Includes\Services\Jobs\Handlers;

use Limb_Chatbot\Includes\Data_Objects\Config;
use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\File;
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\Interfaces\File_Upload_Capable_Interface;
use Limb_Chatbot\Includes\Services\File_Service;
use Limb_Chatbot\Includes\Services\Job\Abstract_Job_Handler;

/**
 * File Upload Job Handler
 *
 * Handles file upload jobs. Migrated from WP_Background_Process system.
 * Uploads files to AI providers (OpenAI, Gemini, DeepSeek).
 *
 * @since 1.1.0
 */
class File_Upload extends Abstract_Job_Handler {

	/**
	 * File service instance.
	 *
	 * @var File_Service
	 * @since 1.1.0
	 */
	private File_Service $file_service;

	/**
	 * Oversize limits by AI provider in bytes.
	 *
	 * Used for validating file sizes before upload.
	 *
	 * @since 1.1.0
	 * @var array<string, int>
	 */
	const OVERSIZE_LIMITS = array(
		'gemini'  => 7 * 1024 * 1024,
		'open-ai' => 50 * 1024 * 1024,
	);

	/**
	 * Constructor.
	 *
	 * @since 1.1.0
	 */
	public function __construct() {
		parent::__construct();
		$this->file_service = new File_Service();
	}

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

	/**
	 * Validate job configuration.
	 *
	 * Validates that config ID and file UUIDs are provided, exist, and are valid.
	 * Extracted from Handler::validate_files()
	 *
	 * @param  array  $config  Job configuration.
	 * @param  string|null  $chatbot_uuid  Chatbot UUID (optional, not used for file upload).
	 *
	 * @return bool True if valid.
	 * @throws Exception If validation fails.
	 * @since 1.1.0
	 */
	public function validate( array $config, ?string $chatbot_uuid = null ): bool {
		$config_id = $config['config_id'] ?? null;
		$files     = $config['files'] ?? [];

		// Validate config ID exists
		if ( empty( $config_id ) ) {
			throw new Exception(
				Error_Codes::EMPTY_VALUE,
				__( 'API Key is not selected. Please select an API Key.', 'limb-chatbot' )
			);
		}

		// Validate config object exists and is valid
		$config_obj = Config::find( $config_id );
		if ( ! $config_obj instanceof Config ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'Invalid API Key. Please select a valid API Key.', 'limb-chatbot' )
			);
		}

		// Validate files array exists
		if ( empty( $files ) || ! is_array( $files ) ) {
			throw new Exception(
				Error_Codes::EMPTY_VALUE,
				__( 'No files are selected for upload. Please select at least one file.', 'limb-chatbot' )
			);
		}

		// Validate each file UUID references a valid File object
		$missing_files = [];
		$files_objs    = [];
		foreach ( $files as $file_uuid ) {
			$file = File::find_by_uuid( $file_uuid );
			if ( ! $file instanceof File ) {
				$missing_files[] = $file_uuid;
				continue;
			}
			$files_objs[] = $file;
		}

		if ( ! empty( $missing_files ) ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'One or more selected files are invalid. Please verify your file selection.', 'limb-chatbot' )
			);
		}

		// Provider-specific validation only for knowledge folder files
		$ai_provider = $config_obj->get_related_to_instance();
		if ( ! $ai_provider instanceof File_Upload_Capable_Interface ) {
			throw new Exception(
				Error_Codes::NOT_SUPPORTED,
				__( 'The selected AI Provider does not support file uploads. Please select a different AI Provider.', 'limb-chatbot' )
			);
		}

		$invalid_mime_files = [];
		$oversize_files    = [];
		$provider_id        = method_exists( $ai_provider, 'get_id' ) ? $ai_provider->get_id() : '';
		$max_size           = self::OVERSIZE_LIMITS[ $provider_id ] ?? ( 50 * 1024 * 1024 );

		foreach ( $files_objs as $file ) {
			// Only validate knowledge folder files
			if ( ! str_contains( $file->get_file_path(), Dataset::FILES_KNOWLEDGE_SUB_DIR ) ) {
				continue;
			}

			if ( ! $ai_provider->supports_mime_type( $file->get_mime_type(), File::PURPOSE_KNOWLEDGE ) ) {
				$invalid_mime_files[] = $file->get_original_name();
			}

			if ( $file->get_file_size() > $max_size ) {
				$oversize_files[] = $file->get_original_name();
			}
		}

		if ( $invalid_mime_files || $oversize_files ) {
			$parts = array();
			if ( $invalid_mime_files ) {
				$parts[] = sprintf( __( 'Unsupported file types: %s.', 'limb-chatbot' ), implode( ', ', $invalid_mime_files ) );
			}
			if ( $oversize_files ) {
				$parts[] = sprintf( __( 'Files exceed max size: %s.', 'limb-chatbot' ), implode( ', ', $oversize_files ) );
			}
			throw new Exception( Error_Codes::VALIDATION_INVALID_VALUE, implode( ' ', $parts ) );
		}

		return true;
	}

	/**
	 * Get total number of files to upload.
	 *
	 * Calculates total without actually fetching files.
	 * Essential for large upload 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 {
		$files = $config['files'] ?? [];

		if ( empty( $files ) || ! is_array( $files ) ) {
			throw new Exception(
				Error_Codes::EMPTY_VALUE,
				__( 'No files found for processing.', 'limb-chatbot' )
			);
		}

		$total = count( $files );

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

		return (int) $total;
	}

	/**
	 * Generate a batch of tasks for file upload.
	 *
	 * Creates one task per file UUID. Each task will upload a single file.
	 *
	 * @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 {
		$files     = $config['files'] ?? [];
		$config_id = $config['config_id'] ?? null;

		if ( empty( $files ) || ! is_array( $files ) || empty( $config_id ) ) {
			return 0;
		}

		// Get the slice of file UUIDs for this batch
		$batch_files = array_slice( $files, $offset, $limit );

		$task_count = 0;

		foreach ( $batch_files as $file_uuid ) {
			// Create task payload with file UUID and config ID
			$payload = [
				'uuid'      => $file_uuid,
				'config_id' => (int) $config_id,
			];

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

		return $task_count;
	}

	/**
	 * Process a single task (upload a file).
	 *
	 * Uploads a file to the AI provider using File_Service.
	 * Handles errors and stores them in file metadata.
	 *
	 * 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();
		$file_uuid = $payload['uuid'] ?? null;
		$config_id = $payload['config_id'] ?? null;

		if ( empty( $file_uuid ) || empty( $config_id ) ) {
			// Invalid payload, skip
			return true;
		}

		// Find the file
		$file = File::find_by_uuid( $file_uuid );

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

		// Retrieve configuration
		$config = Config::find( $config_id );
		if ( ! $config instanceof Config ) {
			// Config not found, skip
			return true;
		}

		try {
			// Upload file to provider
			$this->file_service->upload_to_provider( $file, $config );

			return true;
		} catch ( Exception $e ) {
			// Store error in file metadata
			$this->store_file_error( $file, $e );

			// Re-throw if critical error (will be handled by job system)
			if ( $this->is_critical_error( $e ) ) {
				throw $e;
			}

			// Non-critical errors don't fail the task
			return true;
		}
	}

	/**
	 * Store error information in file metadata.
	 *
	 * Extracted from Process::should_pause_process()
	 *
	 * @param  File  $file  The file that encountered an error.
	 * @param  Exception  $e  The exception that was thrown.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function store_file_error( File $file, Exception $e ): void {
		// Get existing errors or initialize empty array
		$existing_errors = $file->get_errors();
		$errors           = is_string( $existing_errors ) ? json_decode( $existing_errors, true ) : [];
		if ( ! is_array( $errors ) ) {
			$errors = [];
		}

		// Determine if error is critical
		$is_critical = $this->is_critical_error( $e );

		// Create error entry with timestamp
		$error_entry = [
			'message'     => $e->getMessage(),
			'error_code'  => $e->get_error_code(),
			'is_critical' => $is_critical,
			'http_status' => method_exists( $e, 'get_http_status' ) ? $e->get_http_status() : null,
			'error_data'  => method_exists( $e, 'get_error_data' ) ? $e->get_error_data() : null,
			'timestamp'   => current_time( 'mysql' ),
		];

		// Add error to array
		$errors[] = $error_entry;

		// Store errors in file metadata
		$file->update_meta( 'errors', wp_json_encode( $errors ) );
		$file->update_meta( 'external_status', File::EXTERNAL_STATUS_FAILED );

		// Set file status to failed if critical error
		if ( $is_critical ) {
			File::update( $file->get_uuid(), [ 'status' => 'failed' ] );
		}
	}

	/**
	 * 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 );
	}
}
