<?php
/**
 * Meetings API Endpoint
 *
 * Handles meeting management via REST API.
 *
 * @package BoostBot
 * @since   1.0.0
 */

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

/**
 * Class BoostBot_Meetings_Endpoint
 *
 * REST API endpoints for meetings.
 */
class BoostBot_Meetings_Endpoint extends BoostBot_Base_Endpoint {

    /**
     * REST API namespace.
     *
     * @var string
     */
    protected $namespace = 'boostbot/v1';

    /**
     * REST API base.
     *
     * @var string
     */
    protected $rest_base = 'meetings';

    /**
     * Load associated contacts from a widget config, filtering out deleted/invalid contacts.
     *
     * @param int $widget_id The widget ID.
     * @return array{contacts: int[], slot_interval: int, config: array} Filtered contact IDs, slot_interval, and raw config.
     */
    private function load_widget_contacts( int $widget_id ): array {
        $result = array(
            'contacts'      => array(),
            'slot_interval' => 0,
            'config'        => array(),
        );

        if ( $widget_id <= 0 || ! class_exists( 'BoostBot_Widget_Model' ) ) {
            return $result;
        }

        $widget_model = new BoostBot_Widget_Model();
        $widget       = $widget_model->get( $widget_id );
        if ( ! $widget || empty( $widget->config ) ) {
            return $result;
        }

        $config          = is_string( $widget->config ) ? json_decode( $widget->config, true ) : (array) $widget->config;
        $result['config'] = $config;

        if ( ! empty( $config['slot_interval'] ) ) {
            $result['slot_interval'] = absint( $config['slot_interval'] );
        }

        if ( empty( $config['associated_contacts'] ) || ! is_array( $config['associated_contacts'] ) ) {
            return $result;
        }

        $raw_ids       = array_map( 'absint', $config['associated_contacts'] );
        $contact_model = new BoostBot_Contact_Model();

        // Filter out contacts that no longer exist in the database (Fix #4)
        foreach ( $raw_ids as $cid ) {
            $contact = $contact_model->get( $cid );
            if ( $contact ) {
                $result['contacts'][] = $cid;
            }
        }

        return $result;
    }

    /**
     * Register routes.
     *
     * @return void
     */
    public function register(): void {
        $language_arg = array(
            'language' => array(
                'type'              => 'string',
                'sanitize_callback' => 'sanitize_text_field',
            ),
        );

        // Get all meetings (with optional date range)
        $this->register_route(
            '/',
            'GET',
            array( $this, 'get_meetings' ),
            array( $this, 'admin_access' ),
            $language_arg
        );

        // Get single meeting
        $this->register_route(
            '/(?P<id>\d+)',
            'GET',
            array( $this, 'get_meeting' ),
            array( $this, 'admin_access' )
        );

        // Create meeting
        $this->register_route(
            '/',
            'POST',
            array( $this, 'create_meeting' ),
            array( $this, 'admin_access' )
        );

        // Update meeting
        $this->register_route(
            '/(?P<id>\d+)',
            'PUT',
            array( $this, 'update_meeting' ),
            array( $this, 'admin_access' )
        );

        // Delete meeting
        $this->register_route(
            '/(?P<id>\d+)',
            'DELETE',
            array( $this, 'delete_meeting' ),
            array( $this, 'admin_access' )
        );

        // Cancel meeting (with notifications)
        $this->register_route(
            '/(?P<id>\d+)/cancel',
            'POST',
            array( $this, 'cancel_meeting' ),
            array( $this, 'admin_access' )
        );

        // Get upcoming meetings
        $this->register_route(
            '/upcoming',
            'GET',
            array( $this, 'get_upcoming' ),
            array( $this, 'admin_access' )
        );

        // Get today's meetings
        $this->register_route(
            '/today',
            'GET',
            array( $this, 'get_today' ),
            array( $this, 'admin_access' )
        );

        // Get meeting statistics
        $this->register_route(
            '/stats',
            'GET',
            array( $this, 'get_stats' ),
            array( $this, 'admin_access' ),
            $language_arg
        );

        // PUBLIC: Book meeting (for chat widget)
        $this->register_route(
            '/book',
            'POST',
            array( $this, 'book_meeting' ),
            array( $this, 'public_with_rate_limit' )  // Public access for chat widget, rate-limited
        );

        // PUBLIC: Get availability for a specific date (for chat widget)
        $this->register_route(
            '/availability',
            'GET',
            array( $this, 'get_availability' ),
            array( $this, 'public_with_rate_limit' )  // Public access for chat widget, rate-limited
        );

        // PUBLIC: Get available days of week for a widget (for chat widget calendar)
        $this->register_route(
            '/available-days',
            'GET',
            array( $this, 'get_available_days' ),
            array( $this, 'public_with_rate_limit' )
        );
    }

