<?php

namespace Limb_Chatbot\Includes\Repositories;

use Limb_Chatbot\Includes\Data_Objects\Task;
use Limb_Chatbot\Includes\Database_Strategies\WPDB;
use Limb_Chatbot\Includes\Services\Data_Object_Collection;

/**
 * Repository for managing Task records.
 *
 * Provides methods to retrieve, create, update, and delete tasks.
 *
 * @since 1.0.9
 */
class Job_Task_Repository {

	/**
	 * Create multiple tasks in batch.
	 *
	 * @param  array  $tasks_data  Array of task data arrays.
	 *
	 * @return int Number of tasks created.
	 * @since 1.1.0
	 */
	public function create_batch( array $tasks_data ): int {
		$count = 0;
		foreach ( $tasks_data as $data ) {
			if ( $this->create( $data ) ) {
				$count ++;
			}
		}

		return $count;
	}

	/**
	 * Create a new task.
	 *
	 * @param  array  $data  Task data.
	 *
	 * @return Task|null
	 * @since 1.1.0
	 */
	public function create( array $data ): ?Task {
		$task = Task::create( $data );

		return $task ?: null;
	}

	/**
	 * Update an existing task.
	 *
	 * @param  Task  $task  Task object to update.
	 * @param  array  $data  Data to update.
	 *
	 * @return null|Task
	 * @since 1.1.0
	 */
	public function update( Task $task, array $data ): ?Task {
		// Always update the updated_at timestamp
		$data['updated_at'] = current_time( 'mysql', true );

		// Ensure payload is encoded
		if ( isset( $data['payload'] ) && is_array( $data['payload'] ) ) {
			$data['payload'] = wp_json_encode( $data['payload'] );
		}

		return $task->update( [ 'id' => $task->get_id() ], $data );
	}

	/**
	 * Delete tasks by job ID.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return bool
	 * @since 1.1.0
	 */
	public function delete_by_job_id( int $job_id ): bool {
		return Task::delete( [ 'job_id' => $job_id ] );
	}

	/**
	 * Delete a task.
	 *
	 * @param  Task  $task  Task to delete.
	 *
	 * @return bool
	 * @since 1.1.0
	 */
	public function delete( Task $task ): bool {
		return Task::delete( [ 'id' => $task->get_id() ] );
	}

	/**
	 * Get tasks by job ID.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $limit  Maximum number of tasks to retrieve.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_by_job_id( int $job_id, int $limit = 100 ): Data_Object_Collection {
		return $this->where( [ 'job_id' => $job_id ], $limit, 1, 'id', 'ASC' );
	}

	/**
	 * Find tasks by criteria.
	 *
	 * @param  array  $where  Where conditions.
	 * @param  int  $per_page  Results per page.
	 * @param  int  $page  Page number.
	 * @param  string  $orderby  Order by field.
	 * @param  string  $order  Order direction (ASC|DESC).
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function where(
		array $where,
		int $per_page = 10,
		int $page = 1,
		string $orderby = 'id',
		string $order = 'ASC'
	): Data_Object_Collection {
		return Task::where( $where, $per_page, $page, $orderby, $order );
	}

	/**
	 * Get pending tasks for a job.
	 *
	 * Prioritizes child tasks - gets pending child tasks first, then parent tasks if no children are pending.
	 * This ensures that after a parent generates child tasks, those children are processed before the next parent.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $limit  Maximum number of tasks to retrieve.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_pending_tasks( int $job_id, int $limit = 10 ): Data_Object_Collection {
		// First, try to get pending child tasks (tasks with parent_task_id IS NOT NULL)
		$tasks = $this->where( [
			'job_id'          => $job_id,
			'parent_task_id>' => 0,
			'status'          => [ Task::STATUS_PENDING, Task::STATUS_PROCESSING ],
		], $limit );

		if ( ! $tasks->is_empty() ) {
			return $tasks;
		}

		// No child tasks pending, get parent tasks (tasks with parent_task_id IS NULL)
		return $this->where( [
			'job_id'         => $job_id,
			'parent_task_id' => null,
			'status'         => [ Task::STATUS_PENDING, Task::STATUS_CHILD_TASK_PENDING ]
		], $limit );
	}

	/**
	 * Get the next pending task for a job.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return Task|null
	 * @since 1.1.0
	 */
	public function get_next_pending_task( int $job_id ): ?Task {
		return $this->first( [
			'job_id' => $job_id,
			'status' => Task::STATUS_PENDING
		] );
	}

	/**
	 * Get the first task matching criteria.
	 *
	 * @param  array  $where  Where conditions.
	 *
	 * @return Task|null
	 * @since 1.1.0
	 */
	public function first( array $where ): ?Task {
		$results = $this->where( $where, 1, 1 );

		return $results->first();
	}

