<?php
// phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
if (!defined('ABSPATH')) { exit; }

/**
 * YATOON_Recurring  (Pro only)
 *
 * After a booking is confirmed, if the customer opted in to a recurring series,
 * this class creates future bookings at the chosen weekly interval.
 *
 * Flow:
 *  1. Frontend JS shows "Make this recurring?" after booking confirmed (success page)
 *  2. Customer picks frequency (1/2/3/4 weeks) and submits via AJAX
 *  3. This class creates N future bookings and records them in yatoon_recurring
 *  4. Customer can cancel the whole series via manage-booking page
 *  5. Admin can see all series in the Recurring admin view
 */
class YATOON_Recurring {

    public function __construct() {
        // AJAX: set up recurring series from success page
        add_action('wp_ajax_yatoon_setup_recurring',        array($this, 'ajax_setup_recurring'));
        add_action('wp_ajax_nopriv_yatoon_setup_recurring', array($this, 'ajax_setup_recurring'));

        // AJAX: cancel series
        add_action('wp_ajax_yatoon_cancel_recurring_series',        array($this, 'ajax_cancel_series'));
        add_action('wp_ajax_nopriv_yatoon_cancel_recurring_series', array($this, 'ajax_cancel_series'));

        // Hook: when a booking is cancelled, check if it's part of a series
        add_action('yatoon_booking_cancelled', array($this, 'handle_booking_cancelled'), 10, 1);
    }

    // -------------------------------------------------------------------------
    // AJAX: Customer sets up recurring from success page
    // -------------------------------------------------------------------------

    public function ajax_setup_recurring() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        if (function_exists('yatoon_public_rate_limit')) {
            yatoon_public_rate_limit('recurring_setup', 5, 10);
        }

        if (!class_exists('YATOON_License') || !YATOON_License::is_pro()) {
            wp_send_json_error(array('message' => __( 'Recurring appointments require Pro.', 'yatoon-booking-system' )));
        }

        $booking_id      = absint($_POST['booking_id'] ?? 0);
        $manage_token    = sanitize_text_field(wp_unslash($_POST['manage_token'] ?? ''));
        $frequency_weeks = min(4, max(1, intval($_POST['frequency_weeks'] ?? 2)));
        $total_count     = min(12, max(2, intval($_POST['total_count'] ?? 6)));

        if (!$booking_id || !$manage_token) {
            wp_send_json_error(array('message' => __( 'Invalid booking reference.', 'yatoon-booking-system' )));
        }

        global $wpdb;
        $bookings_table  = $wpdb->prefix . 'yatoon_bookings';
        $recurring_table = $wpdb->prefix . 'yatoon_recurring';

        // Verify token
        // 4.6.2: this lookup matched on id + manage_token alone, with no
        // expiry clause, on a wp_ajax_nopriv_ endpoint. A manage link that had
        // already lapsed -- forwarded, indexed, or dug out of an old email
        // months later -- could still open a twelve-appointment series on the
        // salon's calendar. ajax_cancel_series() below already gates on
        // manage_token_expires; this is the same gate, applied here.
        //
        // The cutoff is passed in from PHP with gmdate() rather than written
        // as SQL NOW(): manage_token_expires is a plain `datetime` column
        // (class-yatoon-activator.php:502) written in UTC with gmdate()
        // (class-yatoon-manage-booking.php:120), so NOW() would compare UTC
        // data against the database server's local time zone and expire
        // tokens hours early or late depending on where the host is.
        // Rows written before the column existed carry NULL, and some import
        // paths leave it empty (which MySQL reads as the zero datetime), so
        // both count as "no expiry set" rather than "expired" -- otherwise
        // this fix would lock legitimate older bookings out of the feature.
        $booking = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM $bookings_table
             WHERE id = %d
               AND manage_token = %s
               AND (manage_token_expires IS NULL OR manage_token_expires = '' OR manage_token_expires >= %s)
             LIMIT 1",
            $booking_id, $manage_token, gmdate( 'Y-m-d H:i:s' )
        ));

        if (!$booking) {
            wp_send_json_error(array('message' => __( 'Booking not found.', 'yatoon-booking-system' )));
}

        if (get_option('yatoon_active_platform', 'local') !== 'local') {
            wp_send_json_error(array('message' => __( 'Recurring appointments can only be created in Local mode because external platforms must confirm every future slot.', 'yatoon-booking-system' )));
        }
        // Don't duplicate series
        $existing_series = $wpdb->get_var($wpdb->prepare(
            "SELECT series_id FROM $recurring_table WHERE booking_id = %d LIMIT 1", $booking_id
        ));
        if ($existing_series) {
            wp_send_json_success(array('series_id' => $existing_series, 'created' => 0, 'message' => __( 'Series already exists.', 'yatoon-booking-system' )));
        }

        $series_id = wp_generate_password(16, false);

        // Record this booking as occurrence #1
        $wpdb->insert($recurring_table, array(
            'series_id'       => $series_id,
            'booking_id'      => $booking_id,
            'customer_name'   => $booking->customer_name,
            'customer_email'  => $booking->customer_email,
            'customer_phone'  => $booking->customer_phone,
            'service_id'      => $booking->service_id,
            'staff_id'        => $booking->staff_id,
            'frequency_weeks' => $frequency_weeks,
            'total_count'     => $total_count,
            'occurrence_num'  => 1,
            'status'          => 'active',
        ));

        // Calculate duration
        $recur_tz  = yatoon_timezone();
