<?php

namespace Limb_Chatbot\Includes\Services\Jobs\Managers;

use Limb_Chatbot\Includes\Data_Objects\Dataset;
use Limb_Chatbot\Includes\Data_Objects\Dataset_Entry;
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\Job_Handler_Interface;
use Limb_Chatbot\Includes\Interfaces\Multitask_Handler_Interface;

/**
 * Job Error Handler
 *
 * Handles error creation, classification, and job state management based on errors.
 *
 * @since 1.1.0
 */
class Job_Error_Handler {

	/**
	 * Error type constants.
	 *
	 * @since 1.1.0
	 */
	const ERROR_TYPE_CRITICAL = 'critical'; // Process stops/pauses
	const ERROR_TYPE_WARNING = 'warning';  // Process continues, user should be aware
	const ERROR_TYPE_ERROR = 'error';    // Error occurred but process continues (handled)

	/**
	 * Handle task processing error.
	 *
	 * @param  Task  $task  Task that failed.
	 * @param  Exception  $exception  Exception that occurred.
	 * @param  Job_Handler_Interface|Multitask_Handler_Interface  $handler  Job handler.
	 * @param  Job  $job  Job instance.
	 *
	 * @return void
	 * @since 1.1.0
	 */
	public function handle_task_error(
		Task $task,
		Exception $exception,
		$handler,
		Job $job
	): void {
		// Determine if error is critical (stops process)
		$is_critical = $handler->is_critical_error( $exception );

		// Determine error type - use WARNING for non-critical errors that should be logged
		// but don't stop processing (like "No content found")
		$error_type = $is_critical ? self::ERROR_TYPE_CRITICAL : self::ERROR_TYPE_WARNING;

		// Create structured error
		$error = $this->create_error( $exception );

		// Add error to job (for both warnings and critical errors)
		$job_errors = $job->get_errors() ?? [];
		if ( ! $this->has_duplicate_error( $job_errors, $error ) ) {
			$job->add_error( $error );
			$job->save();
		}
		$this->append_error_to_related_dataset( $task, $job, $error );

		// Only pause job if it's a critical error
		if ( $is_critical ) {
			$this->handle_critical_error( $job, $error );
		}

		// Handle task failure
		$this->handle_task_failure( $task, $error );
	}

	/**
	 * Create a standardized error structure.
	 *
	 * All errors follow the same structure for consistency and easier handling.
	 *
	 * @return array Structured error array.
	 * @since 1.1.0
	 */
	public function create_error( \Throwable $e ): array {
		$error_code  = method_exists( $e, 'get_error_code' ) ? $e->get_error_code() : $e->getCode();
		$message     = $e->getMessage();
		$type        = Job_Error_Handler::ERROR_TYPE_WARNING;
		$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;
		// Validate error type
		$valid_types = [ self::ERROR_TYPE_CRITICAL, self::ERROR_TYPE_WARNING, self::ERROR_TYPE_ERROR ];
		if ( ! in_array( $type, $valid_types, true ) ) {
			$type = self::ERROR_TYPE_ERROR;
		}

		$error = [
			'error_code'  => $error_code,
			'message'     => $message,
			'type'        => $type,
			'is_critical' => $type === self::ERROR_TYPE_CRITICAL,
			'warning'     => $type != self::ERROR_TYPE_CRITICAL || self::ERROR_TYPE_WARNING,
			'timestamp'   => current_time( 'mysql', true ),
		];

		// Add optional fields if provided
		if ( $http_status !== null ) {
			$error['http_status'] = $http_status;
		}

		if ( $error_data !== null ) {
			$error['error_data'] = $error_data;
		}

		return $error;
	}

	/**
	 * Persist the same task error to related dataset meta when possible.
	 *
	 * This guarantees dataset-level visibility for failures from all dataset jobs
	 * (manual generation, sync, regenerate, and child indexing tasks).
	 *
	 * @param  Task   $task   Failed task.
	 * @param  Job    $job    Parent job.
	 * @param  array  $error  Structured error payload.
	 *
	 * @return void
	 * @since 1.1.0
	 */
	public function append_error_to_related_dataset( Task $task, Job $job, array $error ): void {
		$dataset = $this->resolve_dataset_from_task( $task, $job );
		if ( ! $dataset instanceof Dataset ) {
			return;
		}

		$dataset_error = array_merge(
			[
				'stage'     => 'job_task',
				'job_type'  => $job->get_type(),
				'job_id'    => $job->get_id(),
				'task_id'   => $task->get_id(),
				'timestamp' => current_time( 'mysql', true ),
			],
			$error
		);

		$errors = $dataset->get_errors();
		if ( $this->has_duplicate_error( $errors, $dataset_error ) ) {
			return;
		}

		$errors[] = $dataset_error;
		$dataset->update_meta( 'errors', wp_json_encode( $errors ) );
	}

