<?php
/**
 * Base module class
 *
 * @package BlaminhorEssentials
 */

// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Abstract class for all modules
 */
abstract class Blaminhor_Essentials_Module {

    /**
     * Module ID
     *
     * @var string
     */
    protected $id = '';

    /**
     * Module name
     *
     * @var string
     */
    protected $name = '';

    /**
     * Module description
     *
     * @var string
     */
    protected $description = '';

    /**
     * Module settings
     *
     * @var array
     */
    protected $settings = array();

    /**
     * Constructor
     */
    public function __construct() {
        $this->load_settings();
        $this->init();
        $this->register_hooks();
    }

    /**
     * Initialize module
     */
    abstract protected function init();

    /**
     * Register WordPress hooks
     */
    abstract protected function register_hooks();

    /**
     * Render settings page
     */
    abstract public function render_settings_page();

    /**
     * Get module ID
     *
     * @return string
     */
    public function get_id() {
        return $this->id;
    }

    /**
     * Get module name (translated)
     *
     * @return string
     */
    public function get_name() {
        // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation for module name.
        return __( $this->name, 'blaminhor-essentials' );
    }

    /**
     * Get module description (translated)
     *
     * @return string
     */
    public function get_description() {
        // phpcs:ignore WordPress.WP.I18n.NonSingularStringLiteralText -- Dynamic translation for module description.
        return __( $this->description, 'blaminhor-essentials' );
    }

    /**
     * Load module settings from database
     */
    protected function load_settings() {
        $option_name    = 'blaminhor_essentials_module_' . $this->id;
        $this->settings = get_option( $option_name, $this->get_default_settings() );
    }

    /**
     * Save module settings
     *
     * @param array $settings Settings to save.
     * @return bool
     */
    protected function save_settings( $settings ) {
        $option_name    = 'blaminhor_essentials_module_' . $this->id;
        $this->settings = wp_parse_args( $settings, $this->get_default_settings() );
        return update_option( $option_name, $this->settings );
    }

    /**
     * Get setting value
     *
     * Public so module sub-components (composition pattern, e.g. the Cache
     * module's helpers) can read the parent module's settings without needing
     * an explicit getter wrapper.
     *
     * @param string $key     Setting key.
     * @param mixed  $default Default value.
     * @return mixed
     */
    public function get_setting( $key, $default = null ) {
        if ( isset( $this->settings[ $key ] ) ) {
            return $this->settings[ $key ];
        }
        return $default;
    }

    /**
     * Return the settings array merged with defaults.
     *
     * `load_settings()` only stores what `get_option()` returns; if a key was
     * never explicitly written to the DB it is absent from `$this->settings`,
     * even when its default value is `true`. The render layer compensates with
     * a local `wp_parse_args($settings, $defaults)`, but anything else that
     * needs to reason about the *effective* state of a setting (audit
     * recommender, etc.) needs the same merge — hence this helper.
     *
     * @return array
     */
    public function get_effective_settings() {
        return wp_parse_args( $this->settings, $this->get_default_settings() );
    }

    /**
     * Get default settings
     *
     * @return array
     */
    protected function get_default_settings() {
        return array();
    }

    /**
     * Enqueue module-specific assets
     *
     * @param string $hook Current admin page hook.
     */
    public function enqueue_assets( $hook ) {
        // Override in child classes if needed
    }

    /**
     * Add admin submenu for module
     */
    public function add_admin_menu() {
        add_submenu_page(
            'blaminhor-essentials',
            $this->get_name(),
            $this->get_name(),
            'manage_options',
            'blaminhor-essentials-' . $this->id,
            array( $this, 'render_settings_page' )
        );
    }

