<?php
/**
 * Booking model
 *
 * @package WahraRentalsBookings
 */

defined('ABSPATH') || exit;

/**
 * Booking class
 */
class WAHRB_Booking {

    /**
     * Booking ID
     *
     * @var int
     */
    private int $id = 0;

    /**
     * Booking data
     *
     * @var array
     */
    private array $data = [];

    /**
     * Database instance
     *
     * @var WAHRB_Database
     */
    private WAHRB_Database $db;

    /**
     * Constructor
     *
     * @param int|array $booking Booking ID or data
     */
    public function __construct($booking = 0) {
        $this->db = WAHRB_Database::instance();

        if (is_numeric($booking) && $booking > 0) {
            $this->id = (int) $booking;
            $this->read();
        } elseif (is_array($booking)) {
            $this->set_data($booking);
        }
    }

    /**
     * Read booking from database
     */
    private function read(): void {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom tables
        $row = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->db->table('bookings')} WHERE booking_id = %d AND deleted_at IS NULL",
            $this->id
        ), ARRAY_A);

        if ($row) {
            $this->set_data($row);
        }
    }

    /**
     * Set booking data
     *
     * @param array $data Booking data
     */
    private function set_data(array $data): void {
        $this->data = $data;
        if (isset($data['booking_id'])) {
            $this->id = (int) $data['booking_id'];
        }

        // Parse JSON fields
        if (isset($this->data['pricing_snapshot']) && is_string($this->data['pricing_snapshot'])) {
            $this->data['pricing_snapshot'] = json_decode($this->data['pricing_snapshot'], true);
        }
        
        if (isset($this->data['meta_data']) && is_string($this->data['meta_data'])) {
            $this->data['meta_data'] = json_decode($this->data['meta_data'], true) ?: [];
        } elseif (!isset($this->data['meta_data'])) {
            $this->data['meta_data'] = [];
        }
    }

    /**
     * Check if booking exists
     *
     * @return bool
     */
    public function exists(): bool {
        return $this->id > 0 && !empty($this->data);
    }

    /**
     * Get booking ID
     *
     * @return int
     */
    public function get_id(): int {
        return $this->id;
    }

    /**
     * Get booking number
     *
     * @return string
     */
    public function get_booking_number(): string {
        return $this->data['booking_number'] ?? '';
    }

    /**
     * Get booking status
     *
     * @return string
     */
    public function get_status(): string {
        return $this->data['status'] ?? 'pending';
    }

    /**
     * Get property
     *
     * @param string $key Property key
     * @param mixed $default Default value
     * @return mixed
     */
    public function get(string $key, $default = null) {
        return $this->data[$key] ?? $default;
    }

    /**
     * Get all data
     *
     * @return array
     */
    public function get_data(): array {
        return $this->data;
    }

    /**
     * Get meta data
     *
     * @param string $key Meta key
     * @param mixed $default Default value
     * @return mixed
     */
    public function get_meta(string $key, $default = null) {
        return $this->data['meta_data'][$key] ?? $default;
    }

    /**
     * Update meta data
     *
     * @param string $key Meta key
     * @param mixed $value Meta value
     * @return bool
     */
    public function update_meta(string $key, $value): bool {
        $this->data['meta_data'][$key] = $value;
        return $this->save_meta();
    }

    /**
     * Save meta to DB
     *
     * @return bool
     */
    private function save_meta(): bool {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        return $wpdb->update(
            $this->db->table('bookings'),
            ['meta_data' => wp_json_encode($this->data['meta_data'])],
            ['booking_id' => $this->id]
        ) !== false;
    }

    /**
     * Generate booking number
     *
     * @return string
     */
    private static function generate_booking_number(): string {
        global $wpdb;
        $db = WAHRB_Database::instance();
        $prefix = apply_filters('wahrb_booking_number_prefix', 'RES');
        $max_attempts = 5;

        for ($i = 0; $i < $max_attempts; $i++) {
            $number = wp_rand(100000, 999999);
            $booking_number = $prefix . '-' . gmdate('Ymd') . '-' . $number;

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $exists = $wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$db->table('bookings')} WHERE booking_number = %s", $booking_number));

            if (!$exists) {
                return $booking_number;
            }
        }

        // Fallback with microseconds for uniqueness
        return $prefix . '-' . gmdate('Ymd') . '-' . wp_rand(100000, 999999) . '-' . wp_rand(10, 99);
    }

    /**
     * Create a new booking
     *
     * @param array $data Booking data
     * @return WAHRB_Booking|WP_Error
     */
    public static function create(array $data) {
        global $wpdb;
        $db = WAHRB_Database::instance();

        // Validate required fields
        $required = ['vendor_id', 'product_id', 'start_dt', 'end_dt'];
        foreach ($required as $field) {
            if (empty($data[$field])) {
                /* translators: %s: required field name */
                return new WP_Error('missing_field', sprintf(__('Field %s is required', 'wahra-rentals-bookings'), $field));
            }
        }

        // PESSIMISTIC LOCKING: Start Transaction
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction control
        $wpdb->query('START TRANSACTION');

        try {
            // Lock availability rows for this product/date range to prevent race conditions
            // We lock the availability index rows or product row if index doesn't exist yet
            $start_date = gmdate('Y-m-d', strtotime($data['start_dt']));
            $end_date = gmdate('Y-m-d', strtotime($data['end_dt']));
            
            // This query locks the rows in availability index for update, forcing other transactions to wait
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Row locking for concurrency
            $wpdb->query($wpdb->prepare(
                "SELECT avail_id FROM {$db->table('availability_index')} 
                 WHERE product_id = %d AND date_start <= %s AND date_end >= %s 
                 FOR UPDATE",
                $data['product_id'], $end_date, $start_date
            ));
            
            // Also lock holds to be safe
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Row locking
            $wpdb->query($wpdb->prepare(
                "SELECT hold_id FROM {$db->table('holds')} 
                 WHERE product_id = %d AND start_dt < %s AND end_dt > %s 
                 FOR UPDATE",
                $data['product_id'], $data['end_dt'], $data['start_dt']
            ));

            // Check availability (now safely locked)
            $availability = WAHRB_Availability::check(
                $data['product_id'],
                $data['start_dt'],
                $data['end_dt'],
                $data['variation_id'] ?? null,
                $data['qty'] ?? 1
            );

            if (!$availability['available']) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction rollback
                $wpdb->query('ROLLBACK');
                return new WP_Error('not_available', $availability['message']);
            }

            // Generate booking number
            $booking_number = self::generate_booking_number();

            // Calculate pricing
            $pricing = WAHRB_Pricing_Engine::instance()->calculate(
                $data['product_id'],
                $data['start_dt'],
                $data['end_dt'],
                $data['variation_id'] ?? null,
                $data['qty'] ?? 1
            );

            // Prepare insert data
            $insert_data = [
                'booking_number' => $booking_number,
                'vendor_id' => absint($data['vendor_id']),
                'product_id' => absint($data['product_id']),
                'variation_id' => !empty($data['variation_id']) ? absint($data['variation_id']) : null,
                'customer_id' => !empty($data['customer_id']) ? absint($data['customer_id']) : null,
                'booking_mode' => $data['booking_mode'] ?? 'day',
                'start_dt' => $data['start_dt'],
                'end_dt' => $data['end_dt'],
                'qty' => absint($data['qty'] ?? 1),
                'status' => 'pending',
                'customer_name' => sanitize_text_field($data['customer_name'] ?? ''),
                'customer_email' => sanitize_email($data['customer_email'] ?? ''),
                'customer_phone' => sanitize_text_field($data['customer_phone'] ?? ''),
                'notes_customer' => sanitize_textarea_field($data['notes_customer'] ?? ''),
                'pricing_snapshot' => wp_json_encode($pricing),
                'cancellation_policy' => $data['cancellation_policy'] ?? 'flexible',
                'version_hash' => wp_generate_password(32, false),
            ];

            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table insert
            $result = $wpdb->insert($db->table('bookings'), $insert_data);

            if (!$result) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction rollback
                $wpdb->query('ROLLBACK');
                return new WP_Error('db_error', __('Error creating booking', 'wahra-rentals-bookings'));
            }

            $booking_id = $wpdb->insert_id;

            // Create payment schedule
            self::create_payment_schedule($booking_id, $data, $pricing);

            // Commit Transaction
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction commit
            $wpdb->query('COMMIT');

            // Log creation
            $db->log('booking_created', 'booking', $booking_id, [
                'booking_number' => $booking_number,
                'product_id' => $data['product_id'],
                'dates' => $data['start_dt'] . ' - ' . $data['end_dt'],
            ]);

            // Trigger action
            do_action('wahrb_booking_created', $booking_id, $insert_data);

            return new self($booking_id);

        } catch (Exception $e) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction rollback
            $wpdb->query('ROLLBACK');
            return new WP_Error('system_error', $e->getMessage());
        }
    }

    /**
     * Create payment schedule for booking
     *
     * @param int $booking_id Booking ID
     * @param array $data Booking data
     * @param array $pricing Pricing data
     */
    private static function create_payment_schedule(int $booking_id, array $data, array $pricing): void {
        global $wpdb;
        $db = WAHRB_Database::instance();

        // Get per-category payment settings for this product
        $settings = WAHRB_Platform_Categories::get_product_payment_settings((int) $data['product_id']);
        $vendor_settings = get_user_meta($data['vendor_id'], '_wahrb_vendor_settings', true) ?: [];

        // Check if vendor has permission to override deposit amount for this category
        $cat_slug = WAHRB_Platform_Categories::get_product_category((int) $data['product_id']);
        $cat_perms = WAHRB_Platform_Categories::get_vendor_permissions($cat_slug);
        if (!empty($cat_perms['can_set_deposit_amount']) && isset($vendor_settings["deposit_value_{$cat_slug}"])) {
            $settings['deposit_value'] = floatval($vendor_settings["deposit_value_{$cat_slug}"]);
        }

        $total = $pricing['total'] ?? 0;
        $deposit_amount = 0;
        $security_amount = 0;
        $balance_amount = $total;

        // Calculate deposit
        $deposit_enabled = $settings['deposit_enabled'] ?? false;
        if ($deposit_enabled) {
            $deposit_type = $settings['deposit_type'] ?? 'percent';
            $deposit_value = floatval($settings['deposit_value'] ?? 30);

            if ($deposit_type === 'percent') {
                $deposit_amount = round($total * ($deposit_value / 100), 2);
            } else {
                $deposit_amount = min($deposit_value, $total);
            }
            $balance_amount = $total - $deposit_amount;
        }

        // Calculate security deposit
        $security_enabled = $settings['security_deposit_enabled'] ?? false;
        if ($security_enabled) {
            $security_type = $settings['security_deposit_type'] ?? 'fixed';
            $security_value = floatval($settings['security_deposit_value'] ?? 0);

            // Check product-level override
            $product_security = get_post_meta($data['product_id'], '_wahrb_security_deposit', true);
            if ($product_security) {
                $security_amount = floatval($product_security);
            } elseif ($security_type === 'percent') {
                $security_amount = round($total * ($security_value / 100), 2);
            } else {
                $security_amount = $security_value;
            }
        }

        // Calculate balance due date
        $balance_due_days = intval($settings['balance_due_days_before'] ?? 3);
        $end_date = new DateTime($data['end_dt']);
        $balance_due_at = $end_date->modify("-{$balance_due_days} days")->format('Y-m-d H:i:s');

        $schedule_data = [
            'booking_id' => $booking_id,
            'vendor_id' => $data['vendor_id'],
            'product_id' => $data['product_id'],
            'deposit_enabled' => $deposit_enabled ? 1 : 0,
            'deposit_type' => $settings['deposit_type'] ?? 'percent',
            'deposit_value' => $settings['deposit_value'] ?? 30,
            'deposit_amount_final' => $deposit_amount,
            'deposit_status' => $deposit_enabled ? 'pending' : 'not_due',
            'deposit_refundable' => ($settings['deposit_refundable'] ?? true) ? 1 : 0,
            'security_deposit_enabled' => $security_enabled ? 1 : 0,
            'security_deposit_type' => $settings['security_deposit_type'] ?? 'fixed',
            'security_deposit_value' => $security_amount,
            'security_deposit_amount_final' => $security_amount,
            'security_mode' => $settings['security_mode'] ?? 'capture',
            'security_status' => $security_enabled ? 'pending' : 'not_due',
            'balance_enabled' => $balance_amount > 0 ? 1 : 0,
            'balance_amount_final' => $balance_amount,
            'balance_due_at' => $balance_due_at,
            'balance_due_days_before' => $balance_due_days,
            'balance_status' => $balance_amount > 0 ? 'not_due' : 'paid',
            'balance_allow_cod' => ($settings['balance_allow_cod'] ?? true) ? 1 : 0,
            'subtotal_amount' => $pricing['subtotal'] ?? $total,
            'total_amount' => $total,
            'currency_code' => get_woocommerce_currency(),
            'tax_amount' => $pricing['tax_amount'] ?? 0,
            'tax_rate' => $pricing['tax_rate'] ?? 0,
        ];

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table insert
        $wpdb->insert($db->table('payment_schedule'), $schedule_data);
    }

    /**
     * Update booking status
     *
     * @param string $status New status
     * @param string $reason Optional reason for the status change (logged in audit trail)
     * @return bool
     */
    public function update_status(string $status, string $reason = ''): bool {
        $valid_statuses = ['hold', 'pending', 'confirmed', 'checked_in', 'checked_out', 'completed', 'cancelled', 'failed'];

        if (!in_array($status, $valid_statuses)) {
            return false;
        }

        $old_status = $this->get_status();

        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('bookings'),
            [
                'status' => $status,
                'version_hash' => wp_generate_password(32, false),
            ],
            ['booking_id' => $this->id],
            ['%s', '%s'],
            ['%d']
        );

        if ($result !== false) {
            $this->data['status'] = $status;

            // Log status change
            $log_data = [
                'old_status' => $old_status,
                'new_status' => $status,
            ];
            if ($reason !== '') {
                $log_data['reason'] = $reason;
            }
            $this->db->log('booking_status_changed', 'booking', $this->id, $log_data);

            // Trigger action
            do_action('wahrb_booking_status_changed', $this->id, $old_status, $status);

            return true;
        }

        return false;
    }

    /**
     * Confirm booking
     * Also auto-rejects any conflicting pending bookings for same product/dates
     *
     * @return bool
     */
    public function confirm(): bool {
        global $wpdb;

        // Start transaction for atomic conflict resolution
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction control
        $wpdb->query('START TRANSACTION');

        try {
            // Update this booking to confirmed
            if (!$this->update_status('confirmed')) {
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction rollback
                $wpdb->query('ROLLBACK');
                return false;
            }

            // Auto-reject conflicting pending bookings
            $this->reject_conflicting_bookings();

            // Commit transaction
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction commit
            $wpdb->query('COMMIT');

            // Send confirmation email
            do_action('wahrb_send_booking_confirmed_email', $this);

            return true;

        } catch (Exception $e) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Transaction rollback
            $wpdb->query('ROLLBACK');
            return false;
        }
    }

    /**
     * Auto-reject all pending bookings that conflict with this one
     * (same product, overlapping dates, when units are exhausted)
     */
    private function reject_conflicting_bookings(): void {
        global $wpdb;

        $product_id = $this->get('product_id');
        $start_dt = $this->get('start_dt');
        $end_dt = $this->get('end_dt');
        $booking_id = $this->id;

        // Get total units for this product
        $total_units = (int) get_post_meta($product_id, '_wahrb_total_units', true) ?: 1;

        // Count confirmed bookings (including this one) for this date range
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom tables
        $confirmed_qty = (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COALESCE(SUM(qty), 0) FROM {$this->db->table('bookings')}
             WHERE product_id = %d
               AND status = 'confirmed'
               AND start_dt < %s
               AND end_dt > %s
               AND deleted_at IS NULL",
            $product_id, $end_dt, $start_dt
        ));

        $available_units = max(0, $total_units - $confirmed_qty);

        // Get all pending bookings that conflict with this date range
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom tables
        $conflicting = $wpdb->get_results($wpdb->prepare(
            "SELECT booking_id, qty, customer_name, customer_email FROM {$this->db->table('bookings')}
             WHERE product_id = %d
               AND booking_id != %d
               AND status = 'pending'
               AND start_dt < %s
               AND end_dt > %s
               AND deleted_at IS NULL
             ORDER BY created_at ASC",
            $product_id, $booking_id, $end_dt, $start_dt
        ), ARRAY_A);

        $remaining_units = $available_units;

        foreach ($conflicting as $conflict) {
            $conflict_qty = (int) $conflict['qty'];

            if ($conflict_qty > $remaining_units) {
                // This conflicting booking cannot be fulfilled - auto-reject it
                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
                $wpdb->update(
                    $this->db->table('bookings'),
                    [
                        'status' => 'cancelled',
                        'cancellation_reason' => __('Automatically cancelled: another booking was confirmed for the same dates', 'wahra-rentals-bookings'),
                        'version_hash' => wp_generate_password(32, false),
                    ],
                    ['booking_id' => $conflict['booking_id']]
                );

                // Log the auto-rejection
                $this->db->log('booking_auto_rejected', 'booking', $conflict['booking_id'], [
                    'reason' => 'conflict_with_confirmed_booking',
                    'confirmed_booking_id' => $booking_id,
                ]);

                // Send notification email to the rejected customer
                do_action('wahrb_send_booking_rejected_email', $conflict['booking_id'], $conflict['customer_email'], $conflict['customer_name']);
            } else {
                // This booking can still fit - reduce remaining units
                $remaining_units -= $conflict_qty;
            }
        }
    }

    /**
     * Cancel booking
     *
     * @param string $reason Cancellation reason
     * @return bool
     */
    public function cancel(string $reason = ''): bool {
        if (!$this->update_status('cancelled')) {
            return false;
        }

        global $wpdb;

        // Persist cancellation reason
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $wpdb->update(
            $this->db->table('bookings'),
            [
                'cancellation_reason' => $reason,
            ],
            ['booking_id' => $this->id]
        );

        $this->data['status'] = 'cancelled';
        $this->data['cancellation_reason'] = $reason;

        // Calculate refund based on policy
        $refund = $this->calculate_refund();
        if ($refund > 0) {
            $this->process_refund($refund);
        }

        // Release hold if exists
        WAHRB_Hold::release_for_booking($this->id);

        // Log
        $this->db->log('booking_cancelled', 'booking', $this->id, [
            'reason' => $reason,
            'refund_amount' => $refund,
        ]);

        do_action('wahrb_booking_cancelled', $this->id, $reason, $refund);

        return true;
    }

    /**
     * Check in
     *
     * Cannot start before the scheduled start date. Allowed on or after start_dt.
     *
     * @param string $notes Check-in notes
     * @param bool   $force_skip_date_check True only for cron auto-start (date already validated)
     * @return bool|WP_Error
     */
    public function check_in(string $notes = '', bool $force_skip_date_check = false) {
        global $wpdb;

        // Block early check-in (before scheduled start date)
        if (!$force_skip_date_check) {
            $start_dt = $this->get('start_dt');
            if ($start_dt) {
                $start_date = gmdate('Y-m-d', strtotime($start_dt));
                $today      = current_time('Y-m-d');
                if ($start_date > $today) {
                    return new WP_Error(
                        'checkin_too_early',
                        sprintf(
                            /* translators: %s: scheduled start date */
                            __('Cannot start this booking before the scheduled date (%s). You can start it on or after that date.', 'wahra-rentals-bookings'),
                            wp_date(get_option('date_format'), strtotime($start_dt))
                        ),
                        ['status' => 400]
                    );
                }
            }
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('bookings'),
            [
                'status' => 'checked_in',
                'checkin_dt' => current_time('mysql'),
                'checkin_notes' => $notes,
            ],
            ['booking_id' => $this->id]
        );

        if ($result !== false) {
            $this->data['status'] = 'checked_in';
            $this->data['checkin_dt'] = current_time('mysql');
            $this->data['checkin_notes'] = $notes;

            $this->db->log('booking_checked_in', 'booking', $this->id);
            do_action('wahrb_booking_checked_in', $this);

            return true;
        }

        return false;
    }

    /**
     * Check out
     *
     * @param string $notes Check-out notes
     * @param float|null $damage_amount Damage amount to deduct from security deposit
     * @return bool
     */
    public function check_out(string $notes = '', ?float $damage_amount = null): bool {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('bookings'),
            [
                'status' => 'checked_out',
                'checkout_dt' => current_time('mysql'),
                'checkout_notes' => $notes,
            ],
            ['booking_id' => $this->id]
        );

        if ($result !== false) {
            $this->data['status'] = 'checked_out';
            $this->data['checkout_dt'] = current_time('mysql');
            $this->data['checkout_notes'] = $notes;

            // Handle security deposit
            $this->process_security_deposit_release($damage_amount);

            $this->db->log('booking_checked_out', 'booking', $this->id, [
                'damage_amount' => $damage_amount,
            ]);

            do_action('wahrb_booking_checked_out', $this, $damage_amount);

            return true;
        }

        return false;
    }

    /**
     * Mark balance as paid
     *
     * @return bool
     */
    public function mark_balance_paid(): bool {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('payment_schedule'),
            [
                'balance_status' => 'paid',
                'balance_paid_at' => current_time('mysql'),
            ],
            ['booking_id' => $this->id]
        );

        if ($result !== false) {
            $this->db->log('balance_paid', 'booking', $this->id);
            do_action('wahrb_balance_paid', $this);
            return true;
        }

        return false;
    }

    /**
     * Complete booking (checked_out → completed)
     *
     * The vendor may optionally adjust the final price at restitution.
     * For example, if the booking was from day 1-5 but the vendor only handed
     * the product on day 3, the vendor can reduce the total to cover days 3-5.
     * If the client came late, the vendor can keep the original full-period price.
     *
     * @param string     $notes          Completion / restitution notes
     * @param float|null $adjusted_total  New final total decided by vendor (null = keep original)
     * @return bool
     */
    public function complete(string $notes = '', ?float $adjusted_total = null): bool {
        if ($this->get('status') !== 'checked_out') {
            return false;
        }

        global $wpdb;

        $update_data = [
            'status' => 'completed',
        ];

        $existing_meta = $this->get('meta_data', []);
        if (is_string($existing_meta)) {
            $existing_meta = json_decode($existing_meta, true) ?: [];
        }

        if ($notes) {
            $existing_meta['completion_notes'] = $notes;
        }

        // Handle price adjustment at restitution
        if ($adjusted_total !== null && $adjusted_total >= 0) {
            $schedule = $this->get_payment_schedule();
            $original_total = $schedule ? (float) $schedule['total_amount'] : 0;

            // Store adjustment metadata
            $existing_meta['price_adjusted']   = true;
            $existing_meta['original_total']   = $original_total;
            $existing_meta['adjusted_total']   = $adjusted_total;
            $existing_meta['adjustment_diff']  = round($original_total - $adjusted_total, 2);

            // Update payment_schedule total & balance
            if ($schedule) {
                $deposit_paid = (float) ($schedule['deposit_amount_final'] ?? 0);
                $new_balance   = max(0, $adjusted_total - $deposit_paid);

                // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
                $wpdb->update(
                    $this->db->table('payment_schedule'),
                    [
                        'total_amount'        => $adjusted_total,
                        'subtotal_amount'     => $adjusted_total,
                        'balance_amount_final' => $new_balance,
                    ],
                    ['booking_id' => $this->id]
                );
            }

            $this->db->log('price_adjusted', 'booking', $this->id, [
                'original_total' => $original_total,
                'adjusted_total' => $adjusted_total,
            ]);
        }

        $update_data['meta_data'] = wp_json_encode($existing_meta);

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('bookings'),
            $update_data,
            ['booking_id' => $this->id]
        );

        if ($result !== false) {
            $this->data['status'] = 'completed';

            $this->db->log('booking_completed', 'booking', $this->id);
            do_action('wahrb_booking_completed', $this);

            return true;
        }

        return false;
    }

    /**
     * Update a specific payment schedule field
     *
     * Allows vendors to update individual payment fields (deposit status, security status, etc.)
     *
     * @param string $field  The payment field to update (e.g., 'deposit_status', 'security_status', 'balance_status')
     * @param string $value  The new value
     * @return bool|WP_Error
     */
    public function update_payment_field(string $field, string $value) {
        // Allowed fields with their valid values
        $allowed_fields = [
            'deposit_status' => ['not_due', 'pending', 'paid', 'failed', 'refunded'],
            'security_status' => ['not_due', 'pending', 'authorized', 'captured', 'released', 'forfeited', 'partial_forfeited'],
            'balance_status' => ['not_due', 'pending', 'paid', 'failed', 'partial'],
            'balance_payment_method' => ['online', 'cod', 'bank_transfer', 'cash', 'other'],
            'notes' => null, // Free text
        ];

        if (!array_key_exists($field, $allowed_fields)) {
            return new WP_Error('invalid_field', __('Invalid payment field.', 'wahra-rentals-bookings'));
        }

        // Validate value against allowed values (if applicable)
        if ($allowed_fields[$field] !== null && !in_array($value, $allowed_fields[$field], true)) {
            return new WP_Error('invalid_value', __('Invalid value for this field.', 'wahra-rentals-bookings'));
        }

        global $wpdb;

        $update_data = [$field => sanitize_text_field($value)];

        // Auto-set timestamps when marking as paid/captured
        if ($field === 'deposit_status' && $value === 'paid') {
            $update_data['deposit_paid_at'] = current_time('mysql');
        } elseif ($field === 'balance_status' && $value === 'paid') {
            $update_data['balance_paid_at'] = current_time('mysql');
        } elseif ($field === 'security_status' && $value === 'captured') {
            $update_data['security_captured_at'] = current_time('mysql');
        } elseif ($field === 'security_status' && $value === 'released') {
            $update_data['security_released_at'] = current_time('mysql');
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $result = $wpdb->update(
            $this->db->table('payment_schedule'),
            $update_data,
            ['booking_id' => $this->id]
        );

        if ($result !== false) {
            $this->db->log('payment_field_updated', 'booking', $this->id, [
                'field' => $field,
                'value' => $value,
            ]);
            do_action('wahrb_payment_field_updated', $this, $field, $value);
            return true;
        }

        return false;
    }

    /**
     * Calculate refund based on cancellation policy
     *
     * @return float Refund amount
     */
    private function calculate_refund(): float {
        $policy = $this->get('cancellation_policy', 'flexible');
        $pricing = $this->get('pricing_snapshot', []);
        $total = $pricing['total'] ?? 0;

        $policies = get_option('wahrb_settings_cancellation', []);
        $policy_config = $policies[$policy] ?? ['refund_percent' => 0, 'deadline_hours' => 0];

        $start = new DateTime($this->get('start_dt'));
        $now = new DateTime();
        $hours_until_start = ($start->getTimestamp() - $now->getTimestamp()) / 3600;

        if ($hours_until_start >= $policy_config['deadline_hours']) {
            return round($total * ($policy_config['refund_percent'] / 100), 2);
        }

        return 0;
    }

    /**
     * Process refund
     *
     * @param float $amount Refund amount
     */
    private function process_refund(float $amount): void {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $wpdb->update(
            $this->db->table('bookings'),
            ['refund_amount' => $amount],
            ['booking_id' => $this->id]
        );

        // Refund via WooCommerce if order exists
        $order_id = $this->get('customer_order_id');
        if ($order_id) {
            $order = wc_get_order($order_id);
            if ($order) {
                wc_create_refund([
                    'amount' => $amount,
                    'reason' => __('Booking cancellation', 'wahra-rentals-bookings'),
                    'order_id' => $order_id,
                    'refund_payment' => true,
                ]);
            }
        }
    }

    /**
     * Process security deposit release
     *
     * @param float|null $damage_amount Damage amount
     */
    private function process_security_deposit_release(?float $damage_amount): void {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom tables
        $schedule = $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->db->table('payment_schedule')} WHERE booking_id = %d",
            $this->id
        ), ARRAY_A);

        if (!$schedule || !$schedule['security_deposit_enabled']) {
            return;
        }

        $security_amount = floatval($schedule['security_deposit_amount_final']);
        $status = 'released';
        $damage_notes = null;

        if ($damage_amount !== null && $damage_amount > 0) {
            if ($damage_amount >= $security_amount) {
                $status = 'forfeited';
            } else {
                $status = 'partial_forfeited';
            }
            /* translators: %s: damage amount with currency symbol */
            $damage_notes = sprintf(__('Damages noted: %s', 'wahra-rentals-bookings'), wp_kses_post(wc_price($damage_amount)));
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table update
        $wpdb->update(
            $this->db->table('payment_schedule'),
            [
                'security_status' => $status,
                'security_released_at' => current_time('mysql'),
                'security_damage_claim' => $damage_amount,
                'security_damage_notes' => $damage_notes,
            ],
            ['booking_id' => $this->id]
        );

        // Process refund if releasing
        if ($status === 'released' || $status === 'partial_forfeited') {
            $refund_amount = $status === 'released' ? $security_amount : ($security_amount - $damage_amount);
            WAHRB_Security_Deposit::refund($this->id, $refund_amount);
        }
    }

    /**
     * Get payment schedule
     *
     * @return array|null
     */
    public function get_payment_schedule(): ?array {
        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
        return $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$this->db->table('payment_schedule')} WHERE booking_id = %d",
            $this->id
        ), ARRAY_A);
    }

    /**
     * Get bookings for vendor
     *
     * @param int $vendor_id Vendor ID
     * @param array $args Query arguments
     * @return array
     */
    public static function get_for_vendor(int $vendor_id, array $args = []): array {
        global $wpdb;
        $db = WAHRB_Database::instance();

        $defaults = [
            'status' => null,
            'product_id' => null,
            'date_from' => null,
            'date_to' => null,
            'limit' => 20,
            'offset' => 0,
            'orderby' => 'created_at',
            'order' => 'DESC',
        ];

        $args = wp_parse_args($args, $defaults);

        $where = ['vendor_id = %d', 'deleted_at IS NULL'];
        $params = [$vendor_id];

        if ($args['status']) {
            $where[] = 'status = %s';
            $params[] = $args['status'];
        }

        if ($args['product_id']) {
            $where[] = 'product_id = %d';
            $params[] = $args['product_id'];
        }

        if ($args['date_from']) {
            $where[] = 'start_dt >= %s';
            $params[] = $args['date_from'];
        }

        if ($args['date_to']) {
            $where[] = 'end_dt <= %s';
            $params[] = $args['date_to'];
        }

        $where_sql = implode(' AND ', $where);
        $orderby = sanitize_sql_orderby($args['orderby'] . ' ' . $args['order']) ?: 'created_at DESC';

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- Custom table with dynamic where clause
        $sql = $wpdb->prepare(
            "SELECT * FROM {$db->table('bookings')}
             WHERE {$where_sql}
             ORDER BY {$orderby}
             LIMIT %d OFFSET %d",
            array_merge($params, [$args['limit'], $args['offset']])
        );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Pre-prepared query with custom table
        $results = $wpdb->get_results($sql, ARRAY_A);

        return array_map(function($row) {
            return new self($row);
        }, $results ?: []);
    }

    /**
     * Count bookings for vendor
     *
     * @param int $vendor_id Vendor ID
     * @param string|null $status Filter by status
     * @return int
     */
    public static function count_for_vendor(int $vendor_id, ?string $status = null): int {
        global $wpdb;
        $db = WAHRB_Database::instance();

        $where = 'vendor_id = %d AND deleted_at IS NULL';
        $params = [$vendor_id];

        if ($status) {
            $where .= ' AND status = %s';
            $params[] = $status;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        return (int) $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$db->table('bookings')} WHERE {$where}",
            $params
        ));
    }
}