$base_start = new DateTimeImmutable($booking->start_time, $recur_tz);
$base_end   = new DateTimeImmutable($booking->end_time, $recur_tz);
$duration   = $base_end->getTimestamp() - $base_start->getTimestamp();
        $created = 0;
        $failed  = 0;

        for ($i = 2; $i <= $total_count; $i++) {
            $weeks_offset = ( $i - 1 ) * $frequency_weeks;
            $recur_start_dt = $base_start->modify("+{$weeks_offset} weeks");
$recur_end_dt   = $recur_start_dt->modify("+{$duration} seconds");
$new_start      = $recur_start_dt->format('Y-m-d H:i:s');
$new_end        = $recur_end_dt->format('Y-m-d H:i:s');
$lock_name      = 'yatoon_recur_' . md5($booking->staff_id . '|' . $recur_start_dt->format('Y-m-d'));
$has_lock       = (int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock_name)) === 1;
if (!$has_lock) { ++$failed;
continue; }
            // Check basic availability (no existing booking for same staff/time)
            $conflict = $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM $bookings_table
                 WHERE staff_id = %d
                   AND status NOT IN ('cancelled','CANCELLED_BY_CUSTOMER','CANCELLED_BY_SELLER','DECLINED','no_show')
                   AND start_time < %s AND end_time > %s
                 LIMIT 1",
                $booking->staff_id, $new_end, $new_start
            ));

            // Also honor the staff member's own time off/days off for this
            // future date -- a day with zero other bookings looks 'free' by
            // the check above alone even if the staff member simply isn't
            // scheduled to work that day at all.
            if (!$conflict && class_exists('YATOON_Business_Hours')) {
                if (YATOON_Business_Hours::has_staff_time_off_conflict($booking->staff_id, $recur_start_dt, $recur_end_dt, $recur_tz)) {
                    $conflict = true;
}
                $day_hours = YATOON_Business_Hours::get_day_hours($recur_start_dt->format('Y-m-d'));
if (!$day_hours) {
                    $conflict = true;
} else {
                    $open  = new DateTimeImmutable($recur_start_dt->format('Y-m-d') . ' ' . ( $day_hours['start'] ?? '09:00' ), $recur_tz);
$close = new DateTimeImmutable($recur_start_dt->format('Y-m-d') . ' ' . ( $day_hours['end'] ?? '17:00' ), $recur_tz);
if ($recur_start_dt < $open || $recur_end_dt > $close) { $conflict = true;
                }
                    if (!empty($day_hours['break_start']) && !empty($day_hours['break_end'])) {
                        $break_start = new DateTimeImmutable($recur_start_dt->format('Y-m-d') . ' ' . $day_hours['break_start'], $recur_tz);
$break_end   = new DateTimeImmutable($recur_start_dt->format('Y-m-d') . ' ' . $day_hours['break_end'], $recur_tz);
if ($recur_start_dt < $break_end && $recur_end_dt > $break_start) { $conflict = true;
                }
                    }
                }
                if (YATOON_Business_Hours::has_external_busy_conflict($booking->staff_id, $recur_start_dt, $recur_end_dt)) { $conflict = true;
                }
            }

            if (!$conflict && class_exists('YATOON_Resources') && !YATOON_Resources::is_available($booking->service_id, $recur_start_dt, $recur_end_dt)) { $conflict = true;
                }

            if ($conflict) {
                $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
++$failed;
continue;
            }
            // Create the booking
            $manage_token_new = wp_generate_password(32, false);
            yatoon_db_insert($bookings_table, array(
                'service_id'    => $booking->service_id,
                'staff_id'      => $booking->staff_id,
                'customer_name' => $booking->customer_name,
                'customer_email'=> $booking->customer_email,
                'customer_phone'=> $booking->customer_phone,
                'start_time'    => $new_start,
                'end_time'      => $new_end,
                'total_price'   => $booking->total_price,
                'status'        => get_option('yatoon_auto_confirm_bookings', '1') === '1' ? 'confirmed' : 'pending',
                'payment_status'=> 'unpaid',
                'notes'         => 'Recurring series - every ' . $frequency_weeks . ' week(s).',
                'manage_token'  => $manage_token_new,
            ));

            $new_booking_id = $wpdb->insert_id;
