package bookings

import (
	"context"
	"crypto/md5"
	"crussell/clock"
	"crussell/db"
	"crussell/handlers/scheduling"
	"crussell/mw"
	"encoding/json"
	"errors"
	"fmt"
	"github.com/jackc/pgx/v5"
	"log"
	"log/slog"
	"net"
	"net/http"
	"time"
)

// ServiceOverrideRequest represents override values for a specific service in a reservation
type ServiceOverrideRequest struct {
	ServiceID               string `json:"service_id"`
	OverrideDurationMinutes *int   `json:"override_duration_minutes,omitempty"`
}

// AdminReserveSlotRequest represents the request payload for admin slot reservation
type AdminReserveSlotRequest struct {
	UserID           *string                  `json:"user_id"`
	StartTime        time.Time                `json:"start_time"`
	ServiceIDs       []string                 `json:"service_ids"`
	CustomServiceIDs []string                 `json:"custom_service_ids,omitempty"`
	ServiceOverrides []ServiceOverrideRequest `json:"service_overrides"`
	TTLMinutes       int                      `json:"ttl_minutes"`      // 15 for both walk-in and call-in
	ReservationType  string                   `json:"reservation_type"` // "walkin" or "callin"
	DurationMinutes  int                      `json:"duration_minutes"` // explicit duration for walk-in (ignored for call-in)
	OutOfHours       bool                     `json:"out_of_hours"`
}

// AdminReserveSlotResponse represents the response for admin slot reservation
type AdminReserveSlotResponse struct {
	ID              string    `json:"id"`
	StartTime       time.Time `json:"start_time"`
	DurationMinutes int       `json:"duration_minutes"`
	ExpiresAt       time.Time `json:"expires_at"`
	TTLMinutes      int       `json:"ttl_minutes"`
}

