<?php

namespace WPQT\Task;

if (!defined('ABSPATH')) {
    exit;
}

use WPQT\PipelineMissingException;
use WPQT\Services\ServiceLocator;
use WPQT\TaskMissingException;
use WPQT\WPQTException;

if (!class_exists('WPQT\Task\TaskService')) {
    class TaskService
    {
        /**
         * Gets the next task order for a stage.
         *
         * @param int $stageId The ID of the stage.
         * @return int The next task order.
         */
        public function getNextTaskOrder($stageId)
        {
            global $wpdb;

            $result = $wpdb->get_var(
                $wpdb->prepare(
                    'SELECT MAX(task_order) FROM ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . ' WHERE stage_id = %d AND is_archived = 0',
                    $stageId
                )
            );

            return null === $result ? 0 : $result + 1;
        }

        /**
         * Creates a task.
         *
         * @param int|null $stageId The ID of the stage to add the task to.
         * @param array $args The task data.
         * @return object The created task.
         * @throws Exception If required fields are missing or if failed to create task.
         */
        public function createTask($stageId, $args)
        {
            global $wpdb;

            $defaults = [
                'description'          => null,
                'task_focus_color'     => null,
                'due_date'             => null,
                'is_archived'          => 0,
                'task_completed_at'    => null,
                'is_done'              => 0,
                'is_public_submission' => 0,
                'created_by_id'        => null,
                'created_by_type'      => null
            ];

            $args = wp_parse_args($args, $defaults);

            if (!isset($args['name'])) {
                throw new \Exception('createTask required fields are missing');
            }

            $result = $wpdb->insert(TABLE_WP_QUICKTASKER_TASKS, [
                'name'                 => $args['name'],
                'description'          => $args['description'],
                'pipeline_id'          => $args['pipelineId'],
                'task_hash'            => ServiceLocator::get('HashService')->generateTaskHash($args['name']),
                'created_at'           => ServiceLocator::get('TimeRepository')->getCurrentUTCTime(),
                'updated_at'           => ServiceLocator::get('TimeRepository')->getCurrentUTCTime(),
                'task_focus_color'     => $args['task_focus_color'],
                'due_date'             => $args['due_date'],
                'is_archived'          => $args['is_archived'],
                'task_completed_at'    => $args['task_completed_at'],
                'is_done'              => $args['is_done'],
                'is_public_submission' => $args['is_public_submission'],
                'created_by_id'        => $args['created_by_id'],
                'created_by_type'      => $args['created_by_type']
            ]);

            if (false === $result) {
                throw new \Exception('Failed to create a task');
            }

            $taskId = $wpdb->insert_id;

            $taskOrder = $stageId ? $this->getNextTaskOrder($stageId) : 0;

            $this->addTaskLocation($taskId, $stageId, $taskOrder, $args['is_archived']);

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId);
        }

        /**
         * Adds a task order to the database.
         *
         * @param int $taskId The ID of the task.
         * @param int|null $stageId The ID of the stage.
         * @param int $taskOrder The order of the task.
         * @param int $isArchived Whether the task is archived (default is 0).
         * @return int The ID of the inserted task order.
         * @throws Exception If failed to add task order.
         */
        public function addTaskLocation($taskId, $stageId, $taskOrder, $isArchived = 0)
        {
            global $wpdb;

            $result = $wpdb->insert(TABLE_WP_QUICKTASKER_TASKS_LOCATION, [
                'task_id'     => $taskId,
                'stage_id'    => $stageId,
                'task_order'  => $taskOrder,
                'is_archived' => $isArchived,
                'created_at'  => ServiceLocator::get('TimeRepository')->getCurrentUTCTime(),
                'updated_at'  => ServiceLocator::get('TimeRepository')->getCurrentUTCTime()
            ]);

            if (false !== $result) {
                return $wpdb->insert_id;
            } else {
                throw new \Exception('Failed to add task order');
            }
        }

        /**
         * Moves a task to a new stage and updates its order within the stage.
         *
         * @param int $taskId The ID of the task to be moved.
         * @param int $newStageId The ID of the new stage to move the task to.
         * @param int $newOrder The new order of the task within the stage.
         * @return object|null The updated task object, or null on failure.
         * @throws Exception If the task is not found or if the task move fails.
         */
        public function moveTask($taskId, $newStageId, $newOrder)
        {
            global $wpdb;

            $currentTask = ServiceLocator::get('TaskRepository')->getTaskById($taskId);

            if (!$currentTask) {
                throw new WPQTException('Task not found');
            }
            $currentStageId = $currentTask->stage_id;
            $currentOrder = $currentTask->task_order;
            $stageChanged = $currentStageId != $newStageId;

            if (null === $currentOrder) {
                $this->updateTaskOrderAfterTaskRestore($newStageId, $newOrder);
            } elseif (!$stageChanged) {
                $this->updateTaskOrderWithinStage($currentStageId, $currentOrder, $newOrder);
            } else {
                $this->updateTaskOrderAcrossStages($currentStageId, $currentOrder, $newStageId, $newOrder);
            }

            $locationUpdated = $wpdb->update(
                TABLE_WP_QUICKTASKER_TASKS_LOCATION,
                [
                    'stage_id'   => $newStageId,
                    'task_order' => $newOrder,
                    'updated_at' => ServiceLocator::get('TimeRepository')->getCurrentUTCTime()
                ],
                [
                    'task_id' => $taskId
                ],
                [
                    '%d',
                    '%d',
                    '%s'
                ]
            );

            if (false === $locationUpdated) {
                throw new WPQTException('Failed to move the task');
            }

            return (object) [
                'oldStageId'   => $currentStageId,
                'newStageId'   => $newStageId,
                'stageChanged' => $stageChanged,
                'task'         => ServiceLocator::get('TaskRepository')->getTaskById($taskId)
            ];
        }

        private function updateTaskOrderAfterTaskRestore($stageId, $newOrder)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();

            $result = $wpdb->query(
                $wpdb->prepare(
                    'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                    SET task_order = task_order + 1, updated_at = %s
                    WHERE stage_id = %d AND task_order >= %d AND is_archived = 0',
                    $utcTime,
                    $stageId,
                    $newOrder
                )
            );

            if (false === $result) {
                throw new \Exception('Failed to update task order after task restore');
            }

            return $result;
        }

        /**
         * Updates the task order within a stage.
         *
         * @param int $stageId The ID of the stage.
         * @param int $currentOrder The current task order. Archived tasks will have this as null.
         * @param int $newOrder The new task order.
         * @throws \Exception If the current order is null or if the update fails.
         * @return void
         */
        private function updateTaskOrderWithinStage($stageId, $currentOrder, $newOrder)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();

            if (null === $currentOrder) {
                throw new \Exception('Current order cannot be null when updating task order within a stage');
            }

            if ($currentOrder < $newOrder) {
                // Moving down within the same stage
                $result = $wpdb->query(
                    $wpdb->prepare(
                        'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                        SET task_order = task_order - 1, updated_at = %s
                        WHERE stage_id = %d AND task_order > %d AND task_order <= %d AND is_archived = 0',
                        $utcTime,
                        $stageId,
                        $currentOrder,
                        $newOrder
                    )
                );
            } else {
                // Moving up within the same stage
                $result = $wpdb->query(
                    $wpdb->prepare(
                        'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                        SET task_order = task_order + 1, updated_at = %s
                        WHERE stage_id = %d AND task_order < %d AND task_order >= %d AND is_archived = 0',
                        $utcTime,
                        $stageId,
                        $currentOrder,
                        $newOrder
                    )
                );
            }

            if (false === $result) {
                throw new \Exception('Failed to update task order within stage');
            }
        }

        /**
         * Updates the task order across stages.
         *
         * If task location changed to different stage, it will decrement the task order of tasks in the current stage
         * and increment the task order of tasks in the new stage.
         *
         * @param int $currentStageId The ID of the current stage.
         * @param int $currentOrder The current task order.
         * @param int $newStageId The ID of the new stage.
         * @param int $newOrder The new task order.
         * @return void
         */
        private function updateTaskOrderAcrossStages($currentStageId, $currentOrder, $newStageId, $newOrder)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();

            // Decrement the task order of tasks in the current stage
            $result1 = $wpdb->query(
                $wpdb->prepare(
                    'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                    SET task_order = task_order - 1, updated_at = %s
                    WHERE stage_id = %d AND task_order > %d AND is_archived = 0',
                    $utcTime,
                    $currentStageId,
                    $currentOrder
                )
            );

            if (false === $result1) {
                throw new \Exception('Failed to decrement task order in current stage');
            }

            // Increment the task order of tasks in the new stage
            $result2 = $wpdb->query(
                $wpdb->prepare(
                    'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                    SET task_order = task_order + 1, updated_at = %s
                    WHERE stage_id = %d AND task_order >= %d AND is_archived = 0',
                    $utcTime,
                    $newStageId,
                    $newOrder
                )
            );

            if (false === $result2) {
                throw new \Exception('Failed to increment task order in new stage');
            }
        }

        /**
         * Edit a task.
         *
         * @param int $taskId The ID of the task to edit.
         * @param array $args The arguments to update the task with.
         * @return object The updated task.
         * @throws Exception If there is an error editing the task.
         */
        public function editTask($taskId, $args)
        {
            global $wpdb;

            // Handle null values for due_date
            if (isset($args['due_date']) && (null === $args['due_date'] || '' === $args['due_date'])) {
                $args['due_date'] = null;
            }

            $defaults = [
                'updated_at' => ServiceLocator::get('TimeRepository')->getCurrentUTCTime()
            ];

            $args = wp_parse_args($args, $defaults);

            $result = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS, $args, ['id' => $taskId]);

            if (false === $result) {
                throw new \Exception('Failed to edit the task');
            }

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId, true);
        }

        /**
         * Deletes a task.
         *
         * @param int $taskId The ID of the task to delete.
         * @return object The deleted task.
         * @throws Exception If there is an error deleting the task.
         */
        public function deleteTask($taskId)
        {
            global $wpdb;

            $taskToDelete = ServiceLocator::get('TaskRepository')->getTaskById($taskId);
            $result = $wpdb->delete(TABLE_WP_QUICKTASKER_TASKS, ['id' => $taskId]);
            ServiceLocator::get('CommentService')->deleteCommentsByTypeIdAndType($taskId, WP_QUICKTASKER_COMMENT_TYPE_TASK);

            if (false === $result) {
                throw new \Exception('Failed to delete the task');
            }

            $this->shiftTaskOrder($taskToDelete->task_order, $taskToDelete->stage_id);

            return $taskToDelete;
        }

        /**
         * Deletes tasks by their IDs.
         *
         * @param array $taskIds The IDs of the task to delete.
         * @return int The number of rows affected.
         * @throws \Exception If the deletion fails
         */
        public function deleteTasksByTaskIds($taskIds)
        {
            global $wpdb;

            if (empty($taskIds)) {
                return 0;
            }

            $taskIds = (array) $taskIds;
            $placeholders = implode(',', array_fill(0, count($taskIds), '%d'));

            $result = $wpdb->query(
                $wpdb->prepare(
                    'DELETE FROM ' . TABLE_WP_QUICKTASKER_TASKS . " WHERE id IN ($placeholders)",
                    $taskIds
                )
            );

            if (false === $result) {
                throw new \Exception('Failed to delete the tasks');
            }

            return $result;
        }

        /**
         * Archives a task by setting its 'is_archived' status to 1 and updating the 'updated_at' timestamp.
         * This method updates both the main task table and the task location table.
         *
         * @param int $taskId The ID of the task to be archived.
         *
         * @throws \Exception If the task or task location update fails.
         *
         * @return object The updated task object after archiving.
         */
        public function archiveTask($taskId)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();
            $taskToArchive = ServiceLocator::get('TaskRepository')->getTaskById($taskId);
            $result = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS, ['is_archived' => 1, 'updated_at' => $utcTime], ['id' => $taskId]);

            if (false === $result) {
                throw new \Exception('Failed to archive task');
            }

            $result2 = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS_LOCATION, ['is_archived' => 1, 'updated_at' => $utcTime, 'task_order' => null], ['task_id' => $taskId]);

            if (false === $result2) {
                throw new \Exception('Failed to archive task location');
            }

            $this->shiftTaskOrder($taskToArchive->task_order, $taskToArchive->stage_id);

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId);
        }

        public function restoreArchivedTask($taskId, $pipelineId)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();
            $task = ServiceLocator::get('TaskRepository')->getTaskById($taskId);

            if (null === $task) {
                throw new WPQTException('Task not found', true);
            }

            $pipeline = ServiceLocator::get('PipelineRepository')->getPipelineById($pipelineId);

            if (!$pipeline) {
                throw new WPQTException('Board not found', true);
            }

            $pipelineStages = ServiceLocator::get('StageRepository')->getStagesByPipelineId($pipeline->id);

            if (empty($pipelineStages)) {
                throw new WPQTException(esc_html__('Cannot restore the task. The selected board needs at least one stage.', 'quicktasker'), true);
            }

            $result = $wpdb->update(
                TABLE_WP_QUICKTASKER_TASKS,
                [
                    'is_archived' => 0,
                    'updated_at'  => $utcTime,
                    'pipeline_id' => $pipelineId
                ],
                ['id' => $taskId]
            );

            if (false === $result) {
                throw new WPQTException('Failed to restore the task');
            }

            $result2 = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS_LOCATION, ['is_archived' => 0, 'updated_at' => $utcTime], ['task_id' => $taskId]);

            if (false === $result2) {
                throw new WPQTException('Failed to restore task location');
            }

            $this->moveTask($taskId, $pipelineStages[0]->id, 0);

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId);
        }

        /**
         * Deletes archived tasks that do not belong to any pipeline.
         *
         * This method retrieves orphaned archived tasks and deletes them from both the main tasks table
         * and the tasks location table. It returns an array of deleted task IDs.
         *
         * @return array An array of deleted task IDs.
         * @throws \Exception If the deletion fails.
         */
        public function deleteArchivedTasksWithoutPipeline()
        {
            global $wpdb;

            $orphanedTaks = ServiceLocator::get('TaskRepository')->getOrphanedArchivedTasks();

            if (empty($orphanedTaks)) {
                return [];
            }

            $taskIds = wp_list_pluck($orphanedTaks, 'id');
            $taskIdsPlaceholders = implode(',', array_fill(0, count($taskIds), '%d'));

            // Delete tasks from the main tasks table
            $result = $wpdb->query(
                $wpdb->prepare(
                    'DELETE FROM ' . TABLE_WP_QUICKTASKER_TASKS . " WHERE id IN ($taskIdsPlaceholders) AND is_archived = 1",
                    $taskIds
                )
            );
            if (false === $result) {
                throw new \Exception('Failed to delete orphaned archived tasks');
            }

            // Delete tasks from the tasks location table
            $result2 = $wpdb->query(
                $wpdb->prepare(
                    'DELETE FROM ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . " WHERE task_id IN ($taskIdsPlaceholders) AND is_archived = 1",
                    $taskIds
                )
            );

            if (false === $result2) {
                throw new \Exception('Failed to delete orphaned archived tasks location');
            }

            return $taskIds;
        }

        /**
         * Shifts the task order after a task is deleted or archived.
         *
         * @param int $deletedTaskOrder The order of the deleted task.
         * @param int $stageId The ID of the stage.
         * @return int The number of rows affected by the update query.
         * @throws Exception If the update query fails.
         */
        private function shiftTaskOrder($deletedTaskOrder, $stageId)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();
            $result = $wpdb->query(
                $wpdb->prepare(
                    'UPDATE ' . TABLE_WP_QUICKTASKER_TASKS_LOCATION . '
                    SET task_order = task_order - 1, updated_at = %s
                    WHERE stage_id = %d AND task_order > %d AND is_archived = 0',
                    $utcTime,
                    $stageId,
                    $deletedTaskOrder
                )
            );

            if (false === $result) {
                throw new \Exception('Failed to shift tasks order');
            }

            return $result;
        }

        /**
         * Changes the completion status of a task.
         *
         * @param int $taskId The ID of the task to update.
         * @param bool $isDone The new completion status of the task.
         * @return array The updated task data.
         * @throws \Exception If the task status update fails.
         */
        public function changeTaskDoneStatus($taskId, $isDone)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();
            $data = [
                'is_done'           => $isDone,
                'task_completed_at' => $isDone ? $utcTime : null
            ];
            $where = ['id' => $taskId];
            $format = ['%d', $isDone ? '%s' : '%d'];
            $where_format = ['%d'];

            $result = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS, $data, $where, $format, $where_format);

            if (false === $result) {
                throw new \Exception('Failed to change task completed status');
            }

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId);
        }

        /**
         * Updates the focus color of a task.
         *
         * @param int $taskId The ID of the task to update.
         * @param string $color The new focus color for the task.
         * @return object The updated task object.
         * @throws \Exception If the update fails.
         */
        public function updateTaskFocusColor($taskId, $color)
        {
            global $wpdb;

            $utcTime = ServiceLocator::get('TimeRepository')->getCurrentUTCTime();
            $result = $wpdb->update(TABLE_WP_QUICKTASKER_TASKS, ['task_focus_color' => $color, 'updated_at' => $utcTime], ['id' => $taskId]);

            if (false === $result) {
                throw new \Exception('Failed to update task focus color');
            }

            return ServiceLocator::get('TaskRepository')->getTaskById($taskId);
        }

        /**
         * Validates if a task belongs to a pipeline and if the pipeline exists.
         *
         * @param int $taskId The ID of the task to validate.
         * @param int $pipelineId The ID of the pipeline to validate against.
         * @return bool True if the task belongs to the pipeline, false otherwise.
         * @throws TaskMissingException If the task does not exist.
         * @throws PipelineMissingException If the pipeline does not exist.
         * @throws Exception If the task does not belong to the pipeline.
         */
        public function validateTaskAndPipeline($taskId, $pipelineId)
        {
            $task = ServiceLocator::get('TaskRepository')->getTaskById($taskId);
            $pipelineExists = ServiceLocator::get('PipelineRepository')->checkIfPipelineExists($pipelineId);

            if ($pipelineExists && !$task) {
                throw new TaskMissingException("Task with id {$taskId} not found");
            }

            if (!$pipelineExists) {
                throw new PipelineMissingException('No pipeline found with id ' . $pipelineId);
            }

            if ((int) $task->pipeline_id !== (int) $pipelineId) {
                throw new \Exception('Task with id ' . $taskId . ' does not belong to pipeline with id ' . $pipelineId);
            }

            return true;
        }
    }
}
