<?php
/**
 * WooCommerce Preset Model for BoostBot.
 *
 * Handles CRUD operations for WooCommerce presets.
 *
 * @package BoostBot
 * @since   1.3.0
 */

// Prevent direct access.
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_WooCommerce_Preset_Model
 *
 * Model for managing WooCommerce presets.
 *
 * @since 1.3.0
 */
class BoostBot_WooCommerce_Preset_Model extends BoostBot_Base_Model {

    use BoostBot_Localizable;

    /**
     * Table name (set in constructor with prefix).
     *
     * @var string
     */
    protected $table_name;

    /**
     * Primary key column.
     *
     * @var string
     */
    protected $primary_key = 'id';

    /**
     * Fillable columns.
     *
     * @var array
     */
    protected $fillable = array(
        // Basic.
        'name',
        'slug',
        'description',
        'is_default',
        'is_system',
        'is_active',
        'language',

        // Discovery Flow.
        'discovery_mode',
        'discovery_steps',
        'budget_ranges',

        // Bundle Settings.
        'bundle_enabled',
        'bundle_min_products',
        'bundle_max_products',
        'bundle_discount_type',
        'bundle_discount_value',
        'bundle_discount_method',

        // Product Settings.
        'product_mode',
        'allowed_categories',
        'excluded_categories',
        'specific_product_ids',
        'preferred_product_ids',
        'excluded_product_ids',
        'include_on_sale',
        'include_variable',
        'include_featured_only',
        'min_stock',
        'min_rating',
        'max_single_product_percent',

        // Pricing Settings.
        'discount_tiers',
        'max_discount_amount',
        'min_order_for_discount',
        'show_original_prices',
        'show_savings',
        'show_per_product_savings',
        'round_prices',
        'round_to',

        // Product Cards.
        'product_card_enabled',

        // Bundle Product Scope (Tier 2).
        'bundle_product_mode',
        'bundle_allowed_categories',
        'bundle_excluded_categories',
        'bundle_specific_product_ids',
        'bundle_excluded_product_ids',

        // Coupon Settings.
        'coupon_enabled',
        'coupon_mode',
        'auto_coupon_id',
        'auto_select_best_coupon',
        'generate_unique_coupon',
        'generated_coupon_prefix',
        'generated_coupon_discount',
        'generated_coupon_expiry_days',
        'show_coupon_code',
        'show_coupon_value',
        'coupon_max_per_order',

        // UI Settings.
        'allow_customization',
        'cart_redirect',
        'show_cart_button',

        // Messages.
        'intro_message',
        'loading_message',
        'success_message',
        'empty_message',
        'error_message',
    );

    /**
     * Constructor.
     */
    public function __construct() {
        parent::__construct();
        $this->table_name = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_woocommerce_presets' );
        $this->column_types = array(
            'id'                   => 'bigint',
            'is_default'           => 'tinyint',
            'is_system'            => 'tinyint',
            'is_active'            => 'tinyint',
            'product_card_enabled' => 'tinyint',
            'bundle_enabled'       => 'tinyint',
            'include_on_sale'      => 'tinyint',
            'include_variable'     => 'tinyint',
            'include_featured_only' => 'tinyint',
            'coupon_enabled'       => 'tinyint',
            'auto_select_best_coupon' => 'tinyint',
            'generate_unique_coupon'  => 'tinyint',
            'show_coupon_code'     => 'tinyint',
            'show_coupon_value'    => 'tinyint',
            'allow_customization'  => 'tinyint',
            'cart_redirect'        => 'tinyint',
            'show_cart_button'     => 'tinyint',
            'show_original_prices' => 'tinyint',
            'show_savings'         => 'tinyint',
            'show_per_product_savings' => 'tinyint',
            'round_prices'         => 'tinyint',
        );
    }

    /**
     * Get the full table name.
     *
     * @return string
     */
    public function get_table_name(): string {
        return $this->table_name;
    }