    /**
     * Get meetings list.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_meetings( $request ) {
        $model = new BoostBot_Meeting_Model();

        $start_date = $request->get_param( 'start_date' ) ?? gmdate( 'Y-m-d' );
        $end_date   = $request->get_param( 'end_date' ) ?? gmdate( 'Y-m-d', strtotime( '+30 days' ) );
        $chatbot_id = $request->get_param( 'chatbot_id' );
        $language   = $this->get_request_language( $request );

        $meetings = $model->get_by_date_range( $start_date, $end_date, $chatbot_id, $language );

        // Enrich meetings with conversation DB id for frontend linking
        $conv_model = new BoostBot_Conversation_Model();
        foreach ( $meetings as &$meeting ) {
            if ( ! empty( $meeting->conversation_id ) ) {
                $conv = $conv_model->get_by_conversation_id( $meeting->conversation_id );
                $meeting->conversation_db_id = $conv ? $conv->id : null;
            } else {
                $meeting->conversation_db_id = null;
            }
        }
        unset( $meeting );

        return $this->success( array( 'meetings' => $meetings ) );
    }

    /**
     * Get single meeting.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_meeting( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Meeting_Model();

        $meeting = $model->get_with_details( $id );

        if ( ! $meeting ) {
            return $this->not_found( 'Meeting' );
        }

        return $this->success( array( 'meeting' => $meeting ) );
    }

    /**
     * Create a new meeting.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function create_meeting( $request ) {
        $model    = new BoostBot_Meeting_Model();
        $req_lang = BoostBot_Language::with_fallback( $request->get_param( 'language' ) );

        $data = array(
            'language'           => $req_lang,
            'conversation_id'    => $request->get_param( 'conversation_id' ),
            'chatbot_id'         => $request->get_param( 'chatbot_id' ),
            'widget_id'          => $request->get_param( 'widget_id' ),
            'customer_name'      => sanitize_text_field( $request->get_param( 'customer_name' ) ),
            'customer_email'     => sanitize_email( $request->get_param( 'customer_email' ) ),
            'customer_phone'     => sanitize_text_field( $request->get_param( 'customer_phone' ) ),
            'title'              => sanitize_text_field( $request->get_param( 'title' ) ),
            'description'        => sanitize_textarea_field( $request->get_param( 'description' ) ),
            'meeting_date'       => sanitize_text_field( $request->get_param( 'meeting_date' ) ),
            'start_time'         => sanitize_text_field( $request->get_param( 'start_time' ) ),
            'end_time'           => sanitize_text_field( $request->get_param( 'end_time' ) ),
            'timezone'           => sanitize_text_field( $request->get_param( 'timezone' ) ?? 'Asia/Jerusalem' ),
            'attendee_contacts'  => $request->get_param( 'attendee_contacts' ),
            'custom_fields'      => $request->get_param( 'custom_fields' ),
            'status'             => 'scheduled',
        );

        // Validate required fields.
        if ( empty( $data['title'] ) || empty( $data['meeting_date'] ) || empty( $data['start_time'] ) ) {
            return $this->error( __( 'Missing required fields', 'boostbot' ), 400 );
        }

        // Check slot availability.
        if ( ! $model->is_slot_available( $data['meeting_date'], $data['start_time'], $data['end_time'] ) ) {
            return $this->error( __( 'Time slot not available', 'boostbot' ), 400 );
        }

        // Create Google Calendar event with Meet if connected.
        $google = new BoostBot_Google_Calendar_Service();
        if ( $google->is_connected() && $request->get_param( 'create_google_meet' ) !== false ) {
            // Get location from widget config if widget_id provided
            $location = '';
            if ( ! empty( $data['widget_id'] ) ) {
                $widget_model = new BoostBot_Widget_Model();
                $widget = $widget_model->get( $data['widget_id'] );
                if ( $widget ) {
                    $config = json_decode( $widget->config, true );
                    if ( ! empty( $config['location'] ) ) {
                        $location = $config['location'];
                    }
                }
            }
            // Fallback to site address if no location in widget config
            if ( empty( $location ) ) {
                $location = get_option( 'site_address', '' );
            }

            $meet_result = $google->create_meeting(
                array(
                    'title'          => $data['title'],
                    'description'    => $data['description'],
                    'location'       => $location,
                    'start_datetime' => $data['meeting_date'] . 'T' . $data['start_time'] . ':00',
                    'end_datetime'   => $data['meeting_date'] . 'T' . $data['end_time'] . ':00',
                    'timezone'       => $data['timezone'],
                    'attendees'      => array_filter( array( $data['customer_email'] ) ),
                )
            );

            if ( $meet_result['success'] ) {
                $data['google_event_id']      = $meet_result['event_id'];
                $data['google_meet_link']     = $meet_result['meet_link'];
                $data['google_calendar_link'] = $meet_result['html_link'];
            }
        }

        $meeting_id = $model->create( $data );

        if ( ! $meeting_id ) {
            return $this->error( __( 'Failed to create meeting', 'boostbot' ), 500 );
        }

        // Send confirmation email.
        $this->send_confirmation_email( $model->get_with_details( $meeting_id ) );

        BoostBot_Logger::info(
            'Meeting created',
            array(
                'meeting_id' => $meeting_id,
                'date'       => $data['meeting_date'],
            )
        );

        return $this->success(
            array(
                'meeting_id' => $meeting_id,
                'meet_link'  => $data['google_meet_link'] ?? null,
                'message'    => __( 'Meeting created successfully', 'boostbot' ),
            )
        );
    }

    /**
     * Update a meeting.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function update_meeting( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Meeting_Model();

        $meeting = $model->get( $id );
        if ( ! $meeting ) {
            return $this->not_found( 'Meeting' );
        }

        $data = array();

        $fields = array(
            'customer_name',
            'customer_email',
            'customer_phone',
            'title',
            'description',
            'meeting_date',
            'start_time',
            'end_time',
            'timezone',
            'status',
        );

        foreach ( $fields as $field ) {
            $value = $request->get_param( $field );
            if ( null !== $value ) {
                $data[ $field ] = sanitize_text_field( $value );
            }
        }

        if ( empty( $data ) ) {
            return $this->error( __( 'No data to update', 'boostbot' ), 400 );
        }

        $model->update( $id, $data );

        return $this->success(
            array(
                'meeting' => $model->get_with_details( $id ),
                'message' => __( 'Meeting updated successfully', 'boostbot' ),
            )
        );
    }

    /**
     * Delete a meeting.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function delete_meeting( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Meeting_Model();

        $meeting = $model->get( $id );
        if ( ! $meeting ) {
            return $this->not_found( 'Meeting' );
        }

        // Cancel in Google Calendar if exists.
        if ( ! empty( $meeting->google_event_id ) ) {
            $google = new BoostBot_Google_Calendar_Service();
            $google->cancel_event( $meeting->google_event_id );
        }

        $model->delete( $id );

        return $this->success(
            array(
                'message' => __( 'Meeting deleted successfully', 'boostbot' ),
            )
        );
    }

    /**
     * Cancel a meeting.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function cancel_meeting( $request ) {
        $id    = absint( $request->get_param( 'id' ) );
        $model = new BoostBot_Meeting_Model();

        $meeting = $model->get( $id );
        if ( ! $meeting ) {
            return $this->not_found( 'Meeting' );
        }

        // Cancel in Google Calendar if exists.
        if ( ! empty( $meeting->google_event_id ) ) {
            $google = new BoostBot_Google_Calendar_Service();
            $google->cancel_event( $meeting->google_event_id );
        }

        $model->update_status( $id, 'cancelled' );

        // Send cancellation email.
        $this->send_cancellation_email( $meeting );

        BoostBot_Logger::info(
            'Meeting cancelled',
            array(
                'meeting_id' => $id,
            )
        );

        return $this->success(
            array(
                'message' => __( 'Meeting cancelled successfully', 'boostbot' ),
            )
        );
    }

    /**
     * Get upcoming meetings.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_upcoming( $request ) {
        $model      = new BoostBot_Meeting_Model();
        $limit      = absint( $request->get_param( 'limit' ) ?? 10 );
        $chatbot_id = $request->get_param( 'chatbot_id' );

        $meetings = $model->get_upcoming( $limit, $chatbot_id );

        return $this->success( array( 'meetings' => $meetings ) );
    }

    /**
     * Get today's meetings.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_today( $request ) {
        $model      = new BoostBot_Meeting_Model();
        $chatbot_id = $request->get_param( 'chatbot_id' );

        $meetings = $model->get_today( $chatbot_id );

        return $this->success( array( 'meetings' => $meetings ) );
    }

    /**
     * Get meeting statistics.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_stats( $request ) {
        $model      = new BoostBot_Meeting_Model();
        $start_date = $request->get_param( 'start_date' ) ?? gmdate( 'Y-m-01' );
        $end_date   = $request->get_param( 'end_date' ) ?? gmdate( 'Y-m-t' );
        $language   = $this->get_request_language( $request );

        $stats = $model->get_stats( $start_date, $end_date, $language );

        return $this->success( array( 'stats' => $stats ) );
    }

    /**
     * Book a meeting (PUBLIC endpoint for chat widget).
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function book_meeting( $request ) {
        $model = new BoostBot_Meeting_Model();

        // Get parameters
        $date            = sanitize_text_field( $request->get_param( 'date' ) );
        $time            = sanitize_text_field( $request->get_param( 'time' ) );
        $customer_name   = sanitize_text_field( $request->get_param( 'customer_name' ) );
        $customer_email  = sanitize_email( $request->get_param( 'customer_email' ) );
        $customer_phone  = sanitize_text_field( $request->get_param( 'customer_phone' ) );
        $widget_id       = absint( $request->get_param( 'widget_id' ) );
        $bot_id          = absint( $request->get_param( 'bot_id' ) );
        $conversation_id       = sanitize_text_field( $request->get_param( 'conversation_id' ) );
        $conversation_messages = $request->get_param( 'conversation_messages' ); // Frontend-provided messages (race-condition fallback)
        $custom_fields         = $request->get_param( 'custom_fields' );
        
        // Capture page context (source page where meeting was booked)
        $page_url   = esc_url_raw( $request->get_param( 'page_url' ) ?: $request->get_param( 'source_page' ) );
        $page_title = sanitize_text_field( $request->get_param( 'page_title' ) );
        $visitor_id = sanitize_text_field( $request->get_param( 'visitor_id' ) );

        // Debug log
        BoostBot_Logger::info( '📅 Meeting booking started', array(
            'conversation_id' => $conversation_id,
            'customer_name'   => $customer_name,
            'has_conv_id'     => ! empty( $conversation_id ),
        ) );

        // Validate required fields
        if ( empty( $date ) || empty( $time ) ) {
            return $this->error( __( 'נא לבחור תאריך ושעה', 'boostbot' ), 400 );
        }

        if ( empty( $customer_name ) ) {
            return $this->error( __( 'נא להזין שם', 'boostbot' ), 400 );
        }

        if ( empty( $customer_email ) || ! is_email( $customer_email ) ) {
            return $this->error( __( 'נא להזין כתובת אימייל תקינה', 'boostbot' ), 400 );
        }

        if ( empty( $customer_phone ) ) {
            return $this->error( __( 'נא להזין טלפון', 'boostbot' ), 400 );
        }

        // ── Reject bookings in the past or within min-notice window ──
        $timezone_str = wp_timezone_string();
        $booking_tz   = new DateTimeZone( $timezone_str );
        $now          = new DateTime( 'now', $booking_tz );
        $requested_dt = new DateTime( $date . ' ' . $time, $booking_tz );

        if ( $requested_dt < $now ) {
            BoostBot_Logger::warning( '📅 Booking rejected — time is in the past', array(
                'requested' => $requested_dt->format( 'Y-m-d H:i' ),
                'now'       => $now->format( 'Y-m-d H:i' ),
            ) );
            return $this->error( __( 'לא ניתן לקבוע פגישה בזמן שעבר. נא לבחור שעה עתידית.', 'boostbot' ), 409 );
        }

        // Get widget config for title and associated contacts
        $widget_title        = __( 'פגישה', 'boostbot' );
        $associated_contacts = array();
        $duration            = 30; // Default 30 minutes
        $location            = '';
        $slot_interval       = 0; // 0 = use contact's meeting_duration as step
        $wconfig             = array();

        BoostBot_Logger::info( '📅 Widget ID resolution', array(
            'widget_id'     => $widget_id,
            'raw_param'     => $request->get_param( 'widget_id' ),
            'has_widget_id' => $widget_id > 0,
        ) );

        if ( $widget_id ) {
            // Use the helper that auto-filters deleted contacts (Fix #4 / #8)
            $widget_data         = $this->load_widget_contacts( $widget_id );
            $associated_contacts = $widget_data['contacts'];
            $slot_interval       = $widget_data['slot_interval'];
            $wconfig             = $widget_data['config'];

            if ( ! empty( $wconfig['title'] ) ) {
                $widget_title = $wconfig['title'];
            }
            // Legacy fallback: if duration_options is still set in widget config
            if ( ! empty( $wconfig['duration_options'] ) && is_array( $wconfig['duration_options'] ) ) {
                $duration = $wconfig['duration_options'][0];
            }
            if ( ! empty( $wconfig['location'] ) ) {
                $location = $wconfig['location'];
            }

            // Fix #8 — If the widget originally had contacts but they've all been
            // removed / deleted, reject the booking instead of creating an orphan.
            $raw_ids = ! empty( $wconfig['associated_contacts'] ) ? $wconfig['associated_contacts'] : array();
            if ( ! empty( $raw_ids ) && empty( $associated_contacts ) ) {
                BoostBot_Logger::warning( '📅 Booking rejected — all associated contacts were removed', array(
                    'widget_id'    => $widget_id,
                    'raw_ids'      => $raw_ids,
                ) );
                return $this->error(
                    __( 'היומן אינו זמין כרגע. אנא נסה שוב מאוחר יותר.', 'boostbot' ),
                    409
                );
            }

            BoostBot_Logger::info( '📅 Widget config loaded', array(
                'widget_id'            => $widget_id,
                'widget_title'         => $widget_title,
                'associated_contacts'  => $associated_contacts,
                'duration'             => $duration,
                'slot_interval'        => $slot_interval,
                'has_location'         => ! empty( $location ),
            ) );
        } else {
            BoostBot_Logger::warning( '📅 widget_id is 0 — cannot load associated contacts. Check frontend sends widget_id.' );
        }

        // Fallback to site address if no location in widget config
        if ( empty( $location ) ) {
            $location = get_option( 'site_address', '' );
        }

        // ── Enforce min-notice window ──
        $min_notice_hours = $this->resolve_min_notice_hours( $wconfig );
        $notice_minutes   = (int) ( $min_notice_hours * 60 );
        if ( $notice_minutes > 0 ) {
            $cutoff = clone $now;
            $cutoff->modify( "+{$notice_minutes} minutes" );
            if ( $requested_dt < $cutoff ) {
                BoostBot_Logger::warning( '📅 Booking rejected — within min-notice window', array(
                    'requested'          => $requested_dt->format( 'Y-m-d H:i' ),
                    'cutoff'             => $cutoff->format( 'Y-m-d H:i' ),
                    'min_notice_hours'   => $min_notice_hours,
                ) );
                return $this->error( __( 'לא ניתן לקבוע פגישה בהתראה כה קצרה. נא לבחור שעה מאוחרת יותר.', 'boostbot' ), 409 );
            }
        }

        // ── Priority-based contact assignment (inside DB transaction for atomicity) ──
        $assigned_contact_id = null;
        $contact_model       = new BoostBot_Contact_Model();
        $timezone            = wp_timezone_string();
        $tz                  = new DateTimeZone( $timezone );
        $date_obj            = new DateTime( $date, $tz );
        $day_of_week         = (int) $date_obj->format( 'w' );

        // Start a database transaction to prevent race conditions.
        // The SELECT … FOR UPDATE inside is_contact_slot_available_locked() will hold
        // a row-level lock until we COMMIT or ROLLBACK, so a concurrent request
        // that checks the same contact + timeslot will block until we finish.
        global $wpdb;
        // 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().
        $wpdb->query( 'START TRANSACTION' );

        if ( ! empty( $associated_contacts ) ) {
            foreach ( $associated_contacts as $contact_id ) {
                $avail_data = $contact_model->get_meeting_availability( $contact_id );
                if ( ! $avail_data ) {
                    // Contact has no structured availability — skip for priority assignment
                    // but still eligible as a legacy fallback below
                    continue;
                }

                $contact_duration = $contact_model->get_meeting_duration( $contact_id );
                $contact_slots    = $contact_model->generate_time_slots( $contact_id, $day_of_week, $slot_interval );

                // Check if the requested time is one of this contact's available slots
                if ( ! in_array( $time, $contact_slots, true ) ) {
                    continue;
                }

                // Check if this contact has no conflicting meeting at this time
                $start_dt = new DateTime( $date . ' ' . $time );
                $end_dt   = clone $start_dt;
                $end_dt->add( new DateInterval( 'PT' . $contact_duration . 'M' ) );
                $contact_end_time = $end_dt->format( 'H:i' );

                // Use the locking variant so concurrent requests block here
                $is_available = $model->is_contact_slot_available_locked( $contact_id, $date, $time, $contact_end_time );

                // Fix #5 — Also check legacy meetings (assigned_contact_id IS NULL)
                // that block all contacts regardless of assignment.
                if ( $is_available ) {
                    $is_available = ! $model->has_legacy_meeting_overlap( $date, $time, $contact_end_time );
                }

                if ( $is_available ) {
                    $assigned_contact_id = $contact_id;
                    $duration            = $contact_duration; // Use this contact's duration
                    BoostBot_Logger::info( '📅 Contact assigned by priority', array(
                        'assigned_contact_id' => $assigned_contact_id,
                        'contact_duration'    => $duration,
                        'priority_position'   => array_search( $contact_id, $associated_contacts, true ),
                    ) );
                    break;
                }
            }

            // If no contact with meeting_availability was found, check if there are legacy contacts
            // (contacts without meeting_availability configured)
            if ( null === $assigned_contact_id ) {
                // Check if ANY contact with availability was tried
                $any_has_availability = false;
                foreach ( $associated_contacts as $cid ) {
                    if ( $contact_model->get_meeting_availability( $cid ) ) {
                        $any_has_availability = true;
                        break;
                    }
                }

                if ( $any_has_availability ) {
                    // All contacts with availability are booked at this time — reject
                    // 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().
                    $wpdb->query( 'ROLLBACK' );
                    return $this->error( __( 'השעה המבוקשת כבר תפוסה. נא לבחור שעה אחרת.', 'boostbot' ), 409 );
                }
                // else: no contacts have meeting_availability set — fall through to legacy behavior
            }
        }

        // Calculate end time using the resolved duration
        $start_datetime = new DateTime( $date . ' ' . $time );
        $end_datetime   = clone $start_datetime;
        $end_datetime->add( new DateInterval( 'PT' . $duration . 'M' ) );
        $end_time = $end_datetime->format( 'H:i' );

        // Build meeting title
        $title = sprintf( '%s - %s', $widget_title, $customer_name );

        // Get bot name for summary
        $bot_name = 'הנציג';
        if ( $bot_id ) {
            $bot_model = new BoostBot_Bot_Model();
            $bot       = $bot_model->get( $bot_id );
            if ( $bot && ! empty( $bot->name ) ) {
                $bot_name = $bot->name;
            }
        }

        // Generate AI summary of conversation.
        // We pass the frontend-provided messages as a fallback in case the async
        // completeConversation() call hasn't persisted to the DB yet (race condition).
        $conversation_summary = $this->generate_conversation_summary(
            $conversation_id,
            $customer_name,
            $bot_name,
            is_array( $conversation_messages ) ? $conversation_messages : array()
        );

        // Build meeting description with AI summary
        $description = $this->build_meeting_description(
            $customer_name,
            $customer_email,
            $customer_phone,
            $conversation_summary
        );

        // Merge page context into custom_fields
        $custom_fields_array = is_array( $custom_fields ) ? $custom_fields : array();
        if ( ! empty( $page_url ) ) {
            $custom_fields_array['page_url'] = $page_url;
        }
        if ( ! empty( $page_title ) ) {
            $custom_fields_array['page_title'] = $page_title;
        }
        if ( ! empty( $visitor_id ) ) {
            $custom_fields_array['visitor_id'] = $visitor_id;
        }

        $meeting_language = BoostBot_Language::sanitize( $request->get_param( 'language' ) );
        if ( '' === $meeting_language && $bot_id > 0 ) {
            if ( isset( $bot ) && ! empty( $bot->language ) ) {
                $meeting_language = $bot->language;
            } elseif ( class_exists( 'BoostBot_Bot_Model' ) ) {
                $lang_bot = ( new BoostBot_Bot_Model() )->get( $bot_id );
                if ( $lang_bot && ! empty( $lang_bot->language ) ) {
                    $meeting_language = $lang_bot->language;
                }
            }
        }
        if ( '' === $meeting_language ) {
            $meeting_language = BoostBot_Language::get_primary();
        }

        // Prepare meeting data
        $data = array(
            'language'            => $meeting_language,
            'widget_id'           => $widget_id,
            'chatbot_id'          => $bot_id,
            'conversation_id'     => $conversation_id,
            'customer_name'       => $customer_name,
            'customer_email'      => $customer_email,
            'customer_phone'      => $customer_phone,
            'title'               => $title,
            'description'         => $description,
            'meeting_date'        => $date,
            'start_time'          => $time,
            'end_time'            => $end_time,
            'timezone'            => 'Asia/Jerusalem',
            'attendee_contacts'   => $associated_contacts,
            'assigned_contact_id' => $assigned_contact_id,
            'custom_fields'       => ! empty( $custom_fields_array ) ? wp_json_encode( $custom_fields_array ) : null,
            'ai_summary'          => ! empty( $conversation_summary ) ? $conversation_summary : null,
            'status'              => 'scheduled',
        );

        // Create the meeting (inside the transaction started above).
        // NOTE: the Google Calendar event is created AFTER commit (see below) so the
        // external HTTP call never runs while the SELECT … FOR UPDATE row lock is held.
        $meeting_id = $model->create( $data );

        if ( ! $meeting_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().
            $wpdb->query( 'ROLLBACK' );
            return $this->error( __( 'אירעה שגיאה ביצירת הפגישה', 'boostbot' ), 500 );
        }

        // Commit the transaction — the row lock is released, and the INSERT is durable.
        // The lock still guarded the slot check + insert above; the external Google
        // Calendar call below runs with NO lock held so it can never block a
        // concurrent booking on the same contact/timeslot.
        // 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().
        $wpdb->query( 'COMMIT' );

        // ── Google Calendar event (AFTER commit — no DB lock held) ──
        $google = new BoostBot_Google_Calendar_Service();
        if ( $google->is_connected() ) {
            $attendee_emails = array( $customer_email );

            // Add assigned contact's email (priority) or all associated contacts' emails (legacy)
            if ( $assigned_contact_id ) {
                $assigned_contact = $contact_model->get( $assigned_contact_id );
                if ( $assigned_contact && ! empty( $assigned_contact->email ) ) {
                    $attendee_emails[] = $assigned_contact->email;
                }
            } elseif ( ! empty( $associated_contacts ) ) {
                foreach ( $associated_contacts as $contact_id ) {
                    $contact = $contact_model->get( $contact_id );
                    if ( $contact && ! empty( $contact->email ) ) {
                        $attendee_emails[] = $contact->email;
                    }
                }
            }

            BoostBot_Logger::info( '📅 Google Calendar event — attendees', array(
                'attendee_emails'     => $attendee_emails,
                'assigned_contact_id' => $assigned_contact_id,
                'customer_email'      => $customer_email,
            ) );

            $meet_result = $google->create_event(
                array(
                    'title'          => $title,
                    'description'    => $description,
                    'location'       => $location,
                    'start_datetime' => $date . 'T' . $time . ':00',
                    'end_datetime'   => $date . 'T' . $end_time . ':00',
                    'timezone'       => 'Asia/Jerusalem',
                    'attendees'      => $attendee_emails,
                )
            );

            if ( ! is_wp_error( $meet_result ) && ! empty( $meet_result['event_id'] ) ) {
                // Persist the event references in a separate short write.
                $model->update( $meeting_id, array(
                    'google_event_id'      => $meet_result['event_id'],
                    'google_calendar_link' => $meet_result['html_link'] ?? '',
                ) );
                BoostBot_Logger::info( 'Google Calendar event created', array(
                    'event_id' => $meet_result['event_id'],
                ) );
            } else {
                $error_msg = is_wp_error( $meet_result ) ? $meet_result->get_error_message() : 'Unknown error';
                BoostBot_Logger::warning( 'Google Calendar event creation failed', array(
                    'error' => $error_msg,
                ) );
            }
        }

        // Save the AI summary to the conversation record as well
        if ( ! empty( $conversation_summary ) && ! empty( $conversation_id ) ) {
            $conversation_model_for_save = new BoostBot_Conversation_Model();
            $conv = $conversation_model_for_save->get_by_conversation_id( $conversation_id );
            if ( $conv ) {
                $conversation_model_for_save->update( $conv->id, array( 'ai_summary' => $conversation_summary ) );
                BoostBot_Logger::info( '📅 Conversation summary saved to conversation record', array(
                    'conversation_id'   => $conversation_id,
                    'conversation_db_id' => $conv->id,
                    'summary_length'    => strlen( $conversation_summary ),
                ) );
            }
        }

        // Send confirmation emails
        $meeting = $model->get_with_details( $meeting_id );
        $this->send_confirmation_email( $meeting, $conversation_summary );

        // Notify the assigned contact specifically, or all associated contacts (legacy)
        $notify_contacts = $assigned_contact_id ? array( $assigned_contact_id ) : $associated_contacts;
        $this->send_staff_notification_email( $meeting, $notify_contacts, $conversation_summary );

        // Track widget usage for this bot (calendar widget)
        if ( $widget_id > 0 && $bot_id > 0 && class_exists( 'BoostBot_Widget_Model' ) ) {
            $widget_model_for_usage = new BoostBot_Widget_Model();
            if ( method_exists( $widget_model_for_usage, 'track_usage' ) ) {
                $widget_model_for_usage->track_usage( $widget_id, $bot_id, $meeting_language );
            }
        }

        // Fire hook for integrations/webhooks
        if ( $meeting ) {
            do_action( 'boostbot_meeting_booked', $meeting );
        }

        BoostBot_Logger::info(
            'Meeting booked via chat',
            array(
                'meeting_id'          => $meeting_id,
                'date'                => $date,
                'customer_name'       => $customer_name,
                'widget_id'           => $widget_id,
                'conversation_id'     => $conversation_id,
                'assigned_contact_id' => $assigned_contact_id,
                'has_summary'         => ! empty( $conversation_summary ),
            )
        );

        return $this->success(
            array(
                'meeting_id' => $meeting_id,
                'meet_link'  => $data['google_meet_link'] ?? null,
                'message'    => __( 'הפגישה נקבעה בהצלחה! נשלח אליך אישור במייל.', 'boostbot' ),
            )
        );
    }

    /**
     * Get availability for a specific date.
     *
     * Checks both Google Calendar (free/busy) and local database bookings
     * to return available time slots for the requested date.
     *
     * @since 1.9.0
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_availability( $request ) {
        $date = sanitize_text_field( $request->get_param( 'date' ) );

        if ( empty( $date ) || ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
            return $this->error( __( 'Invalid date format. Use YYYY-MM-DD.', 'boostbot' ), 400 );
        }

        // Don't allow dates in the past
        $today = current_time( 'Y-m-d' );
        if ( $date < $today ) {
            return $this->success( array( 'date' => $date, 'slots' => array() ) );
        }

        $widget_id = absint( $request->get_param( 'widget_id' ) );
        $timezone  = wp_timezone_string();
        $tz        = new DateTimeZone( $timezone );

        // Check Google Calendar connection (status display only).
        $google_connected = false;
        if ( class_exists( 'BoostBot_Google_Calendar_Service' ) ) {
            $google = new BoostBot_Google_Calendar_Service();
            $google_connected = $google->is_connected();
        }

        // ── Load widget config and associated contacts (auto-filters deleted contacts) ──
        $widget_data         = $this->load_widget_contacts( $widget_id );
        $associated_contacts = $widget_data['contacts'];
        $slot_interval       = $widget_data['slot_interval'];
        $wconfig             = $widget_data['config'];

        // ── Resolve min_notice_hours from widget config (per-type minimum or global fallback) ──
        $min_notice_hours = $this->resolve_min_notice_hours( $wconfig );

        // ── Determine day of week (0 = Sunday, 6 = Saturday) ──
        $date_obj    = new DateTime( $date, $tz );
        $day_of_week = (int) $date_obj->format( 'w' ); // 0 = Sunday

        // ── Build per-contact availability ──
        $contact_model = new BoostBot_Contact_Model();
        $meeting_model = new BoostBot_Meeting_Model();

        // If we have associated contacts with meeting_availability, use per-contact logic.
        // Otherwise fall back to default business hours.
        $use_contact_based = false;
        $all_slot_times    = array(); // Collect all possible start times across contacts
        // Map: time_string => bool (is any contact still available?)
        $slot_availability = array();
        // Fix #9 — Track meeting duration per slot (from highest-priority available contact)
        $slot_duration_map = array();

        // Fix #5 — Legacy meetings (assigned_contact_id IS NULL) are treated as blocking
        // ALL contacts, since we don't know which contact they belong to.
        $legacy_meetings = $meeting_model->get_unassigned_meetings_for_date( $date );

        try {
            if ( ! empty( $associated_contacts ) ) {
                foreach ( $associated_contacts as $contact_id ) {
                    $avail_data = $contact_model->get_meeting_availability( $contact_id );
                    if ( ! $avail_data ) {
                        continue;
                    }

                    $use_contact_based = true;
                    $duration          = $contact_model->get_meeting_duration( $contact_id );
                    $slots_for_contact = $contact_model->generate_time_slots( $contact_id, $day_of_week, $slot_interval );

                    if ( empty( $slots_for_contact ) ) {
                        continue;
                    }

                    // Get this contact's existing meetings for the date
                    $contact_meetings = $meeting_model->get_by_contact_and_date( $contact_id, $date );
                    // Merge legacy (unassigned) meetings — they block every contact
                    $all_meetings = array_merge( $contact_meetings, $legacy_meetings );

                    foreach ( $slots_for_contact as $slot_time ) {
                        $slot_start = new DateTime( $date . 'T' . $slot_time . ':00', $tz );
                        $slot_end   = clone $slot_start;
                        $slot_end->modify( "+{$duration} minutes" );

                        // Check overlap with this contact's existing + legacy meetings
                        $contact_is_busy = false;
                        foreach ( $all_meetings as $meeting ) {
                            $m_start = new DateTime( $date . 'T' . $this->normalize_time( $meeting->start_time ?? '00:00' ) . ':00', $tz );
                            $m_end   = new DateTime( $date . 'T' . $this->normalize_time( $meeting->end_time ?? '23:59' ) . ':00', $tz );

                            if ( $slot_start < $m_end && $slot_end > $m_start ) {
                                $contact_is_busy = true;
                                break;
                            }
                        }

                        // Register slot availability: available if ANY contact can take it
                        if ( ! isset( $slot_availability[ $slot_time ] ) ) {
                            $slot_availability[ $slot_time ] = false;
                            $all_slot_times[] = $slot_time;
                        }

                        if ( ! $contact_is_busy ) {
                            $slot_availability[ $slot_time ] = true;
                            // Fix #9 — Store the duration from the first available contact (highest priority)
                            if ( ! isset( $slot_duration_map[ $slot_time ] ) ) {
                                $slot_duration_map[ $slot_time ] = $duration;
                            }
                        }
                    }
                }
            }
        } catch ( Exception $e ) {
            // Log the error but don't crash — fall through to legacy fallback
            if ( class_exists( 'BoostBot_Logger' ) ) {
                BoostBot_Logger::error( 'Error computing contact-based availability', array(
                    'date'  => $date,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString(),
                ) );
            }
            // Reset to force legacy fallback
            $use_contact_based = false;
            $all_slot_times    = array();
            $slot_availability = array();
            $slot_duration_map = array();
        }

        // ── Fallback: default business hours if no contact-based slots ──
        if ( ! $use_contact_based ) {
            $slot_duration = absint( $request->get_param( 'duration' ) ) ?: 30;
            $all_slot_times = $this->get_default_time_slots( $slot_duration );

            // Load ALL meetings for this date (legacy site-wide check)
            $local_meetings = $meeting_model->get_by_date_range( $date, $date );
            $all_busy = array();
            foreach ( $local_meetings as $meeting ) {
                if ( in_array( $meeting->status ?? '', array( 'cancelled', 'deleted' ), true ) ) {
                    continue;
                }
                $all_busy[] = array(
                    'start' => $date . 'T' . $this->normalize_time( $meeting->start_time ?? '00:00' ) . ':00',
                    'end'   => $date . 'T' . $this->normalize_time( $meeting->end_time ?? '23:59' ) . ':00',
                );
            }

            foreach ( $all_slot_times as $slot_time ) {
                $slot_start = new DateTime( $date . 'T' . $slot_time . ':00', $tz );
                $slot_end   = clone $slot_start;
                $slot_end->modify( "+{$slot_duration} minutes" );

                $is_busy = false;
                foreach ( $all_busy as $busy ) {
                    $busy_start = new DateTime( $busy['start'] );
                    $busy_end   = new DateTime( $busy['end'] );
                    if ( $slot_start < $busy_end && $slot_end > $busy_start ) {
                        $is_busy = true;
                        break;
                    }
                }
                $slot_availability[ $slot_time ] = ! $is_busy;
            }
        }

        // ── Sort and deduplicate slot times ──
        $all_slot_times = array_unique( $all_slot_times );
        sort( $all_slot_times );

        // ── Fix #9 — Cross-check requested day against Google Calendar free/busy ──
        // When Google is connected, reject slots that overlap a busy period on the
        // real calendar, in addition to the local DB check. Fail open (allow) on any
        // error so a transient Google failure never blocks bookings.
        $google_busy = array();
        if ( $google_connected && isset( $google ) ) {
            try {
                $day_start_iso = ( new DateTime( $date . 'T00:00:00', $tz ) )->format( 'c' );
                $day_end_iso   = ( new DateTime( $date . 'T23:59:59', $tz ) )->format( 'c' );
                $busy_periods  = $google->get_freebusy( $day_start_iso, $day_end_iso );
                if ( is_array( $busy_periods ) ) {
                    foreach ( $busy_periods as $busy ) {
                        if ( empty( $busy['start'] ) || empty( $busy['end'] ) ) {
                            continue;
                        }
                        try {
                            $google_busy[] = array(
                                'start' => new DateTime( $busy['start'] ),
                                'end'   => new DateTime( $busy['end'] ),
                            );
                        } catch ( Exception $e ) {
                            continue; // Skip a malformed busy entry.
                        }
                    }
                }
            } catch ( Exception $e ) {
                // Fail open — never block bookings on a Google error.
                $google_busy = array();
                if ( class_exists( 'BoostBot_Logger' ) ) {
                    BoostBot_Logger::warning( 'Google free/busy check failed — falling back to local availability', array(
                        'date'  => $date,
                        'error' => $e->getMessage(),
                    ) );
                }
            }
        }

        // ── Build final slots, filtering past times + notice period for today ──
        $notice_minutes   = (int) ( $min_notice_hours * 60 );
        $available_slots  = array();
        foreach ( $all_slot_times as $slot_time ) {
            // Filter slots that are in the past or within the min-notice window (today only)
            if ( $date === $today ) {
                $slot_dt = new DateTime( $date . 'T' . $slot_time . ':00', $tz );
                $now     = new DateTime( 'now', $tz );
                $now->modify( "+{$notice_minutes} minutes" );
                if ( $slot_dt < $now ) {
                    continue; // Hide slots that are too close — don't show them at all
                }
            }

            $slot_entry = array(
                'time'      => $slot_time,
                'available' => ! empty( $slot_availability[ $slot_time ] ),
            );
            // Fix #9 — Include meeting duration so the customer knows how long the meeting is
            if ( isset( $slot_duration_map[ $slot_time ] ) ) {
                $slot_entry['duration'] = $slot_duration_map[ $slot_time ];
            }

            // Fix #9 — Mask out slots overlapping a Google Calendar busy period.
            if ( $slot_entry['available'] && ! empty( $google_busy ) ) {
                $slot_start_dt = new DateTime( $date . 'T' . $slot_time . ':00', $tz );
                $slot_len      = isset( $slot_duration_map[ $slot_time ] )
                    ? (int) $slot_duration_map[ $slot_time ]
                    : ( isset( $slot_duration ) ? (int) $slot_duration : 30 );
                $slot_end_dt   = clone $slot_start_dt;
                $slot_end_dt->modify( "+{$slot_len} minutes" );

                foreach ( $google_busy as $busy ) {
                    if ( $slot_start_dt < $busy['end'] && $slot_end_dt > $busy['start'] ) {
                        $slot_entry['available'] = false;
                        break;
                    }
                }
            }

            $available_slots[] = $slot_entry;
        }

        return $this->success( array(
            'date'             => $date,
            'slots'            => $available_slots,
            'google_connected' => $google_connected,
            'timezone'         => $timezone,
        ) );
    }

    /**
     * Get available days of the week for a widget.
     *
     * Returns which days (0 = Sunday, 6 = Saturday) have at least one associated
     * contact with meeting_availability configured for that day.
     * Used by the calendar widget to disable dates that have no availability.
     *
     * @since 1.9.0
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response
     */
    public function get_available_days( $request ) {
        $widget_id = absint( $request->get_param( 'widget_id' ) );

        // ── Load associated contacts from widget config (auto-filters deleted contacts) ──
        $widget_data         = $this->load_widget_contacts( $widget_id );
        $associated_contacts = $widget_data['contacts'];
        $wconfig             = $widget_data['config'];
        $slot_interval       = $widget_data['slot_interval'];

        // ── Resolve min_notice_hours ──
        $min_notice_hours = $this->resolve_min_notice_hours( $wconfig );
        $notice_minutes   = (int) ( $min_notice_hours * 60 );

        // ── Determine which days of the week have availability ──
        $contact_model  = new BoostBot_Contact_Model();
        $available_days = array(); // Set of day numbers (0-6)

        if ( ! empty( $associated_contacts ) ) {
            foreach ( $associated_contacts as $contact_id ) {
                $avail_data = $contact_model->get_meeting_availability( $contact_id );
                if ( ! $avail_data || empty( $avail_data['days'] ) ) {
                    continue;
                }

                foreach ( $avail_data['days'] as $day_key => $ranges ) {
                    if ( ! empty( $ranges ) && is_array( $ranges ) ) {
                        $available_days[ (int) $day_key ] = true;
                    }
                }
            }
        }

        // If no contacts have meeting_availability, fall back to all weekdays (legacy)
        if ( empty( $available_days ) ) {
            $available_days = array( 0 => true, 1 => true, 2 => true, 3 => true, 4 => true, 5 => true );
        }

        $days = array_keys( $available_days );
        sort( $days );

        // ── Compute effective_min_date ──
        // If today's day-of-week is available, check whether any slot remains after
        // the min-notice window. If not, push the minimum selectable date to tomorrow.
        $timezone = wp_timezone_string();
        $tz       = new DateTimeZone( $timezone );
        $now      = new DateTime( 'now', $tz );
        $today    = $now->format( 'Y-m-d' );
        $cutoff   = clone $now;
        $cutoff->modify( "+{$notice_minutes} minutes" );

        $effective_min_date = $today;

        $today_dow = (int) $now->format( 'w' );
        if ( in_array( $today_dow, $days, true ) ) {
            // Check if any contact has a slot that starts at or after the cutoff
            $has_viable_slot = false;
            if ( ! empty( $associated_contacts ) ) {
                foreach ( $associated_contacts as $contact_id ) {
                    $avail_data = $contact_model->get_meeting_availability( $contact_id );
                    if ( ! $avail_data ) {
                        continue;
                    }
                    $slots = $contact_model->generate_time_slots( $contact_id, $today_dow, $slot_interval );
                    foreach ( $slots as $slot_time ) {
                        $slot_dt = new DateTime( $today . 'T' . $slot_time . ':00', $tz );
                        if ( $slot_dt >= $cutoff ) {
                            $has_viable_slot = true;
                            break 2;
                        }
                    }
                }
            } else {
                // Legacy fallback: check default 09:00-17:00 slots
                $fallback_dur   = absint( $wconfig['duration_options'][0] ?? 30 ) ?: 30;
                $default_slots  = $this->get_default_time_slots( $fallback_dur );
                foreach ( $default_slots as $slot_time ) {
                    $slot_dt = new DateTime( $today . 'T' . $slot_time . ':00', $tz );
                    if ( $slot_dt >= $cutoff ) {
                        $has_viable_slot = true;
                        break;
                    }
                }
            }

            if ( ! $has_viable_slot ) {
                $tomorrow = clone $now;
                $tomorrow->modify( '+1 day' );
                $effective_min_date = $tomorrow->format( 'Y-m-d' );
            }
        }

        return $this->success( array(
            'available_days_of_week' => $days,
            'timezone'               => $timezone,
            'effective_min_date'     => $effective_min_date,
            'min_notice_hours'       => $min_notice_hours,
        ) );
    }

