<?php
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Plugin uses custom tables and controlled internal query builders.
namespace SPAATG\Controller\Queue;

if ( ! defined( 'ABSPATH' ) ) {
 exit; // Exit if accessed directly.
}

use SPAATG\Model\Image\ImageModel as ImageModel;
use SPAATG\ShortPixelLogger\ShortPixelLogger as Log;
use SPAATG\Controller\CacheController as CacheController;
use SPAATG\Controller\Optimizer\OptimizeAiController;
use SPAATG\Controller\ResponseController as ResponseController;
use SPAATG\Controller\Queue\QueueItems as QueueItems;
use SPAATG\Model\Queue\QueueItem as QueueItem;


use SPAATG\Helper\UiHelper as UiHelper;
use SPAATG\Model\AiDataModel;
use SPAATG\Controller\Optimizer\OptimizerBase;
use SPAATG\ShortQ\ShortQ as ShortQ;

abstract class Queue
{
    protected $q;
//    protected static $instance;
    protected static $results;
    protected static $isInQueue = [];

    const PLUGIN_SLUG = 'SPAATG';

    // Result status for Run function
    const RESULT_ITEMS = 1;
    const RESULT_PREPARING = 2;
    const RESULT_PREPARING_OVERLIMIT = 5;
    const RESULT_PREPARING_DONE = 3;
    const RESULT_EMPTY = 4;
    const RESULT_QUEUE_EMPTY = 10;
    const RESULT_RECOUNT = 11;
    const RESULT_ERROR = -1;
    const RESULT_UNKNOWN = -10;


    abstract protected function prepare();
    abstract protected function prepareBulkRestore();
    abstract protected function prepareUndoAI();
    abstract public function getType();
    abstract protected function getFilterQueryData();

    protected $queueName = '';
    protected $cacheName;
    protected $options = [];


    public function createNewBulk($args = [])
    {
				$this->resetQueue();

        $this->q->setStatus('preparing', true, false);
				$this->q->setStatus('finished', false, false);
        $this->q->setStatus('bulk_running', true, true);

        $cache = new CacheController();
        $cache->deleteItem($this->cacheName);

        $this->setBulkOptions($args);
        return $args;
    }

    public function startBulk()
    {
        $this->q->setStatus('preparing', false, false);
        $this->q->setStatus('running', true, true);
    }

    public function cleanQueue()
    {
       $this->q->cleanQueue();
    }

    public function resetQueue()
		{
			$this->q->resetQueue();
		}

    // gateway to set custom options for queue.
    public function setOptions($options)
    {
        return $this->q->setOptions($options);
    }

    /** Enqueues a single items into the urgent queue list
    *   - Should not be used for bulk images
    *   @todo This function is deprecated.
	    * @param ImageModel $mediaItem Media library image model.
    * @return mixed
    */
    public function addSingleItem(ImageModel $imageModel, $args = array())
    {

       $defaults = array(
          'forceExclusion' => false,
	          'action' => 'requestAlt',
          'remote_id' => null, // for retrieveAltAction
       );
       $args = wp_parse_args($args, $defaults);

       $qItem = QueueItems::getImageItem($imageModel);

			 $result = new \stdClass;

       $this->q->addItems([$qItem->returnEnqueue()], false);
       $numitems = $this->q->withRemoveDuplicates()->enqueue(); // enqueue returns numitems

       $this->checkQueueCache($imageModel->get('id'));

       $result->qstatus = $this->getQStatus($numitems);
       $result->numitems = $numitems;

	      do_action('spaatg_start_ai_generation', $imageModel->get('id'), $imageModel);
       return $result;
    }

    public function addQueueItem(QueueItem $qItem)
    {
      $this->q->addItems([$qItem->returnEnqueue()], false);
      $item_id = $qItem->item_id;
      $numitems = $this->q->withRemoveDuplicates()->enqueue(); // enqueue returns numitems

      $result = new \stdClass;
      $result->qstatus = $this->getQStatus($numitems);
      $result->numitems = $numitems;

      $this->checkQueueCache($item_id);


	      do_action('spaatg_start_ai_generation', $item_id, $qItem->imageModel);
      return $result;
    }