// AdminReserveSlotHandler creates a temporary admin slot reservation (walk-in or call-in)
func AdminReserveSlotHandler(w http.ResponseWriter, r *http.Request) {
	adminID, ok := r.Context().Value(mw.UserIDKey).(string)
	if !ok || adminID == "" {
		http.Error(w, "Authentication required", http.StatusUnauthorized)
		return
	}

	var req AdminReserveSlotRequest
	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 req.StartTime.IsZero() {
		http.Error(w, "start_time is required", http.StatusBadRequest)
		return
	}

	if req.ReservationType != "walkin" && req.ReservationType != "callin" {
		http.Error(w, "reservation_type must be 'walkin' or 'callin'", http.StatusBadRequest)
		return
	}

	if req.TTLMinutes == 0 {
		req.TTLMinutes = 15
	}

	var svcDuration int

	if req.ReservationType == "callin" {
		if len(req.ServiceIDs) == 0 && len(req.CustomServiceIDs) == 0 {
			http.Error(w, "At least one service or custom service is required for call-in bookings", http.StatusBadRequest)
			return
		}
		if req.StartTime.Before(clock.Now()) {
			http.Error(w, "Start time cannot be in the past", http.StatusBadRequest)
			return
		}
		var err error
		allIDs := append(req.ServiceIDs, req.CustomServiceIDs...)
		svcDuration, err = calculateServiceDurationWithOverrides(r.Context(), allIDs, req.ServiceOverrides)
		if 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
		}
	} else {
		if req.DurationMinutes <= 0 {
			http.Error(w, "duration_minutes is required for walk-in reservations", http.StatusBadRequest)
			return
		}
		svcDuration = req.DurationMinutes
		allowablePast := clock.Now().Add(-1 * time.Minute)
		if req.StartTime.Before(allowablePast) {
			http.Error(w, "Start time cannot be more than 1 minute in the past", http.StatusBadRequest)
			return
		}
	}

	endTime := req.StartTime.Add(time.Duration(svcDuration) * time.Minute)

	if !req.OutOfHours {
		localStart := req.StartTime.In(londonLocation)
		// DB uses 0=Monday..6=Sunday; Go uses 0=Sunday..6=Saturday. Convert.
		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 {
			if errors.Is(err, pgx.ErrNoRows) {
				http.Error(w, "Not open on this day", http.StatusBadRequest)
				return
			}
			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 {
			if errors.Is(err, ErrPastClosing) {
				http.Error(w, "Cannot book this time - services would extend beyond closing hours", http.StatusBadRequest)
			} else {
				http.Error(w, "Invalid closing time in schedule", http.StatusInternalServerError)
			}
			return
		}
	}

	// Clean up existing admin reservation BEFORE the overlap check,
	// using db.Conn.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.
	// Also clean up anonymous reservations matching this admin's IP
	// (edge case: admin previously reserved without authentication).
	ip := r.Header.Get("CF-Connecting-IP")
	if ip == "" {
		ip, _, _ = net.SplitHostPort(r.RemoteAddr)
		if ip == "" {
			ip = r.RemoteAddr
		}
	}
	ipHash := fmt.Sprintf("%x", md5.Sum([]byte(ip)))[:8]
	if _, delErr := db.Conn.Exec(r.Context(), `
		DELETE FROM time_blockers
		WHERE (description LIKE 'RESERVATION:admin:%' AND created_by = $1)
		   OR (description LIKE 'RESERVATION:anon:' || $2 || ':%')
	`, adminID, ipHash); delErr != nil {
		log.Printf("Failed to delete existing admin reservation: %v", delErr)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	blockerOverlap, _, err := scheduling.CheckTimeBlockerOverlap(r.Context(), req.StartTime, endTime, &adminID)
	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
	}

	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
	// pending_release is excluded — those bookings are evicted at creation time
	// by AdminCreateBookingForUserHandler / CreateBookingHandler.
	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
	}

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

	customerID := "guest"
	if req.UserID != nil && *req.UserID != "" {
		customerID = *req.UserID
	}

	description := fmt.Sprintf("RESERVATION:admin:%s:%s:%d", req.ReservationType, customerID, clock.Now().UnixNano())
	var reservationID string
	var createdAt time.Time
	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, adminID).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
	}

	expiresAt := createdAt.Add(time.Duration(req.TTLMinutes) * time.Minute)

	response := AdminReserveSlotResponse{
		ID:              reservationID,
		StartTime:       req.StartTime,
		DurationMinutes: svcDuration,
		ExpiresAt:       expiresAt,
		TTLMinutes:      req.TTLMinutes,
	}


	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)
		return
	}
}

// calculateServiceDurationWithOverrides calculates total duration considering service overrides
func calculateServiceDurationWithOverrides(ctx context.Context, serviceIDs []string, overrides []ServiceOverrideRequest) (int, error) {
	// If no overrides, use simple sum
	if len(overrides) == 0 {
		var duration int
		err := db.Conn.QueryRow(ctx, `
			SELECT COALESCE(SUM(dur), 0) FROM (
				SELECT duration_minutes AS dur FROM services WHERE id = ANY($1)
				UNION ALL
				SELECT duration_minutes FROM custom_services WHERE id = ANY($1)
			) combined
		`, serviceIDs).Scan(&duration)
		return duration, err
	}

	// Build override map
	overrideMap := make(map[string]int)
	for _, o := range overrides {
		if o.OverrideDurationMinutes != nil {
			overrideMap[o.ServiceID] = *o.OverrideDurationMinutes
		}
	}

	// Get all services
	rows, err := db.Conn.Query(ctx, `
		SELECT id, duration_minutes FROM services WHERE id = ANY($1)
		UNION ALL
		SELECT id, duration_minutes FROM custom_services WHERE id = ANY($1)
	`, serviceIDs)
	if err != nil {
		return 0, err
	}
	defer rows.Close()

	var totalDuration int
	for rows.Next() {
		var svcID string
		var baseDuration int
		if err := rows.Scan(&svcID, &baseDuration); err != nil {
			return 0, err
		}
		if overrideDuration, exists := overrideMap[svcID]; exists {
			totalDuration += overrideDuration
		} else {
			totalDuration += baseDuration
		}
	}

	return totalDuration, rows.Err()
}
