<?php
/**
 * Opening Screen Preset Model
 *
 * Handles CRUD operations for opening screen presets.
 *
 * @package BoostBot
 * @since   1.1.0
 */

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

/**
 * Class BoostBot_Opening_Screen_Preset_Model
 *
 * Model for managing opening screen preset data.
 */
class BoostBot_Opening_Screen_Preset_Model extends BoostBot_Base_Model {

    use BoostBot_Localizable;

    /**
     * Table name without prefix.
     *
     * @var string
     */
    protected $table_name = 'boostbot_opening_screen_presets';

    /**
     * Fillable columns.
     *
     * @var array
     */
    protected $fillable = array(
        'name',
        'slug',
        'language',
        'description',
        'is_default',
        'is_system',
        'mode',
        'welcome_text',
        'show_welcome',
        'questions',
        'ai_instructions',
        'qa_pairs',
        'contact_list_id',
        'show_departments',
        'show_contact_cards',
        'attached_files',
        'opening_buttons',
    );

    /**
     * Mode constants.
     */
    const MODE_QUESTIONS_AI = 'questions_ai';
    const MODE_QUESTIONS_ANSWERS = 'questions_answers';
    const MODE_DEPARTMENTS = 'departments';

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

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

    /**
     * Validate data before insert/update.
     *
     * @param array    $data The data to validate.
     * @param int|null $id   Optional. The record ID for updates.
     * @return true|WP_Error True if valid, WP_Error otherwise.
     */
    public function validate( $data, $id = null ) {
        $errors = new WP_Error();

        // Name is required
        if ( null === $id && empty( $data['name'] ) ) {
            $errors->add( 'name_required', __( 'שם הגדרת הפתיחה הוא שדה חובה', 'boostbot' ) );
        }

        // Mode must be valid
        $valid_modes = array(
            self::MODE_QUESTIONS_AI,
            self::MODE_QUESTIONS_ANSWERS,
            self::MODE_DEPARTMENTS,
        );

        if ( isset( $data['mode'] ) && ! in_array( $data['mode'], $valid_modes, true ) ) {
            $errors->add( 'invalid_mode', __( 'סוג הודעת פתיחה לא חוקי', 'boostbot' ) );
        }

        if ( $errors->has_errors() ) {
            return $errors;
        }

        return true;
    }

    /**
     * Sanitize data before insert/update.
     *
     * @param array $data The data to sanitize.
     * @return array The sanitized data.
     */
    public function sanitize( $data ) {
        $sanitized = array();

        if ( isset( $data['name'] ) ) {
            $sanitized['name'] = sanitize_text_field( $data['name'] );
        }
        if ( isset( $data['slug'] ) ) {
            $sanitized['slug'] = sanitize_title( $data['slug'] );
        }
        if ( isset( $data['description'] ) ) {
            $sanitized['description'] = sanitize_textarea_field( $data['description'] );
        }
        if ( isset( $data['is_default'] ) ) {
            $sanitized['is_default'] = absint( $data['is_default'] );
        }
        if ( isset( $data['is_system'] ) ) {
            $sanitized['is_system'] = absint( $data['is_system'] );
        }
        if ( isset( $data['mode'] ) ) {
            $sanitized['mode'] = sanitize_text_field( $data['mode'] );
        }
        if ( isset( $data['welcome_text'] ) ) {
            $sanitized['welcome_text'] = sanitize_text_field( $data['welcome_text'] );
        }
        if ( isset( $data['show_welcome'] ) ) {
            $sanitized['show_welcome'] = absint( $data['show_welcome'] );
        }
        if ( isset( $data['questions'] ) ) {
            $sanitized['questions'] = is_string( $data['questions'] ) 
                ? $data['questions'] 
                : wp_json_encode( $data['questions'] );
        }
        if ( isset( $data['ai_instructions'] ) ) {
            $sanitized['ai_instructions'] = sanitize_textarea_field( $data['ai_instructions'] );
        }
        if ( isset( $data['qa_pairs'] ) ) {
            $sanitized['qa_pairs'] = is_string( $data['qa_pairs'] ) 
                ? $data['qa_pairs'] 
                : wp_json_encode( $data['qa_pairs'] );
        }
        if ( isset( $data['contact_list_id'] ) ) {
            $sanitized['contact_list_id'] = $data['contact_list_id'] ? absint( $data['contact_list_id'] ) : null;
        }
        if ( isset( $data['show_departments'] ) ) {
            $sanitized['show_departments'] = absint( $data['show_departments'] );
        }
        if ( isset( $data['show_contact_cards'] ) ) {
            $sanitized['show_contact_cards'] = absint( $data['show_contact_cards'] );
        }
        if ( isset( $data['attached_files'] ) ) {
            $sanitized['attached_files'] = is_string( $data['attached_files'] ) 
                ? $data['attached_files'] 
                : wp_json_encode( $data['attached_files'] );
        }
        if ( isset( $data['opening_buttons'] ) ) {
            $sanitized['opening_buttons'] = is_string( $data['opening_buttons'] ) 
                ? $data['opening_buttons'] 
                : wp_json_encode( $data['opening_buttons'] );
        }

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

        return $sanitized;
    }