		/** Drop Item if it needs dropping. This can be needed in case of image alteration and it's in the queue */
		public function dropItem($item_id)
		{
				$this->q->removeItems(array(
						'item_id' => $item_id,
				));

		}


    public function run()
    {
       $result = new \stdClass();
       $result->qstatus = self::RESULT_UNKNOWN;
       $result->items = null;

       $custom_operation = $this->getCustomDataItem('customOperation');

       if ( $this->getStatus('preparing') === true) // When preparing a queue for bulk
       {
	            if (false !== $custom_operation && 'bulk-undoAI' === $custom_operation)
	            {
	               $prepared = $this->prepareUndoAI();
	            }
            else {

              $prepared = $this->prepare();
            }

            $result->qstatus = self::RESULT_PREPARING;
            $result->items = $prepared['items']; // number of items.
            $result->images = $prepared['images'];

            if (true === $prepared['overlimit'])
            {
               $result->qstatus = self::RESULT_PREPARING_OVERLIMIT;
            }

            if ($prepared['items'] == 0)
            {
          //     Log::addDebug( $this->queueName . ' Queue, prepared came back as zero ', array($prepared, $result->items));
               if ($prepared['results'] == 0) /// This means no results, empty query.
               {
                $result->qstatus = self::RESULT_PREPARING_DONE;
               }
            }
       }
       elseif ($this->getStatus('bulk_running') == true) // this is a bulk queue, don't start automatically.
       {
          if ($this->getStatus('running') == true)
          {
              $items = $this->deQueue();
          }
          elseif ($this->getStatus('preparing') == false && $this->getStatus('finished') == false)
          {
              $result->qstatus = self::RESULT_PREPARING_DONE;
          }
          elseif ($this->getStatus('finished') == true)
          {
              $result->qstatus = self::RESULT_QUEUE_EMPTY;
          }
       }
       else // regular queue can run whenever.
       {
            $items = $this->deQueue();
       }

       if (isset($items)) // did a dequeue.
       {
          $result->qstatus = $this->getQStatus(count($items));
          $result->items = $items;

       }

       return $result;
    }


    protected function addFilters($filters)
    {
         global $wpdb;
         $start_date = $end_date = false;


         // @todo Probably move all of this to global function and only sql statement to child class
         if (isset($filters['start_date']))
         {
            try {
               $start_date = new \DateTime($filters['start_date']);
            }
            catch (\Exception $e)
            {
               Log::addError('Start date bad', $e);
               unset($filters['start_date']);
            }
         }

         if (isset($filters['end_date']))
         {
            try {
               $end_date = new \DateTime($filters['end_date']);
            }
            catch (\Exception $e)
            {
               Log::addError('End Data bad', $e);
               unset($filters['end_date']);
            }
         }

         if (false !== $start_date && false !== $end_date)
         {
            // Confusing since we do DESC, so just swap dates if one is higher than other.
             if ($start_date->format('U') < $end_date->format('U'))
             {
                  $swap_date = $end_date;
                  $end_date = $start_date;
                  $start_date = $swap_date;
             }
         }

        // Take start date end of this day, since we do DESC and otherwise dates on the day of the start day will be omitted
         if (false !== $start_date)
         {
          $start_date->modify('+23 hours 59 minutes');
         }

         $args = $this->getFilterQueryData();
         $prepare = $args['base_prepare'];
         $base_query = $args['base_query'];
         $prepare = $args['base_prepare'];
         $date_field = $args['date_field'];

         $dateSQL = '';
         //$prepare = [];

         if (isset($start_date) && false !== $start_date)
         {
            $startDateSQL = $date_field . ' <= %s ';
            $prepare[] = $start_date->format("Y-m-d H:i:s");
         }
         if (isset($end_date) && false !== $end_date)
         {
            $endDateSQL = $date_field . ' >= %s';
            $prepare[] = $end_date->format("Y-m-d H:i:s");
         }

         $get_start_id = $get_end_id = false;
         if (isset($startDateSQL) && isset($endDateSQL))
         {
             $dateSQL = $startDateSQL . ' and ' . $endDateSQL;
             $get_start_id = true;
             $get_end_id = true;
         }
         elseif (isset($startDateSQL) && false === isset($endDateSQL))
         {
             $dateSQL = $startDateSQL;
             $get_start_id = true;
         }
         elseif (false === isset($startDateSQL) && isset($endDateSQL))
         {
             $dateSQL = $endDateSQL;
             $get_end_id = true;
         }

         $base_query .= $dateSQL;

            if (true === $get_start_id)
         {
             $startSQL = $base_query . '  ORDER BY ' . $date_field . ' DESC LIMIT 1';
	             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
	             $startSQL = $wpdb->prepare($startSQL, $prepare);
	             // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; dynamic ORDER BY field is selected from an internal allowlist.
	             $start_id = $wpdb->get_var($startSQL);
             if (is_null($start_id))
             {
               $start_id = -1;
             }

             $this->options['filters']['start_id'] = $start_id;
         }

         if (true === $get_end_id)
         {
            $endSQL = $base_query . '  ORDER BY ' . $date_field . ' ASC LIMIT 1';
	            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
	            $endSQL = $wpdb->prepare($endSQL, $prepare);

	            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; dynamic ORDER BY field is selected from an internal allowlist.
	            $end_id = $wpdb->get_var($endSQL);
            if (is_null($end_id))
            {
                $end_id = -1;
            }
            $this->options['filters']['end_id'] = $end_id;


         }

    }


