<?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\Model;

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

use SPAATG\Helper\InstallHelper;
use SPAATG\Helper\UtilHelper;
use SPAATG\ShortPixelLogger\ShortPixelLogger as Log;
use SPAATG\Notices\NoticeController as Notice;

// Class to handle the Database Table Data, store AI relevant data etc.
class AiDataModel
{
    protected $id;
    protected $attach_id;
    protected $type;

    protected static $models = [];

    protected $original = [
        'alt' => null,
        'caption' => null,
        'description' => null,
        'post_title' =>  null,
        'filebase' => null,
    ];

    protected $current = [
        'alt' => null,
        'caption' => null,
        'description' => null,
        'post_title' => null,
    ];

    protected $generated = [
        'alt' => null,
        'caption' => null,
        'description' => null,
        'post_title' => null,
        'filebase' => null,
    ];

    protected $status = 0;

    private $has_record = false;
    private $has_generated = false;
    private $current_is_set = false;

    private $processable_status = 0;

    const TYPE_MEDIA = 1;
    const TYPE_CUSTOM = 2;

    // Status for the whole image, in the main table.
    const AI_STATUS_NOTHING = 0;
    const AI_STATUS_GENERATED = 1;

    // IsProcessable statii
    const P_PROCESSABLE = 0;
    const P_ALREADYDONE = 1;  // Data already there
    const P_EXIFAI = 2;  // When Exif Flag forbids AI doing
    const P_EXTENSION = 3;
    const P_NOJOB = 4;
    const P_NOFIELDS = 5;

    // Descriptive status if certain field is not generated / left alone.
    const F_STATUS_OK = 1;
    const F_STATUS_EXCLUDESETTING = -3;
    const F_STATUS_PREVENTOVERRIDE = -4;


    public function __construct($attach_id, $type = 'media')
    {
          $this->attach_id = $attach_id;
          if ('media' == $type) // only this supported for now
          {
             $this->type = self::TYPE_MEDIA;
          }

          $this->fetchRecord($this->attach_id, $this->type);
    }

    protected function fetchRecord($attach_id, $type)
    {
           global $wpdb;
           $tableName = self::getTableName();

           $sql = ' SELECT * FROM ' . $tableName . ' where attach_id = %d and post_type = %d';
           // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
           $sql = $wpdb->prepare($sql, $attach_id, $type);

	           // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table name comes from the plugin AI data table accessor.
	           $row = $wpdb->get_row($sql);

        if (self::isMissingTableError($wpdb->last_error)) {
            InstallHelper::checkTables();
            $wpdb->last_error = '';

            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table name comes from the plugin AI data table accessor.
            $row = $wpdb->get_row($sql);
        }

        if (self::isMissingTableError($wpdb->last_error)) {
            $this->has_record = false;
            return false;
        }
        if (false == $row)
        {
            $this->has_record = false;
            return;
        }

        $originalData = $this->checkRowData($row->original_data);
        $generatedData = $this->checkRowData($row->generated_data);


        $this->id = $row->id;
        $this->has_record = true;
        $this->status = $row->status;
        $this->original = array_merge($this->original, $originalData);
        $this->generated = array_merge($this->generated, $generatedData);

    }

    private function checkRowData($json)
    {
        $bool = UtilHelper::ValidateJSON($json);
        if (false === $bool)
        {
             return [];
        }

        $data = json_decode($json);

        return (array) $data;

    }