    /**
     * Get all presets.
     *
     * @since 1.3.0
     * @param array $args Query arguments.
     * @return array List of presets.
     */
    public function get_all( $args = array() ) {
        $defaults = array(
            'orderby' => 'is_default DESC, name',
            'order'   => 'ASC',
        );

        $args = wp_parse_args( $args, $defaults );

        return parent::get_all( $args );
    }

    /**
     * Get preset by ID with coupons.
     *
     * @since 1.3.0
     * @param int $id Preset ID.
     * @return object|null Preset with coupons.
     */
    public function get_with_coupons( int $id ): ?object {
        $preset = $this->get( $id );

        if ( ! $preset ) {
            return null;
        }

        // Get associated coupons.
        $coupon_model    = new BoostBot_WooCommerce_Coupon_Model();
        $preset->coupons = $coupon_model->get_by_preset( $id, $preset->language ?? null );

        // Parse all JSON fields.
        $this->parse_preset_json_fields( $preset );

        return $preset;
    }

    /**
     * Parse all JSON fields in a preset object.
     *
     * @since 1.3.0
     * @param object $preset Preset object (modified in place).
     * @return void
     */
    private function parse_preset_json_fields( object &$preset ): void {
        $json_fields = array(
            'discovery_steps',
            'budget_ranges',
            'allowed_categories',
            'excluded_categories',
            'specific_product_ids',
            'preferred_product_ids',
            'excluded_product_ids',
            'discount_tiers',
            'bundle_allowed_categories',
            'bundle_excluded_categories',
            'bundle_specific_product_ids',
            'bundle_excluded_product_ids',
        );

        foreach ( $json_fields as $field ) {
            if ( isset( $preset->$field ) ) {
                $preset->$field = $this->parse_json_field( $preset->$field );
            }
        }
    }