    /**
     * Normalize a time string to HH:MM format.
     *
     * MySQL TIME columns return values as "HH:MM:SS". Appending ":00" to that
     * produces "HH:MM:SS:00" which is an invalid DateTime string. This helper
     * ensures we always get a clean "HH:MM" regardless of input format.
     *
     * @since 1.9.1
     * @param string $time Time string in "HH:MM" or "HH:MM:SS" format.
     * @return string Time string in "HH:MM" format.
     */
    private function normalize_time( string $time ): string {
        // Always take just the first 5 characters (HH:MM) to handle both HH:MM and HH:MM:SS
        return substr( $time, 0, 5 );
    }

    /**
     * Resolve the effective min_notice_hours from a widget config.
     *
     * When meeting type selection is enabled, returns the MINIMUM across all
     * meeting types so that dates are not prematurely disabled for types with
     * shorter notice requirements. The per-type value is enforced later when
     * filtering slots for a specific meeting type.
     *
     * @param array $config Raw widget config.
     * @return float Hours of advance notice required (default 1).
     */
    private function resolve_min_notice_hours( array $config ): float {
        $global = isset( $config['min_notice_hours'] ) ? (float) $config['min_notice_hours'] : 1.0;

        if ( empty( $config['show_meeting_type_selection'] ) || empty( $config['meeting_types'] ) || ! is_array( $config['meeting_types'] ) ) {
            return $global;
        }

        $min = PHP_FLOAT_MAX;
        foreach ( $config['meeting_types'] as $mt ) {
            $val = isset( $mt['min_notice_hours'] ) ? (float) $mt['min_notice_hours'] : $global;
            if ( $val < $min ) {
                $min = $val;
            }
        }

        return $min < PHP_FLOAT_MAX ? $min : $global;
    }