    /** Get all data needed to send API for generating AI texts, depending on settings. This includes all settings minus URL
     *
     * @return array{paramlist: array<string, array{context: mixed, chars: mixed}>, returndatalist: array<string, array<string, int>>}
     */
    public function getOptimizeData($params = [])
    {
        $settings = (object) UtilHelper::getAiSettings($params);

        $ignore_fields = [];
        if (true === $settings->aiPreserve)
        {
            $currentData = $this->getCurrentData();
            $ignore_fields = array_diff(array_keys( array_filter($currentData) ), ['post_title']);
            // Exception via array_diff :: post_title always overwrite because it is always filled
        }

       // $fields = ['ai_gen_alt', 'ai_gen_caption', 'ai_gen_description', 'ai_gen_filename'];
        $fields = ['alt', 'caption', 'description', 'filename', 'post_title'];

        $paramlist = [
            'languages' => $settings->ai_language,
            'context' => $settings->ai_general_context,
        ];

        if (true === $settings->ai_use_post)
        {
            $parent_title = $this->getConnectedPostTitle();
            if (false !== $parent_title && false === is_null($parent_title))
            {
                $paramlist['use_parent_post_title'] = true;
                $paramlist['parent_post_title'] = $parent_title;
            }
        }

        $returnDataList = [];
        $field_status = false; // check if there are any fields to process / not all excluded.

        foreach($fields as $field_name)
        {
            $api_name = $field_name;

            switch($api_name)
            {
                case 'description':
                    $api_name = 'image_description';
	            break;
                case 'filename':
                    $api_name = 'file';
                break;
                case 'post_title':
                    $api_name = 'title';
                break;
            }


            if (false === $settings->{'ai_gen_' . $field_name})
            {
                $returnDataList[$field_name]['status'] = self::F_STATUS_EXCLUDESETTING;
                continue;
            }
            elseif (true === in_array($field_name, $ignore_fields))
            {
                $returnDataList[$field_name]['status'] = self::F_STATUS_PREVENTOVERRIDE;
            }
            else
            {
                $paramlist[$api_name] = [
                        'context' => $settings->{'ai_' . $field_name . '_context'},
                        'chars' => $settings->{'ai_limit_' . $field_name . '_chars'},
                ];
                $returnDataList[$field_name]['status']  = self::F_STATUS_OK;
                $field_status = true;
            }


        }

        if (false === $field_status)
        {
            $this->processable_status = self::P_NOJOB;
        }

        return ['paramlist' => $paramlist, 'returndatalist' => $returnDataList];

    }

    public function handleNewData($data)
    {
        // Save to Database
        foreach($data as $name => $value)
        {
             if ('original_filebase' == $name)
             {
                 $this->current['filebase'] = $value;
             }
             else
             {
                $this->generated[$name] = $value;
             }

        }

        $this->setCurrentData();

        // New Data.
        if (false === $this->has_record)
        {
            $this->original = $this->current;

            $this->status = self::AI_STATUS_GENERATED;
            $this->updateRecord();
        }
        else
        {
            // Not sure if  just categorically deny this, or some smart updater ( with more risks )
            Log::addError('New AI Data already has an entry');
        }

        // Save to WordPress
        $this->updateWPPost($this->generated);
        $this->updateWpMeta($this->generated);

    }

    protected function updateWPPost($data)
    {
        $post = get_post($this->attach_id);
        $post_updated = false;

        if (isset($data['caption']) && false !== $data['caption'] && false === is_numeric($data['caption']))
        {
            $post->post_excerpt = $data['caption'];
            $post_updated = true;
        }

        if (isset($data['description']) && false !== $data['description'] && false === is_numeric($data['description']))
        {
            $post->post_content = $data['description'];
            $post_updated = true;
        }

        if (isset($data['post_title']) && false !== $data['post_title'] && false === is_numeric($data['post_title']))
        {
             $post->post_title = $data['post_title'];
             $post_updated = true;
        }

        if (true === $post_updated)
        {
            wp_update_post($post);
        }
    }

    protected function updateWpMeta($data)
    {
        Log::addTemp('Update WpMeta', $data);
        if (isset($data['alt']) && false !== $data['alt'] && false === is_int($data['alt']))
        {
            $bool = update_post_meta($this->attach_id, '_wp_attachment_image_alt', $data['alt']);
        }
    }

    // Should return our results, from the AI only
    public function getGeneratedData()
    {
        return $this->generated;
    }

    public function getStatus()
    {
         return $this->status;
    }

    public function getAttachId()
    {
         return $this->attach_id;
    }

    public function currentIsDifferent()
    {
         $this->setCurrentData();

         $generated = array_filter($this->generated, [$this, 'mapWPVars'], ARRAY_FILTER_USE_KEY);
         $current = array_filter($this->current, [$this, 'mapWPVars'], ARRAY_FILTER_USE_KEY);

         $diff = array_diff($generated, $current);

         if (count($diff) > 0)
         {
             return true;
         }
         return false;
    }

    private function mapWPVars($key)
    {
         $fields = ['alt', 'caption', 'description'];

         if (false === in_array($key, $fields))
         {
            return false;
         }
         return true;

    }

    protected function getConnectedPostTitle()
    {
         $attach_id = $this->attach_id;
         $post_parent = get_post_parent($attach_id);
         if (! is_null($post_parent))
         {
            $post = get_post($post_parent);
            if (false === is_null($post))
            {
                $post_title = $post->post_title;
                return $post_title;
            }
         }

         return false;

    }