    /**
     * Get the default preset.
     *
     * Defaults are seeded per-language, so when a language is supplied the lookup
     * is scoped to that language; if none exists it falls back to any default.
     *
     * @since 1.3.0
     * @param string $language Optional language code to scope the lookup.
     * @return object|null Default preset.
     */
    public function get_default( string $language = '' ): ?object {
        global $wpdb;

        $table = $this->get_table_name();

        if ( '' !== $language ) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
            $scoped = $wpdb->get_row(
                // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
                $wpdb->prepare(
                    "SELECT * FROM {$table} WHERE is_default = %d AND language = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                    1,
                    $language
                )
            );

            if ( $scoped ) {
                return $scoped;
            }
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $result = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE is_default = %d LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                1
            )
        );

        return $result ?: null;
    }

    /**
     * Get preset by slug.
     *
     * @since 1.3.0
     * @param string $slug Preset slug.
     * @return object|null Preset.
     */
    public function get_by_slug( string $slug ): ?object {
        global $wpdb;

        $table = $this->get_table_name();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $result = $wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE slug = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $slug
            )
        );

        return $result ?: null;
    }

    /**
     * Create a new preset.
     *
     * @since 1.3.0
     * @param array $data Preset data.
     * @return int|false Insert ID or false on failure.
     */
    public function create( $data ) {
        // Generate slug if not provided.
        if ( empty( $data['slug'] ) && ! empty( $data['name'] ) ) {
            $data['slug'] = $this->generate_unique_slug( $data['name'] );
        }

        // Handle default flag. Scope the clear to the preset's language so setting
        // a default in one language does not wipe other languages' defaults.
        if ( ! empty( $data['is_default'] ) && $data['is_default'] ) {
            $this->clear_default( (string) ( $data['language'] ?? '' ) );
        }

        // Encode all JSON fields.
        $data = $this->encode_json_fields( $data );

        return parent::create( $data );
    }

    /**
     * Update a preset.
     *
     * @since 1.3.0
     * @param int   $id   Preset ID.
     * @param array $data Update data.
     * @return bool True on success.
     */
    public function update( $id, $data ) {
        // Handle default flag. Scope the clear to the preset's language (from the
        // update payload, falling back to the stored row) so setting a default in
        // one language does not wipe other languages' defaults.
        if ( ! empty( $data['is_default'] ) && $data['is_default'] ) {
            $language = (string) ( $data['language'] ?? '' );
            if ( '' === $language ) {
                $existing = $this->get( $id );
                $language = (string) ( $existing->language ?? '' );
            }
            $this->clear_default( $language );
        }

        // Encode all JSON fields.
        $data = $this->encode_json_fields( $data );

        return parent::update( $id, $data );
    }

    /**
     * Delete a preset.
     *
     * @since 1.3.0
     * @param int $id Preset ID.
     * @return bool True on success.
     */
    public function delete( $id ) {
        $preset = $this->get( $id );

        // Don't allow deleting system presets.
        if ( $preset && $preset->is_system ) {
            return false;
        }

        // Delete associated coupons first.
        $coupon_model = new BoostBot_WooCommerce_Coupon_Model();
        $coupon_model->delete_by_preset( $id );

        // Update bots that use this preset to use default (scoped to the deleted
        // preset's language so bots move to the default of the same language).
        $this->reassign_bots_to_default( $id, (string) ( $preset->language ?? '' ) );

        return parent::delete( $id );
    }

    /**
     * Clear default flag from presets.
     *
     * When a language is supplied the clear is scoped to that language so the
     * per-language default invariant is preserved (matching the other
     * Localizable models); an empty language clears across all languages.
     *
     * @since 1.3.0
     * @param string $language Optional language code to scope the clear.
     * @return void
     */
    private function clear_default( string $language = '' ): void {
        global $wpdb;

        $table = $this->get_table_name();

        $where        = array( 'is_default' => 1 );
        $where_format = array( '%d' );

        if ( '' !== $language ) {
            $where['language'] = $language;
            $where_format[]    = '%s';
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $table,
            array( 'is_default' => 0 ),
            $where,
            array( '%d' ),
            $where_format
        );
    }

    /**
     * Generate a unique slug.
     *
     * @since 1.3.0
     * @param string $name Base name.
     * @return string Unique slug.
     */
    private function generate_unique_slug( string $name ): string {
        $slug      = sanitize_title( $name );
        $base_slug = $slug;
        $counter   = 1;

        while ( $this->get_by_slug( $slug ) ) {
            $slug = $base_slug . '-' . $counter;
            ++$counter;
        }

        return $slug;
    }

    /**
     * Reassign bots from deleted preset to default.
     *
     * @since 1.3.0
     * @param int    $preset_id Deleted preset ID.
     * @param string $language  Optional language to scope the default lookup.
     * @return void
     */
    private function reassign_bots_to_default( int $preset_id, string $language = '' ): void {
        $default = $this->get_default( $language );

        if ( ! $default ) {
            return;
        }

        global $wpdb;

        $bots_table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_bots' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            $bots_table,
            array( 'woocommerce_preset_id' => $default->id ),
            array( 'woocommerce_preset_id' => $preset_id ),
            array( '%d' ),
            array( '%d' )
        );
    }

    /**
     * Parse JSON field.
     *
     * @since 1.3.0
     * @param mixed $value Field value.
     * @return array Parsed array.
     */
    private function parse_json_field( $value ): array {
        if ( empty( $value ) ) {
            return array();
        }

        if ( is_array( $value ) ) {
            return $value;
        }

        $decoded = json_decode( $value, true );

        return is_array( $decoded ) ? $decoded : array();
    }

    /**
     * Get bots using this preset.
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return array List of bots.
     */
    public function get_linked_bots( int $preset_id ): array {
        global $wpdb;

        $bots_table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_bots' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $bots = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT id, bot_name, bot_slug FROM {$bots_table} WHERE woocommerce_preset_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $preset_id
            )
        );

        return $bots ?: array();
    }

    /**
     * Count bots using this preset.
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return int Number of bots.
     */
    public function count_linked_bots( int $preset_id ): int {
        global $wpdb;

        $bots_table = self::sanitize_identifier( $wpdb->prefix . 'boostbot_bots' );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table identifier sanitized via sanitize_identifier(); values bind via prepare().
        $count = $wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$bots_table} WHERE woocommerce_preset_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $preset_id
            )
        );

        return (int) $count;
    }

    /**
     * Validate preset data.
     *
     * @since 1.3.0
     * @param array    $data Data to validate.
     * @param int|null $id   Preset ID for updates.
     * @return true|WP_Error True if valid, WP_Error otherwise.
     */
    public function validate( $data, $id = null ) {
        if ( empty( $data['name'] ) ) {
            return new \WP_Error( 'missing_name', __( 'Preset name is required.', 'boostbot' ) );
        }

        return true;
    }

    /**
     * Sanitize preset data.
     *
     * @since 1.3.0
     * @param array $data Data to sanitize.
     * @return array Sanitized data.
     */
    public function sanitize( $data ) {
        $sanitized = array();

        foreach ( $this->fillable as $field ) {
            if ( isset( $data[ $field ] ) ) {
                $sanitized[ $field ] = $data[ $field ];
            }
        }

        if ( isset( $sanitized['name'] ) ) {
            $sanitized['name'] = sanitize_text_field( $sanitized['name'] );
        }

        if ( isset( $sanitized['slug'] ) ) {
            $sanitized['slug'] = sanitize_title( $sanitized['slug'] );
        }

        if ( isset( $sanitized['description'] ) ) {
            $sanitized['description'] = sanitize_textarea_field( $sanitized['description'] );
        }

        return $sanitized;
    }

    /**
     * Get bundle settings for a preset (for Bundle Builder Service).
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return array|null Bundle settings or null if not found.
     */
    public function get_bundle_settings( int $preset_id ): ?array {
        $preset = $this->get( $preset_id );

        if ( ! $preset || ! $preset->bundle_enabled ) {
            return null;
        }

        return array(
            // Discovery Flow.
            'discovery_mode'           => 'ai',
            'discovery_steps'          => $this->parse_json_field( $preset->discovery_steps ),
            'budget_ranges'            => $this->parse_json_field( $preset->budget_ranges ),

            // Product Settings.
            'product_settings'         => array(
                'mode'                      => $preset->product_mode ?? 'all',
                'allowed_categories'        => $this->parse_json_field( $preset->allowed_categories ),
                'excluded_categories'       => $this->parse_json_field( $preset->excluded_categories ),
                'specific_product_ids'      => $this->parse_json_field( $preset->specific_product_ids ),
                'preferred_product_ids'     => $this->parse_json_field( $preset->preferred_product_ids ),
                'excluded_product_ids'      => $this->parse_json_field( $preset->excluded_product_ids ),
                'include_on_sale'           => (bool) ( $preset->include_on_sale ?? true ),
                'include_variable'          => (bool) ( $preset->include_variable ?? true ),
                'include_featured_only'     => (bool) ( $preset->include_featured_only ?? false ),
                'min_stock'                 => (int) ( $preset->min_stock ?? 1 ),
                'min_rating'                => (float) ( $preset->min_rating ?? 0 ),
                'min_products_in_bundle'    => (int) ( $preset->bundle_min_products ?? 2 ),
                'max_products_in_bundle'    => (int) ( $preset->bundle_max_products ?? 5 ),
                'max_single_product_percent' => (int) ( $preset->max_single_product_percent ?? 50 ),
            ),

            // Pricing Settings.
            'pricing_settings'         => array(
                'discount_type'          => $preset->bundle_discount_type ?? 'percent',
                'discount_percent'       => (float) ( $preset->bundle_discount_value ?? 10 ),
                'discount_fixed'         => (float) ( $preset->bundle_discount_value ?? 0 ),
                'discount_tiers'         => $this->parse_json_field( $preset->discount_tiers ),
                'max_discount_amount'    => $preset->max_discount_amount ? (float) $preset->max_discount_amount : null,
                'min_order_for_discount' => (float) ( $preset->min_order_for_discount ?? 0 ),
                'show_savings'           => (bool) ( $preset->show_savings ?? true ),
                'show_original_prices'   => (bool) ( $preset->show_original_prices ?? true ),
                'show_per_product_savings' => (bool) ( $preset->show_per_product_savings ?? false ),
                'round_prices'           => (bool) ( $preset->round_prices ?? true ),
                'round_to'               => (int) ( $preset->round_to ?? 0 ),
            ),

            // Coupon Settings.
            'coupon_settings'          => array(
                'enabled'               => (bool) ( $preset->coupon_enabled ?? false ),
                'mode'                  => $preset->coupon_mode ?? 'none',
                'auto_coupon_id'        => $preset->auto_coupon_id ? (int) $preset->auto_coupon_id : null,
                'auto_select_best'      => (bool) ( $preset->auto_select_best_coupon ?? false ),
                'generate_unique'       => (bool) ( $preset->generate_unique_coupon ?? false ),
                'generated_prefix'      => $preset->generated_coupon_prefix ?? 'BUNDLE-',
                'generated_discount'    => (float) ( $preset->generated_coupon_discount ?? 10 ),
                'generated_expiry_days' => (int) ( $preset->generated_coupon_expiry_days ?? 7 ),
                'show_coupon_code'      => (bool) ( $preset->show_coupon_code ?? true ),
                'show_coupon_value'     => (bool) ( $preset->show_coupon_value ?? true ),
            ),

            // UI Settings.
            'allow_customization'      => (bool) ( $preset->allow_customization ?? true ),
            'cart_redirect'            => (bool) ( $preset->cart_redirect ?? true ),
            'show_cart_button'        => (bool) ( $preset->show_cart_button ?? false ),

            // Messages.
            'intro_message'            => $preset->intro_message ?? '',
            'loading_message'          => $preset->loading_message ?? '',
            'success_message'          => $preset->success_message ?? '',
            'empty_message'            => $preset->empty_message ?? '',
            'error_message'            => $preset->error_message ?? '',

            // Legacy format for backwards compatibility.
            'min_products'             => (int) ( $preset->bundle_min_products ?? 2 ),
            'max_products'             => (int) ( $preset->bundle_max_products ?? 5 ),
            'discount_percent'         => (float) ( $preset->bundle_discount_value ?? 10 ),
            'categories'               => $this->parse_json_field( $preset->allowed_categories ),
            'exclude_categories'       => $this->parse_json_field( $preset->excluded_categories ),
            'include_on_sale'          => (bool) ( $preset->include_on_sale ?? true ),
            'include_variable'         => (bool) ( $preset->include_variable ?? true ),
            'show_savings'             => (bool) ( $preset->show_savings ?? true ),
        );
    }

    /**
     * Encode JSON fields before save.
     *
     * @since 1.3.0
     * @param array $data Data to process.
     * @return array Processed data with JSON-encoded fields.
     */
    private function encode_json_fields( array $data ): array {
        $json_fields = array(
            'discovery_steps',
            'budget_ranges',
            'allowed_categories',
            'excluded_categories',
            'specific_product_ids',
            'preferred_product_ids',
            'excluded_product_ids',
            'discount_tiers',
            'bundle_allowed_categories',
            'bundle_excluded_categories',
            'bundle_specific_product_ids',
            'bundle_excluded_product_ids',
        );

        foreach ( $json_fields as $field ) {
            if ( isset( $data[ $field ] ) && is_array( $data[ $field ] ) ) {
                $data[ $field ] = wp_json_encode( $data[ $field ] );
            }
        }

        return $data;
    }

    /**
     * Restrict a SQL identifier to [A-Za-z0-9_] characters.
     *
     * DDL cannot use $wpdb->prepare() for identifiers, so any identifier
     * that ends up interpolated into a query first passes through this
     * allow-list.
     *
     * @param string $identifier Raw identifier.
     * @return string Identifier with disallowed characters removed.
     */
    protected static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}

