<?php
/**
 * Author: Rymera Web Co.
 *
 * @package SAAI\REST\Chat
 */

namespace SAAI\REST\Chat;

use SAAI\Abstracts\Abstract_REST;
use SAAI\Helpers\Helper;
use SAAI\Traits\Singleton_Trait;
use SAAI\Classes\Memory\Posts_List;
use SAAI\Classes\Memory\Data_Upload;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;

defined( 'ABSPATH' ) || exit;

/**
 * Chat_Post_Types REST API controller class.
 *
 * @since 1.1.2
 */
class Chat_Post_Types extends Abstract_REST {

    use Singleton_Trait;

    /**
     * Upload interval in minutes between post type uploads.
     * Used to avoid overwhelming the AI memory with too many posts at once.
     *
     * @since 1.1.2
     */
    const UPLOAD_INTERVAL_MINUTES = 20;

    /**
     * Holds the class instance object
     *
     * @since 1.1.2
     * @var Chat_Post_Types $instance object
     */
    protected static $instance;

    /**
     * WordPress option key for storing chat post types settings.
     *
     * @since 1.1.2
     */
    const OPTION_KEY = 'saai_chat_post_types';

    /**
     * REST constructor.
     *
     * @since 1.1.2
     */
    public function __construct() {
    }

    /**
     * Works as constructor for this class.
     *
     * @since 1.1.2
     * @return void
     */
    public function register_plugin_routes() {
        $this->rest_base = 'chat-post-types';
        parent::register_plugin_routes();
    }

    /**
     * Register the routes for Chat Post Types.
     *
     * @since 1.1.2
     * @return void
     */
    public function register_routes() {
        // Save settings endpoint.
        register_rest_route(
            $this->namespace,
            "/$this->rest_base",
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'save_settings' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => array(
                    'settings'       => array(
                        'required'          => true,
                        'type'              => 'object',
                        'validate_callback' => array( $this, 'validate_settings' ),
                        'sanitize_callback' => array( $this, 'sanitize_settings' ),
                    ),
                    'excluded_posts' => array(
                        'required'          => false,
                        'type'              => 'object',
                        'sanitize_callback' => array( $this, 'sanitize_excluded_posts' ),
                    ),
                ),
            )
        );