    /**
     * Generate default business-hour time slots.
     *
     * Used as fallback when no contacts have meeting_availability configured.
     *
     * @since 1.9.0
     * @param int $duration_minutes Duration in minutes for slot intervals.
     * @return array Array of time strings (e.g., ['09:00', '09:30', ...]).
     */
    private function get_default_time_slots( int $duration_minutes = 30 ): array {
        $interval      = max( 15, min( 120, $duration_minutes ) );
        $slots         = array();
        $start_minutes = 9 * 60;  // 09:00
        $end_minutes   = 17 * 60; // 17:00

        for ( $m = $start_minutes; $m + $interval <= $end_minutes; $m += $interval ) {
            $h     = intdiv( $m, 60 );
            $min   = $m % 60;
            $slots[] = sprintf( '%02d:%02d', $h, $min );
        }

        return $slots;
    }

    /**
     * Generate AI summary of the conversation.
     *
     * @param string $conversation_id The conversation ID.
     * @param string $customer_name   The customer's name.
     * @param string $bot_name        The bot's name.
     * @return string The AI-generated summary, or empty string if failed.
     */
    /**
     * Generate AI summary of a conversation.
     *
     * @param string $conversation_id  The conversation UUID.
     * @param string $customer_name    Customer name for the prompt.
     * @param string $bot_name         Bot/agent display name.
     * @param array  $fallback_messages Optional messages sent from the frontend.
     *                                  Used when the async completeConversation()
     *                                  hasn't persisted to the DB yet.
     * @return string The AI-generated summary, or empty string on failure.
     */
    private function generate_conversation_summary( $conversation_id, $customer_name, $bot_name = 'הנציג', $fallback_messages = array() ) {
        BoostBot_Logger::info( '🔍 generate_conversation_summary called', array(
            'conversation_id'         => $conversation_id,
            'bot_name'                => $bot_name,
            'is_empty'                => empty( $conversation_id ),
            'fallback_messages_count' => count( $fallback_messages ),
        ) );

        if ( empty( $conversation_id ) && empty( $fallback_messages ) ) {
            BoostBot_Logger::warning( '⚠️ conversation_id is empty and no fallback messages, skipping summary' );
            return '';
        }

        try {
            // ── Try to get messages from DB first ──
            $messages = array();

            if ( ! empty( $conversation_id ) ) {
                $conversation_model = new BoostBot_Conversation_Model();
                $conversation = $conversation_model->get_by_conversation_id( $conversation_id );

                BoostBot_Logger::info( '📝 Fetched conversation', array(
                    'found'            => ! empty( $conversation ),
                    'has_session_data' => ! empty( $conversation->session_data ?? null ),
                ) );

                if ( $conversation && ! empty( $conversation->session_data ) ) {
                    $session_data = json_decode( $conversation->session_data, true );

                    if ( isset( $session_data['messages'] ) && is_array( $session_data['messages'] ) ) {
                        $messages = $session_data['messages'];
                    } elseif ( is_array( $session_data ) && ! empty( $session_data ) && isset( $session_data[0]['role'] ) ) {
                        $messages = $session_data;
                    }
                }
            }

            // ── Fallback: use frontend-provided messages if DB has fewer ──
            // The frontend always has the complete conversation in its store,
            // so if DB is stale (async save hasn't completed), prefer the frontend data.
            if ( ! empty( $fallback_messages ) && count( $fallback_messages ) > count( $messages ) ) {
                BoostBot_Logger::info( '🔄 Using frontend-provided messages (more complete than DB)', array(
                    'db_count'       => count( $messages ),
                    'frontend_count' => count( $fallback_messages ),
                ) );

                // Sanitize frontend messages
                $messages = array();
                foreach ( $fallback_messages as $msg ) {
                    if ( is_array( $msg ) && ! empty( $msg['content'] ) ) {
                        $messages[] = array(
                            'role'    => sanitize_text_field( $msg['role'] ?? 'user' ),
                            'content' => sanitize_textarea_field( $msg['content'] ),
                        );
                    }
                }

                // Also persist these messages to the conversation record so the
                // admin panel has them even if completeConversation() never fires.
                if ( ! empty( $conversation_id ) && ! empty( $messages ) ) {
                    $this->ensure_conversation_record( $conversation_id, $messages );
                }
            }

            BoostBot_Logger::info( '📨 Messages for summary', array(
                'messages_count' => count( $messages ),
            ) );

            if ( empty( $messages ) ) {
                return '';
            }

            // Format messages for AI
            $formatted_messages = array();
            foreach ( $messages as $msg ) {
                $role = $msg['role'] ?? 'user';
                $content = $msg['content'] ?? '';
                
                if ( empty( $content ) ) {
                    continue;
                }

                // Convert role names - use bot name for assistant messages
                $display_role = ( $role === 'assistant' || $role === 'bot' ) ? $bot_name : 'לקוח';
                $formatted_messages[] = "{$display_role}: {$content}";
            }

            if ( empty( $formatted_messages ) ) {
                return '';
            }

            $conversation_text = implode( "\n", $formatted_messages );

            // Call backend AI to generate summary (credits-aware path).
            if ( ! class_exists( 'BoostBot_Backend_AI_Client' ) ) {
                throw new Exception( 'Backend AI client not available.' );
            }
            $ai_client = new BoostBot_Backend_AI_Client();
            $response = $ai_client->send_message(
                array(
                    array(
                        'role'    => 'user',
                        'content' => "להלן שיחה בין לקוח לבין {$bot_name} (נציג וירטואלי):\n\n{$conversation_text}\n\n" .
                                     "אנא כתוב סיכום קצר (2-4 משפטים) של מהות השיחה ומה הלקוח מעוניין בו. " .
                                     "הסיכום צריך להיות מנוסח באופן מקצועי ולכלול את הנקודות העיקריות שהלקוח העלה. " .
                                     "השתמש בשם '{$bot_name}' כאשר מתייחס לנציג הוירטואלי (לא 'הבוט'). " .
                                     "כתוב בעברית ובגוף שלישי.",
                    ),
                ),
                array(
                    'max_tokens'  => 300,
                    'temperature' => 0.3,
                    'system'      => "אתה עוזר מקצועי שמסכם שיחות. ענה בעברית בלבד. ספק סיכום תמציתי ומקצועי. כאשר מתייחס לנציג הוירטואלי השתמש בשם '{$bot_name}' ולא במילה 'בוט'.",
                )
            );

            $summary = trim( (string) ( $response['content'] ?? '' ) );

            BoostBot_Logger::info( 'Conversation summary generated', array(
                'conversation_id' => $conversation_id,
                'messages_count'  => count( $messages ),
                'summary_length'  => strlen( $summary ),
            ) );

            return $summary;

        } catch ( Exception $e ) {
            BoostBot_Logger::warning( 'Failed to generate conversation summary', array(
                'error'           => $e->getMessage(),
                'conversation_id' => $conversation_id,
            ) );
            return '';
        }
    }