if (!$new_booking_id) {
                $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
++$failed;
continue;
}

            $option_ids = $wpdb->get_col($wpdb->prepare("SELECT option_id FROM {$wpdb->prefix}yatoon_booking_options WHERE booking_id = %d", $booking_id));
foreach ($option_ids as $option_id) {
                $wpdb->insert($wpdb->prefix . 'yatoon_booking_options', array('booking_id' => $new_booking_id, 'option_id' => absint($option_id)));
            }
            // Record in recurring table
            $wpdb->insert($recurring_table, array(
                'series_id'       => $series_id,
                'booking_id'      => $new_booking_id,
                'customer_name'   => $booking->customer_name,
                'customer_email'  => $booking->customer_email,
                'customer_phone'  => $booking->customer_phone,
                'service_id'      => $booking->service_id,
                'staff_id'        => $booking->staff_id,
                'frequency_weeks' => $frequency_weeks,
                'total_count'     => $total_count,
                'occurrence_num'  => $i,
                'status'          => 'active',
            ));

            // Fire booking created hook for notifications
            do_action('yatoon_booking_created', array(
                'booking_id'    => $new_booking_id,
                'customer_name' => $booking->customer_name,
                'customer_email'=> $booking->customer_email,
                'customer_phone'=> $booking->customer_phone,
                'start_time'    => $new_start,
                'end_time'      => $new_end,
                'service_name'  => '',
                'staff_name'    => '',
                'total_price'   => $booking->total_price,
                'manage_token'  => $manage_token_new,
                'notes'         => '',
            ));

            ++$created;
$wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock_name));
        }
        wp_send_json_success(array(
            'series_id' => $series_id,
            'created'   => $created,
            'failed'    => $failed,
            'message'   => $created . ' future appointment' . ( $created !== 1 ? 's' : '' ) . ' scheduled!'
                         . ( $failed ? ' ' . $failed . ' slot(s) were unavailable and skipped.' : '' ),
        ));
    }

    // -------------------------------------------------------------------------
    // AJAX: Cancel entire series
    // -------------------------------------------------------------------------

    public function ajax_cancel_series() {
        check_ajax_referer('yatoon_frontend_nonce', 'nonce');
        if (function_exists('yatoon_public_rate_limit')) {
            yatoon_public_rate_limit('recurring_cancel', 5, 10);
        }

        $series_id    = sanitize_text_field(wp_unslash($_POST['series_id'] ?? ''));
        $manage_token = sanitize_text_field(wp_unslash($_POST['manage_token'] ?? ''));

        if (!$series_id || !$manage_token) {
            wp_send_json_error(array('message' => __( 'Missing parameters.', 'yatoon-booking-system' )));
        }

        global $wpdb;
        $bookings_table  = $wpdb->prefix . 'yatoon_bookings';
        $recurring_table = $wpdb->prefix . 'yatoon_recurring';

        // Verify the token belongs to a booking in this series
        $valid = $wpdb->get_var($wpdb->prepare(
            "SELECT r.id FROM $recurring_table r
             JOIN $bookings_table b ON b.id = r.booking_id
             WHERE r.series_id = %s
               AND b.manage_token = %s
               AND (b.manage_token_expires IS NULL OR b.manage_token_expires >= %s)
             LIMIT 1",
            $series_id, $manage_token, gmdate( 'Y-m-d H:i:s' )
        ));

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

        // Get all future booking IDs in series
        $now    = current_time('mysql');
        $future = $wpdb->get_col($wpdb->prepare(
            "SELECT r.booking_id FROM $recurring_table r
             JOIN $bookings_table b ON b.id = r.booking_id
             WHERE r.series_id = %s AND r.status = 'active' AND b.start_time > %s",
            $series_id, $now
        ));

        foreach ($future as $bid) {
            $wpdb->update($bookings_table,
                array('status' => 'CANCELLED_BY_CUSTOMER'),
                array('id' => absint($bid))
            );
            $wpdb->update($recurring_table,
                array('status' => 'cancelled', 'cancelled_at' => $now),
                array('booking_id' => absint($bid))
            );
        }

        wp_send_json_success(array(
            'cancelled' => count($future),
            'message'   => count($future) . ' future appointment' . ( count($future) !== 1 ? 's' : '' ) . ' cancelled.',
        ));
    }

    // -------------------------------------------------------------------------
    // Hook: booking cancelled - mark recurring entry
    // -------------------------------------------------------------------------

    public function handle_booking_cancelled( $booking_id ) {
        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'yatoon_recurring',
            array('status' => 'cancelled', 'cancelled_at' => current_time('mysql')),
            array('booking_id' => absint($booking_id))
        );
    }
}
