<?php
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.LikeWildcardsInQuery, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.SafeRedirect.wp_redirect_wp_redirect, WordPress.WP.I18n.MissingTranslatorsComment, WordPress.WP.I18n.UnorderedPlaceholdersText, WordPress.DateTime.RestrictedFunctions.date_date, WordPress.WP.AlternativeFunctions.rand_mt_rand, WordPress.PHP.DevelopmentFunctions.error_log_error_log, Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound, WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound
/**
 * Yatoon Booking - Customer Self-Service (Cancel / Reschedule)
 *
 * Flow:
 *  1. After booking, confirmation email includes a "Manage My Booking" link
 *     with a secure token: /manage-booking/?yatoon_token=XXXX
 *  2. Token is valid for N days (configurable, default 7).
 *  3. Page renders: booking details + Cancel button + Reschedule button.
 *  4. Cancel: marks the full appointment group cancelled, fires notification.
 *  5. Reschedule: shows available slots for same service/staff, lets
 *     customer pick a new time, saves it, fires notification.
 *
 * Shortcode: [yatoon_manage_booking]
 * Add this shortcode to any page (e.g. "Manage Booking").
 * Set the page URL in Settings -> Email -> Manage Booking Page.
 */

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

class Yatoon_Manage_Booking {

    // --- Singleton -----------------------------------------------------------

    private static $instance = null;
    public static function get_instance() {
        if ( self::$instance === null ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    private function __construct() {
        add_shortcode( 'yatoon_manage_booking', array( $this, 'render_shortcode' ) );

        // AJAX handlers (nopriv = unauthenticated customers)
        add_action( 'wp_ajax_nopriv_yatoon_customer_cancel',     array( $this, 'ajax_cancel' ) );
        add_action( 'wp_ajax_yatoon_customer_cancel',            array( $this, 'ajax_cancel' ) );
        add_action( 'wp_ajax_nopriv_yatoon_customer_reschedule', array( $this, 'ajax_reschedule' ) );
        add_action( 'wp_ajax_yatoon_customer_reschedule',        array( $this, 'ajax_reschedule' ) );
        add_action( 'wp_ajax_nopriv_yatoon_get_reschedule_slots', array( $this, 'ajax_get_reschedule_slots' ) );
        add_action( 'wp_ajax_yatoon_get_reschedule_slots',        array( $this, 'ajax_get_reschedule_slots' ) );

        // Add manage_token column on activation (handled in activator, but safe to ensure here too)
        add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
        add_action( 'template_redirect', array( $this, 'maybe_redirect_token_to_manage_page' ), 1 );
    }

    private function get_request_token() {
        $token = sanitize_text_field( $_GET['yatoon_token'] ?? '' );
        if ( empty( $token ) ) {
            $token = sanitize_text_field( $_GET['token'] ?? '' );
        }
        return $token;
    }

    private function parse_booking_datetime( $datetime, DateTimeZone $local_tz ) {
        if ( empty( $datetime ) ) {
            return null;
        }

        try {
            return new DateTime( $datetime, $local_tz );
        } catch ( Exception $e ) {
            return null;
        }
    }

    // --- Token helpers -------------------------------------------------------

    /**
     * Generate and save a manage token for a booking.
     * Called right after a booking is created.
     *
     * @param int|string $booking_id  DB row id
     * @return string  The generated token
     */
    public static function create_token( $booking_id ) {
        // Generate manage token for all booking modes (local, Square, Vagaro)
        global $wpdb;
        $token   = bin2hex( random_bytes( 24 ) ); // 48-char hex
        $days    = intval( get_option( 'yatoon_manage_token_days', 7 ) );
        $expires = date( 'Y-m-d H:i:s', strtotime( "+{$days} days" ) );

        yatoon_db_update(
            $wpdb->prefix . 'yatoon_bookings',
            array(
                'manage_token'         => $token,
                'manage_token_expires' => $expires,
            ),
            array( 'id' => intval( $booking_id ) ),
            array( '%s', '%s' ),
            array( '%d' )
        );

        return $token;
    }

    /**
     * Build the manage-booking URL for a token.
     */
    public static function get_manage_url( $token ) {
        $base = self::get_manage_page_url();
        return add_query_arg( 'yatoon_token', rawurlencode( $token ), $base );
    }

    public static function get_manage_page_url() {
        $page_id = intval( get_option( 'yatoon_manage_booking_page_id', 0 ) );
        if ( $page_id > 0 && get_post_status( $page_id ) ) {
            $url = get_permalink( $page_id );
            if ( $url ) {
                return $url;
            }
        }

        $page_id = self::find_manage_page_id();
        if ( $page_id > 0 ) {
            update_option( 'yatoon_manage_booking_page_id', $page_id );
            $url = get_permalink( $page_id );
            if ( $url ) {
                return $url;
            }
        }

        return home_url( '/' );
    }

    private static function find_manage_page_id() {
        global $wpdb;

        $like = '%' . $wpdb->esc_like( '[yatoon_manage_booking' ) . '%';
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $page_id = absint( yatoon_db_get_var( $wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts}
             WHERE post_type = 'page'
               AND post_status IN ('publish','private')
               AND post_content LIKE %s
             ORDER BY post_status = 'publish' DESC, ID DESC
             LIMIT 1",
            $like
        ) ) );
        if ( $page_id > 0 ) {
            return $page_id;
        }

        foreach ( array( 'yatoon_manage_booking', 'yatoon-manage-booking', 'manage-booking', 'manage-appointment', 'my-booking' ) as $slug ) {
            $page = get_page_by_path( $slug );
            if ( $page && in_array( $page->post_status, array( 'publish', 'private' ), true ) ) {
                return absint( $page->ID );
            }
        }

        return 0;
    }