        // Index status endpoint.
        register_rest_route(
            $this->namespace,
            "/$this->rest_base/index-status",
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_index_status' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
            )
        );

        // Sync endpoint.
        register_rest_route(
            $this->namespace,
            "/$this->rest_base/sync",
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'sync_post_type' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => array(
                    'post_type' => array(
                        'required'          => true,
                        'type'              => 'string',
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                ),
            )
        );

        // Toggle individual post endpoint.
        register_rest_route(
            $this->namespace,
            "/$this->rest_base/toggle-post",
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'toggle_post' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => array(
                    'post_id' => array(
                        'required'          => true,
                        'type'              => 'integer',
                        'sanitize_callback' => 'absint',
                    ),
                    'enabled' => array(
                        'required'          => true,
                        'type'              => 'string',
                        'validate_callback' => function ( $param ) {
                            return in_array( $param, array( 'yes', 'no' ), true );
                        },
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                ),
            )
        );
    }

    /**
     * Save chat post types settings.
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response|WP_Error The response object or error.
     */
    public function save_settings( $request ) {
        global $wpdb;

        $settings       = $request->get_param( 'settings' );
        $excluded_posts = $request->get_param( 'excluded_posts' );

        $previous_enabled_post_types = Helper::get_post_types_with_ai_memory_enabled();

        // Save settings to WordPress options.
        update_option( self::OPTION_KEY, $settings );

        /***************************************************************************
         * Schedule post data upload for each post type that is enabled.
         ***************************************************************************
         *
         * We schedule the upload for the next day at 20 minutes, 40 minutes, etc.
         *
         * This is to avoid overwhelming the AI memory with too many posts at once.
         */
        $minute = 0;
        foreach ( $settings as $post_type => $config ) {
            if ( 'yes' === $config['enabled'] ) {

                $excluded_post_ids = $excluded_posts[ $post_type ] ?? array();

                // If this post type was not previously enabled, start fresh with no exclusions.
                // This prevents a stale "all excluded" state from persisting after a disable/re-enable cycle.
                if ( ! in_array( $post_type, $previous_enabled_post_types, true ) ) {
                    $excluded_post_ids = array();
                }

                $meta_key = Data_Upload::POST_AI_MEMORY_ENABLED_META;

                // Prepare excluded post IDs for SQL (sanitize as integers).
                $excluded_ids_sql = '';
                if ( ! empty( $excluded_post_ids ) && is_array( $excluded_post_ids ) ) {
                    $excluded_post_ids = array_map( 'absint', $excluded_post_ids );
                    $excluded_post_ids = array_filter( $excluded_post_ids ); // Remove any invalid IDs.

                    if ( ! empty( $excluded_post_ids ) ) {
                        // Safe to use absint() values directly in SQL for integers.
                        $excluded_ids_sql = ' AND p.ID NOT IN (' . implode( ',', $excluded_post_ids ) . ')';
                    }
                }

                // Bulk update existing meta entries to 'yes' for non-excluded posts.
                $update_sql = "UPDATE {$wpdb->postmeta} pm
                    INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
                    SET pm.meta_value = 'yes'
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND pm.meta_key = %s
                    {$excluded_ids_sql}";
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $excluded_ids_sql contains sanitized integers via absint().
                $wpdb->query( $wpdb->prepare( $update_sql, $post_type, $meta_key ) );

                // Bulk insert meta entries for posts that don't have the meta key yet.
                $insert_sql = "INSERT INTO {$wpdb->postmeta} (post_id, meta_key, meta_value)
                    SELECT p.ID, %s, 'yes'
                    FROM {$wpdb->posts} p
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    {$excluded_ids_sql}
                    AND NOT EXISTS (
                        SELECT 1 FROM {$wpdb->postmeta} pm
                        WHERE pm.post_id = p.ID
                        AND pm.meta_key = %s
                    )";
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $excluded_ids_sql contains sanitized integers via absint().
                $wpdb->query( $wpdb->prepare( $insert_sql, $meta_key, $post_type, $meta_key ) );

                // Set excluded posts to disabled using bulk update and trigger action hooks.
                if ( ! empty( $excluded_post_ids ) && is_array( $excluded_post_ids ) ) {
                    // Trigger action hooks for each excluded post (hooks are necessary for individual processing).
                    foreach ( $excluded_post_ids as $post_id ) {
                        do_action( 'saai_post_ai_memory_disabled', $post_id, $post_type );
                    }

                    // Batch large arrays to avoid SQL IN clause limits (typically 1000 items).
                    $batch_size = 1000;
                    $batches    = array_chunk( $excluded_post_ids, $batch_size );

                    foreach ( $batches as $batch ) {
                        $batch_sql = implode( ',', $batch );
                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $batch_sql contains sanitized integers via absint().
                        $disable_sql = "UPDATE {$wpdb->postmeta}
                            SET meta_value = 'no'
                            WHERE post_id IN ($batch_sql)
                            AND meta_key = %s";
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $disable_sql contains sanitized integers via absint().
                        $wpdb->query( $wpdb->prepare( $disable_sql, $meta_key ) );
                    }
                }

                Data_Upload::instance()->maybe_schedule_post_data_upload( $post_type, time() + HOUR_IN_SECONDS + ( MINUTE_IN_SECONDS * self::UPLOAD_INTERVAL_MINUTES * $minute ) );
                ++$minute;
            }
        }

        /***************************************************************************
         * Disable AI memory for posts in post types that are no longer enabled.
         ***************************************************************************
         *
         * We disable AI memory for posts in post types that are no longer enabled.
         */
        $disabled_post_types = array_diff( $previous_enabled_post_types, array_keys( $settings ) );
        foreach ( $disabled_post_types as $post_type ) {
            $meta_key = Data_Upload::POST_AI_MEMORY_ENABLED_META;

            // Get all published post IDs for this post type that have AI memory enabled.
            $post_ids = $wpdb->get_col(
                $wpdb->prepare(
                    "SELECT DISTINCT p.ID
                    FROM {$wpdb->posts} p
                    INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
                    WHERE p.post_type = %s
                    AND p.post_status = 'publish'
                    AND pm.meta_key = %s
                    AND pm.meta_value = 'yes'",
                    $post_type,
                    $meta_key
                )
            );

            if ( ! empty( $post_ids ) ) {
                // Sanitize post IDs as integers.
                $post_ids = array_map( 'absint', $post_ids );
                $post_ids = array_filter( $post_ids ); // Remove any invalid IDs.

                if ( ! empty( $post_ids ) ) {
                    // Trigger action hook for each disabled post (hooks are necessary for individual processing).
                    foreach ( $post_ids as $post_id ) {
                        do_action( 'saai_post_ai_memory_disabled', $post_id, $post_type );
                    }

                    // Bulk update meta entries to 'no' for all posts in this disabled post type.
                    // Batch large arrays to avoid SQL IN clause limits (typically 1000 items).
                    $batch_size = 1000;
                    $batches    = array_chunk( $post_ids, $batch_size );

                    foreach ( $batches as $batch ) {
                        $batch_sql = implode( ',', $batch );
                        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $batch_sql contains sanitized integers via absint().
                        $update_sql = "UPDATE {$wpdb->postmeta}
                            SET meta_value = 'no'
                            WHERE post_id IN ($batch_sql)
                            AND meta_key = %s";
                        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $update_sql contains sanitized integers via absint().
                        $wpdb->query( $wpdb->prepare( $update_sql, $meta_key ) );
                    }
                }
            }
        }

        wp_cache_flush();

        return $this->rest_response(
            array(
                'settings' => $settings,
                'message'  => __( 'Chat post types settings saved successfully.', 'storeagent-ai-for-woocommerce' ),
            ),
            $request
        );
    }

    /**
     * Toggle individual post inclusion/exclusion.
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response|WP_Error The response object or error.
     */
    public function toggle_post( $request ) {
        $post_id = $request->get_param( 'post_id' );
        $enabled = $request->get_param( 'enabled' );

        // Verify post exists.
        $post_type = get_post_type( $post_id );
        if ( 'product' === $post_type ) {
            $post = wc_get_product( $post_id );
        } else {
            $post = get_post( $post_id );
        }
        if ( ! $post ) {
            return new WP_Error(
                'post_not_found',
                __( 'Post not found.', 'storeagent-ai-for-woocommerce' ),
                array( 'status' => 404 )
            );
        }

        // Check if post type is allowed (not product or attachment).
        $excluded_post_types = Helper::CHAT_EXCLUDED_POST_TYPES;
        if ( in_array( $post_type, $excluded_post_types, true ) ) {
            return new WP_Error(
                'invalid_post_type',
                __( 'This post type is not allowed for AI memory management.', 'storeagent-ai-for-woocommerce' ),
                array( 'status' => 400 )
            );
        }

        // Update post meta.
        Helper::update_post_meta( $post_id, Data_Upload::POST_AI_MEMORY_ENABLED_META, $enabled );

        if ( 'yes' === $enabled ) {
            Data_Upload::instance()->maybe_schedule_post_data_upload( $post_type, time() + HOUR_IN_SECONDS );
        } else {
            do_action( 'saai_post_ai_memory_disabled', $post_id, $post_type );
        }

        $updated_status_icon = Posts_List::instance()->get_status_icon( $post_id );
        $updated_hover_text  = Posts_List::instance()->get_hover_text( $post_id );

        return $this->rest_response(
            array(
                'success'     => true,
                'post_id'     => $post_id,
                'enabled'     => $enabled,
                'status_icon' => $updated_status_icon,
                'hover_text'  => $updated_hover_text,
                'message'     => 'yes' === $enabled
                    ? sprintf(
                        /* translators: %1$s: post type, %2$d: post ID */
                        __( 'AI memory enabled for %1$s %2$d.', 'storeagent-ai-for-woocommerce' ),
                        $post_type,
                        $post_id
                    )
                    : sprintf(
                        /* translators: %1$s: post type, %2$d: post ID */
                        __( 'AI memory disabled for %1$s %2$d.', 'storeagent-ai-for-woocommerce' ),
                        $post_type,
                        $post_id
                    ),
            ),
            $request
        );
    }

    /**
     * Get index status for all enabled post types.
     *
     * @since 1.1.4
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response The response object.
     */
    public function get_index_status( $request ) {
        $enabled_post_types = Helper::get_post_types_with_ai_memory_enabled();
        $index_status       = Data_Upload::instance()->get_per_post_type_index_status( $enabled_post_types );

        return $this->rest_response(
            array( 'index_status' => $index_status ),
            $request
        );
    }

    /**
     * Sync a specific post type by resetting failed posts and scheduling immediate upload.
     *
     * @since 1.1.4
     *
     * @param WP_REST_Request $request The request object.
     * @return WP_REST_Response|WP_Error The response object or error.
     */
    public function sync_post_type( $request ) {
        global $wpdb;

        $post_type          = $request->get_param( 'post_type' );
        $enabled_post_types = Helper::get_post_types_with_ai_memory_enabled();

        if ( ! in_array( $post_type, $enabled_post_types, true ) ) {
            return new WP_Error(
                'invalid_post_type',
                __( 'This post type is not enabled for AI memory.', 'storeagent-ai-for-woocommerce' ),
                array( 'status' => 400 )
            );
        }

        // Reset retry count for failed posts of this type.
        $wpdb->query(
            $wpdb->prepare(
                "UPDATE {$wpdb->postmeta} pm
                INNER JOIN {$wpdb->posts} p ON pm.post_id = p.ID
                SET pm.meta_value = '0'
                WHERE p.post_type = %s
                AND p.post_status = 'publish'
                AND pm.meta_key = %s
                AND CAST(pm.meta_value AS UNSIGNED) >= %d",
                $post_type,
                Data_Upload::POST_DATA_UPLOAD_RETRY_COUNT_META,
                Data_Upload::MAX_UPLOAD_RETRIES
            )
        );

        // Unschedule existing actions for this post type.
        as_unschedule_all_actions( Data_Upload::POST_DATA_UPLOAD_HOOK, array( $post_type ) );

        // Schedule immediate upload (bypasses maybe_ guard since we just unscheduled).
        Data_Upload::instance()->schedule_post_data_upload( $post_type );

        // Set in-progress transient.
        set_transient( Data_Upload::UPLOAD_IN_PROGRESS_FLAG_KEY, true, HOUR_IN_SECONDS );

        // Return updated index status for this post type.
        $index_status = Data_Upload::instance()->get_per_post_type_index_status( array( $post_type ) );

        return $this->rest_response(
            array(
                'index_status' => $index_status,
                'message'      => sprintf(
                    /* translators: %s: post type name. */
                    __( 'Sync started for %s.', 'storeagent-ai-for-woocommerce' ),
                    $post_type
                ),
            ),
            $request
        );
    }

    /**
     * Validate chat post types settings.
     *
     * @param array $settings The settings array to validate.
     * @return bool True if valid, false otherwise.
     */
    public function validate_settings( $settings ) {
        if ( ! is_array( $settings ) ) {
            return false;
        }

        // Get all public post types.
        $post_types = get_post_types( array( 'public' => true ), 'names' );

        // Exclude products and attachment post types.
        $excluded_post_types = Helper::CHAT_EXCLUDED_POST_TYPES;
        $allowed_post_types  = array_diff( $post_types, $excluded_post_types );

        foreach ( $settings as $post_type => $config ) {
            // Validate post type exists and is not excluded.
            if ( ! in_array( $post_type, $allowed_post_types, true ) ) {
                return false;
            }

            // Validate config structure.
            if ( ! is_array( $config ) ) {
                return false;
            }

            // Validate enabled field.
            if ( isset( $config['enabled'] ) && ! in_array( $config['enabled'], array( 'yes', 'no' ), true ) ) {
                return false;
            }

            // Validate meta_keys field.
            if ( isset( $config['meta_keys'] ) && ! is_array( $config['meta_keys'] ) ) {
                return false;
            }

            // Validate include_wholesale_data field.
            if ( isset( $config['include_wholesale_data'] ) && ! is_bool( $config['include_wholesale_data'] ) ) {
                return false;
            }
        }

        return true;
    }

    /**
     * Sanitize chat post types settings.
     *
     * @param array $settings The settings array to sanitize.
     * @return array Sanitized settings.
     */
    public function sanitize_settings( $settings ) {
        $sanitized = array();

        if ( ! is_array( $settings ) ) {
            return $sanitized;
        }

        // Get all public post types.
        $post_types = get_post_types( array( 'public' => true ), 'names' );

        $excluded_post_types = Helper::CHAT_EXCLUDED_POST_TYPES;
        $allowed_post_types  = array_diff( $post_types, $excluded_post_types );

        foreach ( $settings as $post_type => $config ) {
            // Only allow valid post types that are not excluded.
            if ( ! in_array( $post_type, $allowed_post_types, true ) ) {
                continue;
            }

            $sanitized_config = array();

            // Sanitize enabled field.
            if ( isset( $config['enabled'] ) ) {
                $sanitized_config['enabled'] = in_array( $config['enabled'], array( 'yes', 'no' ), true )
                    ? $config['enabled']
                    : 'no';
            }

            // Sanitize meta_keys field.
            if ( isset( $config['meta_keys'] ) && is_array( $config['meta_keys'] ) ) {
                $sanitized_config['meta_keys'] = array_map( 'sanitize_text_field', $config['meta_keys'] );
            } else {
                $sanitized_config['meta_keys'] = array();
            }

            // Sanitize include_wholesale_data field.
            $sanitized_config['include_wholesale_data'] = ! empty( $config['include_wholesale_data'] );

            $sanitized[ sanitize_text_field( $post_type ) ] = $sanitized_config;
        }

        return $sanitized;
    }

    /**
     * Sanitize excluded posts array.
     *
     * @param array $excluded_posts The excluded posts array to sanitize.
     * @return array Sanitized excluded posts.
     */
    public function sanitize_excluded_posts( $excluded_posts ) {
        $sanitized = array();

        if ( ! is_array( $excluded_posts ) ) {
            return $sanitized;
        }

        foreach ( $excluded_posts as $post_type => $post_ids ) {
            if ( ! is_array( $post_ids ) ) {
                continue;
            }

            $sanitized[ sanitize_text_field( $post_type ) ] = array_map( 'absint', $post_ids );
        }

        return $sanitized;
    }
}
