<?php
/**
 * WooCommerce Coupon Model for BoostBot.
 *
 * Handles CRUD operations for WooCommerce coupons linked to 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_Coupon_Model
 *
 * Model for managing WooCommerce coupons within presets.
 *
 * @since 1.3.0
 */
class BoostBot_WooCommerce_Coupon_Model extends BoostBot_Base_Model {

    use BoostBot_Localizable;

    public const PURPOSE_WIDGET = 'widget';
    public const PURPOSE_BUNDLE = 'bundle';

    /**
     * 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(
        'preset_id',
        'wc_coupon_id',
        'display_name',
        'description',
        'keywords',
        'priority',
        'min_cart_total',
        'max_cart_total',
        'applicable_categories',
        'is_active',
        'for_widget',
        'for_bundle',
        'language',
    );

    /**
     * Constructor.
     */
    public function __construct() {
        parent::__construct();
        $this->table_name = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_woocommerce_coupons' );
    }

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

    /**
     * Get coupons by preset ID.
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return array List of coupons.
     */
    public function get_by_preset( int $preset_id, ?string $language = null ): array {
        global $wpdb;

        $table = $this->get_table_name();
        $sql   = "SELECT * FROM {$table} WHERE preset_id = %d"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $args  = array( $preset_id );

        if ( $language ) {
            $sql   .= ' AND language = %s';
            $args[] = $language;
        }

        $sql .= ' ORDER BY priority DESC, id ASC';

        // 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().
        $coupons = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare( $sql, ...$args ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        );

        // Enrich with WooCommerce coupon data.
        foreach ( $coupons as &$coupon ) {
            $coupon = $this->enrich_with_wc_data( $coupon );
        }

        return $coupons ?: array();
    }

    /**
     * Get active coupons by preset ID.
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return array List of active coupons.
     */
    public function get_active_by_preset( int $preset_id, ?string $language = null ): array {
        global $wpdb;

        $table = $this->get_table_name();
        $sql   = "SELECT * FROM {$table} WHERE preset_id = %d AND is_active = 1"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $args  = array( $preset_id );

        if ( $language ) {
            $sql   .= ' AND language = %s';
            $args[] = $language;
        }

        $sql .= ' ORDER BY priority DESC, id ASC';

        // 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().
        $coupons = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare( $sql, ...$args ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        );

        // Enrich with WooCommerce coupon data.
        foreach ( $coupons as &$coupon ) {
            $coupon = $this->enrich_with_wc_data( $coupon );
        }

        return $coupons ?: array();
    }

    /**
     * Create a new coupon entry.
     *
     * @since 1.3.0
     * @param array $data Coupon data.
     * @return int|false Insert ID or false on failure.
     */
    public function create( $data ) {
        // Validate WooCommerce coupon exists.
        if ( ! empty( $data['wc_coupon_id'] ) && ! $this->wc_coupon_exists( $data['wc_coupon_id'] ) ) {
            return false;
        }

        $data = $this->normalize_purpose_flags( $data );

        // Encode arrays to JSON.
        if ( isset( $data['applicable_categories'] ) && is_array( $data['applicable_categories'] ) ) {
            $data['applicable_categories'] = wp_json_encode( $data['applicable_categories'] );
        }

        return parent::create( $data );
    }