    public function maybe_redirect_token_to_manage_page() {
        $token = $this->get_request_token();
        if ( empty( $token ) || is_admin() || wp_doing_ajax() ) {
            return;
        }

        // This page shows live booking status (confirmed/cancelled) that can
        // change between visits, so it must never be served from a full-page
        // cache (browser, LiteSpeed, WP Rocket, etc.) or customers will see
        // stale status after cancelling/rescheduling.
        if ( ! defined( 'DONOTCACHEPAGE' ) ) {
            define( 'DONOTCACHEPAGE', true );
        }
        nocache_headers();
        if ( ! empty( $_SERVER['SERVER_SOFTWARE'] ) && stripos( $_SERVER['SERVER_SOFTWARE'], 'litespeed' ) !== false ) {
            header( 'X-LiteSpeed-Cache-Control: no-cache' );
        }
        add_filter( 'litespeed_control_set_nocache', '__return_true' );

        $page_id = self::find_manage_page_id();
        if ( $page_id <= 0 || is_page( $page_id ) ) {
            return;
        }

        $target = add_query_arg( 'yatoon_token', rawurlencode( $token ), get_permalink( $page_id ) );
        if ( $target ) {
            wp_safe_redirect( $target );
            exit;
        }
    }

    /**
     * Look up a booking by token (checks expiry and status).
     *
     * @return object|null  Booking row or null if invalid/expired/cancelled.
     */
    private function get_booking_by_token( $token ) {
        global $wpdb;
        if ( empty( $token ) || strlen( $token ) < 40 ) return null;

        $token   = sanitize_text_field( $token );
        $booking = yatoon_db_get_row( $wpdb->prepare(
            "SELECT b.*, s.name AS service_name, s.duration_minutes,
                    st.name AS staff_name
             FROM {$wpdb->prefix}yatoon_bookings b
             LEFT JOIN {$wpdb->prefix}yatoon_services s  ON b.service_id = s.id
             LEFT JOIN {$wpdb->prefix}yatoon_staff   st ON b.staff_id   = st.id
             WHERE b.manage_token = %s
             LIMIT 1",
            $token
        ) );

        if ( ! $booking ) return null;
        $is_cancelled = $this->is_cancelled_status( $booking->status );
        if ( ! $is_cancelled && $booking->manage_token_expires && strtotime( $booking->manage_token_expires ) < time() ) return null;
        // Already cancelled appointments stay viewable so customers are not left at a dead link.
        return $booking;
    }

    private function is_cancelled_status( $status ) {
        return in_array( $status, array( 'cancelled', 'cancelled_by_customer', 'CANCELLED_BY_CUSTOMER', 'CANCELLED_BY_SELLER', 'DECLINED' ), true );
    }

    private function get_related_bookings( $booking, $active_only = false ) {
        global $wpdb;

        if ( ! $booking ) {
            return array();
        }

        $rows = array();
        $table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );
        $status_sql = $active_only
            ? " AND b.status NOT IN ('cancelled','cancelled_by_customer','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED')"
            : '';

