package bookings

import (
	"context"
	"crussell/db"
	"crussell/handlers/notifications"
	"crussell/internal/validators"
	"crussell/handlers/scheduling"
	"crussell/mw"
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/lib/pq"
)

// UserCancelBookingHandler allows an authenticated user to cancel a booking they own.
// The update is performed in a transaction with notification handling.
func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
	bookingID := chi.URLParam(r, "id")
	if bookingID == "" || !validators.IsValidID(bookingID) {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}

	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Get current status before updating
	var originalStatus string
	err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Booking not cancellable", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get booking status %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	res, err := tx.Exec(r.Context(), `
		UPDATE bookings
		SET status = 'client_cancelled', updated_at = $1
		WHERE id = $2 AND user_id = $3 AND status IN ('pending', 'confirmed', 'in_progress')
	`, time.Now(), bookingID, userID)
	if err != nil {
		log.Printf("Failed to cancel booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	rowsAffected := res.RowsAffected()
	if rowsAffected == 0 {
		http.Error(w, "Booking not cancellable", http.StatusNotFound)
		return
	}

	// Acknowledge pending notification if exists
	if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress)
	if originalStatus != "pending" {
		notificationQuery := `
			INSERT INTO admin_notifications (reason, booking_id, user_id)
			VALUES ($1, $2, $3)
		`
		_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
		if err != nil {
			log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit user cancel: %v, %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// AdminCancelBookingHandler allows an admin to cancel any booking.
// The update uses a status filter and checks RowsAffected for existence.
func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
	bookingID := chi.URLParam(r, "id")
	if bookingID == "" || !validators.IsValidID(bookingID) {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}

	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Get current status before updating
	var originalStatus string
	err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&originalStatus)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Booking not cancellable", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get booking status %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	res, err := tx.Exec(r.Context(), `
		UPDATE bookings
		SET status = 'we_cancelled', updated_at = $1
		WHERE id = $2 AND status IN ('pending', 'confirmed', 'in_progress')
	`, time.Now(), bookingID)
	if err != nil {
		log.Printf("Failed to admin cancel booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	rowsAffected := res.RowsAffected()
	if rowsAffected == 0 {
		http.Error(w, "Booking not cancellable", http.StatusNotFound)
		return
	}

	// Acknowledge pending notification if exists
	if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress)
	if originalStatus != "pending" {
		notificationQuery := `
			INSERT INTO admin_notifications (reason, booking_id, user_id)
			SELECT 'cancelled_booking', $1, user_id FROM bookings WHERE id = $1
		`
		_, err = tx.Exec(r.Context(), notificationQuery, bookingID)
		if err != nil {
			log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit admin cancel: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// AdminListPendingBookingsHandler returns all bookings with status `pending` by delegating to the existing admin list handler.
func AdminListPendingBookingsHandler(w http.ResponseWriter, r *http.Request) {
	r = r.Clone(r.Context())
	q := r.URL.Query()
	q.Set("status", "pending")
	r.URL.RawQuery = q.Encode()

	GetAllAdminBookingsHandler(w, r)
}

// AdminGetInProgressBookingHandler returns the booking that is currently in progress.
// It joins the bookings table with users to populate the UserSummary in the returned Booking.
func AdminGetInProgressBookingHandler(w http.ResponseWriter, r *http.Request) {
	var b Booking
	var userID, fullName string

	err := db.DB.QueryRow(r.Context(), `
		SELECT
			b.id,
			b.start_time,
			b.status,
			b.notes,
			b.created_at,
			b.updated_at,
			b.created_by,
			u.id,
			u.fn
		FROM bookings b
		LEFT JOIN users u ON b.user_id = u.id
		WHERE b.status = 'in_progress'
		ORDER BY b.start_time
		LIMIT 1
	`).Scan(
		&b.ID,
		&b.StartTime,
		&b.Status,
		&b.Notes,
		&b.CreatedAt,
		&b.UpdatedAt,
		&b.CreatedBy,
		&userID,
		&fullName,
	)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "No in-progress booking found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to fetch in-progress booking: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Populate the UserSummary field
	b.User = &UserSummary{
		ID:        userID,
		FullName:  fullName,
		FirstName: "", // not available here
		LastName:  "", // not available here
	}

	w.Header().Set("Content-Type", "application/json")
	if err := json.NewEncoder(w).Encode(b); err != nil {
		log.Printf("Failed to encode booking response: %v", err)
	}
}

// AdminEditBookingHandler allows an admin to modify the start time of any booking.
// Admin can edit any booking EXCEPT completed or cancelled bookings.
// Admin can create/edit bookings outside working hours (with warning).
// Admin can create/edit bookings that overlap with existing bookings (with warning).
func AdminEditBookingHandler(w http.ResponseWriter, r *http.Request) {
	bookingID := chi.URLParam(r, "id")
	if bookingID == "" || !validators.IsValidID(bookingID) {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}
	var req EditBookingRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Basic validation: ensure the new time is not in the past
	if time.Now().After(req.StartTime) {
		http.Error(w, "Start time must be in the future", http.StatusBadRequest)
		return
	}

	// Check if booking exists and is not completed/cancelled
	var currentStatus string
	err := db.DB.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&currentStatus)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Booking not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get booking status %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Block edits on completed or cancelled bookings
	if currentStatus == "completed" || currentStatus == "client_cancelled" || currentStatus == "we_cancelled" {
		http.Error(w, "Cannot edit a completed or cancelled booking", http.StatusForbidden)
		return
	}

	// Get booking duration for overlap check
	var durationMinutes int
	err = db.DB.QueryRow(r.Context(), `
		SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
		FROM booking_services bs
		JOIN services s ON bs.service_id = s.id
		WHERE bs.booking_id = $1
	`, bookingID).Scan(&durationMinutes)
	if err != nil {
		log.Printf("Failed to get booking duration %s: %v", bookingID, err)
		durationMinutes = 60 // fallback
	}

	// Check for overlapping bookings (excluding the current booking)
	var overlapCount int
	newEndTime := req.StartTime.Add(time.Duration(durationMinutes) * time.Minute)
	err = db.DB.QueryRow(r.Context(), `
		SELECT COUNT(*) FROM bookings
		WHERE id != $1
		AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled')
		AND start_time < $3
		AND start_time + (INTERVAL '1 minute' * (
			SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
			FROM booking_services bs
			JOIN services s ON bs.service_id = s.id
			WHERE bs.booking_id = bookings.id
		)) > $2
	`, bookingID, req.StartTime, newEndTime).Scan(&overlapCount)
	if err != nil {
		log.Printf("Failed to check overlap %s: %v", bookingID, err)
	}

	// Check if salon is closed (exceptional hours) - admin gets warning but can proceed
	weekday := int(req.StartTime.Weekday())
	bookingTime := req.StartTime.Format("15:04:05")
	daysToMonday := weekday
	if daysToMonday == 0 {
		daysToMonday = 7
	}
	weekStart := req.StartTime.AddDate(0, 0, -daysToMonday+1).Truncate(24 * time.Hour)

	// Check if salon is closed (exceptional hours)
	var isClosed bool
	err = db.DB.QueryRow(r.Context(), `
		SELECT EXISTS (
			SELECT 1 FROM exceptional_working_hours ewh
			JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id
			WHERE ega.week_start = $1
			  AND ewh.weekday = $2
			  AND ewh.is_open = false
			  AND ewh.start_time <= $3
			  AND ewh.end_time >= $3
		)
	`, weekStart, weekday, bookingTime).Scan(&isClosed)
	if err != nil {
		log.Printf("Failed to check exceptional hours: %v", err)
	}

	isOutsideWorkingHours := isClosed

	// Prevent overlap - block admin
	if overlapCount > 0 {
		http.Error(w, "This booking overlaps with an existing booking", http.StatusConflict)
		return
	}

	// Build warning for outside working hours (admin can proceed with warning)
	var warnings []string
	if isOutsideWorkingHours {
		warnings = append(warnings, "Warning: This booking is outside standard working hours")
	}

	// Check for time blocker overlap - admin can proceed with warning
	blockerOverlap, blockerDesc, err := scheduling.CheckTimeBlockerOverlap(r.Context(), req.StartTime, newEndTime)
	if err != nil {
		log.Printf("Failed to check time blocker overlap: %v", err)
	}
	if blockerOverlap {
		warnings = append(warnings, fmt.Sprintf("Warning: This booking overlaps with a time blocker: %s", blockerDesc))
	}

	// Perform the update
	res, err := db.DB.Exec(r.Context(), `
		UPDATE bookings
		SET start_time = $1, updated_at = $2
		WHERE id = $3
	`, req.StartTime, time.Now(), bookingID)
	if err != nil {
		log.Printf("Failed to edit booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	rowsAffected := res.RowsAffected()
	if rowsAffected == 0 {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}

	// Clear any pending edit requests for this booking (admin edit takes priority)
	_, err = db.DB.Exec(r.Context(), `
		DELETE FROM booking_edit_requests
		WHERE booking_id = $1
	`, bookingID)
	if err != nil {
		log.Printf("Failed to clear edit requests for booking %s: %v", bookingID, err)
		// Don't fail the request, just log the error
	}

	// Return warnings if any
	if len(warnings) > 0 {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]interface{}{
			"message":  "Booking updated",
			"warnings": warnings,
		})
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

type AdminCreateBookingForUserRequest struct {
	UserID           string            `json:"user_id" validate:"required"`
	StartTime        time.Time         `json:"start_time" validate:"required"`
	ServiceIDs       []string          `json:"service_ids" validate:"required,min=1"`
	ServiceOverrides []ServiceOverride `json:"service_overrides,omitempty"`
	Notes            *string           `json:"notes,omitempty"` // appointment notes, visible to customers and staff
	EnforceDeposits  *bool             `json:"enforce_deposits,omitempty"` // Optional: if true, enforce outstanding deposit checks; if false or omitted, bypass checks
}

func AdminCreateBookingForUserHandler(w http.ResponseWriter, r *http.Request) {
	// Admin identity (creator)
	adminID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || adminID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var req AdminCreateBookingForUserRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		log.Printf("Failed to decode request: %v", err)
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	// Extract idempotency key from header
	idempotencyKey := r.Header.Get("Idempotency-Key")

	// If idempotency key provided, check for existing booking
	if idempotencyKey != "" {
		var existingID string
		err := db.DB.QueryRow(r.Context(), `SELECT id FROM bookings WHERE idempotency_key = $1`, idempotencyKey).Scan(&existingID)
		if err == nil {
			// Booking already exists with this key — fetch and return it
			var existingBooking Booking
			existingBooking.User = &UserSummary{}
			err := db.DB.QueryRow(r.Context(), `
				SELECT b.id, b.user_id, b.start_time, b.status, b.notes, b.created_at, b.updated_at, b.created_by, b.deposit_required
				FROM bookings b WHERE b.id = $1
			`, existingID).Scan(
				&existingBooking.ID, &existingBooking.User.ID, &existingBooking.StartTime, &existingBooking.Status,
				&existingBooking.Notes, &existingBooking.CreatedAt, &existingBooking.UpdatedAt, &existingBooking.CreatedBy,
				&existingBooking.DepositRequired,
			)
			if err == nil {
				// Fetch services for the response
				rows, err := db.DB.Query(r.Context(), `
					SELECT bs.booking_id, bs.service_id, bs.override_price, bs.override_duration_minutes,
					       s.name, s.description, s.price, s.duration_minutes
					FROM booking_services bs
					JOIN services s ON bs.service_id = s.id
					WHERE bs.booking_id = $1
				`, existingID)
				if err == nil {
					defer rows.Close()
					for rows.Next() {
						var bs BookingService
						if err := rows.Scan(
							&bs.BookingID, &bs.ServiceID, &bs.OverridePrice, &bs.OverrideDurationMinutes,
							&bs.ServiceName, &bs.ServiceDescription, &bs.Price, &bs.DurationMinutes,
						); err != nil {
							break
						}
						existingBooking.Services = append(existingBooking.Services, bs)
					}
				}

				// Get deposit info
				var depositRequired bool
				var preStartPaid float64
				db.DB.QueryRow(r.Context(), `SELECT deposit_required FROM bookings WHERE id = $1`, existingID).Scan(&depositRequired)
				db.DB.QueryRow(r.Context(), `SELECT COALESCE(SUM(amount), 0) FROM payments WHERE booking_id = $1 AND payment_type IN ('deposit', 'full') AND status = 'completed'`, existingID).Scan(&preStartPaid)
				populateDepositFields(&existingBooking, depositRequired, preStartPaid)

				w.Header().Set("Content-Type", "application/json")
				w.WriteHeader(http.StatusOK)
				json.NewEncoder(w).Encode(existingBooking)
				return
			}
		}
	}

	// Basic validation
	if req.UserID == "" {
		http.Error(w, "User ID is required", http.StatusBadRequest)
		return
	}
	if req.StartTime.IsZero() {
		http.Error(w, "Start time is required", http.StatusBadRequest)
		return
	}
	if len(req.ServiceIDs) == 0 {
		http.Error(w, "At least one service is required", http.StatusBadRequest)
		return
	}

	// Check patch test requirements for all services
	for _, serviceID := range req.ServiceIDs {
		// Find patch test for this service
		var patchTestID string
		var noticeHours int
		err := db.DB.QueryRow(r.Context(), `
			SELECT id, notice_duration_hours
			FROM patch_tests
			WHERE $1 = ANY(service_ids)
		`, serviceID).Scan(&patchTestID, &noticeHours)

		if err == nil {
			// Service requires a patch test - check if user has valid record
			var testedAt time.Time
			err = db.DB.QueryRow(r.Context(), `
				SELECT tested_at
				FROM user_patch_tests
				WHERE user_id = $1 AND patch_test_id = $2
			`, req.UserID, patchTestID).Scan(&testedAt)

			if err != nil {
				// No valid patch test record
				http.Error(w, "Patch test required for this service. Please complete a patch test first.", http.StatusBadRequest)
				return
			}

			// Check if notice period has passed
			eligibleFrom := testedAt.Add(time.Duration(noticeHours) * time.Hour)
			if req.StartTime.Before(eligibleFrom) {
				hoursNeeded := time.Until(eligibleFrom).Hours()
				http.Error(w, fmt.Sprintf("Booking time is before the %.0f hour notice period after patch test. Earliest booking: %s", hoursNeeded, eligibleFrom.Format("2006-01-02 15:04")), http.StatusBadRequest)
				return
			}

			// Check if patch test has expired
			var expiryMonths int
			err = db.DB.QueryRow(r.Context(), `SELECT expiry_months FROM patch_tests WHERE id = $1`, patchTestID).Scan(&expiryMonths)
			if err == nil {
				expiresAt := testedAt.AddDate(0, expiryMonths, 0)
				if req.StartTime.After(expiresAt) {
					http.Error(w, "Your patch test has expired. Please complete a new patch test.", http.StatusBadRequest)
					return
				}
			}
		}
	}

	// Enforce deposit checks if requested (default: true if not specified)
	enforceDeposits := true
	if req.EnforceDeposits != nil {
		enforceDeposits = *req.EnforceDeposits
	}

	if enforceDeposits {
		// Read live deposits_required from user
		var depositsRequired int
		if err := db.DB.QueryRow(r.Context(), `SELECT deposits_required FROM users WHERE id = $1`, req.UserID).Scan(&depositsRequired); err != nil {
			log.Printf("Failed to fetch deposits_required for user %s: %v", req.UserID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		// Check one-active-booking limit when deposits are outstanding
		if depositsRequired > 0 {
			var activeCount int
			if err := db.DB.QueryRow(r.Context(), `
				SELECT COUNT(*) FROM bookings
				WHERE user_id = $1 AND status IN ('pending', 'confirmed')
			`, req.UserID).Scan(&activeCount); err != nil {
				log.Printf("Failed to check active bookings for user %s: %v", req.UserID, err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
				return
			}
			if activeCount > 0 {
				http.Error(w, "User already has an active booking. Cannot create another until deposit requirements are cleared.", http.StatusConflict)
				return
			}
		}
	}

	// Validate overrides
	if req.UserID == "" {
		http.Error(w, "User ID is required", http.StatusBadRequest)
		return
	}
	if req.StartTime.IsZero() {
		http.Error(w, "Start time is required", http.StatusBadRequest)
		return
	}
	if len(req.ServiceIDs) == 0 {
		http.Error(w, "At least one service is required", http.StatusBadRequest)
		return
	}

	// Validate overrides
	for _, override := range req.ServiceOverrides {
		if override.ServiceID == "" {
			http.Error(w, "Service ID is required for overrides", http.StatusBadRequest)
			return
		}
		if override.OverridePrice != nil && *override.OverridePrice < 0 {
			http.Error(w, "Override price cannot be negative", http.StatusBadRequest)
			return
		}
		if override.OverrideDurationMinutes != nil && *override.OverrideDurationMinutes <= 0 {
			http.Error(w, "Override duration must be positive", http.StatusBadRequest)
			return
		}
	}

	// Check if booking time falls within a closed exceptional hours period
	// Calculate the Monday of the week containing the booking date
	weekday := int(req.StartTime.Weekday())
	daysToMonday := weekday
	if daysToMonday == 0 {
		daysToMonday = 7 // Sunday -> next Monday
	}
	weekStart := req.StartTime.AddDate(0, 0, -daysToMonday+1).Truncate(24 * time.Hour)
	bookingTime := req.StartTime.Format("15:04:05")

	// Check if there's an exceptional hours entry that makes this time unavailable
	var isClosed bool
	var checkErr error
	checkErr = db.DB.QueryRow(r.Context(), `
		SELECT EXISTS (
			SELECT 1 FROM exceptional_working_hours ewh
			JOIN exceptional_group_applications ega ON ewh.group_id = ega.group_id
			WHERE ega.week_start = $1
			  AND ewh.weekday = $2
			  AND ewh.is_open = false
			  AND ewh.start_time <= $3
			  AND ewh.end_time >= $3
		)
	`, weekStart, weekday, bookingTime).Scan(&isClosed)
	if checkErr != nil {
		log.Printf("Failed to check exceptional hours: %v", checkErr)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if isClosed {
		http.Error(w, "Cannot book during holiday hours when the salon is closed", http.StatusConflict)
		return
	}

	// Check for overlapping confirmed/in_progress/completed bookings
	var dur int
	db.DB.QueryRow(r.Context(), `
		SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1)
	`, req.ServiceIDs).Scan(&dur)
	newEnd := req.StartTime.Add(time.Duration(dur) * time.Minute)
	var cnt int
	db.DB.QueryRow(r.Context(), `
		SELECT COUNT(*) FROM bookings WHERE status IN ('confirmed','in_progress','completed') AND start_time < $2 AND start_time + (INTERVAL '1 minute' * (SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes,s.duration_minutes)),60) FROM booking_services bs JOIN services s ON bs.service_id=s.id WHERE bs.booking_id=bookings.id)) > $1
	`, req.StartTime, newEnd).Scan(&cnt)
	if cnt > 0 {
		http.Error(w, "Cannot create booking - time slot overlaps with existing booking", http.StatusConflict)
		return
	}

	// Check for time blocker overlap - admin can proceed with warning
	blockerOverlap, blockerDesc, err := scheduling.CheckTimeBlockerOverlap(r.Context(), req.StartTime, newEnd)
	if err != nil {
		log.Printf("Failed to check time blocker overlap: %v", err)
	}


	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Create booking directly as confirmed
	bookingQuery := `
		INSERT INTO bookings (
			user_id,
			start_time,
			status,
			notes,
			created_by,
			idempotency_key
		)
		VALUES ($1, $2, 'confirmed', $3, $4, $5)
		RETURNING id, user_id, start_time, status, notes, created_at, updated_at, created_by
	`

	var booking Booking
	booking.User = &UserSummary{}

	err = tx.QueryRow(
		r.Context(),
		bookingQuery,
		req.UserID,
		req.StartTime,
		req.Notes,
		adminID,
		sql.NullString{String: idempotencyKey, Valid: idempotencyKey != ""},
	).Scan(
		&booking.ID,
		&booking.User.ID,
		&booking.StartTime,
		&booking.Status,
		&booking.Notes,
		&booking.CreatedAt,
		&booking.UpdatedAt,
		&booking.CreatedBy,
	)

	if err != nil {
		log.Printf("Failed to create admin booking: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Insert booking services
	serviceInsertQuery := `
		INSERT INTO booking_services (booking_id, service_id)
		VALUES ($1, $2)
	`

	for _, serviceID := range req.ServiceIDs {
		_, err := tx.Exec(r.Context(), serviceInsertQuery, booking.ID, serviceID)
		if err != nil {
			log.Printf("Failed to insert booking service %s: %v", serviceID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	// Apply overrides (optional)
	if len(req.ServiceOverrides) > 0 {
		// Ensure overrides only reference services in this booking
		serviceCheckQuery := `
			SELECT COUNT(*) FROM booking_services
			WHERE booking_id = $1 AND service_id = ANY($2)
		`

		overrideServiceIDs := make([]string, len(req.ServiceOverrides))
		for i, o := range req.ServiceOverrides {
			overrideServiceIDs[i] = o.ServiceID
		}

		var count int
		err = tx.QueryRow(
			r.Context(),
			serviceCheckQuery,
			booking.ID,
			overrideServiceIDs,
		).Scan(&count)

		if err != nil {
			log.Printf("Failed to verify service overrides: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		if count != len(req.ServiceOverrides) {
			http.Error(w, "One or more service overrides do not belong to this booking", http.StatusBadRequest)
			return
		}

		overrideUpdateQuery := `
			UPDATE booking_services
			SET override_price = $1,
			    override_duration_minutes = $2
			WHERE booking_id = $3 AND service_id = $4
		`

		for _, override := range req.ServiceOverrides {
			_, err := tx.Exec(
				r.Context(),
				overrideUpdateQuery,
				override.OverridePrice,
				override.OverrideDurationMinutes,
				booking.ID,
				override.ServiceID,
			)
			if err != nil {
				log.Printf(
					"Failed to apply override (booking %s, service %s): %v",
					booking.ID,
					override.ServiceID,
					err,
				)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
				return
			}
		}
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit admin booking creation: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	// Build response with warnings if any

	warnings := []string{}

	if blockerOverlap {

		warnings = append(warnings, fmt.Sprintf("Warning: This booking overlaps with a time blocker: %s", blockerDesc))

	}


	response := map[string]interface{}{

		"booking":  booking,

		"warnings": warnings,

	}


	w.Header().Set("Content-Type", "application/json")

	w.WriteHeader(http.StatusCreated)

	if err := json.NewEncoder(w).Encode(response); err != nil {

		log.Printf("Failed to encode response: %v", err)

		http.Error(w, "Internal server error", http.StatusInternalServerError)

	}

}

// =============================================================================
// Booking Edit Request Handlers
// =============================================================================

// BookingEditRequest represents a user's request to edit a booking
type BookingEditRequest struct {
	ID           string     `json:"id"`
	BookingID    string     `json:"booking_id"`
	RequestedBy  string     `json:"requested_by"`
	NewStartTime *time.Time `json:"new_start_time,omitempty"`
	NewServices  []string   `json:"new_services"`
	Notes        *string    `json:"notes,omitempty"`
	HasOverrides bool       `json:"has_overrides"`
	UpdatedAt    time.Time  `json:"updated_at"`
	// Joined fields
	Booking *Booking     `json:"booking,omitempty"`
	User    *UserSummary `json:"user,omitempty"`
}

// DeleteEditRequestHandler allows a user to delete/cancel their pending edit request
func DeleteEditRequestHandler(w http.ResponseWriter, r *http.Request) {
	bookingID := chi.URLParam(r, "id")
	if bookingID == "" || !validators.IsValidID(bookingID) {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}

	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	// Verify user owns this booking
	var ownerID string
	err := db.DB.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&ownerID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Booking not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if ownerID != userID {
		http.Error(w, "Access denied", http.StatusForbidden)
		return
	}

	// Use transaction to delete edit request and associated admin notification
	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Delete the edit request for this booking
	res, err := tx.Exec(r.Context(), `
		DELETE FROM booking_edit_requests
		WHERE booking_id = $1 AND requested_by = $2
	`, bookingID, userID)
	if err != nil {
		log.Printf("Failed to delete edit request for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	rowsAffected := res.RowsAffected()
	if rowsAffected == 0 {
		http.Error(w, "No edit request found", http.StatusNotFound)
		return
	}

	_, _ = tx.Exec(r.Context(), `
		DELETE FROM time_blockers
		WHERE description = $1
	`, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID))

	// Delete the admin notification for this edit request
	_, err = tx.Exec(r.Context(), `
		DELETE FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested' AND user_id = $2
	`, bookingID, userID)
	if err != nil {
		log.Printf("Failed to delete admin notification for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit delete edit request: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// RequestEditHandler allows a user to request an edit to their booking
func RequestEditHandler(w http.ResponseWriter, r *http.Request) {
	bookingID := chi.URLParam(r, "id")
	if bookingID == "" || !validators.IsValidID(bookingID) {
		http.Error(w, "Booking not found", http.StatusNotFound)
		return
	}

	userID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || userID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var req struct {
		NewStartTime *time.Time `json:"new_start_time,omitempty"`
		NewServices  []string   `json:"new_services"`
		Notes        *string    `json:"notes,omitempty"`
	}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "Invalid request body", http.StatusBadRequest)
		return
	}

	// Validate: at least one of new_start_time, new_services, or notes must be provided
	if req.NewStartTime == nil && len(req.NewServices) == 0 && req.Notes == nil {
		http.Error(w, "At least one of new_start_time, new_services, or notes is required", http.StatusBadRequest)
		return
	}

	// Verify user owns this booking
	var ownerID string
	err := db.DB.QueryRow(r.Context(), "SELECT user_id FROM bookings WHERE id = $1", bookingID).Scan(&ownerID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Booking not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if ownerID != userID {
		http.Error(w, "Access denied", http.StatusForbidden)
		return
	}

	// Check booking is not already completed/cancelled
	var currentStatus string
	err = db.DB.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&currentStatus)
	if err != nil {
		log.Printf("Failed to get booking status %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if currentStatus == "completed" || currentStatus == "client_cancelled" || currentStatus == "we_cancelled" {
		http.Error(w, "Cannot edit a completed or cancelled booking", http.StatusForbidden)
		return
	}
	if len(req.NewServices) > 0 {
		var overrideCount int
		err = db.DB.QueryRow(r.Context(), `
			SELECT COUNT(*) FROM booking_services
			WHERE booking_id = $1 AND (override_price IS NOT NULL OR override_duration_minutes IS NOT NULL)
		`, bookingID).Scan(&overrideCount)
		if err != nil {
			log.Printf("Failed to check overrides for booking %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		if overrideCount > 0 {
			http.Error(w, "Cannot change services on a booking that has overrides. Please contact the salon.", http.StatusForbidden)
			return
		}
	}

	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Delete any existing edit request for this booking (upsert behavior)
	_, err = tx.Exec(r.Context(), `
		DELETE FROM booking_edit_requests
		WHERE booking_id = $1 AND requested_by = $2
	`, bookingID, userID)
	if err != nil {
		log.Printf("Failed to delete existing edit request for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Create edit request - has_overrides is false since user can't override
	var editReq BookingEditRequest
	err = tx.QueryRow(r.Context(), `
		INSERT INTO booking_edit_requests (booking_id, requested_by, new_start_time, new_services, notes, has_overrides)
		VALUES ($1, $2, $3, $4, $5, $6)
		RETURNING id, booking_id, requested_by, new_start_time, new_services, notes, has_overrides, updated_at
	`, bookingID, userID, req.NewStartTime, pq.Array(req.NewServices), req.Notes, false).Scan(
		&editReq.ID,
		&editReq.BookingID,
		&editReq.RequestedBy,
		&editReq.NewStartTime,
		pq.Array(&editReq.NewServices),
		&editReq.Notes,
		&editReq.HasOverrides,
		&editReq.UpdatedAt,
	)
	if err != nil {
		log.Printf("Failed to create edit request for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if req.NewStartTime != nil {
		_, _ = tx.Exec(r.Context(), `
			DELETE FROM time_blockers
			WHERE description = $1
		`, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID))

		var durationMinutes int
		if len(req.NewServices) > 0 {
			_ = tx.QueryRow(r.Context(), `
				SELECT COALESCE(SUM(s.duration_minutes), 60)
				FROM services s
				WHERE s.id = ANY($1)
			`, req.NewServices).Scan(&durationMinutes)
		} else {
			_ = tx.QueryRow(r.Context(), `
				SELECT COALESCE(SUM(s.duration_minutes), 60)
				FROM booking_services bs
				JOIN services s ON bs.service_id = s.id
				WHERE bs.booking_id = $1
			`, bookingID).Scan(&durationMinutes)
		}

		_, err = tx.Exec(r.Context(), `
			INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
			VALUES ($1, $2, $3, $4)
		`, *req.NewStartTime, durationMinutes, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID), userID)
		if err != nil {
			log.Printf("Failed to create time_blocker reservation for edit request %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	// Always create low-priority notification for edit requests
	_, err = tx.Exec(r.Context(), `
		DELETE FROM admin_notifications
		WHERE booking_id = $1 AND reason = 'edit_requested'
	`, bookingID)
	if err != nil {
		log.Printf("Failed to delete old admin notification for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_, err = tx.Exec(r.Context(), `
		INSERT INTO admin_notifications (reason, booking_id, user_id)
		VALUES ('edit_requested', $1, $2)
	`, bookingID, userID)
	if err != nil {
		log.Printf("Failed to create admin notification for edit request %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// If booking is pending, also create high-priority approval notification
	if currentStatus == "pending" {
		_, err = tx.Exec(r.Context(), `
			UPDATE admin_notifications
			SET acknowledged_at = NOW()
			WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL
		`, bookingID)
		if err != nil {
			log.Printf("Failed to acknowledge pending booking notification for %s: %v", bookingID, err)
		}

		_, err = tx.Exec(r.Context(), `
			INSERT INTO admin_notifications (reason, booking_id, user_id)
			VALUES ('pending_booking', $1, $2)
		`, bookingID, userID)
		if err != nil {
			log.Printf("Failed to create pending booking notification for %s: %v", bookingID, err)
		}
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit edit request: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(editReq)
}

// AdminListEditRequestsHandler returns all edit requests
func AdminListEditRequestsHandler(w http.ResponseWriter, r *http.Request) {
	baseQuery := `
		SELECT ber.id, ber.booking_id, ber.requested_by, ber.new_start_time,
		       ber.new_services, ber.notes, ber.has_overrides, ber.updated_at,
		       b.start_time as original_start_time, b.status as booking_status,
		       u.fn as user_name
		FROM booking_edit_requests ber
		JOIN bookings b ON ber.booking_id = b.id
		JOIN users u ON ber.requested_by = u.id
	`

	countQuery := `SELECT COUNT(*) FROM booking_edit_requests ber`

	var args []interface{}

	baseQuery += " ORDER BY ber.updated_at DESC"

	// Get total count
	var total int
	err := db.DB.QueryRow(r.Context(), countQuery, args...).Scan(&total)
	if err != nil {
		log.Printf("Failed to count edit requests: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	rows, err := db.DB.Query(r.Context(), baseQuery, args...)
	if err != nil {
		log.Printf("Failed to fetch edit requests: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	var requests []BookingEditRequest
	for rows.Next() {
		var req BookingEditRequest
		var origStartTime time.Time
		var bookingStatus string
		var userName string
		var newServices []string

		err := rows.Scan(
			&req.ID,
			&req.BookingID,
			&req.RequestedBy,
			&req.NewStartTime,
			pq.Array(&newServices),
			&req.Notes,
			&req.HasOverrides,
			&req.UpdatedAt,
			&origStartTime,
			&bookingStatus,
			&userName,
		)
		if err != nil {
			log.Printf("Failed to scan edit request: %v", err)
			continue
		}

		req.NewServices = newServices

		req.Booking = &Booking{
			ID:        req.BookingID,
			StartTime: origStartTime,
			Status:    bookingStatus,
		}
		req.User = &UserSummary{
			ID:       req.RequestedBy,
			FullName: userName,
		}

		requests = append(requests, req)
	}

	if requests == nil {
		requests = []BookingEditRequest{}
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]interface{}{
		"requests": requests,
		"total":    total,
	})
}

// AdminApproveEditRequestHandler approves an edit request and updates the booking
func AdminApproveEditRequestHandler(w http.ResponseWriter, r *http.Request) {
	requestID := chi.URLParam(r, "request_id")
	if requestID == "" || !validators.IsValidID(requestID) {
		http.Error(w, "Edit request not found", http.StatusNotFound)
		return
	}

	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Get the edit request
	var bookingID string
	var newStartTime *time.Time
	var newServices []string
	var notes *string
	var hasOverrides bool
	err = tx.QueryRow(r.Context(), `
		SELECT booking_id, new_start_time, new_services, notes, has_overrides
		FROM booking_edit_requests
		WHERE id = $1
	`, requestID).Scan(&bookingID, &newStartTime, pq.Array(&newServices), &notes, &hasOverrides)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Edit request not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get edit request %s: %v", requestID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// If new_services provided and has_overrides is true, block with error
	if len(newServices) > 0 && hasOverrides {
		http.Error(w, "Cannot change services on a booking that has overrides. Please update services manually.", http.StatusForbidden)
		return
	}

	// Calculate duration for overlap check - use overrides if has_overrides is true
	var durationMinutes int
	if hasOverrides {
		// Use the existing booking_services with overrides
		err = tx.QueryRow(r.Context(), `
			SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
			FROM booking_services bs
			JOIN services s ON bs.service_id = s.id
			WHERE bs.booking_id = $1
		`, bookingID).Scan(&durationMinutes)
	} else {
		// Use standard durations or new_services if provided
		if len(newServices) > 0 {
			// Use new services to calculate duration
			err = tx.QueryRow(r.Context(), `
				SELECT COALESCE(SUM(s.duration_minutes), 60)
				FROM services s
				WHERE s.id = ANY($1)
			`, newServices).Scan(&durationMinutes)
		} else {
			// Use existing booking services
			err = tx.QueryRow(r.Context(), `
				SELECT COALESCE(SUM(s.duration_minutes), 60)
				FROM booking_services bs
				JOIN services s ON bs.service_id = s.id
				WHERE bs.booking_id = $1
			`, bookingID).Scan(&durationMinutes)
		}
	}
	if err != nil {
		log.Printf("Failed to calculate duration: %v", err)
		durationMinutes = 60 // fallback
	}

	// Check for overlapping bookings if start time is being changed
	if newStartTime != nil {
		newEndTime := newStartTime.Add(time.Duration(durationMinutes) * time.Minute)
		var overlapCount int
		err = tx.QueryRow(r.Context(), `
			SELECT COUNT(*) FROM bookings
			WHERE id != $1
			AND status NOT IN ('completed', 'client_cancelled', 'we_cancelled')
			AND start_time < $3
			AND start_time + (INTERVAL '1 minute' * (
				SELECT COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 60)
				FROM booking_services bs
				JOIN services s ON bs.service_id = s.id
				WHERE bs.booking_id = bookings.id
			)) > $2
		`, bookingID, *newStartTime, newEndTime).Scan(&overlapCount)
		if err != nil {
			log.Printf("Failed to check overlap: %v", err)
		}
		if overlapCount > 0 {
			http.Error(w, "This edit would cause an overlap with an existing booking", http.StatusConflict)
			return
		}

		blockerOverlap, blockerDesc, err := scheduling.CheckTimeBlockerOverlap(r.Context(), *newStartTime, newEndTime)
		if err != nil {
			log.Printf("Failed to check time blocker overlap: %v", err)
		}
		if blockerOverlap {
			http.Error(w, fmt.Sprintf("This edit would overlap with a time blocker: %s", blockerDesc), http.StatusConflict)
			return
		}
	}

	// Build update query for bookings table
	if newStartTime != nil || notes != nil {
		var setClauses []string
		var args []interface{}
		argNum := 1

		if newStartTime != nil {
			setClauses = append(setClauses, fmt.Sprintf("start_time = $%d", argNum))
			args = append(args, *newStartTime)
			argNum++
		}
		if notes != nil {
			setClauses = append(setClauses, fmt.Sprintf("notes = $%d", argNum))
			args = append(args, *notes)
			argNum++
		}
		setClauses = append(setClauses, fmt.Sprintf("updated_at = $%d", argNum))
		args = append(args, time.Now())
		argNum++

		args = append(args, bookingID)

		query := fmt.Sprintf("UPDATE bookings SET %s WHERE id = $%d", strings.Join(setClauses, ", "), argNum)
		_, err = tx.Exec(r.Context(), query, args...)
		if err != nil {
			log.Printf("Failed to update booking %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	// Handle new_services (only if has_overrides is false)
	if len(newServices) > 0 && !hasOverrides {
		// Delete existing booking_services
		_, err = tx.Exec(r.Context(), "DELETE FROM booking_services WHERE booking_id = $1", bookingID)
		if err != nil {
			log.Printf("Failed to delete existing services for booking %s: %v", bookingID, err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		// Insert new services
		for _, serviceID := range newServices {
			_, err = tx.Exec(r.Context(), `
				INSERT INTO booking_services (booking_id, service_id)
				VALUES ($1, $2)
			`, bookingID, serviceID)
			if err != nil {
				log.Printf("Failed to insert booking service %s: %v", serviceID, err)
				http.Error(w, "Internal server error", http.StatusInternalServerError)
				return
			}
		}
	}

	// Delete the edit request row (not update status)
	_, err = tx.Exec(r.Context(), "DELETE FROM booking_edit_requests WHERE id = $1", requestID)
	if err != nil {
		log.Printf("Failed to delete edit request %s: %v", requestID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_, _ = tx.Exec(r.Context(), `
		DELETE FROM time_blockers
		WHERE description = $1
	`, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID))

	// Acknowledge the admin notification for this edit request
	_, err = tx.Exec(r.Context(), `
		UPDATE admin_notifications
		SET acknowledged_at = NOW()
		WHERE booking_id = $1 AND reason = 'edit_requested' AND acknowledged_at IS NULL
	`, bookingID)
	if err != nil {
		log.Printf("Failed to acknowledge admin notification for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// TODO: Notify user that their edit request was approved.
	// Options: (a) INSERT into user_notifications table (needs schema), (b) send email via SMTP provider.
	// The user_notification_preferences table exists but no delivery mechanism is wired yet.
	// See: obsidian/Crussell/Future Work - Gap Backlog.md → E5 (Email/SMS notification system).

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// AdminRejectEditRequestHandler rejects an edit request by deleting it and acknowledging the admin notification
func AdminRejectEditRequestHandler(w http.ResponseWriter, r *http.Request) {
	requestID := chi.URLParam(r, "request_id")
	if requestID == "" || !validators.IsValidID(requestID) {
		http.Error(w, "Edit request not found", http.StatusNotFound)
		return
	}

	// First get the booking_id from the edit request before deleting
	var bookingID string
	err := db.DB.QueryRow(r.Context(), `
		SELECT booking_id FROM booking_edit_requests WHERE id = $1
	`, requestID).Scan(&bookingID)
	if err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			http.Error(w, "Edit request not found", http.StatusNotFound)
			return
		}
		log.Printf("Failed to get edit request %s: %v", requestID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Use transaction to delete edit request and acknowledge associated admin notification
	tx, err := db.DB.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to start transaction: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer tx.Rollback(r.Context())

	// Delete the edit request
	_, err = tx.Exec(r.Context(), `
		DELETE FROM booking_edit_requests
		WHERE id = $1
	`, requestID)
	if err != nil {
		log.Printf("Failed to reject edit request %s: %v", requestID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	_, _ = tx.Exec(r.Context(), `
		DELETE FROM time_blockers
		WHERE description = $1
	`, fmt.Sprintf("RESERVATION:edit_request:%s", bookingID))

	// Acknowledge the admin notification for this edit request
	_, err = tx.Exec(r.Context(), `
		UPDATE admin_notifications
		SET acknowledged_at = NOW()
		WHERE booking_id = $1 AND reason = 'edit_requested' AND acknowledged_at IS NULL
	`, bookingID)
	if err != nil {
		log.Printf("Failed to acknowledge admin notification for booking %s: %v", bookingID, err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// TODO: Notify user that their edit request was denied.
	// Same as approve TODO above — needs user_notifications table or email delivery (E5).

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit reject edit request: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	w.WriteHeader(http.StatusNoContent)
}

// ========================================
// NO-SHOW HELPER FUNCTIONS
// ========================================

// CountUnforgivenNoShows counts the number of no-shows in the last 6 months
// that have not been forgiven (not in forgiven_no_shows table)
func CountUnforgivenNoShows(ctx context.Context, userID string) (int, error) {
	var count int
	err := db.DB.QueryRow(ctx, `
		SELECT COUNT(*)
		FROM bookings b
		WHERE b.user_id = $1
		  AND b.status = 'no_show'
		  AND b.start_time >= NOW() - INTERVAL '6 months'
		  AND NOT EXISTS (
		    SELECT 1 FROM forgiven_no_shows WHERE booking_id = b.id
		  )
	`, userID).Scan(&count)
	return count, err
}

// ApplyDepositsIfNeeded checks if user has 2+ unforgiven no-shows
// and applies 3 deposits if so. Returns true if deposits were applied.
func ApplyDepositsIfNeeded(ctx context.Context, userID string) (bool, error) {
	count, err := CountUnforgivenNoShows(ctx, userID)
	if err != nil {
		return false, err
	}
	if count >= 2 {
		// Apply 3 deposits
		_, err := db.DB.Exec(ctx, `
			UPDATE users SET deposits_required = 3 WHERE id = $1
		`, userID)
		if err != nil {
			return false, err
		}
		return true, nil
	}
	return false, nil
}


