<?php
namespace ACFWF\Models\REST_API;

use ACFWF\Abstracts\Abstract_Main_Plugin_Class;
use ACFWF\Helpers\Helper_Functions;
use ACFWF\Helpers\Plugin_Constants;
use ACFWF\Interfaces\Model_Interface;
use ACFWF\Models\Objects\ACFW_Settings;

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

/**
 * Model that houses the Settings module logic.
 * Public Model.
 *
 * @since 1.2
 */
class API_Settings implements Model_Interface {
    /*
    |--------------------------------------------------------------------------
    | Class Properties
    |--------------------------------------------------------------------------
     */

    /**
     * Property that holds the single main instance of URL_Coupon.
     *
     * @since 1.2
     * @access private
     * @var Cart_Conditions
     */
    private static $_instance;

    /**
     * Model that houses all the plugin constants.
     *
     * @since 1.2
     * @access private
     * @var Plugin_Constants
     */
    private $_constants;

    /**
     * Property that houses all the helper functions of the plugin.
     *
     * @since 1.2
     * @access private
     * @var Helper_Functions
     */
    private $_helper_functions;

    /**
     * Property that houses the ACFW_Settings instance.
     *
     * @since 1.2
     * @access private
     * @var ACFW_Settings
     */
    private $_acfw_settings;

    /**
     * Custom REST API base.
     *
     * @since 1.2
     * @access private
     * @var string
     */
    private $_base = 'settings';

    /**
     * Property that holds all settings sections.
     *
     * @since 1.2
     * @access private
     * @var array
     */
    private $_settings_sections;

    /**
     * Property that holds all settings sections options.
     *
     * @since 1.2
     * @access private
     * @var array
     */
    private $_sections_options;

    /*
    |--------------------------------------------------------------------------
    | Class Methods
    |--------------------------------------------------------------------------
     */

    /**
     * Class constructor.
     *
     * @since 1.2
     * @access public
     *
     * @param Abstract_Main_Plugin_Class $main_plugin      Main plugin object.
     * @param Plugin_Constants           $constants        Plugin constants object.
     * @param Helper_Functions           $helper_functions Helper functions object.
     */
    public function __construct( Abstract_Main_Plugin_Class $main_plugin, Plugin_Constants $constants, Helper_Functions $helper_functions ) {
        $this->_constants        = $constants;
        $this->_helper_functions = $helper_functions;

        $main_plugin->add_to_all_plugin_models( $this );
        $main_plugin->add_to_public_models( $this );
    }

    /**
     * Ensure that only one instance of this class is loaded or can be loaded ( Singleton Pattern ).
     *
     * @since 1.2
     * @access public
     *
     * @param Abstract_Main_Plugin_Class $main_plugin      Main plugin object.
     * @param Plugin_Constants           $constants        Plugin constants object.
     * @param Helper_Functions           $helper_functions Helper functions object.
     * @return Cart_Conditions
     */
    public static function get_instance( Abstract_Main_Plugin_Class $main_plugin, Plugin_Constants $constants, Helper_Functions $helper_functions ) {
        if ( ! self::$_instance instanceof self ) {
            self::$_instance = new self( $main_plugin, $constants, $helper_functions );
        }

        return self::$_instance;
    }

    /*
    |--------------------------------------------------------------------------
    | Routes.
    |--------------------------------------------------------------------------
     */

    /**
     * Initialize ACFW Settings.
     *
     * @since 1.2
     * @access private
     */
    private function _init_acfw_settings() {
        if ( ! class_exists( 'WC_Settings_Page', false ) ) {
            include_once WP_PLUGIN_DIR . '/woocommerce/includes/admin/settings/class-wc-settings-page.php';
        }

        $this->_acfw_settings = new ACFW_Settings( $this->_constants, $this->_helper_functions );
    }