        if ( ! empty( $booking->group_booking_id ) ) {
            $rows = yatoon_db_get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                "SELECT b.*, s.name AS service_name, st.name AS staff_name
                 FROM {$table} b
                 LEFT JOIN {$wpdb->prefix}yatoon_services s ON b.service_id = s.id
                 LEFT JOIN {$wpdb->prefix}yatoon_staff st ON b.staff_id = st.id
                 WHERE b.group_booking_id = %s {$status_sql}
                 ORDER BY b.service_order ASC, b.start_time ASC, b.id ASC",
                $booking->group_booking_id
            ) );
        }

        // Legacy / Square fallback: older multi-guest Square bookings may have
        // accidentally received different group IDs. Same customer + same start
        // time is the safest way to reunite those appointment rows.
        if ( count( $rows ) <= 1 && ! empty( $booking->start_time ) && ( ! empty( $booking->customer_email ) || ! empty( $booking->customer_phone ) ) ) {
            $fallback_rows = yatoon_db_get_results( $wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
                "SELECT b.*, s.name AS service_name, st.name AS staff_name
                 FROM {$table} b
                 LEFT JOIN {$wpdb->prefix}yatoon_services s ON b.service_id = s.id
                 LEFT JOIN {$wpdb->prefix}yatoon_staff st ON b.staff_id = st.id
                 WHERE b.start_time = %s
                   AND ((b.customer_email <> '' AND b.customer_email = %s) OR (b.customer_phone <> '' AND b.customer_phone = %s))
                   {$status_sql}
                 ORDER BY b.service_order ASC, b.start_time ASC, b.id ASC",
                $booking->start_time,
                (string) $booking->customer_email,
                (string) $booking->customer_phone
            ) );
            if ( ! empty( $fallback_rows ) ) {
                $rows = array_merge( $rows, $fallback_rows );
            }
        }

        if ( empty( $rows ) && ( ! $active_only || ! $this->is_cancelled_status( $booking->status ) ) ) {
            $rows = array( $booking );
        }

        $unique = array();
        foreach ( $rows as $row ) {
            $unique[ intval( $row->id ) ] = $row;
        }

        return array_values( $unique );
    }

    // --- Shortcode -----------------------------------------------------------

    public function enqueue_assets() {
        $token = $this->get_request_token();
        if ( empty( $token ) ) return;
        wp_enqueue_script(
            'yatoon-manage-booking',
            plugins_url( '../assets/js/manage-booking' . YATOON_ASSET_SUFFIX . '.js', __FILE__ ),
            array( 'jquery' ),
            SBS_VERSION,
            true
        );
        wp_localize_script( 'yatoon-manage-booking', 'yatoonManage', array(
            'ajaxurl' => admin_url( 'admin-ajax.php' ),
            'nonce'   => wp_create_nonce( 'yatoon_manage_booking' ),
            'token'   => $token,
            'bookUrl' => esc_url_raw( get_option( 'yatoon_book_new_url', '' ) ?: home_url( '/' ) ),
            'i18n'    => array(
                'cancel_confirm'     => __( 'Are you sure you want to cancel this entire appointment? This cannot be undone.', 'yatoon-booking-system' ),
                'reschedule_confirm' => __( 'Confirm reschedule to the selected time?', 'yatoon-booking-system' ),
                'loading'            => __( 'Loading...', 'yatoon-booking-system' ),
                'select_slot'        => __( 'Please select a new time slot.', 'yatoon-booking-system' ),
            ),
        ) );
        wp_enqueue_style(
            'yatoon-manage-booking',
            plugins_url( '../assets/css/manage-booking' . YATOON_ASSET_SUFFIX . '.css', __FILE__ ),
            array(),
            SBS_VERSION
        );
        wp_enqueue_style(
            'yatoon-apple-polish',
            plugins_url( '../assets/css/apple-polish' . YATOON_ASSET_SUFFIX . '.css', __FILE__ ),
            array( 'yatoon-manage-booking' ),
            SBS_VERSION
        );
        wp_enqueue_style(
            'yatoon-design-system',
            plugins_url( '../assets/css/yatoon-design-system' . YATOON_ASSET_SUFFIX . '.css', __FILE__ ),
            array( 'yatoon-apple-polish' ),
            SBS_VERSION
        );
        wp_add_inline_style( 'yatoon-apple-polish', yatoon_apple_polish_inline_css() );
    }

    public function render_shortcode( $atts ) {
        $token = $this->get_request_token();

        if ( empty( $token ) ) {
            return '<div class="yatoon-manage-wrap"><p>' . esc_html__( 'No booking token provided. Please use the link from your confirmation email.', 'yatoon-booking-system' ) . '</p></div>';
        }

        $booking = $this->get_booking_by_token( $token );

        if ( ! $booking ) {
            return '<div class="yatoon-manage-wrap"><p class="yatoon-manage-error">' . esc_html__( 'This link is invalid or has expired. Please contact us directly to make changes.', 'yatoon-booking-system' ) . '</p></div>';
        }

        $business_name  = get_option( 'yatoon_business_name', get_bloginfo( 'name' ) );
        $business_phone = get_option( 'yatoon_contact_phone', '' );
        $primary_color  = get_option( 'yatoon_primary_color', '#ec4899' );

        $local_tz   = new DateTimeZone( get_option( 'timezone_string' ) ?: 'America/New_York' );
        $start_dt   = $this->parse_booking_datetime( $booking->start_time, $local_tz );
        $end_dt     = $this->parse_booking_datetime( $booking->end_time, $local_tz );

        // Fetch the full booking group so customers see and cancel the complete appointment.
        $group_bookings        = $this->get_related_bookings( $booking, false );
        $active_group_bookings = $this->get_related_bookings( $booking, true );
        $display_bookings      = ! empty( $active_group_bookings ) ? $active_group_bookings : $group_bookings;

        if ( ! $start_dt || ! $end_dt ) {
            return '<div class="yatoon-manage-wrap"><p class="yatoon-manage-error">' . esc_html__( 'This booking could not be loaded because its date or time is invalid.', 'yatoon-booking-system' ) . '</p></div>';
        }

        $is_cancelled    = empty( $active_group_bookings );
        $is_past         = $start_dt < new DateTime( 'now', $local_tz );
        $can_cancel      = ! $is_cancelled && ! $is_past;
        $reschedule_on   = get_option( 'yatoon_manage_allow_reschedule', '1' ) === '1';
        $can_reschedule  = $can_cancel && $reschedule_on;
        $display_status  = $is_cancelled ? 'cancelled' : ( ! empty( $active_group_bookings[0]->status ) ? $active_group_bookings[0]->status : $booking->status );
        $can_pick_cancel = $can_cancel && count( $active_group_bookings ) > 1;

        // Lead time check
        $min_hours = intval( get_option( 'yatoon_manage_min_hours_before', 2 ) );
        if ( $min_hours > 0 ) {
            $cutoff = new DateTime( 'now', $local_tz );
            $cutoff->modify( "+{$min_hours} hours" );
            if ( $start_dt <= $cutoff ) {
                $can_cancel     = false;
                $can_reschedule = false;
            }
        }

        $min_reschedule_dt = new DateTime( 'now', $local_tz );
        $min_reschedule_dt->setTime( 0, 0, 0 );
        $min_reschedule_dt->modify( '+1 day' );
        $max_reschedule_dt = clone $min_reschedule_dt;
        $max_reschedule_dt->modify( '+59 days' );
        $default_reschedule_dt = clone $start_dt;
        $default_reschedule_dt->setTime( 0, 0, 0 );
        if ( $default_reschedule_dt < $min_reschedule_dt ) {
            $default_reschedule_dt = clone $min_reschedule_dt;
        } elseif ( $default_reschedule_dt > $max_reschedule_dt ) {
            $default_reschedule_dt = clone $max_reschedule_dt;
        }

        ob_start();
        ?>
        <div class="yatoon-manage-wrap" style="--yatoon-primary:<?php echo esc_attr( $primary_color ); ?>">

            <div class="yatoon-manage-card">
                <div class="yatoon-manage-header">
                    <h2><?php echo esc_html( $business_name ); ?></h2>
                    <p class="yatoon-manage-subtitle"><?php esc_html_e( 'Manage Your Appointment', 'yatoon-booking-system' ); ?></p>
                </div>

                <div class="yatoon-manage-details">
                    <div class="yatoon-manage-row"><span class="ymr-label"><?php esc_html_e( 'Services', 'yatoon-booking-system' ); ?></span><span class="ymr-value">
                        <?php foreach ( $display_bookings as $idx => $group_item ) : ?>
                            <?php
                            $group_start = $this->parse_booking_datetime( $group_item->start_time, $local_tz );
                            $group_end   = $this->parse_booking_datetime( $group_item->end_time, $local_tz );
                            ?>
                            <span class="yatoon-manage-service-row" style="display:block;margin-bottom:6px;">
                                <?php if ( $can_pick_cancel && ! $this->is_cancelled_status( $group_item->status ) ) : ?>
                                    <label class="yatoon-cancel-choice">
                                        <input type="checkbox" class="yatoon-cancel-item" value="<?php echo esc_attr( intval( $group_item->id ) ); ?>" checked>
                                        <span>
                                <?php endif; ?>
                                <?php echo esc_html( ($idx + 1) . '. ' . ( $group_item->service_name ?? '-' ) ); ?>
                                <?php if ( ! empty( $group_item->staff_name ) ) : ?>
                                    <?php echo esc_html( ' - ' . $group_item->staff_name ); ?>
                                <?php endif; ?>
                                <?php if ( $group_start && $group_end ) : ?>
                                    <small style="display:block;color:#6b7280;"><?php echo esc_html( $group_start->format( 'g:i A' ) . ' - ' . $group_end->format( 'g:i A' ) ); ?></small>
                                <?php endif; ?>
                                <?php if ( $can_pick_cancel && ! $this->is_cancelled_status( $group_item->status ) ) : ?>
                                        </span>
                                    </label>
                                <?php endif; ?>
                            </span>
                        <?php endforeach; ?>
                    </span></div>
                    <div class="yatoon-manage-row"><span class="ymr-label"><?php esc_html_e( 'Date', 'yatoon-booking-system' ); ?></span><span class="ymr-value"><?php echo esc_html( $start_dt->format( 'F j, Y' ) ); ?></span></div>
                    <div class="yatoon-manage-row"><span class="ymr-label"><?php esc_html_e( 'Time', 'yatoon-booking-system' ); ?></span><span class="ymr-value"><?php echo esc_html( $start_dt->format( 'g:i A' ) . ' - ' . $end_dt->format( 'g:i A' ) ); ?></span></div>
                    <div class="yatoon-manage-row"><span class="ymr-label"><?php esc_html_e( 'Name', 'yatoon-booking-system' ); ?></span><span class="ymr-value"><?php echo esc_html( $booking->customer_name ); ?></span></div>
                    <div class="yatoon-manage-row"><span class="ymr-label"><?php esc_html_e( 'Status', 'yatoon-booking-system' ); ?></span><span class="ymr-value yatoon-status-<?php echo esc_attr( strtolower( $display_status ) ); ?>"><?php echo esc_html( ucfirst( str_replace( '_', ' ', $display_status ) ) ); ?></span></div>
                </div>

                <?php if ( $is_cancelled ) : ?>
                    <div class="yatoon-manage-notice yatoon-notice-info">
                        <?php esc_html_e( 'This appointment has been cancelled.', 'yatoon-booking-system' ); ?>
                        <?php if ( $business_phone ) : ?>
                            <?php echo esc_html__( 'To book again, call us at ', 'yatoon-booking-system' ) . '<a href="tel:' . esc_attr( $business_phone ) . '">' . esc_html( $business_phone ) . '</a>'; ?>
                        <?php endif; ?>
                    </div>
                <?php elseif ( $is_past ) : ?>
                    <div class="yatoon-manage-notice yatoon-notice-info">
                        <?php esc_html_e( 'This appointment has already passed.', 'yatoon-booking-system' ); ?>
                    </div>
                <?php elseif ( ! $can_cancel && $min_hours > 0 ) : ?>
                    <div class="yatoon-manage-notice yatoon-notice-warning">
                        <?php printf( esc_html__( 'Changes must be made at least %s hour(s) before your appointment. Please call us directly.', 'yatoon-booking-system' ), esc_html( (string) $min_hours ) ); ?>
                        <?php if ( $business_phone ) echo ' <a href="tel:' . esc_attr( $business_phone ) . '">' . esc_html( $business_phone ) . '</a>'; ?>
                    </div>
                <?php else : ?>

                    <div class="yatoon-manage-actions" id="yatoon-manage-actions">
                        <?php if ( $can_reschedule ) : ?>
                            <button type="button" class="yatoon-btn yatoon-btn-primary" id="yatoon-reschedule-btn">
                                <?php esc_html_e( 'Reschedule', 'yatoon-booking-system' ); ?>
                            </button>
                        <?php endif; ?>
                        <?php if ( $can_cancel ) : ?>
                            <button type="button" class="yatoon-btn yatoon-btn-outline" id="yatoon-cancel-btn">
                                <?php echo esc_html( $can_pick_cancel ? __( 'Cancel Selected Items', 'yatoon-booking-system' ) : __( 'Cancel Appointment', 'yatoon-booking-system' ) ); ?>
                            </button>
                        <?php endif; ?>
                    </div>

                    <!-- Reschedule panel (hidden by default) -->
                    <?php if ( $can_reschedule ) : ?>
                    <div id="yatoon-reschedule-panel" style="display:none;" class="yatoon-reschedule-panel">
                        <h3><?php esc_html_e( 'Choose a New Date & Time', 'yatoon-booking-system' ); ?></h3>
                        <div class="yatoon-reschedule-date-row">
                            <label for="yatoon-reschedule-date"><?php esc_html_e( 'Date', 'yatoon-booking-system' ); ?></label>
                            <input type="date" id="yatoon-reschedule-date"
                                   value="<?php echo esc_attr( $default_reschedule_dt->format( 'Y-m-d' ) ); ?>"
                                   min="<?php echo esc_attr( $min_reschedule_dt->format( 'Y-m-d' ) ); ?>"
                                   max="<?php echo esc_attr( $max_reschedule_dt->format( 'Y-m-d' ) ); ?>">
                            <button type="button" class="yatoon-btn yatoon-btn-sm" id="yatoon-load-slots-btn">
                                <?php esc_html_e( 'Show Available Times', 'yatoon-booking-system' ); ?>
                            </button>
                        </div>
                        <div id="yatoon-slots-wrap" style="display:none;">
                            <div id="yatoon-slots-list" class="yatoon-slots-list"></div>
                            <div class="yatoon-reschedule-confirm-row" id="yatoon-reschedule-confirm-row" style="display:none;">
                                <button type="button" class="yatoon-btn yatoon-btn-primary" id="yatoon-confirm-reschedule-btn">
                                    Yes <?php esc_html_e( 'Confirm Reschedule', 'yatoon-booking-system' ); ?>
                                </button>
                                <button type="button" class="yatoon-btn yatoon-btn-ghost" id="yatoon-cancel-reschedule-btn">
                                    <?php esc_html_e( 'Back', 'yatoon-booking-system' ); ?>
                                </button>
                            </div>
                        </div>
                        <div id="yatoon-slots-msg" class="yatoon-manage-notice" style="display:none;"></div>
                    </div>
                    <?php endif; ?>

                    <div id="yatoon-manage-msg" class="yatoon-manage-notice" style="display:none;"></div>

                <?php endif; ?>

                <div class="yatoon-manage-footer">
                    <?php if ( $business_phone ) : ?>
                        <p><?php esc_html_e( 'Need help? Call us:', 'yatoon-booking-system' ); ?> <a href="tel:<?php echo esc_attr( $business_phone ); ?>"><?php echo esc_html( $business_phone ); ?></a></p>
                    <?php endif; ?>
                </div>

            </div>

        </div>
        <?php
        return ob_get_clean();
    }

    // --- AJAX: Cancel --------------------------------------------------------

    public function ajax_cancel() {
        check_ajax_referer( 'yatoon_manage_booking', 'nonce' );
        global $wpdb;

        $token   = sanitize_text_field( $_POST['token'] ?? '' );
        $booking = $this->get_booking_by_token( $token );

        if ( ! $booking ) {
            wp_send_json_error( array( 'message' => __( 'Invalid or expired link.', 'yatoon-booking-system' ) ) );
        }

        $active_rows = $this->get_related_bookings( $booking, true );
        if ( empty( $active_rows ) ) {
            wp_send_json_error( array( 'message' => __( 'This appointment is already cancelled.', 'yatoon-booking-system' ) ) );
        }

        $requested_ids_raw = isset( $_POST['booking_ids'] ) ? wp_unslash( $_POST['booking_ids'] ) : '';
        $requested_ids = array();
        if ( is_string( $requested_ids_raw ) && $requested_ids_raw !== '' ) {
            $decoded = json_decode( $requested_ids_raw, true );
            if ( is_array( $decoded ) ) {
                $requested_ids = array_map( 'absint', $decoded );
            }
        } elseif ( is_array( $requested_ids_raw ) ) {
            $requested_ids = array_map( 'absint', $requested_ids_raw );
        }
        $requested_ids = array_values( array_filter( array_unique( $requested_ids ) ) );

        $allowed_rows = array();
        foreach ( $active_rows as $row ) {
            $allowed_rows[ intval( $row->id ) ] = $row;
        }

        if ( ! empty( $requested_ids ) ) {
            $rows_to_cancel = array();
            foreach ( $requested_ids as $requested_id ) {
                if ( isset( $allowed_rows[ $requested_id ] ) ) {
                    $rows_to_cancel[] = $allowed_rows[ $requested_id ];
                }
            }
            if ( empty( $rows_to_cancel ) ) {
                wp_send_json_error( array( 'message' => __( 'Please select at least one appointment item to cancel.', 'yatoon-booking-system' ) ) );
            }
        } else {
            $rows_to_cancel = $active_rows;
        }

        $selected_ids = array_map( static function( $row ) {
            return intval( $row->id );
        }, $rows_to_cancel );

        foreach ( $rows_to_cancel as $row_to_cancel ) {
            if ( empty( $row_to_cancel->square_booking_id ) ) {
                continue;
            }
            foreach ( $active_rows as $active_row ) {
                if (
                    ! empty( $active_row->square_booking_id )
                    && $active_row->square_booking_id === $row_to_cancel->square_booking_id
                    && ! in_array( intval( $active_row->id ), $selected_ids, true )
                ) {
                    wp_send_json_error( array(
                        'message' => __( 'Some selected services are linked together in one Square appointment. Please select all linked services for that time, or contact us for help changing only part of it.', 'yatoon-booking-system' ),
                    ) );
                }
            }
        }

        $local_tz = new DateTimeZone( get_option( 'timezone_string' ) ?: 'America/New_York' );
        $start_dt = $this->parse_booking_datetime( $booking->start_time, $local_tz );
        if ( ! $start_dt ) {
            wp_send_json_error( array( 'message' => __( 'This booking has an invalid scheduled time.', 'yatoon-booking-system' ) ) );
        }
        $min_hours = intval( get_option( 'yatoon_manage_min_hours_before', 2 ) );
        if ( $min_hours > 0 ) {
            $cutoff = new DateTime( 'now', $local_tz );
            $cutoff->modify( "+{$min_hours} hours" );
            if ( $start_dt <= $cutoff ) {
                wp_send_json_error( array( 'message' => sprintf( __( 'Cancellations must be made at least %d hour(s) in advance. Please call us directly.', 'yatoon-booking-system' ), $min_hours ) ) );
            }
        }

        $table = esc_sql( $wpdb->prefix . 'yatoon_bookings' );

        $square_api = null;
        $cancelled_square_ids = array();
        foreach ( $rows_to_cancel as $row_to_cancel ) {
            if ( ! empty( $row_to_cancel->square_booking_id ) ) {
                if ( ! in_array( $row_to_cancel->square_booking_id, $cancelled_square_ids, true ) ) {
                    if ( ! $square_api ) {
                        $square_api = new YATOON_Square_API();
                    }
                    $sq_resp = $square_api->get_booking( $row_to_cancel->square_booking_id );
                    $version = null;
                    if ( ! is_wp_error( $sq_resp ) && isset( $sq_resp['booking']['version'] ) ) {
                        $version = intval( $sq_resp['booking']['version'] );
                    }
                    $cancel_resp = $square_api->cancel_booking( $row_to_cancel->square_booking_id, $version );
                    if ( is_wp_error( $cancel_resp ) ) {
                        error_log( '[Yatoon] Square cancel failed for booking ' . $row_to_cancel->id . ': ' . $cancel_resp->get_error_message() );
                    }
                    $cancelled_square_ids[] = $row_to_cancel->square_booking_id;
                }
            }

            yatoon_db_update(
                $table,
                array(
                    'status'       => 'cancelled',
                    'cancelled_at' => current_time( 'mysql' ),
                ),
                array( 'id' => intval( $row_to_cancel->id ) ),
                array( '%s', '%s' ),
                array( '%d' )
            );
        }

        $notification_booking = ! empty( $rows_to_cancel[0] ) ? $rows_to_cancel[0] : $booking;

        // Fire notifications
        do_action( 'yatoon_booking_cancelled_by_customer', $notification_booking );

        // Send cancel email via notifications class
        $notifications = new Yatoon_Notifications();
        $notifications->send_cancel_email( (array) $notification_booking );

        // Notify admin
        $this->notify_admin_cancel( $notification_booking );

        $cancelled_count = count( $rows_to_cancel );
        $message = $cancelled_count > 1
            ? sprintf( __( 'Your selected %d appointment items have been cancelled. We hope to see you again soon!', 'yatoon-booking-system' ), $cancelled_count )
            : __( 'Your appointment has been cancelled. We hope to see you again soon!', 'yatoon-booking-system' );

        wp_send_json_success( array(
            'message'         => $message,
            'cancelled_count' => $cancelled_count,
        ) );
    }

    // --- AJAX: Get reschedule slots ------------------------------------------

    public function ajax_get_reschedule_slots() {
        check_ajax_referer( 'yatoon_manage_booking', 'nonce' );

        $token   = sanitize_text_field( $_POST['token'] ?? '' );
        $date    = sanitize_text_field( $_POST['date'] ?? '' );
        $booking = $this->get_booking_by_token( $token );

        if ( ! $booking || empty( $date ) ) {
            wp_send_json_error( array( 'message' => __( 'Invalid request.', 'yatoon-booking-system' ) ) );
        }

        if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
            wp_send_json_error( array( 'message' => __( 'Invalid date format.', 'yatoon-booking-system' ) ) );
        }

        $service_id = intval( $booking->service_id );
        $staff_id   = intval( $booking->staff_id );

        $slots = Yatoon_Business_Hours::get_available_slots_for_date(
            $date,
            $service_id,
            $staff_id,
            intval( $booking->id ) // exclude current booking from conflict check
        );

        if ( empty( $slots ) ) {
            wp_send_json_error( array( 'message' => __( 'No available times on this date. Please try another day.', 'yatoon-booking-system' ) ) );
        }

        wp_send_json_success( array( 'slots' => $slots ) );
    }

    // --- AJAX: Reschedule ----------------------------------------------------

    public function ajax_reschedule() {
        check_ajax_referer( 'yatoon_manage_booking', 'nonce' );
        global $wpdb;

        $token      = sanitize_text_field( $_POST['token'] ?? '' );
        $new_time   = sanitize_text_field( $_POST['new_time'] ?? '' ); // 'Y-m-d H:i:s' in local TZ
        $booking    = $this->get_booking_by_token( $token );

        if ( ! $booking ) {
            wp_send_json_error( array( 'message' => __( 'Invalid or expired link.', 'yatoon-booking-system' ) ) );
        }

        if ( empty( $new_time ) || ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $new_time ) ) {
            wp_send_json_error( array( 'message' => __( 'Please select a valid time slot.', 'yatoon-booking-system' ) ) );
        }

        $local_tz = new DateTimeZone( get_option( 'timezone_string' ) ?: 'America/New_York' );

        $new_start_local = new DateTime( $new_time, $local_tz );

        $duration_min = intval( $booking->duration_minutes ?: 60 );
        $new_end_local  = ( clone $new_start_local )->modify( "+{$duration_min} minutes" );

        // Validate slot is still available (exclude current booking)
        if ( ! Yatoon_Business_Hours::is_valid_slot(
            $new_start_local->format( 'Y-m-d\TH:i:s' ),
            intval( $booking->service_id ),
            intval( $booking->staff_id ),
            intval( $booking->id )
        ) ) {
            wp_send_json_error( array( 'message' => __( 'That time slot is no longer available. Please choose another.', 'yatoon-booking-system' ) ) );
        }

        $old_start = $booking->start_time;

        yatoon_db_update(
            $wpdb->prefix . 'yatoon_bookings',
            array(
                'start_time' => $new_start_local->format( 'Y-m-d H:i:s' ),
                'end_time'   => $new_end_local->format( 'Y-m-d H:i:s' ),
                'status'     => 'pending',
            ),
            array( 'id' => intval( $booking->id ) ),
            array( '%s', '%s', '%s' ),
            array( '%d' )
        );

        // Sync rescheduled time to Square if this booking was created via Square
        if ( ! empty( $booking->square_booking_id ) ) {
            $square_api = new YATOON_Square_API();
            // Square requires the current booking version to update
            $sq_resp = $square_api->get_booking( $booking->square_booking_id );
            $version = null;
            if ( ! is_wp_error( $sq_resp ) && isset( $sq_resp['booking']['version'] ) ) {
                $version = intval( $sq_resp['booking']['version'] );
            }
            // Square API expects start_at in RFC 3339 UTC format
            $new_start_utc = ( clone $new_start_local )->setTimezone( new DateTimeZone( 'UTC' ) );
            $update_resp = $square_api->update_booking(
                $booking->square_booking_id,
                array( 'start_at' => $new_start_utc->format( 'Y-m-d\TH:i:s\Z' ) ),
                $version
            );
            if ( is_wp_error( $update_resp ) ) {
                error_log( '[Yatoon] Square reschedule failed for booking ' . $booking->id . ': ' . $update_resp->get_error_message() );
            }
        }

        // Refresh token expiry (another N days from now)
        $days    = intval( get_option( 'yatoon_manage_token_days', 7 ) );
        $expires = date( 'Y-m-d H:i:s', strtotime( "+{$days} days" ) );
        yatoon_db_update(
            $wpdb->prefix . 'yatoon_bookings',
            array( 'manage_token_expires' => $expires ),
            array( 'id' => intval( $booking->id ) ),
            array( '%s' ),
            array( '%d' )
        );

        // Update booking object for emails
        $booking->start_time = $new_start_local->format( 'Y-m-d H:i:s' );
        $booking->end_time   = $new_end_local->format( 'Y-m-d H:i:s' );

        // Send reschedule notification
        $notifications = new Yatoon_Notifications();
        $notifications->send_reschedule_email( (array) $booking );
        $this->notify_admin_reschedule( $booking, $old_start );

        do_action( 'yatoon_booking_rescheduled_by_customer', $booking );

        $new_fmt = $new_start_local->format( 'F j, Y \a\t g:i A' );
        wp_send_json_success( array(
            'message' => sprintf( __( 'Your appointment has been rescheduled to %s. A confirmation email has been sent.', 'yatoon-booking-system' ), $new_fmt ),
        ) );
    }

    // --- Admin notifications -------------------------------------------------

    private function notify_admin_cancel( $booking ) {
        if ( get_option( 'yatoon_manage_notify_admin_cancel', '1' ) !== '1' ) return;
        $admin_email   = get_option( 'yatoon_contact_email', get_option( 'admin_email' ) );
        $business_name = get_option( 'yatoon_business_name', get_bloginfo( 'name' ) );
        $local_tz      = new DateTimeZone( get_option( 'timezone_string' ) ?: 'America/New_York' );
        $start_dt      = $this->parse_booking_datetime( $booking->start_time, $local_tz );

        if ( ! $start_dt ) {
            return;
        }

        $subject = sprintf( '[%s] Appointment Cancelled by Customer', $business_name );
        $body    = sprintf(
            "A customer has cancelled their appointment.\n\nCustomer: %s\nEmail: %s\nService: %s\nWas scheduled: %s\n\nLog into %s to view details.",
            $booking->customer_name,
            $booking->customer_email,
            $booking->service_name ?? '-',
            $start_dt->format( 'F j, Y g:i A' ),
            admin_url( 'admin.php?page=yatoon-booking-system&tab=bookings' )
        );
        wp_mail( $admin_email, $subject, $body );
    }

    private function notify_admin_reschedule( $booking, $old_start ) {
        if ( get_option( 'yatoon_manage_notify_admin_reschedule', '1' ) !== '1' ) return;
        $admin_email   = get_option( 'yatoon_contact_email', get_option( 'admin_email' ) );
        $business_name = get_option( 'yatoon_business_name', get_bloginfo( 'name' ) );
        $local_tz      = new DateTimeZone( get_option( 'timezone_string' ) ?: 'America/New_York' );
        $old_dt        = $this->parse_booking_datetime( $old_start, $local_tz );
        $new_dt        = $this->parse_booking_datetime( $booking->start_time, $local_tz );

        if ( ! $old_dt || ! $new_dt ) {
            return;
        }

        $subject = sprintf( '[%s] Appointment Rescheduled by Customer', $business_name );
        $body    = sprintf(
            "A customer has rescheduled their appointment.\n\nCustomer: %s\nEmail: %s\nService: %s\nOld time: %s\nNew time: %s\n\nLog into %s to view details.",
            $booking->customer_name,
            $booking->customer_email,
            $booking->service_name ?? '-',
            $old_dt->format( 'F j, Y g:i A' ),
            $new_dt->format( 'F j, Y g:i A' ),
            admin_url( 'admin.php?page=yatoon-booking-system&tab=bookings' )
        );
        wp_mail( $admin_email, $subject, $body );
    }
}

Yatoon_Manage_Booking::get_instance();