    /**
     * Ensure a conversation record exists in the DB with the given messages.
     *
     * In the orchestrated flow, the async completeConversation() call may not
     * have fired yet when book_meeting() runs. This method creates the record
     * (or updates it if stale) so the admin can see the conversation.
     *
     * @param string $conversation_id The conversation UUID.
     * @param array  $messages        Array of {role, content} message arrays.
     */
    private function ensure_conversation_record( string $conversation_id, array $messages ): void {
        if ( ! class_exists( 'BoostBot_Conversation_Model' ) ) {
            return;
        }

        $conversation_model = new BoostBot_Conversation_Model();
        $conversation = $conversation_model->get_by_conversation_id( $conversation_id );

        $session_data = wp_json_encode( array( 'messages' => $messages ) );

        if ( ! $conversation ) {
            // Create a new conversation record
            $conversation_model->create( array(
                'conversation_id' => $conversation_id,
                'visitor_id'      => '',
                'session_data'    => $session_data,
                'total_messages'  => count( $messages ),
                'last_message_at' => current_time( 'mysql' ),
                'started_at'      => current_time( 'mysql' ),
            ) );

            BoostBot_Logger::info( '📝 Created conversation record from frontend messages', array(
                'conversation_id' => $conversation_id,
                'message_count'   => count( $messages ),
            ) );
        } else {
            // Update with more complete data if DB is stale
            $existing_data = json_decode( $conversation->session_data ?? '{}', true );
            $existing_count = 0;
            if ( isset( $existing_data['messages'] ) ) {
                $existing_count = count( $existing_data['messages'] );
            } elseif ( is_array( $existing_data ) && isset( $existing_data[0]['role'] ) ) {
                $existing_count = count( $existing_data );
            }

            if ( count( $messages ) > $existing_count ) {
                $conversation_model->update( $conversation->id, array(
                    'session_data'    => $session_data,
                    'total_messages'  => count( $messages ),
                    'last_message_at' => current_time( 'mysql' ),
                ) );

                BoostBot_Logger::info( '📝 Updated stale conversation record with frontend messages', array(
                    'conversation_id' => $conversation_id,
                    'old_count'       => $existing_count,
                    'new_count'       => count( $messages ),
                ) );
            }
        }
    }