	/**
	 * Check if the same logical error already exists.
	 *
	 * Uses a stable fingerprint that ignores volatile fields (timestamps, stage, task IDs)
	 * so repeated reporting of the same root cause is stored once.
	 *
	 * @param  array  $errors   Existing errors.
	 * @param  array  $error    Candidate error.
	 *
	 * @return bool
	 * @since 1.1.0
	 */
	private function has_duplicate_error( array $errors, array $error ): bool {
		$candidate_fingerprint = $this->build_error_fingerprint( $error );

		foreach ( $errors as $existing_error ) {
			if ( ! is_array( $existing_error ) ) {
				continue;
			}

			if ( $this->build_error_fingerprint( $existing_error ) === $candidate_fingerprint ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Build a stable, comparable fingerprint for an error payload.
	 *
	 * @param  array  $error
	 *
	 * @return string
	 * @since 1.1.0
	 */
	private function build_error_fingerprint( array $error ): string {
		$payload = [
			'error_code'  => (string) ( $error['error_code'] ?? '' ),
			'message'     => trim( (string) ( $error['message'] ?? '' ) ),
			'type'        => (string) ( $error['type'] ?? '' ),
			'http_status' => isset( $error['http_status'] ) ? (string) $error['http_status'] : '',
			'error_data'  => isset( $error['error_data'] ) ? wp_json_encode( $error['error_data'] ) : '',
		];

		return md5( wp_json_encode( $payload ) );
	}

	/**
	 * Try to resolve the dataset related to the task payload.
	 *
	 * @param  Task  $task  Task that failed.
	 * @param  Job   $job   Parent job.
	 *
	 * @return Dataset|null
	 * @since 1.1.0
	 */
	private function resolve_dataset_from_task( Task $task, Job $job ): ?Dataset {
		$dataset_job_types = [
			Job::TYPE_DATASET_GENERATING,
			Job::TYPE_DATASET_SYNC,
			Job::TYPE_DATASET_REGENERATE,
		];

		if ( ! in_array( $job->get_type(), $dataset_job_types, true ) ) {
			return null;
		}

		$payload = $task->get_payload() ?? [];

		if ( ! empty( $payload['dataset_id'] ) ) {
			$dataset = Dataset::find( (int) $payload['dataset_id'] );
			if ( $dataset instanceof Dataset ) {
				return $dataset;
			}
		}

		if ( ! empty( $payload['dataset_entry_id'] ) ) {
			$entry = Dataset_Entry::find( (int) $payload['dataset_entry_id'] );
			if ( $entry instanceof Dataset_Entry ) {
				$dataset = $entry->dataset();
				if ( $dataset instanceof Dataset ) {
					return $dataset;
				}
			}
		}

		$parent_task_id = $task->get_parent_task_id();
		if ( empty( $parent_task_id ) ) {
			return null;
		}

		$parent_task = Task::find( (int) $parent_task_id );
		if ( ! $parent_task instanceof Task ) {
			return null;
		}

		$parent_payload = $parent_task->get_payload() ?? [];
		if ( empty( $parent_payload['dataset_id'] ) ) {
			return null;
		}

		$dataset = Dataset::find( (int) $parent_payload['dataset_id'] );

		return $dataset instanceof Dataset ? $dataset : null;
	}

	/**
	 * Handle critical error that pauses the job.
	 *
	 * @param  Job  $job  Job to pause.
	 * @param  array  $error  Error information.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function handle_critical_error( Job $job, array $error ): void {
		$job->set_status( Job::STATUS_PAUSED );
		$job->save();
	}

	/**
	 * Handle task failure.
	 *
	 * @param  Task  $task  Task that failed.
	 * @param  array  $error  Error information.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.1.0
	 */
	private function handle_task_failure( Task $task, array $error ): void {
		// Mark task as failed if max attempts exceeded
		if ( $task->has_exceeded_max_attempts() ) {
			$task->mark_failed( $error );
			$task->save();
		} else {
			// Reset task to pending for retry and store error for reference
			$task->set_status( Task::STATUS_PENDING );
			$task->set_error( $error );
			$task->save();
		}
	}
}