    /**
     * Update a coupon entry.
     *
     * @since 1.3.0
     * @param int   $id   Coupon ID.
     * @param array $data Update data.
     * @return bool True on success.
     */
    public function update( $id, $data ) {
        $data = $this->normalize_purpose_flags( $data, false );

        // Encode arrays to JSON.
        if ( isset( $data['applicable_categories'] ) && is_array( $data['applicable_categories'] ) ) {
            $data['applicable_categories'] = wp_json_encode( $data['applicable_categories'] );
        }

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

    /**
     * Delete all coupons by preset ID.
     *
     * @since 1.3.0
     * @param int $preset_id Preset ID.
     * @return bool True on success.
     */
    public function delete_by_preset( int $preset_id ): bool {
        global $wpdb;

        $table = $this->get_table_name();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->delete(
            $table,
            array( 'preset_id' => $preset_id ),
            array( '%d' )
        );

        return false !== $result;
    }

    /**
     * Check if a WooCommerce coupon exists.
     *
     * @since 1.3.0
     * @param int $wc_coupon_id WooCommerce coupon ID.
     * @return bool True if exists.
     */
    private function wc_coupon_exists( int $wc_coupon_id ): bool {
        if ( ! class_exists( 'WC_Coupon' ) ) {
            return false;
        }

        $coupon = new WC_Coupon( $wc_coupon_id );

        return $coupon->get_id() > 0;
    }

    /**
     * Enrich coupon data with WooCommerce coupon info.
     *
     * @since 1.3.0
     * @param object $coupon Coupon object.
     * @return object Enriched coupon.
     */
    private function enrich_with_wc_data( object $coupon ): object {
        if ( ! class_exists( 'WC_Coupon' ) || empty( $coupon->wc_coupon_id ) ) {
            $coupon->wc_coupon_code = '';
            $coupon->wc_discount_type = '';
            $coupon->wc_discount_amount = 0;
            $coupon->wc_is_valid = false;
            return $coupon;
        }

        $wc_coupon = new WC_Coupon( $coupon->wc_coupon_id );

        if ( ! $wc_coupon->get_id() ) {
            $coupon->wc_coupon_code = '';
            $coupon->wc_discount_type = '';
            $coupon->wc_discount_amount = 0;
            $coupon->wc_is_valid = false;
            return $coupon;
        }

        $coupon->wc_coupon_code     = $wc_coupon->get_code();
        $coupon->wc_discount_type   = $wc_coupon->get_discount_type();
        $coupon->wc_discount_amount = $wc_coupon->get_amount();
        $coupon->wc_is_valid        = $this->is_coupon_valid( $wc_coupon );

        // Parse JSON fields.
        if ( ! empty( $coupon->applicable_categories ) && is_string( $coupon->applicable_categories ) ) {
            $coupon->applicable_categories = json_decode( $coupon->applicable_categories, true ) ?: array();
        }

        return $coupon;
    }

    /**
     * Check if a WooCommerce coupon is currently valid.
     *
     * @since 1.3.0
     * @param WC_Coupon $wc_coupon WooCommerce coupon.
     * @return bool True if valid.
     */
    private function is_coupon_valid( $wc_coupon ): bool {
        // Check expiry.
        $expiry = $wc_coupon->get_date_expires();
        if ( $expiry && $expiry->getTimestamp() < time() ) {
            return false;
        }

        // Check usage limits.
        $usage_limit = $wc_coupon->get_usage_limit();
        $usage_count = $wc_coupon->get_usage_count();

        if ( $usage_limit > 0 && $usage_count >= $usage_limit ) {
            return false;
        }

        return true;
    }

    /**
     * Check if coupon is already in preset.
     *
     * @since 1.3.0
     * @param int $preset_id    Preset ID.
     * @param int $wc_coupon_id WooCommerce coupon ID.
     * @return bool True if exists.
     */
    public function exists_in_preset( int $preset_id, int $wc_coupon_id ): bool {
        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().
        $count = $wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare(
                "SELECT COUNT(*) FROM {$table} WHERE preset_id = %d AND wc_coupon_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $preset_id,
                $wc_coupon_id
            )
        );

        return (int) $count > 0;
    }

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

