<?php
/**
 * Cryptoniq Options Framework
 *
 * Lightweight, self-contained options framework for the Cryptoniq plugin.
 * Replaces the Redux Framework dependency while keeping the existing
 * storage format and field IDs intact, so installed users keep their data.
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit();
}

if ( class_exists( 'Cryptoniq_Options' ) ) {
    return;
}

class Cryptoniq_Options {

    /** @var array Registered config. */
    private static $config = array();

    /** @var array Saved values (cached per-request). */
    private static $values = null;

    /** @var array Errors keyed by field id (after save). */
    private static $errors = array();

    /** @var string Transient key for storing post-save errors across the redirect. */
    const ERROR_TRANSIENT = 'cryptoniq_options_errors';

    /**
     * Register the options panel.
     *
     * @param array $config See admin/options.php for the expected shape.
     */
    public static function register( array $config ) {
        $defaults = array(
            'opt_name'    => '',
            'menu_title'  => 'Options',
            'page_title'  => 'Options',
            'page_slug'   => 'cryptoniq-options',
            'page_parent' => 'themes.php',
            'capability'  => 'manage_options',
            'version'     => '1.0.0',
            'sections'    => array(),
        );

        self::$config = wp_parse_args( $config, $defaults );

        add_action( 'admin_menu', array( __CLASS__, 'register_menu' ), 50 );
        add_action( 'admin_init', array( __CLASS__, 'maybe_save' ) );
        add_action( 'admin_init', array( __CLASS__, 'ensure_defaults' ), 20 );
        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
    }

    /**
     * Add the options page under the configured parent menu.
     */
    public static function register_menu() {
        $cfg = self::$config;

        // Top-level menu when no parent is set.
        if ( empty( $cfg['page_parent'] ) ) {
            add_menu_page(
                $cfg['page_title'],
                $cfg['menu_title'],
                $cfg['capability'],
                $cfg['page_slug'],
                array( __CLASS__, 'render_page' ),
                isset( $cfg['menu_icon'] ) ? $cfg['menu_icon'] : 'dashicons-admin-generic',
                isset( $cfg['menu_position'] ) ? $cfg['menu_position'] : null
            );
            return;
        }

        add_submenu_page(
            $cfg['page_parent'],
            $cfg['page_title'],
            $cfg['menu_title'],
            $cfg['capability'],
            $cfg['page_slug'],
            array( __CLASS__, 'render_page' )
        );
    }

    /**
     * Enqueue framework CSS/JS on the options page only.
     */
    public static function enqueue_assets( $hook ) {
        if ( ! self::is_options_page() ) {
            return;
        }

        $cfg     = self::$config;
        $url     = plugin_dir_url( __FILE__ );
        $dir     = plugin_dir_path( __FILE__ );
        $css_ver = $cfg['version'] . '.' . (string) @filemtime( $dir . 'assets/css/admin.css' );
        $js_ver  = $cfg['version'] . '.' . (string) @filemtime( $dir . 'assets/js/admin.js' );

        wp_enqueue_style( 'wp-color-picker' );
        wp_enqueue_script( 'wp-color-picker' );

        wp_enqueue_style(
            'cryptoniq-options',
            $url . 'assets/css/admin.css',
            array(),
            $css_ver
        );

        wp_enqueue_script(
            'cryptoniq-options',
            $url . 'assets/js/admin.js',
            array( 'jquery', 'wp-color-picker' ),
            $js_ver,
            true
        );
    }

    /**
     * Are we currently on the options page?
     */
    public static function is_options_page() {
        if ( ! is_admin() ) {
            return false;
        }
        return isset( $_GET['page'] ) && $_GET['page'] === self::$config['page_slug'];
    }

    /**
     * Return the entire stored options array.
     */
    public static function get_all() {
        if ( self::$values === null ) {
            $stored = get_option( self::$config['opt_name'], array() );
            self::$values = is_array( $stored ) ? $stored : array();
        }
        return self::$values;
    }

    /**
     * Get a single option value.
     */
    public static function get( $key, $default = '' ) {
        $all = self::get_all();
        return isset( $all[ $key ] ) ? $all[ $key ] : $default;
    }

    /**
     * Update a single option value (writes the full array back).
     */
    public static function set( $key, $value ) {
        $all = self::get_all();
        $all[ $key ] = $value;
        self::$values = $all;
        update_option( self::$config['opt_name'], $all );
    }

    /**
     * Iterate every field in every section.
     *
     * @return Generator yielding [$section_id, $field]
     */
    public static function each_field() {
        foreach ( self::$config['sections'] as $sid => $section ) {
            if ( empty( $section['fields'] ) || ! is_array( $section['fields'] ) ) {
                continue;
            }
            foreach ( $section['fields'] as $field ) {
                if ( empty( $field['id'] ) || empty( $field['type'] ) ) {
                    continue;
                }
                yield array( $sid, $field );
            }
        }
    }

    /**
     * Write defaults on first install / when keys are missing.
     */
    public static function ensure_defaults() {
        $stored = get_option( self::$config['opt_name'], null );
        $needs_save = ! is_array( $stored );
        $merged = is_array( $stored ) ? $stored : array();

        foreach ( self::each_field() as $entry ) {
            list( , $field ) = $entry;

            if ( array_key_exists( $field['id'], $merged ) ) {
                continue;
            }

            $default = self::field_default( $field );
            if ( $default !== null ) {
                $merged[ $field['id'] ] = $default;
                $needs_save = true;
            }
        }

        if ( $needs_save ) {
            update_option( self::$config['opt_name'], $merged );
            self::$values = $merged;
        }
    }

    /**
     * Compute the default value for a field, in the storage shape the
     * existing read-side code expects.
     */
    private static function field_default( array $field ) {
        $type = $field['type'];

        switch ( $type ) {
            case 'checkbox':
            case 'image_select':
                if ( empty( $field['options'] ) || ! is_array( $field['options'] ) ) {
                    return null;
                }
                $out = array();
                $defaults = isset( $field['default'] ) && is_array( $field['default'] )
                    ? $field['default']
                    : array();
                foreach ( $field['options'] as $k => $_ ) {
                    $out[ $k ] = ! empty( $defaults[ $k ] ) ? '1' : '';
                }
                return $out;

            case 'image_radio':
                // Single-string default; falls back to first option key if no default.
                $opts = isset( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array();
                if ( isset( $field['default'] ) && array_key_exists( (string) $field['default'], $opts ) ) {
                    return (string) $field['default'];
                }
                return $opts ? (string) array_key_first( $opts ) : '';

            case 'switch':
                if ( array_key_exists( 'default', $field ) ) {
                    return $field['default'] ? '1' : '';
                }
                return '';

            case 'multi_text':
                return array();

            default:
                return isset( $field['default'] ) ? $field['default'] : '';
        }
    }

    /**
     * Handle form submission.
     */
    public static function maybe_save() {
        if ( empty( $_POST['cryptoniq_options_submit'] ) ) {
            return;
        }
        if ( ! current_user_can( self::$config['capability'] ) ) {
            return;
        }
        check_admin_referer( 'cryptoniq_options_save', 'cryptoniq_options_nonce' );

        $existing = self::get_all();
        $new      = $existing; // start from existing so untouched keys survive
        $errors   = array();

        $posted = isset( $_POST[ self::$config['opt_name'] ] ) && is_array( $_POST[ self::$config['opt_name'] ] )
            ? wp_unslash( $_POST[ self::$config['opt_name'] ] )
            : array();

        foreach ( self::each_field() as $entry ) {
            list( , $field ) = $entry;
            $id   = $field['id'];
            $type = $field['type'];

            // Locked fields can't be updated from the UI — keep the existing
            // value untouched. Server-side enforcement: even a tampered form
            // can't push values into a locked field.
            if ( ! empty( $field['locked'] ) ) {
                continue;
            }

            $raw = isset( $posted[ $id ] ) ? $posted[ $id ] : null;

            $clean = self::sanitize_field( $field, $raw );

            // Built-in validators.
            if ( ! empty( $field['validate'] ) ) {
                $vresult = self::run_builtin_validator( $field['validate'], $clean, $field );
                if ( $vresult['error'] ) {
                    $errors[ $id ] = $vresult['error'];
                }
                $clean = $vresult['value'];
            }

            // Custom validate_callback. Signature matches the existing
            // Redux callbacks in admin/validators.php:
            //     callback( $field, $value, $existing_value )
            //     -> [ 'value' => mixed, 'error' => $field|false ]
            if ( ! empty( $field['validate_callback'] ) && is_callable( $field['validate_callback'] ) ) {
                $prev = isset( $existing[ $id ] ) ? $existing[ $id ] : null;

                if ( $type === 'multi_text' && is_array( $clean ) && empty( $field['validate_full'] ) ) {
                    // Per-row mode (default for multi_text). Validator receives a
                    // single string $value per row. Used when each row stands
                    // alone (e.g. format check).
                    $row_errors = array();
                    $kept       = array();
                    foreach ( $clean as $row ) {
                        $r = call_user_func( $field['validate_callback'], $field, $row, $prev );
                        $kept[] = isset( $r['value'] ) ? $r['value'] : $row;
                        if ( ! empty( $r['error'] ) ) {
                            $row_errors[] = is_array( $r['error'] ) && isset( $r['error']['msg'] )
                                ? $r['error']['msg']
                                : 'Invalid value';
                        }
                    }
                    $clean = $kept;
                    if ( $row_errors ) {
                        $errors[ $id ] = array(
                            'id'  => $id,
                            'msg' => implode( ' / ', array_unique( $row_errors ) ),
                        );
                    }
                } else {
                    // Field-level mode. Validator receives the full $clean value
                    // (array for multi_text, scalar otherwise). Used when the
                    // value as a whole needs to be validated against an external
                    // system (e.g. one API call per field instead of per row).
                    $r = call_user_func( $field['validate_callback'], $field, $clean, $prev );
                    if ( isset( $r['value'] ) ) {
                        $clean = $r['value'];
                    }
                    if ( ! empty( $r['error'] ) ) {
                        $errors[ $id ] = is_array( $r['error'] )
                            ? $r['error']
                            : array( 'id' => $id, 'msg' => (string) $r['error'] );
                    }
                }
            }

            $new[ $id ] = $clean;
        }

        update_option( self::$config['opt_name'], $new );
        self::$values = $new;

        if ( $errors ) {
            set_transient( self::ERROR_TRANSIENT, $errors, 60 );
        } else {
            delete_transient( self::ERROR_TRANSIENT );
        }

        $url = add_query_arg(
            array(
                'page'           => self::$config['page_slug'],
                'settings-saved' => $errors ? '0' : '1',
            ),
            admin_url( 'admin.php' )
        );

        // Preserve the active tab.
        if ( ! empty( $_POST['cryptoniq_active_tab'] ) ) {
            $url = add_query_arg( 'tab', sanitize_key( wp_unslash( $_POST['cryptoniq_active_tab'] ) ), $url );
        }

        wp_safe_redirect( $url );
        exit;
    }

    /**
     * Type-aware sanitization. Always returns a value shaped exactly the
     * way the existing read-side code expects (see functions.php, gateway.php).
     */
    private static function sanitize_field( array $field, $raw ) {
        $type = $field['type'];

        switch ( $type ) {
            case 'text':
                return is_scalar( $raw ) ? sanitize_text_field( (string) $raw ) : '';

            case 'textarea':
                return is_scalar( $raw ) ? sanitize_textarea_field( (string) $raw ) : '';

            case 'select':
                $val = is_scalar( $raw ) ? (string) $raw : '';
                if ( ! empty( $field['options'] ) && is_array( $field['options'] ) ) {
                    if ( ! array_key_exists( $val, $field['options'] ) ) {
                        $val = isset( $field['default'] ) ? (string) $field['default'] : '';
                    }
                }
                return $val;

            case 'switch':
                return ! empty( $raw ) ? '1' : '';

            case 'color':
                $val = is_scalar( $raw ) ? trim( (string) $raw ) : '';
                if ( $val === '' ) {
                    return isset( $field['default'] ) ? (string) $field['default'] : '';
                }
                if ( preg_match( '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $val ) ) {
                    return $val;
                }
                return isset( $field['default'] ) ? (string) $field['default'] : '';

            case 'checkbox':
            case 'image_select':
                // Storage shape matches Redux: every known option key present,
                // value '1' when checked, '' when not. gateway.php:67 relies
                // on this (`if (!$enabled) continue;`).
                //
                // Locked options are forced to '' regardless of what was POSTed,
                // so a tampered form can't activate a feature the user hasn't
                // unlocked.
                $out = array();
                $posted = is_array( $raw ) ? $raw : array();
                if ( ! empty( $field['options'] ) && is_array( $field['options'] ) ) {
                    foreach ( $field['options'] as $k => $opt ) {
                        $is_locked = is_array( $opt ) && ! empty( $opt['locked'] );
                        $out[ $k ] = ( ! $is_locked && ! empty( $posted[ $k ] ) ) ? '1' : '';
                    }
                }
                return $out;

            case 'image_radio':
                // Single string, must be an UNLOCKED option key — never empty.
                $val  = is_scalar( $raw ) ? (string) $raw : '';
                $opts = isset( $field['options'] ) && is_array( $field['options'] ) ? $field['options'] : array();

                $is_locked_key = function ( $k ) use ( $opts ) {
                    if ( ! array_key_exists( $k, $opts ) ) return true;
                    return is_array( $opts[ $k ] ) && ! empty( $opts[ $k ]['locked'] );
                };

                if ( $val !== '' && ! $is_locked_key( $val ) ) {
                    return $val;
                }
                if ( isset( $field['default'] ) && ! $is_locked_key( (string) $field['default'] ) ) {
                    return (string) $field['default'];
                }
                // Pick the first unlocked option.
                foreach ( $opts as $k => $_ ) {
                    if ( ! $is_locked_key( (string) $k ) ) {
                        return (string) $k;
                    }
                }
                return '';

            case 'multi_text':
                if ( ! is_array( $raw ) ) {
                    return array();
                }
                // When the field config sets 'unique' => true, drop duplicate
                // rows (case-insensitive). Useful for things like wallet
                // addresses where the same value twice is meaningless.
                $unique = ! empty( $field['unique'] );
                $out    = array();
                $seen   = array();
                foreach ( $raw as $row ) {
                    if ( ! is_scalar( $row ) ) {
                        continue;
                    }
                    $clean = sanitize_text_field( (string) $row );
                    if ( $clean === '' ) {
                        continue;
                    }
                    if ( $unique ) {
                        $key = strtolower( $clean );
                        if ( isset( $seen[ $key ] ) ) {
                            continue;
                        }
                        $seen[ $key ] = true;
                    }
                    $out[] = $clean;
                }
                return $out;

            default:
                return is_scalar( $raw ) ? sanitize_text_field( (string) $raw ) : $raw;
        }
    }

    /**
     * Built-in validator keywords. Returns [ 'value' => mixed, 'error' => array|false ].
     */
    private static function run_builtin_validator( $rule, $value, array $field ) {
        $error = false;

        switch ( $rule ) {
            case 'not_empty':
                if ( $value === '' || $value === null || ( is_array( $value ) && empty( $value ) ) ) {
                    $error = array(
                        'id'  => $field['id'],
                        'msg' => 'This field cannot be empty.',
                    );
                    $value = isset( $field['default'] ) ? $field['default'] : $value;
                }
                break;

            case 'color':
                if ( is_string( $value ) && $value !== ''
                    && ! preg_match( '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $value ) ) {
                    $error = array(
                        'id'  => $field['id'],
                        'msg' => 'Invalid color value.',
                    );
                    $value = isset( $field['default'] ) ? $field['default'] : '';
                }
                break;
        }

        return array( 'value' => $value, 'error' => $error );
    }

    /**
     * Render the options page.
     */
    public static function render_page() {
        $cfg     = self::$config;
        $values  = self::get_all();
        $errors  = get_transient( self::ERROR_TRANSIENT );
        if ( $errors ) {
            delete_transient( self::ERROR_TRANSIENT );
        }
        self::$errors = is_array( $errors ) ? $errors : array();

        $sections   = $cfg['sections'];
        $first_key  = $sections ? array_key_first( $sections ) : '';
        $active_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : $first_key;
        if ( ! isset( $sections[ $active_tab ] ) ) {
            $active_tab = $first_key;
        }

        do_action( 'cryptoniq_options/' . $cfg['opt_name'] . '/before_render' );

        $form_action = admin_url( 'admin.php?page=' . $cfg['page_slug'] );
        ?>
        <div class="cryptoniq-options-wrap" data-opt-name="<?php echo esc_attr( $cfg['opt_name'] ); ?>">
            <div class="cn-header">
                <div class="cn-header-title">
                    <h1><?php echo esc_html( $cfg['page_title'] ); ?></h1>
                    <span class="cn-version">v<i><?php echo esc_html( $cfg['version'] ); ?></i></span>
                    <?php do_action( 'cryptoniq_options/' . $cfg['opt_name'] . '/header_title_extra' ); ?>
                </div>
                <div class="cn-header-extra"><?php do_action( 'cryptoniq_options/' . $cfg['opt_name'] . '/header_extra' ); ?></div>
            </div>

            <?php if ( isset( $_GET['settings-saved'] ) && $_GET['settings-saved'] === '1' ) : ?>
                <div class="cn-toast cn-toast-success" role="status">
                    <i class="cn-toast-icon icon ion-md-checkmark-circle" aria-hidden="true"></i>
                    <span>Settings saved</span>
                </div>
            <?php elseif ( isset( $_GET['settings-saved'] ) && $_GET['settings-saved'] === '0' ) : ?>
                <div class="cn-toast cn-toast-error" role="alert">
                    <i class="cn-toast-icon icon ion-md-alert" aria-hidden="true"></i>
                    <span>Saved with errors</span>
                </div>
            <?php endif; ?>

            <form method="post" action="<?php echo esc_url( $form_action ); ?>" class="cn-form" novalidate>
                <?php wp_nonce_field( 'cryptoniq_options_save', 'cryptoniq_options_nonce' ); ?>
                <input type="hidden" name="cryptoniq_options_submit" value="1" />
                <input type="hidden" name="cryptoniq_active_tab" class="cn-active-tab-input" value="<?php echo esc_attr( $active_tab ); ?>" />

                <div class="cn-layout">
                    <nav class="cn-tabs" role="tablist">
                        <?php foreach ( $sections as $sid => $section ) :
                            $icon = isset( $section['icon'] ) ? $section['icon'] : '';
                        ?>
                            <a href="#" class="cn-tab <?php echo $sid === $active_tab ? 'is-active' : ''; ?>"
                               data-tab="<?php echo esc_attr( $sid ); ?>" role="tab">
                                <?php if ( $icon ) : ?>
                                    <i class="cn-tab-icon <?php echo esc_attr( $icon ); ?>"></i>
                                <?php endif; ?>
                                <span><?php echo esc_html( $section['title'] ); ?></span>
                            </a>
                        <?php endforeach; ?>
                    </nav>

                    <div class="cn-panels">
                        <?php foreach ( $sections as $sid => $section ) :
                            $has_content = isset( $section['content'] );
                        ?>
                            <section class="cn-panel <?php echo $sid === $active_tab ? 'is-active' : ''; ?>"
                                     data-tab="<?php echo esc_attr( $sid ); ?>">
                                <header class="cn-panel-head">
                                    <h2><?php echo esc_html( $section['title'] ); ?></h2>
                                    <?php if ( ! empty( $section['desc'] ) ) : ?>
                                        <div class="cn-panel-desc"><?php echo wp_kses_post( $section['desc'] ); ?></div>
                                    <?php endif; ?>
                                </header>

                                <?php if ( $has_content ) :
                                    // `content` may be a string of HTML or a callable returning a string.
                                    $html = is_callable( $section['content'] )
                                        ? (string) call_user_func( $section['content'] )
                                        : (string) $section['content'];
                                ?>
                                    <div class="cn-panel-content"><?php
                                        // Trusted markup — comes from the plugin's own options.php config,
                                        // not from user input. Use wp_kses_post for a permissive but safe filter.
                                        echo wp_kses_post( $html );
                                    ?></div>
                                <?php endif; ?>

                                <?php if ( ! empty( $section['fields'] ) && is_array( $section['fields'] ) ) : ?>
                                    <div class="cn-fields">
                                        <?php foreach ( $section['fields'] as $field ) :
                                            self::render_field( $field, $values );
                                        endforeach; ?>
                                    </div>
                                <?php endif; ?>
                            </section>
                        <?php endforeach; ?>

                        <div class="cn-footer">
                            <button type="submit" class="cn-btn cn-btn-primary cn-save">Save Changes</button>
                        </div>
                    </div>
                </div>
            </form>
        </div>
        <?php

        do_action( 'cryptoniq_options/' . $cfg['opt_name'] . '/after_render' );
    }

    /**
     * Render a single field row.
     */
    private static function render_field( array $field, array $values ) {
        if ( empty( $field['id'] ) || empty( $field['type'] ) ) {
            return;
        }

        $id    = $field['id'];
        $type  = $field['type'];
        $value = array_key_exists( $id, $values ) ? $values[ $id ] : self::field_default( $field );

        $error = isset( self::$errors[ $id ] ) ? self::$errors[ $id ] : null;
        $error_msg = ( is_array( $error ) && isset( $error['msg'] ) ) ? $error['msg'] : '';

        $required = isset( $field['required'] ) && is_array( $field['required'] )
            ? $field['required']
            : null;

        $is_locked = ! empty( $field['locked'] );

        $classes = array( 'cn-field', 'cn-field-' . $type );
        if ( ! empty( $field['class'] ) ) {
            $classes[] = $field['class'];
        }
        if ( $error_msg ) {
            $classes[] = 'has-error';
        }
        if ( $is_locked ) {
            $classes[] = 'cn-field-locked';
        }

        $attrs = '';
        if ( $required ) {
            $attrs .= ' data-required-target="' . esc_attr( $required[0] ) . '"';
            $attrs .= ' data-required-op="' . esc_attr( $required[1] ) . '"';
            $attrs .= ' data-required-value="' . esc_attr( is_scalar( $required[2] ) ? $required[2] : '' ) . '"';
        }
        ?>
        <div class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
             data-field-id="<?php echo esc_attr( $id ); ?>"<?php echo $attrs; ?>>
            <div class="cn-field-label">
                <?php if ( ! empty( $field['title'] ) ) : ?>
                    <label for="cn-field-<?php echo esc_attr( $id ); ?>"><?php echo esc_html( $field['title'] ); ?></label>
                <?php endif; ?>
                <?php if ( ! empty( $field['subtitle'] ) ) : ?>
                    <p class="cn-field-sub"><?php echo wp_kses_post( $field['subtitle'] ); ?></p>
                <?php endif; ?>
            </div>
            <div class="cn-field-input">
                <?php self::render_input( $field, $value ); ?>
                <?php if ( $is_locked ) :
                    $locked_text = ! empty( $field['locked_label'] )
                        ? $field['locked_label']
                        : __( 'Not Activated', 'cryptoniq-pro' );
                ?>
                    <div class="cn-field-locked-overlay" aria-hidden="true">
                        <span class="cn-field-locked-badge">
                            <i class="icon ion-md-lock"></i>
                            <span><?php echo esc_html( $locked_text ); ?></span>
                        </span>
                    </div>
                <?php endif; ?>
                <?php if ( $error_msg ) : ?>
                    <p class="cn-field-error"><?php echo esc_html( $error_msg ); ?></p>
                <?php endif; ?>
                <?php if ( ! empty( $field['desc'] ) ) : ?>
                    <p class="cn-field-desc"><?php echo wp_kses_post( $field['desc'] ); ?></p>
                <?php endif; ?>
            </div>
        </div>
        <?php
    }

    /**
     * Dispatch to the appropriate input renderer.
     */
    private static function render_input( array $field, $value ) {
        $type = $field['type'];
        $file = __DIR__ . '/fields/' . str_replace( '_', '-', $type ) . '.php';

        if ( ! file_exists( $file ) ) {
            echo '<em>Unknown field type: ' . esc_html( $type ) . '</em>';
            return;
        }

        $name_base = self::$config['opt_name'] . '[' . $field['id'] . ']';
        include $file;
    }
}