    protected function prepareItems($items)
    {
	        do_action('spaatg/queue/prepare_items', $items);

        $return = array('items' => 0, 'images' => 0, 'results' => 0,
      'overlimit' => false);

				$settings = \wpSPAATG()->settings();
        $env = \wpSPAATG()->env();
        $queueOptions = $this->getOptions();

          if (count($items) == 0)
          {
              $this->q->setStatus('preparing', false);
              Log::addDebug('PrepareItems: Items can back as empty array. Nothing to prepare');
              return $return;
          }

          $fs = \wpSPAATG()->filesystem();

          $queue = array();
          $imageCount = 0;

          $operation = $this->getCustomDataItem('customOperation'); // false or value (or null)

					if (is_null($operation))
						$operation = false;

          $i = 0;
          $customData = $this->getStatus('custom_data');

          // maybe while on the whole function, until certain time has elapsed?
          foreach($items as $item_id)
          {
              $counterUpdated = false;

	                $mediaItem = $fs->getImage($item_id, $this->getType() );

            //checking if the $mediaItem actually exists
            if ( is_object($mediaItem) ) {

                $optimizeAiController = OptimizeAiController::getInstance();

                // If autoAi is on the bulk, add operation to the item
                $enqueueAi = false;
                $enqueueRegular = false;
                $doMedia = isset($queueOptions['doMedia']) ? filter_var($queueOptions['doMedia'], FILTER_VALIDATE_BOOLEAN) : false;
                $doAi = isset($queueOptions['doAi']) ? filter_var($queueOptions['doAi'], FILTER_VALIDATE_BOOLEAN) : false;

                if ('media' === $mediaItem->get('type'))
                {
                  if (false === $doMedia)
                  {
                     $enqueueRegular = false;
                  }

                  $bulkAiEnabled = (true === OptimizerBase::isImageOptimizationDisabled()) ? true : $settings->autoAIBulk;

                  if (true === $optimizeAiController->isAiEnabled() &&
                  true === $bulkAiEnabled &&
                  true === $doAi)
                  {
                    $aiDataModel = AiDataModel::getModelByAttachment($mediaItem->get('id'));
                    $enqueueAi = $aiDataModel->isProcessable();
                  }
                }

                // @todo This whole structure on ai / not-ai for enqueue is getting messy
                if (true === $enqueueRegular)
                {
                }
                else
                { // @todo Incorporate these actions here.  . Perhaps operations should all be on top?
                   if($operation !== false)
                   {
                    // Possibly these should become propert qItems as well when enqueueing (?)
	                      if ('bulk-undoAI' == $operation)
	                      {
	                         $qObject = new \stdClass;
	                         $qObject->action = 'undoAI';
                         $queue[] = ['id' => $mediaItem->get('id'), 'value' => $qObject];
                      }
                   }
                   elseif(true === $enqueueAi)
                   {
                          $qItem = QueueItems::getImageItem($mediaItem);
                          $qItem->requestAltAction();
                          $queue[] = $qItem->returnEnqueue();

                          $counts = $qItem->data()->counts;
                          $customData->aiCount += $counts->aiCount;
                          $counterUpdated = true;
                   }
									 else
									 {
												$response = array(
							 					 	'is_error' => true,
							 						'item_type' => ResponseController::ISSUE_QUEUE_FAILED,
							 						'message ' => ' Item failed: ' . $mediaItem->getProcessableReason(),
							 				 );
							 				  ResponseController::addData($item_id, $response);
									 }

                }
    			  }
    			  else
    			  {
        				 $response = array(
        					 	'is_error' => true,
        						'item_type' => ResponseController::ISSUE_QUEUE_FAILED,
        						'message ' => ' Enqueing of item failed : invalid post content or post type',
        				 );
        				 	ResponseController::addData($item_id, $response);
        				  Log::addWarn('The item with id ' . $item_id . ' cannot be processed because it is either corrupted or an invalid post type');
        			}

              if (true === $env->IsOverMemoryLimit($i) || true === $env->IsOverTimeLimit())
              {
                 Log::addMemory('PrepareItems: OverLimit! Breaking on index ' . $i);
                 $this->q->setStatus('custom_data', $customData, false); // save the counts.
                 $this->q->setStatus('last_item_id', $item_id);
                 $return['overlimit'] = true; // lockout return
                 break;
              }

              $i++;
        } // Loop Items

          $this->q->additems($queue);
          $numitems = $this->q->enqueue();

          if (true === $counterUpdated)
          {
            $this->q->setStatus('custom_data', $customData, false);
          }

          // mediaItem should be last_item_id, save this one.
          $this->q->setStatus('last_item_id', $item_id); // enum status to prevent a hang when no items are enqueued, thus last_item_id is not raised. save to DB.

          $qCount = count($queue);

          $return['items'] = $qCount;
          $return['images'] = $imageCount;
					/** NOTE! The count items is the amount of items queried and checked. It might be they never enqueued, just that the check process is running.
					*/
          $return['results'] = count($items); // This is the return of the query. Preparing should not be 'done' before the query ends, but it can return 0 on the qcount if all results are already optimized.

          return $return; // only return real amount.
    }