        return true;
    }

    /**
     * Sanitize coupon 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['display_name'] ) ) {
            $sanitized['display_name'] = sanitize_text_field( $sanitized['display_name'] );
        }

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

        foreach ( array( 'preset_id', 'wc_coupon_id', 'priority', 'is_active', 'for_widget', 'for_bundle' ) as $int_field ) {
            if ( isset( $sanitized[ $int_field ] ) ) {
                $sanitized[ $int_field ] = (int) $sanitized[ $int_field ];
            }
        }

        return $sanitized;
    }

    /**
     * Get coupon for scoring (for Coupon Selector Service).
     *
     * @since 1.3.0
     * @param int $id Coupon ID.
     * @return array|null Coupon data for scoring.
     */
    public function get_for_scoring( int $id ): ?array {
        $coupon = $this->get( $id );

        if ( ! $coupon ) {
            return null;
        }

        $coupon = $this->enrich_with_wc_data( $coupon );

        return array(
            'id'                => $coupon->id,
            'wc_coupon_id'      => $coupon->wc_coupon_id,
            'coupon_code'       => $coupon->wc_coupon_code,
            'discount_type'     => $coupon->wc_discount_type,
            'discount_amount'   => $coupon->wc_discount_amount,
            'display_name'      => $coupon->display_name,
            'description'       => $coupon->description,
            'keywords'          => $coupon->keywords,
            'priority'          => (int) $coupon->priority,
            'min_cart_total'    => $coupon->min_cart_total ? (float) $coupon->min_cart_total : null,
            'max_cart_total'    => $coupon->max_cart_total ? (float) $coupon->max_cart_total : null,
            'is_valid'          => $coupon->wc_is_valid,
        );
    }

    /**
     * Get coupons by preset ID filtered for a specific purpose.
     *
     * @param int    $preset_id Preset ID.
     * @param string $purpose   Purpose key.
     * @return array
     */
    public function get_by_preset_and_purpose( int $preset_id, string $purpose, ?string $language = null ): array {
        return $this->query_by_preset_and_purpose( $preset_id, $purpose, false, $language );
    }

    /**
     * Get active coupons by preset ID filtered for a specific purpose.
     *
     * @param int    $preset_id Preset ID.
     * @param string $purpose   Purpose key.
     * @return array
     */
    public function get_active_by_preset_and_purpose( int $preset_id, string $purpose, ?string $language = null ): array {
        return $this->query_by_preset_and_purpose( $preset_id, $purpose, true, $language );
    }

    /**
     * Query coupons for a preset and optional purpose filter.
     *
     * @param int    $preset_id    Preset ID.
     * @param string $purpose      Purpose key.
     * @param bool   $active_only  Whether to restrict to active rows.
     * @return array
     */
    private function query_by_preset_and_purpose( int $preset_id, string $purpose, bool $active_only, ?string $language = null ): array {
        global $wpdb;

        $table          = $this->get_table_name();
        $purpose_column = $this->get_purpose_column( $purpose );
        if ( ! $purpose_column ) {
            return array();
        }

        $sql  = "SELECT * FROM {$table} WHERE preset_id = %d AND {$purpose_column} = 1"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $args = array( $preset_id );
        if ( $active_only ) {
            $sql .= ' AND is_active = 1';
        }
        if ( $language ) {
            $sql   .= ' AND language = %s';
            $args[] = $language;
        }
        $sql .= ' ORDER BY priority DESC, id ASC';

        // 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().
        $coupons = $wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $wpdb->prepare( $sql, ...$args ) // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
        );

        foreach ( $coupons as &$coupon ) {
            $coupon = $this->enrich_with_wc_data( $coupon );
        }

        return $coupons ?: array();
    }

    /**
     * Convert purpose key to DB column name.
     *
     * @param string $purpose Purpose key.
     * @return string|null
     */
    private function get_purpose_column( string $purpose ): ?string {
        if ( self::PURPOSE_WIDGET === $purpose ) {
            return 'for_widget';
        }

        if ( self::PURPOSE_BUNDLE === $purpose ) {
            return 'for_bundle';
        }

        return null;
    }

    /**
     * Ensure purpose flags have sane defaults.
     *
     * @param array $data                 Coupon payload.
     * @param bool  $apply_default_values Whether to default missing flags to enabled.
     * @return array
     */
    private function normalize_purpose_flags( array $data, bool $apply_default_values = true ): array {
        foreach ( array( 'for_widget', 'for_bundle' ) as $field ) {
            if ( array_key_exists( $field, $data ) ) {
                $data[ $field ] = empty( $data[ $field ] ) ? 0 : 1;
            } elseif ( $apply_default_values ) {
                $data[ $field ] = 1;
            }
        }

        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 );
    }
}

