<?php
/**
 * Daily Availability Index Service
 *
 * Handles synchronization and optimization of daily availability data
 * for fast calendar lookups and gap detection.
 *
 * @package WahraRentalsBookings
 */

defined('ABSPATH') || exit;

/**
 * Daily Availability class
 */
class WAHRB_Daily_Availability {

    /**
     * Single instance
     *
     * @var WAHRB_Daily_Availability|null
     */
    private static ?WAHRB_Daily_Availability $instance = null;

    /**
     * Get single instance
     *
     * @return WAHRB_Daily_Availability
     */
    public static function instance(): WAHRB_Daily_Availability {
        if (is_null(self::$instance)) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     */
    private function __construct() {
        // Hook into booking events to trigger sync
        add_action('wahrb_booking_created', [$this, 'sync_for_booking'], 10, 1);
        add_action('wahrb_booking_updated', [$this, 'sync_for_booking'], 10, 1);
        add_action('wahrb_booking_cancelled', [$this, 'sync_for_booking'], 10, 1);
        add_action('wahrb_hold_created', [$this, 'sync_for_hold'], 10, 1);
        add_action('wahrb_hold_expired', [$this, 'sync_for_hold'], 10, 1);
    }

    /**
     * Sync daily availability for a date range
     *
     * @param int $product_id Product ID
     * @param string $start_date Start date (Y-m-d)
     * @param string $end_date End date (Y-m-d)
     * @param int|null $variation_id Variation ID
     */
    public function sync_range(int $product_id, string $start_date, string $end_date, ?int $variation_id = null): void {
        global $wpdb;
        $db = WAHRB_Database::instance();

        $total_units = (int) get_post_meta($product_id, '_wahrb_total_units', true) ?: 1;
        $base_price = (float) get_post_meta($product_id, '_wahrb_base_price', true) ?: 0;

        $current = new DateTime($start_date);
        $end = new DateTime($end_date);

        while ($current <= $end) {
            $date_str = $current->format('Y-m-d');

            // Count booked units for this day
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $booked = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(qty), 0) FROM {$db->table('bookings')}
                 WHERE product_id = %d
                 AND (variation_id IS NULL OR variation_id = %d)
                 AND status IN ('confirmed', 'checked_in', 'checked_out', 'completed')
                 AND deleted_at IS NULL
                 AND DATE(start_dt) <= %s AND DATE(end_dt) > %s",
                $product_id,
                $variation_id ?: 0,
                $date_str,
                $date_str
            ));