    // Used by Optimizecontroller on handlesuccess.
    public function getQueueName()
    {
          return $this->queueName;
    }


    protected function getQStatus($numitems)
    {

      if ($numitems == 0)
      {
        if ($this->getStatus('items') == 0 && $this->getStatus('errors') == 0 && $this->getStatus('in_process') == 0) // no items, nothing waiting in retry. Signal finished.
        {
          $qstatus = self::RESULT_QUEUE_EMPTY;
        }
        else
        {
          $qstatus = self::RESULT_EMPTY;
        }
      }
      else
      {
        $qstatus = self::RESULT_ITEMS;
      }

      return $qstatus;
    }


    public function getStats()
    {
      $stats = new \stdClass; // For frontend reporting back.
      $stats->is_preparing = (bool) $this->getStatus('preparing');
      $stats->is_running = (bool) $this->getStatus('running');
      $stats->is_finished = (bool) $this->getStatus('finished');
      $stats->in_queue = max(0, (int) $this->getStatus('items'));
      $stats->in_process = max(0, (int) $this->getStatus('in_process'));
			$stats->awaiting = $stats->in_queue + $stats->in_process; // calculation used for WP-CLI.

      $stats->errors = max(0, (int) $this->getStatus('errors'));
      $stats->fatal_errors = max(0, (int) $this->getStatus('fatal_errors'));
      $stats->done = max(0, (int) $this->getStatus('done'));
      $stats->bulk_running = (bool) $this->getStatus('bulk_running');

			$customData = $this->getStatus('custom_data');

			if ($this->isCustomOperation())
			{
					  $stats->customOperation = $this->getCustomDataItem('customOperation');
            $stats->isCustomOperation = '10'; // numeric value for the bulk JS
			}

      $stats->total = $stats->in_queue + $stats->fatal_errors + $stats->errors + $stats->done + $stats->in_process;
      if ($stats->total > 0)
			{
        $stats->percentage_done = round((100 / $stats->total) * ($stats->done + $stats->fatal_errors), 0, PHP_ROUND_HALF_DOWN);
        $stats->percentage_done = min(100, max(0, (int) $stats->percentage_done));
			}
			else
        $stats->percentage_done = 100; // no items means all done.


      if (! $stats->is_running)
      {
        $stats->images = $this->countQueue();
      }

     // Log::addTemp('GetStats - ' . $this->queueName, $stats);
      return $stats;
    }