    /**
     * Handle settings form submission
     */
    public function handle_settings_submission() {
        $nonce_name = 'blaminhor_essentials_module_nonce_' . $this->id;

        if ( ! isset( $_POST[ $nonce_name ] ) ) {
            return;
        }

        if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $nonce_name ] ) ), 'blaminhor_essentials_module_' . $this->id ) ) {
            add_action( 'admin_notices', function() {
                blaminhor_essentials_admin_notice( __( 'Security check failed. Please try again.', 'blaminhor-essentials' ), 'error' );
            });
            return;
        }

        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        $this->process_settings( $_POST );
    }

    /**
     * Process and save settings
     *
     * @param array $data Form data.
     */
    protected function process_settings( $data ) {
        // Override in child classes
    }

    /**
     * Apply settings without going through the POST/nonce pipeline.
     *
     * Public bridge used by WP-CLI commands (and future REST endpoints) to
     * validate, sanitize and persist module settings via the same business
     * logic as the admin form — no HTTP request, no nonce. Delegates to the
     * module's `process_settings()` so validation stays centralised.
     *
     * Override only if a module needs a different mutation pipeline; never
     * bypass `save_settings()` (which merges with defaults).
     *
     * @param array $data Raw settings array to validate and persist.
     * @return true|WP_Error True on success, WP_Error on capability failure
     *                       or unexpected exception during processing.
     */
    public function apply_settings( array $data ) {
        if ( ! current_user_can( 'manage_options' ) ) {
            return new WP_Error(
                'blaminhor_essentials_forbidden',
                __( 'Insufficient capabilities to modify this module.', 'blaminhor-essentials' )
            );
        }

        // Some modules (SEO Manager, etc.) call wp_safe_redirect() at the end
        // of process_settings() to implement the admin POST-Redirect-Get
        // pattern. That's meaningless in CLI/REST contexts and surfaces as a
        // noisy WP-CLI "Some code is trying to do a URL redirect" warning +
        // backtrace. We neutralise both: short-circuit the redirect itself
        // (priority 1, returns false) AND temporarily detach WP-CLI's own
        // wp_redirect_handler (which prints the warning regardless of the
        // filtered $location value).
        $block_redirect       = static function () { return false; };
        $wp_cli_handler       = 'WP_CLI\\Utils\\wp_redirect_handler';
        $wp_cli_handler_prio  = function_exists( 'has_filter' ) ? has_filter( 'wp_redirect', $wp_cli_handler ) : false;
        add_filter( 'wp_redirect', $block_redirect, 1 );
        if ( false !== $wp_cli_handler_prio ) {
            remove_filter( 'wp_redirect', $wp_cli_handler, (int) $wp_cli_handler_prio );
        }

        try {
            $this->process_settings( $data );
        } catch ( \Throwable $e ) {
            remove_filter( 'wp_redirect', $block_redirect, 1 );
            if ( false !== $wp_cli_handler_prio ) {
                add_filter( 'wp_redirect', $wp_cli_handler, (int) $wp_cli_handler_prio );
            }
            return new WP_Error(
                'blaminhor_essentials_apply_failed',
                $e->getMessage()
            );
        }

        remove_filter( 'wp_redirect', $block_redirect, 1 );
        if ( false !== $wp_cli_handler_prio ) {
            add_filter( 'wp_redirect', $wp_cli_handler, (int) $wp_cli_handler_prio );
        }
        return true;
    }

    /**
     * Render nonce field for settings form
     */
    protected function render_nonce_field() {
        wp_nonce_field( 'blaminhor_essentials_module_' . $this->id, 'blaminhor_essentials_module_nonce_' . $this->id );
    }

    /**
     * Verify AJAX request has valid nonce and user capability
     *
     * Sends JSON error and exits if verification fails.
     *
     * @param string $capability Required capability (default: manage_options).
     * @return bool True if verified.
     */
    protected function verify_ajax_request( $capability = 'manage_options' ) {
        check_ajax_referer( 'blaminhor_essentials_admin', 'nonce' );

        if ( ! current_user_can( $capability ) ) {
            wp_send_json_error( __( 'Unauthorized', 'blaminhor-essentials' ) );
        }

        return true;
    }

    /**
     * Get sanitized POST value
     *
     * @param string $key     POST key.
     * @param mixed  $default Default value.
     * @param string $type    Sanitization type (text, key, int, bool, url, email).
     * @return mixed Sanitized value.
     */
    protected function get_post_value( $key, $default = '', $type = 'text' ) {
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce is verified by the calling method before using this helper
        if ( ! isset( $_POST[ $key ] ) ) {
            return $default;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified by caller, value is sanitized below based on $type
        $value = wp_unslash( $_POST[ $key ] );

        switch ( $type ) {
            case 'key':
                return sanitize_key( $value );
            case 'int':
                return absint( $value );
            case 'bool':
                return filter_var( $value, FILTER_VALIDATE_BOOLEAN );
            case 'url':
                return esc_url_raw( $value );
            case 'email':
                return sanitize_email( $value );
            case 'textarea':
                return sanitize_textarea_field( $value );
            case 'filename':
                return sanitize_file_name( $value );
            case 'text':
            default:
                return sanitize_text_field( $value );
        }
    }

    /**
     * Get the module's option key
     *
     * @return string
     */
    protected function get_option_key() {
        return 'blaminhor_essentials_module_' . $this->id . '_settings';
    }

}