            // Count held units for this day
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $held = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COALESCE(SUM(qty), 0) FROM {$db->table('holds')}
                 WHERE product_id = %d
                 AND (variation_id IS NULL OR variation_id = %d)
                 AND is_active = 1
                 AND converted_to_booking = 0
                 AND expires_at > NOW()
                 AND DATE(start_dt) <= %s AND DATE(end_dt) > %s",
                $product_id,
                $variation_id ?: 0,
                $date_str,
                $date_str
            ));

            // Check if blackout (exclude soft-deleted)
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
            $is_blackout = (int) $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$db->table('blackout_dates')} WHERE product_id = %d AND (variation_id IS NULL OR variation_id = %d) AND date_start <= %s AND date_end >= %s AND deleted_at IS NULL",
                $product_id,
                $variation_id ?: 0,
                $date_str,
                $date_str
            )) > 0;

            $available = max(0, $total_units - $booked - $held);
            $is_weekend = in_array($current->format('N'), ['6', '7']);

            // Upsert daily record
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
            $wpdb->query($wpdb->prepare(
                "INSERT INTO {$db->table('daily_availability')}
                 (product_id, variation_id, availability_date, total_units, booked_units, held_units, available_units, is_blackout, is_weekend, base_price, last_sync_at)
                 VALUES (%d, %d, %s, %d, %d, %d, %d, %d, %d, %f, NOW())
                 ON DUPLICATE KEY UPDATE
                 total_units = %d, booked_units = %d, held_units = %d, available_units = %d, is_blackout = %d, base_price = %f, last_sync_at = NOW()",
                $product_id,
                $variation_id ?: 0,
                $date_str,
                $total_units,
                $booked,
                $held,
                $available,
                $is_blackout ? 1 : 0,
                $is_weekend ? 1 : 0,
                $base_price,
                // UPDATE values
                $total_units,
                $booked,
                $held,
                $available,
                $is_blackout ? 1 : 0,
                $base_price
            ));

            $current->modify('+1 day');
        }

        // After sync, check for gaps and set min_stay_override if needed
        $this->calculate_dynamic_min_stay($product_id, $start_date, $end_date, $variation_id);
    }

    /**
     * Calculate dynamic minimum stay to prevent orphan gaps
     *
     * Strategy: If accepting a booking would create a gap smaller than min_stay,
     * increase min_stay_override for the gap dates.
     *
     * @param int $product_id Product ID
     * @param string $start_date Start date
     * @param string $end_date End date
     * @param int|null $variation_id Variation ID
     */
    public function calculate_dynamic_min_stay(int $product_id, string $start_date, string $end_date, ?int $variation_id = null): void {
        global $wpdb;
        $db = WAHRB_Database::instance();

        $min_stay = (int) get_post_meta($product_id, '_wahrb_min_duration', true) ?: 1;

        // Get all available date ranges (gaps)
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $dates = $wpdb->get_results($wpdb->prepare(
            "SELECT availability_date, available_units, booked_units
             FROM {$db->table('daily_availability')}
             WHERE product_id = %d
             AND (variation_id IS NULL OR variation_id = %d)
             AND availability_date BETWEEN %s AND %s
             AND is_blackout = 0
             ORDER BY availability_date ASC",
            $product_id,
            $variation_id ?: 0,
            $start_date,
            $end_date
        ));

        if (empty($dates)) {
            return;
        }

        // Find gaps (consecutive available days surrounded by bookings)
        $gap_start = null;
        $gap_length = 0;
        $in_gap = false;

        foreach ($dates as $i => $day) {
            $is_available = $day->available_units > 0;
            $prev_booked = $i > 0 && $dates[$i - 1]->available_units == 0;
            $next_booked = isset($dates[$i + 1]) && $dates[$i + 1]->available_units == 0;

            if ($is_available) {
                if (!$in_gap && $prev_booked) {
                    // Starting a gap
                    $gap_start = $day->availability_date;
                    $gap_length = 1;
                    $in_gap = true;
                } elseif ($in_gap) {
                    $gap_length++;
                }

                // Check if gap ends
                if ($in_gap && ($next_booked || !isset($dates[$i + 1]))) {
                    // Gap ended - check if it's an orphan
                    if ($gap_length > 0 && $gap_length < $min_stay) {
                        // This gap is too small to book - set min_stay_override
                        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                        $wpdb->query($wpdb->prepare(
                            "UPDATE {$db->table('daily_availability')}
                             SET min_stay_override = %d
                             WHERE product_id = %d
                             AND (variation_id IS NULL OR variation_id = %d)
                             AND availability_date BETWEEN %s AND DATE_ADD(%s, INTERVAL %d DAY)",
                            $gap_length, // Force min stay to match gap exactly
                            $product_id,
                            $variation_id ?: 0,
                            $gap_start,
                            $gap_start,
                            $gap_length - 1
                        ));
                    }
                    $in_gap = false;
                    $gap_length = 0;
                }
            } else {
                $in_gap = false;
                $gap_length = 0;
            }
        }
    }

    /**
     * Get fast availability for a date range using the index
     *
     * @param int $product_id Product ID
     * @param string $start_date Start date
     * @param string $end_date End date
     * @param int|null $variation_id Variation ID
     * @param int $qty Quantity requested
     * @return array Availability result
     */
    public function check_fast(int $product_id, string $start_date, string $end_date, ?int $variation_id = null, int $qty = 1): array {
        global $wpdb;
        $db = WAHRB_Database::instance();

        // Get minimum available units across the date range
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $min_available = $wpdb->get_var($wpdb->prepare(
            "SELECT MIN(available_units) FROM {$db->table('daily_availability')}
             WHERE product_id = %d
             AND (variation_id IS NULL OR variation_id = %d)
             AND availability_date >= %s AND availability_date < %s
             AND is_blackout = 0",
            $product_id,
            $variation_id ?: 0,
            $start_date,
            $end_date
        ));

        // Check for any blackout dates in range
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $has_blackout = $wpdb->get_var($wpdb->prepare(
            "SELECT COUNT(*) FROM {$db->table('daily_availability')}
             WHERE product_id = %d
             AND (variation_id IS NULL OR variation_id = %d)
             AND availability_date >= %s AND availability_date < %s
             AND is_blackout = 1",
            $product_id,
            $variation_id ?: 0,
            $start_date,
            $end_date
        )) > 0;

        if ($has_blackout) {
            return [
                'available' => false,
                'reason' => 'blackout',
                'message' => __('Some dates are blocked', 'wahra-rentals-bookings'),
            ];
        }

        if ($min_available === null) {
            // No index data - fall back to full check
            return WAHRB_Availability::check($product_id, $start_date, $end_date, $variation_id, $qty);
        }

        $min_available = (int) $min_available;

        if ($min_available >= $qty) {
            return [
                'available' => true,
                'available_units' => $min_available,
                'message' => __('Available', 'wahra-rentals-bookings'),
            ];
        }

        return [
            'available' => false,
            'available_units' => $min_available,
            'requested_units' => $qty,
            'reason' => 'no_availability',
            'message' => __('This slot is not available', 'wahra-rentals-bookings'),
        ];
    }

    /**
     * Sync for a booking event
     *
     * @param int $booking_id Booking ID
     */
    public function sync_for_booking(int $booking_id): void {
        global $wpdb;
        $db = WAHRB_Database::instance();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $booking = $wpdb->get_row($wpdb->prepare(
            "SELECT product_id, variation_id, start_dt, end_dt FROM {$db->table('bookings')} WHERE booking_id = %d",
            $booking_id
        ));

        if ($booking) {
            $this->sync_range(
                (int) $booking->product_id,
                gmdate('Y-m-d', strtotime($booking->start_dt)),
                gmdate('Y-m-d', strtotime($booking->end_dt)),
                $booking->variation_id ? (int) $booking->variation_id : null
            );
        }
    }

    /**
     * Sync for a hold event
     *
     * @param int $hold_id Hold ID
     */
    public function sync_for_hold(int $hold_id): void {
        global $wpdb;
        $db = WAHRB_Database::instance();

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $hold = $wpdb->get_row($wpdb->prepare(
            "SELECT product_id, variation_id, start_dt, end_dt FROM {$db->table('holds')} WHERE hold_id = %d",
            $hold_id
        ));

        if ($hold) {
            $this->sync_range(
                (int) $hold->product_id,
                gmdate('Y-m-d', strtotime($hold->start_dt)),
                gmdate('Y-m-d', strtotime($hold->end_dt)),
                $hold->variation_id ? (int) $hold->variation_id : null
            );
        }
    }

    /**
     * Rebuild entire index for a product (async-safe)
     *
     * @param int $product_id Product ID
     * @param int $months_ahead Number of months to index ahead
     */
    public function rebuild_for_product(int $product_id, int $months_ahead = 12): void {
        $start = gmdate('Y-m-d');
        $end = gmdate('Y-m-d', strtotime("+{$months_ahead} months"));

        $this->sync_range($product_id, $start, $end);
    }
}