    /** Recounts the ItemSum for the Queue
    *
    * Note that this is not the same number as preparing adds to the cache, which counts across the installation how much images were already optimized. However, we don't want to stop and reset cache just for a few lost numbers so we should accept a flawed outcome here perhaps.
    */
    protected function countQueue()
    {
        $recount = $this->q->itemSum('countbystatus');
        $customData = $this->getStatus('custom_data');
        $count = (object) [
            'images' => $recount[ShortQ::QSTATUS_WAITING],
            'images_done' => $recount[ShortQ::QSTATUS_DONE],
            'images_inprocess' => $recount[ShortQ::QSTATUS_INPROCESS],
        ];

        $count->images_webp = 0;
        $count->images_avif = 0;
        $count->images_ai = 0;

        if (is_object($customData))
        {
          $count->images_webp = (int) $customData->webpCount;
          $count->images_avif = (int) $customData->avifCount;
					$count->images_basecount = (int) $customData->baseCount;
          if (property_exists($customData, 'aiCount'))
          {
            $count->images_ai = (int) $customData->aiCount;
          }
        }


        $count->total_images_without_ai = 0;
        if ($count->images_ai > 0)
        {
           $count->total_images_without_ai = max(($count->images - $count->images_ai), 0);
        }
        else
        {
          $count->total_images_without_ai = $count->images;
        }

        return $count;
    }

    /** Get options which the queue was started with.  Formerly custom_data but now for all options.
     *
     * @return array
     */
    public function getOptions()
    {
         $options = $this->getCustomDataItem('queueOptions');
         return $options;
    }


    protected function getStatus($name = false)
    {
       // Slow name and purpose change on this one.
        if ($name == 'custom_data' || 'options' == $name)
        {
            $customData = $this->q->getStatus('custom_data');
            if (! is_object($customData))
            {
               $customData = $this->createCustomData();
            }
            return $customData;
        }
        return $this->q->getStatus($name);
    }

    public function setBulkOptions($options = [] )
    {
        if (0 === count($options))
          return false;

        $customData = $this->getStatus('custom_data');


        if (isset($options['customOp']))
        {
           $customOp = $options['customOp'];
           $customData->customOperation = $customOp;
           unset($options['customOp']);
        }

        if (is_array($options) && count($options) > 0)
        {
          $customData->queueOptions = $options;
        }
        else
        {
          $customData->queueOptions  = [] ;
        }
        $this->getShortQ()->setStatus('custom_data', $customData);
    }

		// Return if this queue has any special operation outside of normal optimizing.
		// Use to give the go processing when out of credits (ie)
		public function isCustomOperation()
		{
      $customOp = $this->getCustomDataItem('customOperation');
			if ($this->getCustomDataItem('customOperation') && false !== $this->getCustomDataItem('customOperation'))
			{
				return true;
			}
			return false;
		}

    public function getCustomDataItem($name)
    {
        $customData = $this->getStatus('custom_data');
        if (is_object($customData) && property_exists($customData, $name))
        {
           return $customData->$name;
        }
        return false;
    }

    protected function deQueue()
    {
       $items = $this->q->deQueue(); // Items, can be multiple different according to throttle.

       $items = array_map(array($this, 'queueToMediaItem'), $items);
       return $items;
    }