    // Should return the current situation. If not stored in the database - or different from meta - uh something should be returned.
    protected function setCurrentData()
    {
        $attach_id = $this->attach_id;
        $current_alt = get_post_meta($attach_id, '_wp_attachment_image_alt', true);
        $post = get_post($attach_id);

        $current_description = $post->post_content;
        $current_caption = $post->post_excerpt;
        $current_post_title = $post->post_title;

        /*$this->current = [
             'alt' => $current_alt,
             'description' => $current_description,
             'caption' => $current_caption,

        ]; */

        $this->current['alt'] = $current_alt;
        $this->current['description'] = $current_description;
        $this->current['caption'] = $current_caption;
        $this->current['post_title'] = $current_post_title;

        $this->current_is_set = true;

    }

    public function getCurrentData()
    {
          if (false === $this->current_is_set)
          {
             $this->setCurrentData();
          }

          return $this->current;
    }

    // This should return originals, or what the system thinks is the last user-generated content here.
    public function getOriginalData()
    {
        return $this->original;
    }

    // Check if the stored data still correlates to reality
    public function checkStoredData()
    {
        if (false === $this->has_record)
        {
            return true;
        }

        $this->setCurrentData();

    }

    /** Function to check if on this item there is something to AI
     *
     * @return boolean
     */
    public function isProcessable()
    {
        if (true === $this->has_record && true === $this->isSomeThingGenerated())
        {
             $this->processable_status = SELF::P_ALREADYDONE;
             return false;
        }

        // Stash here other conditions on top with && to build a big processable function
        $processable = ( $this->isExifProcesssable() && $this->isExtensionIncluded() && $this->hasSomethingGeneratable() ) ? true : false;
        return $processable;
    }


    private function isExifProcesssable()
    {
        $fs = \wpSPAATG()->filesystem();
        $imageModel = $fs->getMediaImage($this->attach_id);

        if (false === $imageModel->isSomethingOptimized())
        {
            return true;
        }

        $imageObj = $imageModel->getSomethingOptimized();


        $keepExif = $imageObj->getMeta('did_keepExif');

        // 2-3 are exif_ai combined settings with keep-exif. 0-1 are when default settings are used and unset / unused
        if (in_array($keepExif, [0,1,2,3]))
        {
            return true;
        }

        $this->processable_status = self::P_EXIFAI;
        return false;

    }

    public function getProcessableReason($returnStatus = false )
    {
        $message = false;

        if (true === $returnStatus)
        {
            return $this->processable_status;
        }

        switch($this->processable_status)
        {
            case self::P_PROCESSABLE:
                $message = __('AI is processable', 'shortpixel-ai-alt-text-generator');
            break;
            case self::P_ALREADYDONE:
                $message = __('This image already has generated data', 'shortpixel-ai-alt-text-generator');
            break;
            case self::P_EXIFAI:
                $message = __('Image Exif settings restrict AI usage', 'shortpixel-ai-alt-text-generator');
            break;
            case self::P_EXTENSION:
                 $message = __('File Extension not supported', 'shortpixel-ai-alt-text-generator');
            break;
            case self::P_NOJOB:
	                $message = __('No fields to generate', 'shortpixel-ai-alt-text-generator');
	            break;
            default:
                 /* translators: %s: AI data status code. */
                 $message = sprintf(__('Status %s unknown', 'shortpixel-ai-alt-text-generator'), $this->processable_status);
            break;
        }

        return $message;
    }

    protected function isExtensionIncluded()
    {
        $fs = \wpSPAATG()->filesystem();
        $imageModel = $fs->getMediaImage($this->attach_id);

        // Gif removed here, since we (temporarily don't support it)
        $extensions = ['png', 'jpeg', 'webp', 'jpg'];

        if (in_array($imageModel->getExtension(), $extensions))
        {
            return true;
        }

        $this->processable_status = self::P_EXTENSION;
        return false;
    }

    protected function hasSomethingGeneratable()
    {
        $optimizeData = $this->getOptimizeData();

        if (self::P_NOJOB === $this->processable_status)
        {
             return false;

        }
        return true;
    }

    public function isSomeThingGenerated()
    {
        if (false === $this->has_record)
        {
             return false;
        }

        if (count(array_keys(array_filter($this->generated))) > 0)
        {
             return true;
        }
        return false;
    }

	 private static function getTableName()
	 {
	     global $wpdb;
	     return $wpdb->prefix . 'spaatg_aipostmeta';
	 }

    public static function getSupportedMimeTypes()
    {
        return ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
    }