    /**
     * Register settings API routes.
     *
     * @since 1.2
     * @access public
     */
    public function register_routes() {
        $this->_init_acfw_settings();

        \register_rest_route(
            Plugin_Constants::REST_API_NAMESPACE,
            '/' . $this->_base . '/sections',
            array(
                array(
                    'methods'             => \WP_REST_Server::READABLE,
                    'permission_callback' => array( $this, 'get_settings_admin_permissions_check' ),
                    'callback'            => array( $this, 'get_settings_sections' ),
                    'args'                => $this->get_settings_admin_collection_params(),
                ),
            )
        );

        \register_rest_route(
            Plugin_Constants::REST_API_NAMESPACE,
            '/' . $this->_base . '/sections/(?P<section>[\w]+)',
            array(
                array(
                    'methods'             => \WP_REST_Server::READABLE,
                    'permission_callback' => array( $this, 'get_settings_admin_permissions_check' ),
                    'callback'            => array( $this, 'get_settings_section_options' ),
                ),
            )
        );

        \register_rest_route(
            Plugin_Constants::REST_API_NAMESPACE,
            '/' . $this->_base . '/(?P<id>[\w]+)',
            array(
                'args' => array(
                    'id' => array(
                        'description' => __( 'Unique identified for the settings option', 'advanced-coupons-for-woocommerce-free' ),
                        'type'        => 'string',
                    ),
                ),
                array(
                    'methods'             => \WP_REST_Server::READABLE,
                    'permission_callback' => '__return_true',
                    'callback'            => array( $this, 'get_setting_option' ),
                ),
                array(
                    'methods'             => \WP_REST_Server::EDITABLE,
                    'permission_callback' => array( $this, 'update_setting_option_permissions_check' ),
                    'callback'            => array( $this, 'update_setting_option' ),
                ),
                array(
                    'methods'             => \WP_REST_Server::DELETABLE,
                    'permission_callback' => array( $this, 'update_setting_option_permissions_check' ),
                    'callback'            => array( $this, 'delete_setting_option' ),
                ),
            )
        );

        \register_rest_route(
            Plugin_Constants::REST_API_NAMESPACE,
            '/clear-detached-store-credits',
            array(
                'methods'             => \WP_REST_Server::CREATABLE,
                'permission_callback' => array( $this, 'get_settings_admin_permissions_check' ),
                'callback'            => array( $this, 'start_clear_detached_store_credit_entries' ),
            ),
        );

        do_action( 'acfw_after_register_routes' );
    }

    /*
    |--------------------------------------------------------------------------
    | Permissions.
    |--------------------------------------------------------------------------
     */

    /**
     * Checks if a given request has access to read list of settings options.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function get_settings_admin_permissions_check( $request ) {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return new \WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit settings options.', 'advanced-coupons-for-woocommerce-free' ), array( 'status' => \rest_authorization_required_code() ) );
        }

        return apply_filters( 'acfw_get_settings_admin_permissions_check', true, $request );
    }

    /**
     * Checks if a given request has access to read update a setting option.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function update_setting_option_permissions_check( $request ) {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return new \WP_Error( 'rest_forbidden_context', __( 'Sorry, you are not allowed to edit settings options.', 'advanced-coupons-for-woocommerce-free' ), array( 'status' => \rest_authorization_required_code() ) );
        }

        return apply_filters( 'acfw_get_settings_admin_permissions_check', true, $request );
    }

    /*
    |--------------------------------------------------------------------------
    | CRUD methods.
    |--------------------------------------------------------------------------
     */