    protected function queueToMediaItem($qItem)
    {
        /* $item = new \stdClass;
        $item = $qItem->value;
        $item->_queueItem = $qItem; */

//        $item->item_id = $qItem->item_id;
//        $item->tries = $qItem->tries;

        $item = QueueItems::getEmptyItem($qItem->item_id, $this->getType());
        $item->setFromQueueData($qItem->value);
        $item->setData('tries', $qItem->tries);
        $item->setData('queue_list_order', $qItem->list_order);
        $item->data()->addKeepDataArgs('queue_list_order');
        $item->set('queueItem', $qItem);

				/* Dunno about this, the decode should handle arrays properly
				if (property_exists($item->data(), 'files'))
				{ // This must be array.
					$item->files = json_decode(json_encode($item->files), true);
				} */
        return $item;
    }

    protected function mediaItemToQueue(QueueItem $item)
    {
        // @todo Test this assumption
        return $item->getQueueItem();
    }

    public function getItem($item_id)
    {
        $itemObj = $this->q->getItem($item_id);
        if (false === is_object($itemObj))
        {
           return $itemObj; // probably boolean / not found.
        }

        return $this->queueToMediaItem(($itemObj));
    }


		// Check if item is in queue. Considered not in queue if status is done.
		public function isItemInQueue($item_id)
		{
        if (isset(self::$isInQueue[$item_id]))
        {
           $itemObj = self::$isInQueue[$item_id];
        }
        else
        {
          $itemObj = $this->q->getItem($item_id);
          self::$isInQueue[$item_id] = $itemObj; // cache this, since interface requests this X amount of times.
        }

				$notQ = array(ShortQ::QSTATUS_DONE, ShortQ::QSTATUS_FATAL);
				if (is_object($itemObj) && in_array(floor($itemObj->status), $notQ) === false )
				{
          return true;
				}
				return false;
		}

    protected function checkQueueCache($item_id)
    {
      if (isset(self::$isInQueue[$item_id]) && false === self::$isInQueue[$item_id])
      {
         unset(self::$isInQueue[$item_id]);
      }
    }

    public function itemFailed(QueueItem $qItem, $fatal = false)
    {
			  if ($fatal)
			  {
					 Log::addError('Item failed while optimizing', $qItem->result());
				}

          // It can happen that the item is not in the queue yet ( directAction doing )
        $item = $this->mediaItemToQueue($qItem);
        if (false === $item)
        {
           return;
        }

        $this->q->itemFailed($item, $fatal);
        $this->q->updateItemValue($item);
    }

		public function updateItem($item)
		{
			$qItem = $this->mediaItemToQueue($item); // convert again
			$this->q->updateItemValue($qItem);
		}

		public function isDuplicateActive($mediaItem, $queue = array() )
		{
			if ($mediaItem->get('type') === 'custom')
				return false;

			$WPMLduplicates = $mediaItem->getWPMLDuplicates();
			$qitems = array();
			if (count($queue) > 0)
			{
				 foreach($queue as $qitem)
				 {
					  $qitems[] = $qitem['id'];
				 }
			}

			if (is_array($WPMLduplicates) && count($WPMLduplicates) > 0)
			{
				 $duplicateActive = false;
				 foreach($WPMLduplicates as $duplicate_id)
				 {
					  if (in_array($duplicate_id, $qitems))
						{
							Log::addDebug('Duplicate Item is in queue already, skipping (ar). Duplicate:' . $duplicate_id);
							$duplicateActive = true;
							break;
						}
						elseif ($this->isItemInQueue($duplicate_id))
						{
							 Log::addDebug('Duplicate Item is in queue already, skipping (db). Duplicate:' . $duplicate_id);
							 $duplicateActive = true;
							 break;
						}
				 }
				 if (true === $duplicateActive)
				 {
						return $duplicateActive;
				 }
			}
			return false;
		}

    public function itemDone ($item)
    {
      $qItem = $this->mediaItemToQueue($item); // convert again
      $this->q->itemDone($qItem);
    }

    public function uninstall()
    {
        $this->q->uninstall();
    }

    public function activatePlugin()
    {
        $this->q->resetQueue();
    }

    public function getShortQ()
    {
        return $this->q;
    }

    // All custom Data in the App should be created here.
    private function createCustomData()
    {
        $data = new \stdClass;
        $data->webpCount = 0;
        $data->avifCount = 0;
				$data->baseCount = 0;
        $data->aiCount = 0;
        $data->customOperation = false;

        return $data;
    }



} // class