    private static function queryCandidateMediaItemIds($args = [])
    {
        $defaults = [
            'limit' => 200,
            'last_id' => null,
            'start_id' => null,
            'end_id' => null,
        ];
        $args = wp_parse_args($args, $defaults);

        global $wpdb;

        $sql = "SELECT DISTINCT pm.post_id
            FROM {$wpdb->postmeta} pm
            INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
            WHERE (pm.meta_key = %s OR pm.meta_key = %s)
              AND p.post_type = %s
              AND p.post_status != %s";

        $prepare = ['_wp_attached_file', '_wp_attachment_metadata', 'attachment', 'trash'];

        if (! empty($args['last_id'])) {
            $sql .= ' AND pm.post_id < %d';
            $prepare[] = (int) $args['last_id'];
        } elseif (! is_null($args['start_id'])) {
            $sql .= ' AND pm.post_id <= %d';
            $prepare[] = (int) $args['start_id'];
        }

        if (! is_null($args['end_id'])) {
            $sql .= ' AND pm.post_id >= %d';
            $prepare[] = (int) $args['end_id'];
        }

        $sql .= ' ORDER BY pm.post_id DESC LIMIT %d';
        $prepare[] = (int) $args['limit'];

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
        $sql = $wpdb->prepare($sql, $prepare);
	        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table names come from $wpdb.
	        $results = $wpdb->get_col($sql);

        return array_filter(array_map('intval', $results));
    }

    public static function hasPendingGenerationFields()
    {
        $settings = \wpSPAATG()->settings();
        $fields = ['ai_gen_alt', 'ai_gen_caption', 'ai_gen_description', 'ai_gen_filename', 'ai_gen_post_title'];

        foreach ($fields as $field) {
            if (! empty($settings->$field)) {
                return true;
            }
        }

        return false;
    }

    public static function countCandidateMediaItems()
    {
        global $wpdb;

        $mimes = self::getSupportedMimeTypes();
        $mime_placeholders = implode(', ', array_fill(0, count($mimes), '%s'));

        $sql = "SELECT COUNT(DISTINCT pm.post_id)
            FROM {$wpdb->postmeta} pm
            INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
            WHERE (pm.meta_key = %s OR pm.meta_key = %s)
              AND p.post_type = %s
              AND p.post_status != %s
              AND p.post_mime_type IN ({$mime_placeholders})";

        $prepare = array_merge(
            ['_wp_attached_file', '_wp_attachment_metadata', 'attachment', 'trash'],
            $mimes
        );

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains validated dynamic table or clause fragments and is prepared before execution.
        $sql = $wpdb->prepare($sql, $prepare);

	        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above; table names come from $wpdb.
	        return (int) $wpdb->get_var($sql);
    }

    public static function queryPendingMediaItems($args = [])
    {
        $defaults = [
            'limit' => 200,
            'last_id' => null,
            'start_id' => null,
            'end_id' => null,
        ];
        $args = wp_parse_args($args, $defaults);

        if (false === self::hasPendingGenerationFields()) {
            return [];
        }

        $pending = [];
        $batch_limit = max((int) $args['limit'], 200);
        $last_id = $args['last_id'];
        $use_start_id = true;

        while (count($pending) < $args['limit']) {
            $candidates = self::queryCandidateMediaItemIds([
                'limit' => $batch_limit,
                'last_id' => $last_id,
                'start_id' => ($use_start_id) ? $args['start_id'] : null,
                'end_id' => $args['end_id'],
            ]);

            if (count($candidates) === 0) {
                break;
            }

            foreach ($candidates as $candidate_id) {
                $aiDataModel = new self($candidate_id, 'media');
                if (true === $aiDataModel->isProcessable()) {
                    $pending[] = $candidate_id;

                    if (count($pending) >= $args['limit']) {
                        break;
                    }
                }
            }

            $use_start_id = false;
            $last_id = end($candidates);

            if (count($candidates) < $batch_limit) {
                break;
            }
        }

        return $pending;
    }

    public static function countPendingMediaItems()
    {
        if (false === self::hasPendingGenerationFields()) {
            return 0;
        }

        $count = 0;
        $batch_limit = 500;
        $last_id = null;

        while (true) {
            $candidates = self::queryCandidateMediaItemIds([
                'limit' => $batch_limit,
                'last_id' => $last_id,
            ]);

            if (count($candidates) === 0) {
                break;
            }

            foreach ($candidates as $candidate_id) {
                $aiDataModel = new self($candidate_id, 'media');
                if (true === $aiDataModel->isProcessable()) {
                    $count++;
                }
            }

            $last_id = end($candidates);

            if (count($candidates) < $batch_limit) {
                break;
            }
        }

        return $count;
    }

