-- =====================================================
-- Atomic Booking Creation with Overlap Prevention
--
-- Problem: Creating a booking requires checking room
-- availability, validating capacity, and inserting — all
-- as separate queries. Race conditions can double-book
-- a room for overlapping dates.
--
-- Fix:
--  A) Enable btree_gist extension for exclusion constraints.
--  B) Add GiST exclusion constraint to prevent overlapping
--     bookings for the same room (non-cancelled/no_show).
--  C) Single transactional PG function that locks the room,
--     validates inputs, calculates price, and inserts. The
--     exclusion constraint provides a final safety net.
-- =====================================================

-- A) Enable btree_gist extension (required for exclusion constraints on non-GiST types)
CREATE EXTENSION IF NOT EXISTS btree_gist;

-- B) Add exclusion constraint to prevent overlapping bookings (idempotent)
DO $$
BEGIN
  IF NOT EXISTS (
    SELECT 1 FROM pg_constraint WHERE conname = 'excl_booking_room_dates'
  ) THEN
    ALTER TABLE public.bookings
      ADD CONSTRAINT excl_booking_room_dates
      EXCLUDE USING gist (
        room_id WITH =,
        daterange(check_in, check_out) WITH &&
      ) WHERE (status NOT IN ('cancelled', 'no_show'));
  END IF;
END;
$$;

-- C) Atomic booking creation function
CREATE OR REPLACE FUNCTION public.create_booking_atomic(
  p_account_id uuid,
  p_room_id uuid,
  p_guest_id uuid DEFAULT NULL,
  p_check_in date DEFAULT NULL,
  p_check_out date DEFAULT NULL,
  p_adults integer DEFAULT 1,
  p_children integer DEFAULT 0,
  p_status text DEFAULT 'confirmed',
  p_total_price numeric DEFAULT NULL,
  p_notes text DEFAULT NULL
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
  v_room record;
  v_computed_price numeric(10,2);
  v_booking_id uuid;
BEGIN
  -- 1. Lock the room row to serialize booking attempts
  SELECT * INTO v_room
  FROM public.rooms
  WHERE id = p_room_id
  FOR UPDATE;

  -- 2. Validate room exists
  IF v_room IS NULL THEN
    RAISE EXCEPTION 'Room % not found', p_room_id
      USING ERRCODE = 'P0002';
  END IF;

  -- 3. Validate check_out > check_in
  IF p_check_in IS NULL OR p_check_out IS NULL THEN
    RAISE EXCEPTION 'check_in and check_out dates are required'
      USING ERRCODE = 'P0001';
  END IF;

  IF p_check_out <= p_check_in THEN
    RAISE EXCEPTION 'check_out (%) must be after check_in (%)', p_check_out, p_check_in
      USING ERRCODE = 'P0001';
  END IF;

  -- 4. Validate total guests do not exceed room capacity
  IF (p_adults + p_children) > v_room.capacity THEN
    RAISE EXCEPTION 'Total guests (%) exceed room capacity (%)', (p_adults + p_children), v_room.capacity
      USING ERRCODE = 'P0001';
  END IF;

  -- 5. Calculate price if not provided
  IF p_total_price IS NOT NULL THEN
    v_computed_price := p_total_price;
  ELSE
    v_computed_price := v_room.price_per_night * (p_check_out - p_check_in);
  END IF;

  -- 6. Insert the booking (exclusion constraint prevents double-booking)
  INSERT INTO public.bookings (
    account_id,
    room_id,
    guest_id,
    check_in,
    check_out,
    adults,
    children,
    status,
    total_price,
    notes
  ) VALUES (
    p_account_id,
    p_room_id,
    p_guest_id,
    p_check_in,
    p_check_out,
    p_adults,
    p_children,
    p_status,
    v_computed_price,
    p_notes
  )
  RETURNING id INTO v_booking_id;

  -- 7. Return the new booking id
  RETURN v_booking_id;
END;
$$;

GRANT EXECUTE ON FUNCTION public.create_booking_atomic(uuid, uuid, uuid, date, date, integer, integer, text, numeric, text) TO authenticated;
GRANT EXECUTE ON FUNCTION public.create_booking_atomic(uuid, uuid, uuid, date, date, integer, integer, text, numeric, text) TO service_role;