	/**
	 * Get failed tasks for a job.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $limit  Maximum number of tasks to retrieve.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_failed_tasks( int $job_id, int $limit = 100 ): Data_Object_Collection {
		return $this->where(
			[
				'job_id' => $job_id,
				'status' => Task::STATUS_FAILED
			],
			$limit,
			1,
			'id',
			'ASC'
		);
	}

	/**
	 * Get task counts by status for a job.
	 *
	 * Uses a single SQL query for better performance.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return array Array with counts for each status.
	 * @since 1.1.0
	 */
	public function get_task_counts_by_status( int $job_id ): array {
		// Use direct SQL query for better performance with large datasets
		$wpdb       = WPDB::instance()->get_wpdb();
		$table_name = $wpdb->prefix . Task::TABLE_NAME;

		$results = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT 
					COUNT(*) as total,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as pending,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as processing,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as completed,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as failed
				FROM {$table_name}
				WHERE job_id = %d",
				Task::STATUS_PENDING,
				Task::STATUS_PROCESSING,
				Task::STATUS_COMPLETED,
				Task::STATUS_FAILED,
				$job_id
			),
			ARRAY_A
		);

		if ( ! $results ) {
			return [
				'total'      => 0,
				'pending'    => 0,
				'processing' => 0,
				'completed'  => 0,
				'failed'     => 0,
			];
		}

		$total      = (int) ( $results['total'] ?? 0 );
		$pending    = (int) ( $results['pending'] ?? 0 );
		$processing = (int) ( $results['processing'] ?? 0 );
		$completed  = (int) ( $results['completed'] ?? 0 );
		$failed     = (int) ( $results['failed'] ?? 0 );

		// Ensure total matches sum of all statuses (data integrity check)
		$calculated_total = $pending + $processing + $completed + $failed;
		if ( $total !== $calculated_total ) {
			// Use calculated total for accuracy
			$total = $calculated_total;
		}

		return [
			'total'      => $total,
			'pending'    => $pending,
			'processing' => $processing,
			'completed'  => $completed,
			'failed'     => $failed,
		];
	}

	/**
	 * Count tasks by status for a job.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  string  $status  Task status.
	 *
	 * @return int
	 * @since 1.1.0
	 */
	public function count_by_status( int $job_id, string $status ): int {
		return $this->count( [
			'job_id' => $job_id,
			'status' => $status
		] );
	}

	/**
	 * Count tasks matching criteria.
	 *
	 * @param  array  $where  Where conditions.
	 *
	 * @return int
	 * @since 1.1.0
	 */
	public function count( array $where = [] ): int {
		return Task::count( $where ) ?? 0;
	}

	/**
	 * Get completed tasks with duration for calculating average.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $limit  Maximum number of tasks to retrieve.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_completed_tasks_with_duration( int $job_id, int $limit = 100 ): Data_Object_Collection {
		return $this->get_completed_tasks( $job_id, $limit );
	}

	/**
	 * Get completed tasks for a job.
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $limit  Maximum number of tasks to retrieve.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_completed_tasks( int $job_id, int $limit = 100 ): Data_Object_Collection {
		return $this->where(
			[
				'job_id' => $job_id,
				'status' => Task::STATUS_COMPLETED
			],
			$limit,
			1,
			'id',
			'ASC'
		);
	}

	/**
	 * Get child tasks for a specific parent task.
	 *
	 * Gets pending and processing tasks (to handle interrupted tasks).
	 *
	 * @param  int  $job_id  Job ID.
	 * @param  int  $parent_task_id  Parent task ID.
	 *
	 * @return Data_Object_Collection
	 * @since 1.1.0
	 */
	public function get_child_tasks_for_parent( int $job_id, int $parent_task_id ): Data_Object_Collection {
		$tasks    = Task::where( [
			'job_id'         => $job_id,
			'parent_task_id' => $parent_task_id,
			'status'         => [ Task::STATUS_PENDING, Task::STATUS_PROCESSING ],
		], - 1, - 1, 'id', 'ASC' );
		$task_ids = ! $tasks->is_empty() ? $tasks->pluck( 'id' ) : [];

		if ( empty( $task_ids ) ) {
			return new Data_Object_Collection( [] );
		}

		// Load tasks by IDs
		$tasks = [];
		foreach ( $task_ids as $task_id ) {
			$task = Task::find( $task_id );
			if ( $task instanceof Task ) {
				$tasks[] = $task;
			}
		}

		return new Data_Object_Collection( $tasks );
	}

	/**
	 * Find a task by ID.
	 *
	 * @param  int  $task_id  Task ID.
	 *
	 * @return Task|null
	 * @since 1.1.0
	 */
	public function find( int $task_id ): ?Task {
		return Task::find( $task_id );
	}

	/**
	 * Get child task counts by status for a job.
	 *
	 * For multitask handlers, stats should be based on child tasks, not parent tasks.
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return array Array with counts for each status (only child tasks).
	 * @since 1.1.0
	 */
	public function get_child_task_counts_by_status( int $job_id ): array {
		// Use direct SQL query for better performance with large datasets
		$wpdb       = WPDB::instance()->get_wpdb();
		$table_name = $wpdb->prefix . Task::TABLE_NAME;

		$results = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT 
					COUNT(*) as total,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as pending,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as processing,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as completed,
					SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as failed
				FROM {$table_name}
				WHERE job_id = %d AND parent_task_id IS NOT NULL",
				Task::STATUS_PENDING,
				Task::STATUS_PROCESSING,
				Task::STATUS_COMPLETED,
				Task::STATUS_FAILED,
				$job_id
			),
			ARRAY_A
		);

		if ( ! $results ) {
			return [
				'total'      => 0,
				'pending'    => 0,
				'processing' => 0,
				'completed'  => 0,
				'failed'     => 0,
			];
		}

		$total      = (int) ( $results['total'] ?? 0 );
		$pending    = (int) ( $results['pending'] ?? 0 );
		$processing = (int) ( $results['processing'] ?? 0 );
		$completed  = (int) ( $results['completed'] ?? 0 );
		$failed     = (int) ( $results['failed'] ?? 0 );

		// Ensure total matches sum of all statuses (data integrity check)
		$calculated_total = $pending + $processing + $completed + $failed;
		if ( $total !== $calculated_total ) {
			// Use calculated total for accuracy
			$total = $calculated_total;
		}

		return [
			'total'      => $total,
			'pending'    => $pending,
			'processing' => $processing,
			'completed'  => $completed,
			'failed'     => $failed,
		];
	}

	/**
	 * Get parent task completion percentages.
	 *
	 * For each parent task, calculates completion based on its status and child tasks.
	 * Progress calculation:
	 * - Before parent starts (PENDING): 50% of parent's contribution
	 * - After parent generates child tasks (CHILD_TASK_PENDING): 50% + (child completion % * 50%)
	 * - All child tasks complete: 100% of parent's contribution
	 *
	 * Returns array of parent_task_id => completion_percentage (0-100).
	 *
	 * @param  int  $job_id  Job ID.
	 *
	 * @return array Array of parent_task_id => completion_percentage.
	 * @since 1.1.0
	 */
	public function get_parent_task_completion_percentages( int $job_id ): array {
		$wpdb       = WPDB::instance()->get_wpdb();
		$table_name = $wpdb->prefix . Task::TABLE_NAME;
		// Get ALL parent tasks with their statuses, ordered by ID (use -1 to get all, no limit)
		$parent_tasks = Task::where( [ 'job_id' => $job_id, 'parent_task_id' => null ], -1, -1, 'id', 'ASC' );
		if ( $parent_tasks->is_empty() ) {
			return [];
		}
		$completions      = [];
		$parent_task_list = $parent_tasks->get();
		$first_parent_id  = ! empty( $parent_task_list ) ? (int) $parent_task_list[0]->get_id() : null;

		// Track which parents are done (have all children completed)
		$done_parent_ids = [];

		foreach ( $parent_task_list as $parent_task ) {
			$parent_task_id  = (int) $parent_task->get_id();
			$parent_status   = $parent_task->get_status() ?? Task::STATUS_PENDING;
			$is_first_parent = ( $parent_task_id === $first_parent_id );

			// Check if this parent has all children done
			$results = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT 
						COUNT(*) as total,
						SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as pending,
						SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as processing
					FROM {$table_name}
					WHERE job_id = %d AND parent_task_id = %d",
					Task::STATUS_PENDING,
					Task::STATUS_PROCESSING,
					$job_id,
					$parent_task_id
				),
				ARRAY_A
			);

			$total_children      = $results ? (int) ( $results['total'] ?? 0 ) : 0;
			$pending_children    = $results ? (int) ( $results['pending'] ?? 0 ) : 0;
			$processing_children = $results ? (int) ( $results['processing'] ?? 0 ) : 0;

			// Parent is done if it has children and all are completed/failed (no pending/processing)
			// OR if it has no children and status is not PENDING
			if ( $total_children > 0 && $pending_children === 0 && $processing_children === 0 ) {
				$done_parent_ids[] = $parent_task_id;
			} elseif ( $total_children === 0 && $parent_status !== Task::STATUS_PENDING ) {
				$done_parent_ids[] = $parent_task_id;
			}
		}

		// Now calculate completions
		foreach ( $parent_task_list as $parent_task ) {
			$parent_task_id  = (int) $parent_task->get_id();
			$parent_status   = $parent_task->get_status() ?? Task::STATUS_PENDING;
			$is_first_parent = ( $parent_task_id === $first_parent_id );

			// Check if all previous parents (by ID) are done
			$all_previous_done = true;
			foreach ( $parent_task_list as $prev_parent ) {
				$prev_id = (int) $prev_parent->get_id();
				if ( $prev_id < $parent_task_id && ! in_array( $prev_id, $done_parent_ids, true ) ) {
					$all_previous_done = false;
					break;
				}
			}

			// Get child task counts for this parent (including failed tasks)
			$results = $wpdb->get_row(
				$wpdb->prepare(
					"SELECT 
						COUNT(*) as total,
						SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as completed,
						SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) as failed
					FROM {$table_name}
					WHERE job_id = %d AND parent_task_id = %d",
					Task::STATUS_COMPLETED,
					Task::STATUS_FAILED,
					$job_id,
					$parent_task_id
				),
				ARRAY_A
			);

			$total_children = $results ? (int) ( $results['total'] ?? 0 ) : 0;
			$completed      = $results ? (int) ( $results['completed'] ?? 0 ) : 0;
			$failed         = $results ? (int) ( $results['failed'] ?? 0 ) : 0;
			$processed      = $completed + $failed; // Count both completed and failed as processed

			// Calculate completion percentage for this parent (0-100%)
			// Logic: 50% before parent starts (PENDING), 50% when parent generates children,
			// remaining 50% distributed among children as they complete
			// 
			// Example with 2 parents (each contributes 50% of total):
			// - Before parent 1 starts: 50% of parent 1's contribution = 25% total
			// - After parent 1 generates children: still 50% = 25% total
			// - As children complete: 50% + (child_completion * 50%) = 25% to 50% total
			// - Before parent 2 starts: 50% (parent 1 done) + 25% (50% of parent 2) = 75% total

			if ( $parent_status === Task::STATUS_PENDING ) {
				// Before parent starts
				// For the first parent, show 50% (which becomes 25% of total for 2 parents)
				// This ensures progress shows 25% before dataset generation starts
				// For subsequent parents, show 50% if all previous parents are done (about to start)
				// Otherwise show 0% until they start
				if ( $is_first_parent ) {
					$completions[ $parent_task_id ] = 50.0;
				} elseif ( $all_previous_done ) {
					// All previous parents are done, so this parent is about to start
					$completions[ $parent_task_id ] = 50.0;
				} else {
					$completions[ $parent_task_id ] = 0.0;
				}
			} elseif ( $parent_status === Task::STATUS_PROCESSING ) {
				// Parent is generating child tasks - 50% (same as before starting)
				$completions[ $parent_task_id ] = 50.0;
			} elseif ( $parent_status === Task::STATUS_CHILD_TASK_PENDING || $total_children > 0 ) {
				// Parent has generated child tasks
				if ( $total_children > 0 ) {
					// 50% base (for starting) + (child completion % * 50%)
					// Each child contributes to the remaining 50% of the parent's contribution
					$child_completion_percent       = ( $processed / $total_children ) * 100.0;
					$completion                     = 50.0 + ( $child_completion_percent * 0.5 );
					$completions[ $parent_task_id ] = round( min( 100.0, max( 50.0, $completion ) ), 2 );
				} else {
					// No child tasks yet but status indicates generation is done - 50%
					$completions[ $parent_task_id ] = 50.0;
				}
			} elseif ( $parent_status === Task::STATUS_FAILED ) {
				// Parent task failed
				if ( $total_children > 0 ) {
					// Parent failed but had child tasks - count child processing
					// 50% for starting + (child completion % * 50%)
					$child_completion_percent       = ( $processed / $total_children ) * 100.0;
					$completion                     = 50.0 + ( $child_completion_percent * 0.5 );
					$completions[ $parent_task_id ] = round( min( 100.0, max( 50.0, $completion ) ), 2 );
				} else {
					// Parent failed without generating child tasks - count as 100%
					// Even though it failed, it was attempted and is complete from progress perspective
					$completions[ $parent_task_id ] = 100.0;
				}
			} elseif ( $parent_status === Task::STATUS_COMPLETED ) {
				// Parent completed (shouldn't happen for multitask, but handle it)
				$completions[ $parent_task_id ] = 100.0;
			} else {
				// Unknown status - default to 50% (before starting)
				$completions[ $parent_task_id ] = 50.0;
			}
		}

		return $completions;
	}
}