    public static function countGeneratedMediaItems($args = [])
    {
        global $wpdb;

        $defaults = [
            'months_ago' => null,
        ];
        $args = wp_parse_args($args, $defaults);

        $sql = 'SELECT COUNT(id) FROM ' . self::getTableName() . ' WHERE post_type = %d AND status = %d';
        $prepare = [self::TYPE_MEDIA, self::AI_STATUS_GENERATED];

        if (is_numeric($args['months_ago'])) {
            $months_ago = (int) $args['months_ago'];
            $date = new \DateTime();
            $date->sub(new \DateInterval('P' . $months_ago . 'M'));

            $date_until = new \DateTime();
            $date_until->sub(new \DateInterval('P' . max($months_ago - 1, 0) . 'M'));

            $sql .= ' AND tsUpdated >= %s AND tsUpdated <= %s';
            $prepare[] = $date->format('Y-m-d H:i:s');
            $prepare[] = $date_until->format('Y-m-d H:i:s');
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query string contains plugin table name and is prepared before execution.
        $sql = $wpdb->prepare($sql, $prepare);

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Query is prepared above.
        return (int) $wpdb->get_var($sql);
    }

    protected function updateRecord($data = [])
    {
        global $wpdb;
        $is_new = is_null($this->id) ? true : false;

        $fields = [
            'attach_id' => $this->attach_id,
            'status' => $this->status,
            'generated_data' => json_encode($this->generated),
            'original_data' => json_encode($this->original),
            'tsUpdated' => UtilHelper::timestampToDB(time()),
        ];

        $format = ['%d', '%d', '%s', '%s', '%s'];

        if (false === $this->has_record)
        {
            $inserted = $wpdb->insert(self::getTableName(), $fields, $format);

            if (false === $inserted && self::isMissingTableError($wpdb->last_error)) {
                InstallHelper::checkTables();
                $wpdb->last_error = '';
                $inserted = $wpdb->insert(self::getTableName(), $fields, $format);
            }

            if (false === $inserted) {
                return;
            }

            $this->id = $wpdb->insert_id;
            $this->has_record = true;
        }
        else
        {
            $updated = $wpdb->update(self::getTableName(),$fields, ['id' => $this->id],$format);

            if (false === $updated && self::isMissingTableError($wpdb->last_error)) {
                InstallHelper::checkTables();
                $wpdb->last_error = '';
                $wpdb->update(self::getTableName(),$fields, ['id' => $this->id],$format);
            }
        }

    }

    private static function isMissingTableError($error)
    {
        $error = strtolower((string) $error);

        return strpos($error, "doesn't exist") !== false || strpos($error, 'does not exist') !== false;
    }

    public function migrate($data)
    {
        $updated = false;
        if (false === is_array($data))
        {
            return false;
        }

        if (is_null($this->original['alt']))
        {
            $this->original['alt'] = $data['original_alt'];
            $updated = true;
        }
        if (is_null($this->generated['alt']))
        {
            $this->generated['alt'] = $data['result_alt'];
            $updated = true;
        }

        if (true === $updated)
        {
            $this->status = self::AI_STATUS_GENERATED;
            $this->updateRecord();

        }

        return true;
    }

    public function revert()
    {
        if (true === $this->has_record)
        {
            global $wpdb;
            $wpdb->delete(self::getTableName(), ['id' => $this->id], ['%s']);

        }

        $this->updateWPPost($this->original);
        $this->updateWpMeta($this->original);

    }

    public static function getMostRecent()
    {
        global $wpdb;
         $sql = 'SELECT attach_id FROM ' . self::getTableName() . ' order by tsUpdated desc limit 1';
	         // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- Static query; table name comes from the plugin AI data table accessor.
	         $attach_id = $wpdb->get_var($sql);

        if (false === $attach_id)
        {
             return false;
        }

        return new AiDataModel($attach_id);
    }


    public static function getModelByAttachment($attach_id, $type = 'media')
    {
        if (false === isset(self::$models[$attach_id]))
        {
             self::$models[$attach_id]  = new AiDataModel($attach_id, $type);
        }

        return self::$models[$attach_id];

    }

    public static function flushModelCache($attach_id, $type = 'media')
    {
        if (isset(self::$models[$attach_id]))
        {
             unset(self::$models[$attach_id]);
        }
        else
        {
             Log::addTemp('Ai MODEL not found in cache!', $attach_id);
        }

    }




} // class
