package bookings

import (
	"crypto/md5"
	"encoding/json"
	"fmt"
	"log"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"crussell/auth"
	"crussell/clock"
	"crussell/db"
	"crussell/handlers/scheduling"
	"crussell/internal/validators"
	"crussell/mw"
	"net"
)

// ReserveSlotRequest represents the request body for reserving a slot
type ReserveSlotRequest struct {
	StartTime  time.Time `json:"start_time" validate:"required"`
	ServiceIDs []string  `json:"service_ids" validate:"required,min=1"`
}

// ReserveSlotResponse represents the response for a successful reservation
type ReserveSlotResponse struct {
	ID              string    `json:"id"`
	StartTime       time.Time `json:"start_time"`
	DurationMinutes int       `json:"duration_minutes"`
	ExpiresAt       time.Time `json:"expires_at"`
	IsAnonymous     bool      `json:"is_anonymous"`
}

// ReserveSlotHandler creates temporary slot reservations for both logged-in and anonymous users
func ReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
	// a. Parse JSON request body, validate start_time and service_ids are present
	var req ReserveSlotRequest
	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
	}

	if err := validators.Validate.Struct(&req); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	// M8
	// L5

	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
	}

	// b. Extract client IP: check CF-Connecting-IP → X-Real-IP → X-Forwarded-For → RemoteAddr
	ip := r.Header.Get("CF-Connecting-IP")
	if ip == "" {
		ip, _, _ = net.SplitHostPort(r.RemoteAddr)
		if ip == "" {
			ip = r.RemoteAddr
		}
	}

	// c. Detect auth: try context first (set by OptionalAuth middleware).
	// The inline Bearer fallback below is a safety net for the 22+ test
	// invocations that call ReserveSlotHandler via http.HandlerFunc directly
	// (without middleware). Keeping it is deliberate: the fallback never
	// executes in production (middleware always sets context first), removing it
	// would require refactoring those tests, and it acts as defense-in-depth
	// against accidental middleware misconfiguration.
	userID, hasUser := r.Context().Value(mw.UserIDKey).(string)
	hasAuth := hasUser && userID != ""
	if !hasAuth {
		authHeader := r.Header.Get("Authorization")
		if after, ok := strings.CutPrefix(authHeader, "Bearer "); ok {
			tokenString := after
			var err error
			userID, _, _, err = auth.VerifyToken(tokenString, r.Context())
			if err != nil {
				userID = ""
			}
			hasAuth = userID != ""
		}
	}

	// d. Calculate total duration: SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1)
	var svcDuration int
	if err := db.Conn.QueryRow(r.Context(), `
		SELECT COALESCE(SUM(duration_minutes), 0) FROM services WHERE id = ANY($1)
	`, req.ServiceIDs).Scan(&svcDuration); err != nil {
		log.Printf("Failed to calculate duration: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	if svcDuration == 0 {
		http.Error(w, "Invalid service IDs", http.StatusBadRequest)
		return
	}

	// e. Validate start_time not in the past
	if req.StartTime.Before(clock.Now()) {
		http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
		return
	}

	// f. Validate working hours: SELECT end_time::text FROM working_hours WHERE weekday = $1
	// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
	localStart := req.StartTime.In(londonLocation)
	weekday := int((localStart.Weekday() + 6) % 7)
	var closeStr string
	if err := db.Conn.QueryRow(r.Context(), `SELECT end_time FROM working_hours WHERE weekday = $1`, weekday).Scan(&closeStr); err != nil {
		log.Printf("Failed to get hours: %v", err)
		http.Error(w, "Could not verify hours", http.StatusInternalServerError)
		return
	}

	localEndLondon := localStart.Add(time.Duration(svcDuration) * time.Minute).In(londonLocation)
	if err := checkClosingHours(localEndLondon, closeStr); err != nil {
		http.Error(w, "Cannot book this time - services would extend beyond closing hours", http.StatusBadRequest)
		return
	}

	// g. Check existing booking overlap (same query as CreateBookingHandler)
	endTime := req.StartTime.Add(time.Duration(svcDuration) * time.Minute)

	// Clean up existing reservation BEFORE the overlap check,
	// using db.Conn.Exec (not tx.Exec) so the delete is visible to the separate
	// connection used by CheckTimeBlockerOverlap. The in-transaction DELETE
	// is kept as a safety net for the insert-phase.
	if hasAuth {
		// Also compute IP hash to clean up anonymous reservations
		// that may have been created before the user logged in.
		ipHash := fmt.Sprintf("%x", md5.Sum([]byte(ip)))[:8]
		if _, delErr := db.Conn.Exec(r.Context(), `
			DELETE FROM time_blockers
			WHERE (created_by = $1 AND description LIKE 'RESERVATION:user:%')
			   OR (description LIKE 'RESERVATION:anon:' || $2 || ':%')
		`, userID, ipHash); delErr != nil {
			log.Printf("Failed to delete existing reservation: %v", delErr)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	}

	// h. Check time blocker overlap using scheduling.CheckTimeBlockerOverlap
	// Pass excludeUserID so this user's own RESERVATION doesn't self-block.
	// The pre-overlap DELETE (above) and excludeUserID are complementary:
	// the DELETE cleans stale reservations from prior attempts; excludeUserID
	// prevents any remaining own-RESERVATION entries from blocking.
	var excludeUserID *string
	if hasAuth {
		excludeUserID = &userID
	}
	blockerOverlap, _, err := scheduling.CheckTimeBlockerOverlap(r.Context(), req.StartTime, endTime, excludeUserID)
	if err != nil {
		log.Printf("Failed to check time blocker overlap: %v", err)
	} else if blockerOverlap {
		http.Error(w, "Cannot book this time - slot is blocked", http.StatusConflict)
		return
	}

	// j. Branch on auth
	var reservationID string
	var createdAt time.Time

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

		// Check booking overlap inside transaction (TOCTOU fix)
		// pending_release is excluded — those bookings are evicted at creation time.
		overlapRows, err := tx.Query(r.Context(), `
			SELECT 1 FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
			AND start_time < $2
			AND end_time > $1
			FOR UPDATE
		`, req.StartTime, endTime)
		if err != nil {
			log.Printf("Failed to check overlap: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		var cnt int
		for overlapRows.Next() {
			cnt++
		}
		overlapRows.Close()
		if err := overlapRows.Err(); err != nil {
			log.Printf("Overlap row iteration error: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		if cnt > 0 {
			http.Error(w, "Cannot book this time - slot overlaps with an existing booking", http.StatusConflict)
			return
		}

		// LOGGED IN: Delete existing reservation
		_, err = tx.Exec(r.Context(), `
			DELETE FROM time_blockers
			WHERE created_by = $1 AND description LIKE 'RESERVATION:user:%'
		`, userID)
		if err != nil {
			log.Printf("Failed to delete existing reservation: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		// Insert new reservation
		description := fmt.Sprintf("RESERVATION:user:%s:%d", userID, clock.Now().UnixNano())
		err = tx.QueryRow(r.Context(), `
			INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
			VALUES ($1, $2, $3, $4)
			RETURNING id, created_at
		`, req.StartTime, svcDuration, description, userID).Scan(&reservationID, &createdAt)
		if err != nil {
			log.Printf("Failed to create reservation: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		if err := tx.Commit(r.Context()); err != nil {
			log.Printf("Failed to commit transaction: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
	} else {
		// Calculate ipHash from IP address
		ipHash := fmt.Sprintf("%x", md5.Sum([]byte(ip)))[:8]

		// ANONYMOUS: Use transaction for atomic rate cap + overlap check + insert
		tx, err := db.Conn.Begin(r.Context())
		if err != nil {
			log.Printf("Failed to start transaction: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

		// Check anon rate cap inside transaction
		tenMinutesAgo := clock.Now().Add(-10 * time.Minute)
		var anonCount int
		if err := tx.QueryRow(r.Context(), `
			SELECT COUNT(*) FROM time_blockers
			WHERE description LIKE 'RESERVATION:anon:%' AND created_at > $1
		`, tenMinutesAgo).Scan(&anonCount); err != nil {
			log.Printf("Failed to check anon cap: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		if anonCount >= 50 {
			http.Error(w, "Too many active reservations. Please wait or log in.", http.StatusTooManyRequests)
			return
		}

		// Check overlap inside transaction
		// pending_release is excluded — those bookings are evicted at creation time.
		overlapRows, err := tx.Query(r.Context(), `
			SELECT 1 FROM bookings WHERE status IN ('pending','confirmed','in_progress','completed')
			AND start_time < $2
			AND end_time > $1
			FOR UPDATE
		`, req.StartTime, endTime)
		if err != nil {
			log.Printf("Failed to check overlap: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		var anonCnt int
		for overlapRows.Next() {
			anonCnt++
		}
		overlapRows.Close()
		if err := overlapRows.Err(); err != nil {
			log.Printf("Overlap row iteration error: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		if anonCnt > 0 {
			http.Error(w, "Cannot book this time - slot overlaps with an existing booking", http.StatusConflict)
			return
		}

		description := fmt.Sprintf("RESERVATION:anon:%s:%d", ipHash, clock.Now().UnixNano())
		err = tx.QueryRow(r.Context(), `
			INSERT INTO time_blockers (start_time, duration_minutes, description, created_by)
			VALUES ($1, $2, $3, NULL)
			RETURNING id, created_at
		`, req.StartTime, svcDuration, description).Scan(&reservationID, &createdAt)
		if err != nil {
			log.Printf("Failed to create reservation: %v", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

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

	// k. Calculate expires_at: logged-in = created_at + 1 hour, anon = created_at + 10 minutes
	var expiresAt time.Time
	if hasAuth {
		expiresAt = createdAt.Add(1 * time.Hour)
	} else {
		expiresAt = createdAt.Add(10 * time.Minute)
	}

	// l. Return 201 with JSON response
	response := ReserveSlotResponse{
		ID:              reservationID,
		StartTime:       req.StartTime,
		DurationMinutes: svcDuration,
		ExpiresAt:       expiresAt,
		IsAnonymous:     !hasAuth,
	}

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