    /**
     * Build meeting description with customer details and AI summary.
     *
     * @param string $customer_name    Customer name.
     * @param string $customer_email   Customer email.
     * @param string $customer_phone   Customer phone.
     * @param string $conversation_summary AI-generated conversation summary.
     * @return string The formatted meeting description.
     */
    private function build_meeting_description( $customer_name, $customer_email, $customer_phone, $conversation_summary ) {
        $parts = array();
        
        // Customer details section
        $parts[] = "📋 פרטי הלקוח:";
        $parts[] = "שם: {$customer_name}";
        $parts[] = "טלפון: {$customer_phone}";
        $parts[] = "אימייל: {$customer_email}";

        // Add conversation summary if available
        if ( ! empty( $conversation_summary ) ) {
            $parts[] = "";
            $parts[] = "💬 מהות הפגישה:";
            $parts[] = $conversation_summary;
        }

        return implode( "\n", $parts );
    }

    /**
     * Send confirmation email to customer.
     * Uses a beautiful HTML template with inline CSS and Heebo font.
     *
     * @param object $meeting Meeting object.
     * @param string $conversation_summary Optional AI-generated conversation summary.
     * @return void
     */
    private function send_confirmation_email( $meeting, $conversation_summary = '' ) {
        if ( empty( $meeting->customer_email ) ) {
            return;
        }

        $site_name     = get_bloginfo( 'name' );
        $customer_name = esc_html( $meeting->customer_name ?? '' );
        $title         = esc_html( $meeting->title ?? '' );
        $date          = esc_html( $meeting->meeting_date ?? '' );
        $time          = esc_html( $this->normalize_time( $meeting->start_time ?? '' ) );
        $meet_link     = esc_url( $meeting->google_meet_link ?? '' );

        /* translators: %s: site name */
        $subject = sprintf( __( 'אישור פגישה - %s', 'boostbot' ), $site_name );

        // Detect if this is a paid installation meeting (from BoostBot Sales)
        $is_installation = false;
        if ( ! empty( $meeting->custom_fields ) ) {
            $cf = is_string( $meeting->custom_fields ) ? json_decode( $meeting->custom_fields, true ) : $meeting->custom_fields;
            if ( is_array( $cf ) && ( $cf['service_type'] ?? '' ) === 'plugin_installation' ) {
                $is_installation = true;
            }
        }

        // Build HTML email
        $message = $this->build_confirmation_email_html(
            $customer_name,
            $title,
            $date,
            $time,
            $meet_link,
            $conversation_summary,
            $site_name,
            $is_installation
        );

        // Set content type to HTML
        add_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );

        wp_mail( $meeting->customer_email, $subject, $message );