    /**
     * Get all presets.
     *
     * @return array Array of preset objects.
     */
    public function get_all( $args = array() ) {
        // Honor $args (WHERE / LIMIT / OFFSET) so language scoping and pagination work.
        // Previously this override ignored $args entirely, leaking presets of every
        // language into a language-scoped list and ignoring the requested limit.
        $where_sql = '';
        $params    = array();

        if ( ! empty( $args['where'] ) && is_array( $args['where'] ) ) {
            $clauses = array();
            foreach ( $args['where'] as $col => $val ) {
                $col = preg_replace( '/[^a-zA-Z0-9_]/', '', (string) $col );
                if ( '' === $col ) {
                    continue;
                }
                $clauses[] = "`{$col}` = %s";
                $params[]  = $val;
            }
            if ( $clauses ) {
                $where_sql = ' WHERE ' . implode( ' AND ', $clauses );
            }
        }

        $sql = "SELECT * FROM {$this->table_name}{$where_sql} ORDER BY is_default DESC, is_system DESC, name ASC";

        if ( isset( $args['limit'] ) ) {
            $sql     .= ' LIMIT %d OFFSET %d';
            $params[] = max( 0, (int) $args['limit'] );
            $params[] = isset( $args['offset'] ) ? max( 0, (int) $args['offset'] ) : 0;
        }

        if ( $params ) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Column names sanitized to [a-zA-Z0-9_] via preg_replace() and values bound through prepare(); table name is an internal constant.
            return $this->wpdb->get_results( $this->wpdb->prepare( $sql, $params ) );
        }

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- No user input in this branch; table name is an internal constant and the ORDER BY is a fixed literal.
        return $this->wpdb->get_results( $sql );
    }

    /**
     * Get the default preset.
     *
     * @return object|null The default preset or null.
     */
    public function get_default() {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $this->wpdb->get_row(
            "SELECT * FROM {$this->table_name} WHERE is_default = 1 LIMIT 1" // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        );
    }

    /**
     * Get preset by slug.
     *
     * @param string $slug The preset slug.
     * @return object|null The preset or null.
     */
    public function get_by_slug( $slug ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $this->wpdb->get_row(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table_name} WHERE slug = %s LIMIT 1", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $slug
            )
        );
    }

    /**
     * Set a preset as the default.
     *
     * @param int $id The preset ID.
     * @return bool True on success.
     */
    public function set_as_default( $id ) {
        $id = (int) $id;

        // Determine the preset's language so we clear the existing default ONLY within
        // that language (per-language default invariant, matching the other Localizable
        // models). Clearing across all languages left other languages with no default.
        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $language = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT language FROM {$this->table_name} WHERE id = %d", $id ) );

        $clear_where = array( 'is_default' => 1 );
        if ( ! empty( $language ) ) {
            $clear_where['language'] = $language;
        }

        // First, unset the default within the same language
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $this->wpdb->update(
            $this->table_name,
            array( 'is_default' => 0 ),
            $clear_where
        );

        // Set the new default
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return (bool) $this->wpdb->update(
            $this->table_name,
            array( 'is_default' => 1 ),
            array( 'id' => $id )
        );
    }

    /**
     * Generate a unique slug.
     *
     * @param string $name The preset name.
     * @param int    $exclude_id Optional. ID to exclude from uniqueness check.
     * @return string The unique slug.
     */
    public function generate_slug( $name, $exclude_id = null ) {
        $slug = sanitize_title( $name );
        $original_slug = $slug;
        $counter = 1;

        while ( $this->slug_exists( $slug, $exclude_id ) ) {
            $slug = $original_slug . '-' . $counter;
            $counter++;
        }

        return $slug;
    }

    /**
     * Check if a slug exists.
     *
     * @param string $slug       The slug to check.
     * @param int    $exclude_id Optional. ID to exclude.
     * @return bool True if exists.
     */
    private function slug_exists( $slug, $exclude_id = null ) {
        $query = "SELECT COUNT(*) FROM {$this->table_name} WHERE slug = %s"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $params = array( $slug );

        if ( $exclude_id ) {
            $query .= ' AND id != %d';
            $params[] = $exclude_id;
        }

        // 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 = $this->wpdb->get_var( $this->wpdb->prepare( $query, $params ) );

        return (int) $count > 0;
    }

    /**
     * Parse JSON fields when getting a preset.
     *
     * @param object $preset The preset object.
     * @return object The preset with parsed JSON fields.
     */
    public function parse_json_fields( $preset ) {
        if ( ! $preset ) {
            return $preset;
        }

        if ( ! empty( $preset->questions ) ) {
            $preset->questions = json_decode( $preset->questions, true ) ?: array();
        } else {
            $preset->questions = array();
        }

        if ( ! empty( $preset->qa_pairs ) ) {
            $preset->qa_pairs = json_decode( $preset->qa_pairs, true ) ?: array();
        } else {
            $preset->qa_pairs = array();
        }

        if ( ! empty( $preset->attached_files ) ) {
            $preset->attached_files = json_decode( $preset->attached_files, true ) ?: array();
        } else {
            $preset->attached_files = array();
        }

        if ( ! empty( $preset->opening_buttons ) ) {
            $preset->opening_buttons = json_decode( $preset->opening_buttons, true ) ?: array();
        } else {
            $preset->opening_buttons = array();
        }

        return $preset;
    }

    /**
     * Get bots linked to this preset.
     *
     * @param int $preset_id The preset ID.
     * @return array Array of bot objects with id and bot_name.
     */
    public function get_linked_bots( $preset_id ) {
        $bot_table = self::sanitize_identifier( $this->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().
        return $this->wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $this->wpdb->prepare(
                "SELECT id, bot_name FROM {$bot_table} WHERE opening_screen_preset_id = %d ORDER BY bot_name ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $preset_id
            )
        );
    }

    /**
     * Get all available modes.
     *
     * @return array Modes with labels.
     */
    public static function get_modes() {
        return array(
            self::MODE_QUESTIONS_AI => __( 'שאלות + AI', 'boostbot' ),
            self::MODE_QUESTIONS_ANSWERS => __( 'שאלות + תשובות', 'boostbot' ),
            self::MODE_DEPARTMENTS => __( 'מחלקות', 'boostbot' ),
        );
    }

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