    /**
     * Retrieves a collection of settings sections.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_settings_sections( $request ) {
        do_action( 'acfw_before_get_setting_fields' );

        $current_section = $request->get_header( 'section' );
        $response        = \rest_ensure_response( $this->_get_settings_sections( $current_section ) );

        return apply_filters( 'acfw_filter_get_settings_admin', $response );
    }

    /**
     * Retrieves a collection of options for the specificed settings section.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_settings_section_options( $request ) {
        do_action( 'acfw_before_get_setting_fields' );

        $section  = sanitize_text_field( $request['section'] );
        $response = \rest_ensure_response(
            array(
                'id'     => $section,
                'fields' => $this->_get_single_section_fields( $section ),
            )
        );

        return apply_filters( 'acfw_get_settings_section_options', $response, $section );
    }

    /**
     * Retrieves a single option value.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function get_setting_option( $request ) {
        $option = sanitize_text_field( $request['id'] );

        if ( ! $this->_is_setting_valid( $option ) ) {
            return new \WP_Error(
                'rest_forbidden_context',
                __( 'The provided ID is not a valid Advanced Coupons setting.', 'advanced-coupons-for-woocommerce-free' ),
                array(
                    'status' => 400,
                    'data'   => $option,
                )
            );
        }

        $response = \rest_ensure_response(
            array(
                'id'    => $option,
                'value' => get_option( $option ),
            )
        );

        return apply_filters( 'acfw_get_setting_option', $response, $option );
    }

    /**
     * Updates a single option value and returns updated value.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function update_setting_option( $request ) {
        $option = sanitize_text_field( $request['id'] );

        if ( ! $this->_is_setting_valid( $option ) ) {
            return new \WP_Error(
                'rest_forbidden_context',
                __( 'The provided ID is not a valid Advanced Coupons setting.', 'advanced-coupons-for-woocommerce-free' ),
                array(
                    'status' => 400,
                    'data'   => $option,
                )
            );
        }

        // Check if the option is valid if it is a URL, If it's not return an error.
        $option_is_url = in_array(
            $option,
            array(
                Plugin_Constants::AFTER_APPLY_COUPON_REDIRECT_URL_GLOBAL,
                Plugin_Constants::INVALID_COUPON_REDIRECT_URL,
            ),
            true
        );
        $value         = $request->get_param( 'value' );
        $url_is_valid  = filter_var( $value, FILTER_VALIDATE_URL );
        if ( $option_is_url && '' !== $value && ! $url_is_valid ) {
            return new \WP_Error(
                'rest_forbidden_context',
                __( 'The provided URL is not valid.', 'advanced-coupons-for-woocommerce-free' ),
                array(
                    'status' => 400,
                    'data'   => $option,
                )
            );
        }

        $type  = sanitize_text_field( $request->get_param( 'type' ) );
        $value = $this->_helper_functions->api_sanitize_value( $request->get_param( 'value' ), $type );

        if ( update_option( $option, $value ) ) {
            $value = get_option( $option );
        }

        $response = \rest_ensure_response(
            array(
                'id'    => $option,
                'value' => $value,
            )
        );

        return apply_filters( 'acfw_update_setting_option', $response );
    }

    /**
     * Deletes the specified option entry.
     *
     * @since 1.2
     * @access public
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function delete_setting_option( $request ) {
        $option = sanitize_text_field( $request['id'] );

        if ( ! $this->_is_setting_valid( $option ) ) {
            return new \WP_Error(
                'rest_forbidden_context',
                __( 'The provided ID is not a valid Advanced Coupons setting.', 'advanced-coupons-for-woocommerce-free' ),
                array(
                    'status' => 400,
                    'data'   => $option,
                )
            );
        }

        $previous = array(
            'id'    => $option,
            'value' => get_option( $option ),
        );

        $response = \rest_ensure_response(
            array(
                'updated'  => delete_option( $option ),
                'previous' => $previous,
            )
        );

        return apply_filters( 'acfw_delete_setting_option', $response, $option, $previous );
    }

    /**
     * Get all settings sections.
     *
     * @since 1.2
     * @access private
     *
     * @param string $current_section Current section id.
     * @return array List of sections and its readable names.
     */
    private function _get_settings_sections( $current_section = null ) {
        $sections = array(
            array(
                'id'     => 'general_section',
                'title'  => __( 'General', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'general_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => true,
                'module' => false,
            ),
            array(
                'id'     => 'checkout_section',
                'title'  => __( 'Checkout', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'checkout_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => true,
                'module' => false,
            ),
            array(
                'id'     => 'bogo_deals_section',
                'title'  => __( 'BOGO Deals', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'bogo_deals_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => $this->_helper_functions->is_module( Plugin_Constants::BOGO_DEALS_MODULE ),
                'module' => Plugin_Constants::BOGO_DEALS_MODULE,
            ),
            array(
                'id'     => 'scheduler_section',
                'title'  => __( 'Scheduler', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'scheduler_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => $this->_helper_functions->is_module( Plugin_Constants::SCHEDULER_MODULE ),
                'module' => Plugin_Constants::SCHEDULER_MODULE,
            ),
            array(
                'id'     => 'role_restrictions_section',
                'title'  => __( 'Role Restrictions', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'role_restrictions_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => $this->_helper_functions->is_module( Plugin_Constants::ROLE_RESTRICT_MODULE ),
                'module' => Plugin_Constants::ROLE_RESTRICT_MODULE,
            ),
            array(
                'id'     => 'url_coupons_section',
                'title'  => __( 'URL Coupons', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'url_coupons_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => $this->_helper_functions->is_module( Plugin_Constants::URL_COUPONS_MODULE ),
                'module' => Plugin_Constants::URL_COUPONS_MODULE,
            ),
            array(
                'id'     => 'store_credits_section',
                'title'  => __( 'Store Credits', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'store_credits_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => $this->_helper_functions->is_module( Plugin_Constants::STORE_CREDITS_MODULE ),
                'module' => Plugin_Constants::STORE_CREDITS_MODULE,
            ),
            array(
                'id'     => 'modules_section',
                'title'  => __( 'Modules', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'modules_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => true,
                'module' => false,
            ),
            array(
                'id'     => 'advanced_section',
                'title'  => __( 'Advanced', 'advanced-coupons-for-woocommerce-free' ),
                'fields' => 'advanced_section' === $current_section ? $this->_get_single_section_fields( $current_section ) : array(),
                'show'   => true,
                'module' => false,
            ),
        );

        return apply_filters( 'acfw_api_settings_get_sections', $sections, $current_section );
    }

    /**
     * Get single section fields.
     *
     * @since 1.2
     * @access private
     *
     * @param string $section Section id.
     * @return array Section fields.
     */
    private function _get_single_section_fields( $section ) {
        $raw = $this->_acfw_settings->get_settings( 'acfw_setting_' . $section );

        return $this->_helper_functions->prepare_setting_fields_for_api( $raw, $section );
    }

    /*
    |--------------------------------------------------------------------------
    | Utility methods.
    |--------------------------------------------------------------------------
     */

    /**
     * Retrieves the query params for the settings admin collection.
     *
     * @since 1.2
     * @access public
     *
     * @return array Collection parameters.
     */
    public function get_settings_admin_collection_params() {
        $query_params = array(
            'settings_sections' => array(
                'description' => __( 'Order sort attribute ascending or descending.', 'advanced-coupons-for-woocommerce-free' ),
                'type'        => 'array',
                'items'       => array(
                    'type' => 'string',
                    'enum' => array_keys( $this->_get_settings_sections() ),
                ),
            ),
        );

        return \apply_filters( 'wpb_filter_settings_admin_collection_params', $query_params );
    }

    /**
     * Check if setting is valid.
     *
     * @since 3.1.1
     * @access private
     *
     * @param string $setting Setting name to validate.
     * @return bool True if valid, false othewise.
     */
    private function _is_setting_valid( $setting ) {
        $sections       = array_keys( $this->_acfw_settings->get_sections() );
        $valid_settings = array_reduce(
            $sections,
            function ( $c, $section ) {
                return array_merge(
                    $c,
                    array_column( $this->_acfw_settings->get_settings( $section ), 'id' )
                );
            },
            array()
        );

        return in_array( $setting, $valid_settings, true );
    }

    /*
    |--------------------------------------------------------------------------
    | Clear detached store credit entries (Advanced tool)
    |--------------------------------------------------------------------------
     */

    /**
     * Check whether the store credits table exists in the database.
     *
     * @since 4.7.2
     * @access private
     *
     * @return bool True if the table exists, false otherwise.
     */
    private function _is_store_credits_table_present() {
        global $wpdb;
        $table = $wpdb->prefix . Plugin_Constants::STORE_CREDITS_DB_NAME;
        return $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) === $table;
    }

    /**
     * Start clearing detached store credit entries in the background.
     *
     * @since 4.7.2
     * @access public
     *
     * @param \WP_REST_Request $request Full details about the request.
     * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error on failure.
     */
    public function start_clear_detached_store_credit_entries( $request ) {
        if ( ! $this->_is_store_credits_table_present() ) {
            return new \WP_Error(
                'acfw_store_credits_table_missing',
                __( 'Store credits table does not exist.', 'advanced-coupons-for-woocommerce-free' ),
                array( 'status' => 400 )
            );
        }

        if ( function_exists( 'as_schedule_single_action' ) ) {
            as_schedule_single_action(
                time(),
                Plugin_Constants::CLEAR_DETACHED_STORE_CREDITS_SCHEDULE_HOOK,
                array(),
                'acfw-clear-detached-store-credit-entries'
            );
            $message = __( 'The cleanup has been started and is running in the background.', 'advanced-coupons-for-woocommerce-free' );
        } else {
            // No Action Scheduler: run synchronously.
            $this->run_clear_detached_store_credit_entries();
            $message = __( 'The cleanup has been completed.', 'advanced-coupons-for-woocommerce-free' );
        }

        return \rest_ensure_response(
            array(
                'status'  => 'success',
                'message' => $message,
            )
        );
    }

    /**
     * Run the actual delete of detached store credit entries (callback for Action Scheduler or sync fallback).
     * Deletes in batches to avoid long table locks on large datasets.
     *
     * @since 4.7.2
     * @access public
     */
    public function run_clear_detached_store_credit_entries() {
        global $wpdb;

        if ( ! $this->_is_store_credits_table_present() ) {
            return;
        }

        $table_store_credits = $wpdb->prefix . Plugin_Constants::STORE_CREDITS_DB_NAME;
        $table_users         = $wpdb->users;
        $batch_size          = 500;

        do {
            $deleted = $wpdb->query(
                $wpdb->prepare(
                    'DELETE FROM %i WHERE user_id NOT IN (SELECT ID FROM %i) LIMIT %d',
                    $table_store_credits,
                    $table_users,
                    $batch_size
                )
            );
        } while ( $batch_size === $deleted );
    }

    /*
    |--------------------------------------------------------------------------
    | Fulfill implemented interface contracts
    |--------------------------------------------------------------------------
     */

    /**
     * Execute Settings class.
     *
     * @since 1.2
     * @access public
     * @inherit ACFWF\Interfaces\Model_Interface
     */
    public function run() {
        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
        add_action( Plugin_Constants::CLEAR_DETACHED_STORE_CREDITS_SCHEDULE_HOOK, array( $this, 'run_clear_detached_store_credit_entries' ) );
    }
}