        // Reset content type to avoid affecting other emails
        remove_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );
    }

    /**
     * Set email content type to HTML.
     *
     * @return string
     */
    public function set_html_content_type() {
        return 'text/html';
    }

    /**
     * Build beautiful HTML email for meeting confirmation.
     * All CSS is inlined. Uses Heebo font (max weight 700).
     *
     * @param string $customer_name
     * @param string $title
     * @param string $date
     * @param string $time
     * @param string $meet_link
     * @param string $conversation_summary
     * @param string $site_name
     * @param bool   $is_installation Whether this is a paid installation meeting.
     * @return string HTML email body.
     */
    /**
     * Format a date string as Hebrew display (e.g., "18 בפברואר 2026").
     *
     * @param string $date Date in Y-m-d format.
     * @return string Hebrew-formatted date.
     */
    private function format_date_hebrew( string $date ): string {
        if ( preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $date, $m ) ) {
            $months_he = array( '', 'ינואר', 'פברואר', 'מרץ', 'אפריל', 'מאי', 'יוני', 'יולי', 'אוגוסט', 'ספטמבר', 'אוקטובר', 'נובמבר', 'דצמבר' );
            $month_idx = (int) $m[2];
            $day_num   = (int) $m[3];
            if ( isset( $months_he[ $month_idx ] ) ) {
                return $day_num . ' ב' . $months_he[ $month_idx ] . ' ' . $m[1];
            }
        }
        return $date;
    }

    /**
     * Shared email shell — wraps content sections in a beautiful HTML template.
     *
     * @param string $hero_emoji     Large emoji for the hero area.
     * @param string $hero_title     Main title text.
     * @param string $hero_subtitle  Subtitle / tagline.
     * @param string $hero_bg        CSS gradient for the hero strip.
     * @param string $inner_html     Pre-built inner content rows (<tr>…</tr>).
     * @param string $site_name      Site display name for footer.
     * @return string Full HTML document.
     */
    private function build_email_shell( string $hero_emoji, string $hero_title, string $hero_subtitle, string $hero_bg, string $inner_html, string $site_name ): string {
        $f  = "'Heebo',Arial,sans-serif"; // font shorthand
        $yr = gmdate( 'Y' );

        // This HTML body is emitted inside wp_mail() — it is not rendered by
        // the WordPress page pipeline, so wp_enqueue_style / wp_head() don't
        // apply. We deliberately do NOT link an external webfont stylesheet:
        // most email clients strip <link> tags anyway, and doing so avoids an
        // outbound request from the recipient's mail client. The font-family
        // stack below falls back to Arial/sans-serif on clients without Heebo.

        return '<!DOCTYPE html>
<html lang="he" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
</head>
<body style="margin:0;padding:0;background-color:#f4f4f5;-webkit-text-size-adjust:none;font-family:' . $f . ';direction:rtl;">
<table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f4f4f5;padding:40px 16px;font-family:' . $f . ';">
<tr><td align="center">
<table width="600" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;width:100%;background-color:#ffffff;border-radius:16px;overflow:hidden;border:1px solid #e4e4e7;font-family:' . $f . ';">

  <!-- Hero strip -->
  <tr>
    <td style="background:' . $hero_bg . ';padding:36px 32px 28px;text-align:center;font-family:' . $f . ';">
      <div style="font-size:48px;line-height:1;margin-bottom:12px;">' . $hero_emoji . '</div>
      <div style="font-size:22px;font-weight:700;color:#ffffff;line-height:1.3;font-family:' . $f . ';">' . $hero_title . '</div>
      <div style="font-size:13px;font-weight:400;color:rgba(255,255,255,0.85);margin-top:6px;font-family:' . $f . ';">' . $hero_subtitle . '</div>
    </td>
  </tr>

  <!-- Content -->
  ' . $inner_html . '

  <!-- Footer -->
  <tr>
    <td style="padding:24px 32px;text-align:center;border-top:1px solid #f4f4f5;font-family:' . $f . ';">
      <div style="font-size:11px;color:#a1a1aa;line-height:1.7;font-family:' . $f . ';">
        הודעה זו נשלחה אוטומטית ממערכת BoostBot<br>
        &copy; ' . $yr . ' ' . esc_html( $site_name ) . '
      </div>
    </td>
  </tr>

</table>
</td></tr>
</table>
</body>
</html>';
    }

    /**
     * Build beautiful HTML email for meeting confirmation (customer-facing).
     */
    private function build_confirmation_email_html( $customer_name, $title, $date, $time, $meet_link, $conversation_summary, $site_name, $is_installation ) {
        $f  = "'Heebo',Arial,sans-serif";
        $date_display = $this->format_date_hebrew( $date );

        // ── Greeting ──
        $rows = '
  <tr>
    <td style="padding:28px 32px 6px;font-family:' . $f . ';font-size:15px;color:#3f3f46;">
      שלום <strong style="font-family:' . $f . ';color:#18181b;">' . $customer_name . '</strong>,
    </td>
  </tr>
  <tr>
    <td style="padding:0 32px 24px;font-family:' . $f . ';font-size:14px;color:#71717a;line-height:1.7;">
      שמחנו לאשר שהפגישה נקבעה בהצלחה. להלן הפרטים:
    </td>
  </tr>';

        // ── Meeting details card ──
        $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#fafafa;border-radius:12px;border:1px solid #f4f4f5;font-family:' . $f . ';">
        <tr>
          <td style="padding:20px 24px;font-family:' . $f . ';">
            <!-- Title -->
            <div style="padding-bottom:14px;border-bottom:1px solid #f4f4f5;margin-bottom:14px;font-family:' . $f . ';">
              <div style="font-size:11px;font-weight:500;color:#a1a1aa;letter-spacing:0.4px;margin-bottom:3px;font-family:' . $f . ';">כותרת</div>
              <div style="font-size:15px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $title . '</div>
            </div>
            <!-- Date + Time side by side -->
            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">
              <tr>
                <td width="50%" style="vertical-align:top;font-family:' . $f . ';">
                  <div style="font-size:11px;font-weight:500;color:#a1a1aa;letter-spacing:0.4px;margin-bottom:3px;font-family:' . $f . ';">&#128197; תאריך</div>
                  <div style="font-size:14px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $date_display . '</div>
                </td>
                <td width="50%" style="vertical-align:top;font-family:' . $f . ';">
                  <div style="font-size:11px;font-weight:500;color:#a1a1aa;letter-spacing:0.4px;margin-bottom:3px;font-family:' . $f . ';">&#128336; שעה</div>
                  <div style="font-size:14px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $time . '</div>
                </td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>';

        // ── Meet link ──
        if ( ! empty( $meet_link ) ) {
            $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">
        <tr>
          <td align="center" style="font-family:' . $f . ';">
            <a href="' . esc_url( $meet_link ) . '" style="display:inline-block;background:#0d9488;color:#ffffff;font-family:' . $f . ';font-size:14px;font-weight:600;text-decoration:none;padding:12px 32px;border-radius:8px;">&#128279; הצטרפות לפגישה</a>
          </td>
        </tr>
        <tr>
          <td align="center" style="padding-top:8px;font-family:' . $f . ';">
            <a href="' . esc_url( $meet_link ) . '" style="font-family:' . $f . ';font-size:12px;color:#71717a;text-decoration:underline;">' . esc_html( $meet_link ) . '</a>
          </td>
        </tr>
      </table>
    </td>
  </tr>';
        }

        // ── Conversation summary ──
        if ( ! empty( $conversation_summary ) ) {
            $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#f0fdfa;border-radius:12px;border:1px solid #ccfbf1;font-family:' . $f . ';">
        <tr>
          <td style="padding:18px 22px;font-family:' . $f . ';">
            <div style="font-size:13px;font-weight:600;color:#0f766e;margin-bottom:8px;font-family:' . $f . ';">&#128172; סיכום בקשתך</div>
            <div style="font-size:13px;color:#3f3f46;line-height:1.8;font-family:' . $f . ';">' . esc_html( $conversation_summary ) . '</div>
          </td>
        </tr>
      </table>
    </td>
  </tr>';
        }

        // ── Installation prep (BoostBot Sales only) ──
        if ( $is_installation ) {
            $check = '&#9745;';
            $items = array(
                array( 'גישה ל-wp-admin', 'שם משתמש וסיסמה ללוח הבקרה של וורדפרס (רמת מנהל)' ),
                array( 'גרסת PHP 8.0 ומעלה', 'פנו לספק האחסון שלכם וודאו שגרסת ה-PHP מעודכנת ל-8.0 לפחות' ),
                array( 'התקנת SSL (https)', 'ודאו שהאתר שלכם פועל עם תעודת אבטחה (HTTPS)' ),
                array( 'גיבוי זמני של מידע האתר', 'אם יש הנחיות מיוחדות לגבי תוכן או קהל יעד, הכינו תיאור קצר' ),
                array( 'חיבור אינטרנט יציב ומחשב', 'הפגישה תתקיים בזום, ודאו שיש לכם חיבור יציב' ),
            );
            $checklist = '';
            foreach ( $items as $idx => $item ) {
                $bb = $idx < count( $items ) - 1 ? 'border-bottom:1px solid #dbeafe;' : '';
                $checklist .= '
              <tr>
                <td style="padding:8px 0;' . $bb . 'font-family:' . $f . ';">
                  <table cellpadding="0" cellspacing="0" border="0"><tr>
                    <td style="width:24px;font-size:15px;vertical-align:top;padding-top:2px;color:#2563eb;font-family:' . $f . ';">' . $check . '</td>
                    <td style="font-size:13px;color:#1f2937;line-height:1.6;font-family:' . $f . ';"><strong style="font-family:' . $f . ';">' . $item[0] . '</strong> &mdash; ' . $item[1] . '</td>
                  </tr></table>
                </td>
              </tr>';
            }
            $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#eff6ff;border-radius:12px;border:1px solid #bfdbfe;font-family:' . $f . ';">
        <tr>
          <td style="padding:22px 24px;font-family:' . $f . ';">
            <div style="font-size:15px;font-weight:700;color:#1e40af;margin-bottom:6px;font-family:' . $f . ';">&#128221; מה להכין לפני הפגישה?</div>
            <div style="font-size:13px;color:#374151;line-height:1.7;margin-bottom:12px;font-family:' . $f . ';">כדי שנוכל לבצע את ההתקנה בצורה חלקה ומהירה:</div>
            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">' . $checklist . '
            </table>
            <div style="margin-top:14px;padding:10px 14px;background-color:#dbeafe;border-radius:8px;font-size:12px;color:#1e40af;line-height:1.6;font-family:' . $f . ';">
              &#128161; <strong style="font-family:' . $f . ';">טיפ:</strong> אם אינכם בטוחים לגבי אחד מהסעיפים &mdash; אל דאגה! נעזור לכם לבדוק בתחילת הפגישה.
            </div>
          </td>
        </tr>
      </table>
    </td>
  </tr>';
        }

        // ── Closing ──
        $rows .= '
  <tr>
    <td style="padding:8px 32px 28px;font-family:' . $f . ';">
      <div style="font-size:14px;color:#71717a;line-height:1.7;font-family:' . $f . ';">נשמח לראות אתכם!</div>
      <div style="font-size:14px;font-weight:600;color:#18181b;margin-top:2px;font-family:' . $f . ';">' . esc_html( $site_name ) . '</div>
    </td>
  </tr>';

        return $this->build_email_shell(
            '&#10004;',
            'הפגישה שלך אושרה',
            'אישור נשלח לכתובת האימייל שלך',
            'linear-gradient(135deg,#0d9488 0%,#0f766e 100%)',
            $rows,
            $site_name
        );
    }

    /**
     * Send notification email to staff members.
     *
     * @param object $meeting Meeting object.
     * @param array  $contact_ids Array of contact IDs to notify.
     * @param string $conversation_summary AI-generated conversation summary.
     * @return void
     */
    private function send_staff_notification_email( $meeting, $contact_ids, $conversation_summary = '' ) {
        if ( empty( $contact_ids ) ) {
            BoostBot_Logger::warning( '📅 send_staff_notification_email skipped — no contact_ids', array(
                'meeting_id' => $meeting->id ?? null,
            ) );
            return;
        }

        $f  = "'Heebo',Arial,sans-serif";
        $site_name     = esc_html( get_bloginfo( 'name' ) );
        $customer_name = esc_html( $meeting->customer_name ?? '' );
        $title         = esc_html( $meeting->title ?? '' );
        $date_display  = $this->format_date_hebrew( $meeting->meeting_date ?? '' );
        $time_start    = $this->normalize_time( $meeting->start_time ?? '' );
        $time_end      = $this->normalize_time( $meeting->end_time ?? '' );
        $time_display  = $time_start . ( $time_end ? ' - ' . $time_end : '' );

        $subject = sprintf( '🗓️ פגישה חדשה נקבעה עם %s', $meeting->customer_name );

        // ── Build inner HTML ──
        $rows = '
  <tr>
    <td style="padding:28px 32px 20px;font-family:' . $f . ';font-size:14px;color:#71717a;line-height:1.7;">
      פגישה חדשה נקבעה דרך הצ\'אטבוט. להלן הפרטים:
    </td>
  </tr>';

        // Meeting details
        $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#fafafa;border-radius:12px;border:1px solid #f4f4f5;font-family:' . $f . ';">
        <tr>
          <td style="padding:20px 24px;font-family:' . $f . ';">
            <div style="font-size:13px;font-weight:600;color:#0f766e;margin-bottom:14px;font-family:' . $f . ';">&#128197; פרטי הפגישה</div>
            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">כותרת</td>
                <td style="padding:4px 0;font-size:14px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $title . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">תאריך</td>
                <td style="padding:4px 0;font-size:14px;font-weight:500;color:#18181b;font-family:' . $f . ';">' . $date_display . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">שעה</td>
                <td style="padding:4px 0;font-size:14px;font-weight:500;color:#18181b;font-family:' . $f . ';">' . $time_display . '</td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>';

        // Meet link
        if ( ! empty( $meeting->google_meet_link ) ) {
            $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
        <td align="center" style="font-family:' . $f . ';">
          <a href="' . esc_url( $meeting->google_meet_link ) . '" style="display:inline-block;background:#0d9488;color:#ffffff;font-family:' . $f . ';font-size:14px;font-weight:600;text-decoration:none;padding:12px 32px;border-radius:8px;">&#128279; הצטרפות לפגישה</a>
        </td>
      </tr></table>
    </td>
  </tr>';
        }

        // Customer details
        $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#fafafa;border-radius:12px;border:1px solid #f4f4f5;font-family:' . $f . ';">
        <tr>
          <td style="padding:20px 24px;font-family:' . $f . ';">
            <div style="font-size:13px;font-weight:600;color:#6d28d9;margin-bottom:14px;font-family:' . $f . ';">&#128100; פרטי הלקוח</div>
            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">שם</td>
                <td style="padding:4px 0;font-size:14px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $customer_name . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">טלפון</td>
                <td style="padding:4px 0;font-size:14px;color:#18181b;font-family:' . $f . ';" dir="ltr">' . esc_html( $meeting->customer_phone ?? '' ) . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">אימייל</td>
                <td style="padding:4px 0;font-family:' . $f . ';"><a href="mailto:' . esc_attr( $meeting->customer_email ?? '' ) . '" style="font-size:14px;color:#0d9488;text-decoration:none;font-family:' . $f . ';">' . esc_html( $meeting->customer_email ?? '' ) . '</a></td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>';

        // Conversation summary
        if ( ! empty( $conversation_summary ) ) {
            $rows .= '
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#faf5ff;border-radius:12px;border:1px solid #e9d5ff;font-family:' . $f . ';">
        <tr>
          <td style="padding:18px 22px;font-family:' . $f . ';">
            <div style="font-size:13px;font-weight:600;color:#7c3aed;margin-bottom:8px;font-family:' . $f . ';">&#128172; מהות הפגישה (סיכום שיחה)</div>
            <div style="font-size:13px;color:#3f3f46;line-height:1.8;font-family:' . $f . ';">' . esc_html( $conversation_summary ) . '</div>
          </td>
        </tr>
      </table>
    </td>
  </tr>';
        }

        // Closing
        $rows .= '
  <tr>
    <td style="padding:8px 32px 28px;font-family:' . $f . ';">
      <div style="font-size:14px;color:#71717a;font-family:' . $f . ';">בהצלחה!</div>
      <div style="font-size:14px;font-weight:600;color:#18181b;margin-top:2px;font-family:' . $f . ';">' . $site_name . '</div>
    </td>
  </tr>';

        $message = $this->build_email_shell(
            '&#128197;',
            'פגישה חדשה נקבעה',
            'עם ' . $customer_name,
            'linear-gradient(135deg,#4f46e5 0%,#6d28d9 100%)',
            $rows,
            $site_name
        );

        // Send as HTML
        add_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );

        $contact_model = new BoostBot_Contact_Model();
        $sent_emails = array();

        foreach ( $contact_ids as $contact_id ) {
            $contact = $contact_model->get( $contact_id );
            $email   = $contact ? $contact->email : null;

            if ( $email && ! in_array( $email, $sent_emails, true ) ) {
                wp_mail( $email, $subject, $message );
                $sent_emails[] = $email;
            }
        }

        remove_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );

        BoostBot_Logger::info( 'Staff notification emails sent', array(
            'meeting_id'    => $meeting->id ?? null,
            'emails_sent'   => count( $sent_emails ),
            'has_summary'   => ! empty( $conversation_summary ),
        ) );
    }

    /**
     * Send cancellation email to customer.
     *
     * @param object $meeting Meeting object.
     * @return void
     */
    private function send_cancellation_email( $meeting ) {
        if ( empty( $meeting->customer_email ) ) {
            return;
        }

        $f  = "'Heebo',Arial,sans-serif";
        $site_name     = esc_html( get_bloginfo( 'name' ) );
        $customer_name = esc_html( $meeting->customer_name ?? '' );
        $title         = esc_html( $meeting->title ?? '' );
        $date_display  = $this->format_date_hebrew( $meeting->meeting_date ?? '' );
        $time_display  = $this->normalize_time( $meeting->start_time ?? '' );

        $subject = sprintf( 'ביטול פגישה - %s', get_bloginfo( 'name' ) );

        $rows = '
  <tr>
    <td style="padding:28px 32px 6px;font-family:' . $f . ';font-size:15px;color:#3f3f46;">
      שלום <strong style="font-family:' . $f . ';color:#18181b;">' . $customer_name . '</strong>,
    </td>
  </tr>
  <tr>
    <td style="padding:0 32px 24px;font-family:' . $f . ';font-size:14px;color:#71717a;line-height:1.7;">
      לצערנו, הפגישה הבאה בוטלה:
    </td>
  </tr>
  <tr>
    <td style="padding:0 32px 20px;font-family:' . $f . ';">
      <table width="100%" cellpadding="0" cellspacing="0" border="0" style="background-color:#fafafa;border-radius:12px;border:1px solid #f4f4f5;font-family:' . $f . ';">
        <tr>
          <td style="padding:20px 24px;font-family:' . $f . ';">
            <table width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:' . $f . ';">
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">כותרת</td>
                <td style="padding:4px 0;font-size:14px;font-weight:600;color:#18181b;font-family:' . $f . ';">' . $title . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">תאריך</td>
                <td style="padding:4px 0;font-size:14px;font-weight:500;color:#18181b;font-family:' . $f . ';">' . $date_display . '</td>
              </tr>
              <tr>
                <td style="padding:4px 0;font-size:12px;color:#a1a1aa;width:70px;vertical-align:top;font-family:' . $f . ';">שעה</td>
                <td style="padding:4px 0;font-size:14px;font-weight:500;color:#18181b;font-family:' . $f . ';">' . $time_display . '</td>
              </tr>
            </table>
          </td>
        </tr>
      </table>
    </td>
  </tr>
  <tr>
    <td style="padding:0 32px 28px;font-family:' . $f . ';">
      <div style="font-size:14px;color:#71717a;line-height:1.7;font-family:' . $f . ';">מתנצלים על אי הנוחות. ניתן לתאם פגישה חדשה בכל עת.</div>
      <div style="font-size:14px;font-weight:600;color:#18181b;margin-top:4px;font-family:' . $f . ';">' . $site_name . '</div>
    </td>
  </tr>';

        $message = $this->build_email_shell(
            '&#10060;',
            'הפגישה בוטלה',
            'ניתן לתאם מועד חדש',
            'linear-gradient(135deg,#dc2626 0%,#b91c1c 100%)',
            $rows,
            $site_name
        );

        add_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );
        wp_mail( $meeting->customer_email, $subject, $message );
        remove_filter( 'wp_mail_content_type', array( $this, 'set_html_content_type' ) );
    }

    /**
     * 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.
     */
    private static function sanitize_identifier( $identifier ) {
        return preg_replace( '/[^A-Za-z0-9_]/', '', (string) $identifier );
    }
}


