package today

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"log/slog"
	"net/http"
	"strings"
	"time"

	"crussell/clock"
	"crussell/db"
	"github.com/jackc/pgx/v5"
)

var londonLocation = func() *time.Location {
	loc, err := time.LoadLocation("Europe/London")
	if err != nil {
		panic("failed to load Europe/London timezone: " + err.Error())
	}
	return loc
}()

type ServiceInfo struct {
	ServiceName        *string  `json:"service_name,omitempty"`
	ServiceDescription *string  `json:"service_description,omitempty"`
	Price              *float64 `json:"price,omitempty"`
	DurationMinutes    *int     `json:"duration_minutes,omitempty"`
}

type UserInfo struct {
	ID                string  `json:"id"`
	FullName          string  `json:"full_name"`
	Phone             *string `json:"phone,omitempty"`
	Email             *string `json:"email,omitempty"`
	ProfilePicURL     *string `json:"profile_pic_url,omitempty"`
	PreviousFirstName *string `json:"previous_first_name,omitempty"`
	PreviousLastName  *string `json:"previous_last_name,omitempty"`
}

type AppointmentInfo struct {
	ID              string        `json:"id"`
	StartTime       time.Time     `json:"start_time"`
	Status          string        `json:"status"`
	Notes           *string       `json:"notes,omitempty" validate:"omitempty,max=1000000"`
	User            *UserInfo     `json:"user,omitempty"`
	Services        []ServiceInfo `json:"services"`
	DurationMinutes int           `json:"duration_minutes"`
	TotalAmount     float64       `json:"total_amount"`
}

type ServiceBookingCount struct {
	ServiceName string `json:"service_name"`
	Count       int    `json:"count"`
}

type DailySummary struct {
	TotalPaymentsToday float64               `json:"total_payments_today"`
	TotalTipsToday     float64               `json:"total_tips_today"`
	TotalVATCollected  float64               `json:"total_vat_collected,omitempty"`
	AmountDueToday     float64               `json:"amount_due_today"`
	TotalDurationSpent int                   `json:"total_duration_spent"`
	CustomersServed    int                   `json:"customers_served"`
	TotalBookings      int                   `json:"total_bookings"`
	NewCustomers       int                   `json:"new_customers"`
	ReturningCustomers int                   `json:"returning_customers"`
	GuestCustomers     int                   `json:"guest_customers"`
	GiftCardsSold      int                   `json:"gift_cards_sold"`
	LastCustomerName   string                `json:"last_customer_name,omitempty"`
	LastCustomerVisits int                   `json:"last_customer_visits,omitempty"`
	NewBookingServices []ServiceBookingCount `json:"new_booking_services,omitempty"`
	SummaryScope       string                `json:"summary_scope"`
	SummaryStartDate   string                `json:"summary_start_date"`
	SummaryEndDate     string                `json:"summary_end_date"`
}

type CurrentNextResponse struct {
	Current     *AppointmentInfo `json:"current"`
	Next        *AppointmentInfo `json:"next"`
	ClosingTime *string          `json:"closing_time,omitempty"`
	DoneForDay  *bool            `json:"done_for_day,omitempty"`
	Summary     *DailySummary    `json:"summary,omitempty"`
	WeekSummary *DailySummary    `json:"week_summary,omitempty"`
}

// GET /api/admin/today/current-next
func GetCurrentAndNextHandler(w http.ResponseWriter, r *http.Request) {
	now := clock.Now()
	// Align the daily summary boundary with the UK business day (midnight local time)
	// instead of UTC midnight. This prevents a 1-hour shift during BST.
	londonNow := now.In(londonLocation)
	todayStart := time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day(), 0, 0, 0, 0, londonLocation).UTC()
	todayEnd := todayStart.Add(24 * time.Hour)

	// Auto-transition confirmed bookings that have started but not ended to in_progress
	// Auto-transition in_progress bookings that have ended to completed
	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	_, err = tx.Exec(r.Context(), `
		UPDATE bookings 
		SET status = 'in_progress'
		WHERE status = 'confirmed'
		  AND start_time <= $1
		  AND end_time > $1
	`, now)
	if err != nil {
		log.Printf("Failed to auto-transition bookings to in_progress: %v", err)
		return
	}

	_, err = tx.Exec(r.Context(), `
		UPDATE bookings 
		SET status = 'completed'
		WHERE status = 'in_progress'
		  AND end_time <= $1
	`, now)
	if err != nil {
		log.Printf("Failed to auto-transition bookings to completed: %v", err)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		return
	}

	var current *AppointmentInfo
	var next *AppointmentInfo

	// Step 1: Try to find current in-progress appointment
	currentBooking, err := fetchAppointment(r, `
		SELECT b.id, b.start_time, b.status, b.notes, b.user_id
		FROM bookings b
		WHERE b.status = 'in_progress'
		  AND b.start_time >= $1
		  AND b.start_time < $2
		ORDER BY b.start_time ASC
		LIMIT 1
	`, todayStart, todayEnd)

	if err != nil && !errors.Is(err, pgx.ErrNoRows) {
		log.Printf("Error querying current appointment: %v", err)
	}

	if err == nil && currentBooking != nil {
		current = currentBooking

		// Also fetch the appointment AFTER this one within today
		nextBooking, err := fetchAppointment(r, `
			SELECT b.id, b.start_time, b.status, b.notes, b.user_id
			FROM bookings b
			WHERE b.start_time > $1
			  AND b.start_time < $2
			  AND (b.status = 'confirmed' OR b.status = 'pending' OR b.status = 'in_progress')
			ORDER BY b.start_time ASC
			LIMIT 1
		`, currentBooking.StartTime, todayEnd)

		if err == nil && nextBooking != nil {
			next = nextBooking
		}
	} else {
		// No current in-progress appointment, find the next upcoming one within today
		nextBooking, err := fetchAppointment(r, `
			SELECT b.id, b.start_time, b.status, b.notes, b.user_id
			FROM bookings b
			WHERE b.start_time >= $1
			  AND b.start_time < $2
			  AND (b.status = 'confirmed' OR b.status = 'pending')
			ORDER BY b.start_time ASC
			LIMIT 1
		`, now, todayEnd)

		if err != nil && !errors.Is(err, pgx.ErrNoRows) {
			log.Printf("Error querying next appointment: %v", err)
		}

		if err == nil && nextBooking != nil {
			current = nextBooking

			// Fetch the one after for free time calculation
			afterNext, err := fetchAppointment(r, `
				SELECT b.id, b.start_time, b.status, b.notes, b.user_id
				FROM bookings b
				WHERE b.start_time > $1
				  AND b.start_time < $2
				  AND (b.status = 'confirmed' OR b.status = 'pending' OR b.status = 'in_progress')
				ORDER BY b.start_time ASC
				LIMIT 1
			`, nextBooking.StartTime, todayEnd)

			if err == nil && afterNext != nil {
				next = afterNext
			}
		}
	}

	response := CurrentNextResponse{
		Current: current,
		Next:    next,
	}

	// Get closing time respecting exceptional hours
	todayOpen := isDayOpen(r, londonNow)
	if todayOpen {
		closeTime := getClosingTime(r, londonNow)
		if closeTime != "" {
			response.ClosingTime = &closeTime
		}
	}

	// Determine if we're done for the day: no current, no next
	if current == nil && next == nil {
		done := true
		response.DoneForDay = &done

		if todayOpen {
			// Regular done-for-the-day: show daily summary
			summary := computeAggregateSummary(r, todayStart, todayEnd)
			summary.SummaryScope = "day"
			summary.SummaryStartDate = todayStart.Format("2006-01-02")
			summary.SummaryEndDate = todayEnd.Format("2006-01-02")
			response.Summary = summary

			// If tomorrow is closed, also compute a week summary
			tomorrow := londonNow.AddDate(0, 0, 1)
			if !isDayOpen(r, tomorrow) {
				weekStart, _ := findWeekSummaryRange(r, tomorrow)
				ws := computeAggregateSummary(r, weekStart, todayEnd)
				ws.SummaryScope = "week"
				ws.SummaryStartDate = weekStart.Format("2006-01-02")
				ws.SummaryEndDate = todayStart.Format("2006-01-02")
				response.WeekSummary = ws
			}
		} else {
			// Closed day: show week summary — from start of last work period to now
			weekStart, _ := findWeekSummaryRange(r, londonNow)
			summary := computeAggregateSummary(r, weekStart, todayEnd)
			summary.SummaryScope = "week"
			summary.SummaryStartDate = weekStart.Format("2006-01-02")
			summary.SummaryEndDate = todayStart.Format("2006-01-02")
			response.Summary = summary
		}
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	if err := json.NewEncoder(w).Encode(response); err != nil {
		log.Printf("Failed to encode current/next response: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
}

func computeAggregateSummary(r *http.Request, rangeStart, rangeEnd time.Time) *DailySummary {
	summary := &DailySummary{}

	// Single combined query replacing 9 separate round-trips
	query := `
		SELECT
			COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type != 'tip'), 0) AS total_payments,
			COALESCE(SUM(p.amount) FILTER (WHERE p.status = 'completed' AND p.payment_type = 'tip'), 0) AS total_tips,
			COALESCE(SUM(p.vat_amount) FILTER (WHERE p.status = 'completed' AND p.payment_type != 'tip'), 0) AS total_vat,
			COALESCE(SUM(sub.amount_due) FILTER (WHERE sub.amount_due > 0), 0) AS amount_due,
			COALESCE(SUM(sub.duration_minutes), 0) AS total_duration,
			COUNT(DISTINCT b.id) FILTER (WHERE b.status NOT IN ('client_cancelled','we_cancelled','no_show','deposit_lapsed')) AS total_bookings,
			COUNT(DISTINCT b.user_id) FILTER (
				WHERE b.status NOT IN ('client_cancelled','we_cancelled','no_show')
				  AND b.user_id IS NOT NULL
				  AND NOT EXISTS (SELECT 1 FROM bookings b2 WHERE b2.user_id = b.user_id AND b2.start_time < $1)
			) AS new_customers,
			COUNT(DISTINCT b.user_id) FILTER (
				WHERE b.status NOT IN ('client_cancelled','we_cancelled','no_show')
				  AND b.user_id IS NOT NULL
				  AND EXISTS (SELECT 1 FROM bookings b2 WHERE b2.user_id = b.user_id AND b2.start_time < $1)
			) AS returning_customers,
			(SELECT COUNT(*) FROM gift_cards WHERE created_at >= $1 AND created_at < $2 AND is_inventory = false) AS gift_cards_sold,
			COALESCE((SELECT u.fn FROM bookings b2
				JOIN users u ON u.id = b2.user_id
				WHERE b2.start_time >= $1 AND b2.start_time < $2
					AND b2.status = 'completed' AND b2.user_id IS NOT NULL
				ORDER BY b2.start_time DESC LIMIT 1), '') AS last_customer_name,
			COALESCE((WITH last_customer AS (
				SELECT b2.user_id FROM bookings b2
				WHERE b2.start_time >= $1 AND b2.start_time < $2
					AND b2.status = 'completed' AND b2.user_id IS NOT NULL
				ORDER BY b2.start_time DESC LIMIT 1
			)
			SELECT COUNT(b3.id)
			FROM last_customer lc
			LEFT JOIN bookings b3 ON b3.user_id = lc.user_id
				AND b3.status NOT IN ('client_cancelled','we_cancelled')), 0) AS last_customer_visits,
			COUNT(DISTINCT b.id) FILTER (WHERE b.status NOT IN ('client_cancelled','we_cancelled','no_show') AND b.user_id IS NULL) AS guest_customers
		FROM bookings b
		LEFT JOIN payments p ON p.booking_id = b.id AND p.status = 'completed'
		LEFT JOIN LATERAL (
			SELECT
				COALESCE(SUM(COALESCE(bs.override_price, s.price)), 0)
				+ COALESCE(SUM(COALESCE(bcs.override_price, cs.price)), 0)
				- COALESCE((SELECT SUM(amount) FROM payments WHERE booking_id = b.id AND status = 'completed'), 0) AS amount_due,
				COALESCE(SUM(COALESCE(bs.override_duration_minutes, s.duration_minutes)), 0)
				+ COALESCE(SUM(COALESCE(bcs.override_duration_minutes, cs.duration_minutes)), 0) AS duration_minutes
			FROM bookings b_inner
			LEFT JOIN booking_services bs ON bs.booking_id = b_inner.id
			LEFT JOIN services s ON s.id = bs.service_id
			LEFT JOIN booking_custom_services bcs ON bcs.booking_id = b_inner.id
			LEFT JOIN custom_services cs ON cs.id = bcs.custom_service_id
			WHERE b_inner.id = b.id
			GROUP BY b_inner.id
		) sub ON true
		WHERE b.start_time >= $1 AND b.start_time < $2
	`
	_ = db.Conn.QueryRow(r.Context(), query, rangeStart, rangeEnd).Scan(
		&summary.TotalPaymentsToday,
		&summary.TotalTipsToday,
		&summary.TotalVATCollected,
		&summary.AmountDueToday,
		&summary.TotalDurationSpent,
		&summary.TotalBookings,
		&summary.NewCustomers,
		&summary.ReturningCustomers,
		&summary.GiftCardsSold,
		&summary.LastCustomerName,
		&summary.LastCustomerVisits,
		&summary.GuestCustomers,
	)

	// Customers served = new + returning + guest (distinct people, not bookings)
	summary.CustomersServed = summary.NewCustomers + summary.ReturningCustomers + summary.GuestCustomers

	// 10. New bookings since last working day close
	summary.NewBookingServices = findNewBookingServices(r, rangeStart)

	return summary
}

func findNewBookingServices(r *http.Request, rangeStart time.Time) []ServiceBookingCount {
	now := clock.Now()

	// Single query to find the last working day's closing time
	var lastClose time.Time
	var dayDate time.Time
	var closeTimeStr string
	err := db.Conn.QueryRow(r.Context(), `
		WITH days AS (
			SELECT
				(($1::timestamptz AT TIME ZONE 'Europe/London')::date - i) AS day_date,
				CASE WHEN EXTRACT(DOW FROM ($1::timestamptz AT TIME ZONE 'Europe/London')::date - i) = 0 THEN 6
					 ELSE EXTRACT(DOW FROM ($1::timestamptz AT TIME ZONE 'Europe/London')::date - i)::integer - 1
				END AS weekday
			FROM generate_series(1, 14) AS i
		)
		SELECT
			d.day_date,
			COALESCE(eh.close_time, wh.close_time) AS close_time
		FROM days d
		LEFT JOIN LATERAL (
			SELECT ewh.end_time AS close_time
			FROM exceptional_working_hours ewh
			JOIN exceptional_working_hours_groups ewhg ON ewhg.id = ewh.group_id
			JOIN exceptional_group_applications ega ON ega.group_id = ewhg.id
			WHERE ewh.weekday = d.weekday
				AND ega.week_start <= d.day_date
				AND ega.week_start + INTERVAL '7 days' > d.day_date
				AND ewh.is_open = true
			LIMIT 1
		) eh ON true
		LEFT JOIN LATERAL (
			SELECT wh.end_time AS close_time
			FROM working_hours wh
			WHERE wh.weekday = d.weekday AND wh.is_open = true
			LIMIT 1
		) wh ON true
		WHERE COALESCE(eh.close_time, wh.close_time) IS NOT NULL
		ORDER BY d.day_date DESC
		LIMIT 1
	`, now).Scan(&dayDate, &closeTimeStr)

	if err == nil {
		parts := strings.Split(closeTimeStr, ":")
		h, m := 0, 0
		_, _ = fmt.Sscanf(parts[0], "%d", &h)
		if len(parts) > 1 {
			_, _ = fmt.Sscanf(parts[1], "%d", &m)
		}
		lastClose = time.Date(dayDate.Year(), dayDate.Month(), dayDate.Day(), h, m, 0, 0, londonLocation).UTC()
	} else {
		// Fallback: 5pm yesterday
		londonNow := now.In(londonLocation)
		lastClose = time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day()-1, 17, 0, 0, 0, londonLocation).UTC()
	}

	// Query services across all new bookings, aggregated by service name
	rows, err := db.Conn.Query(r.Context(), `
		SELECT name, COUNT(*) as count
		FROM (
			SELECT s.name FROM booking_services bs2
			JOIN services s ON s.id = bs2.service_id
			JOIN bookings b ON b.id = bs2.booking_id
			WHERE b.created_at > $1 AND b.created_at <= $2
			  AND b.status != 'pending'
			UNION ALL
			SELECT cs.name FROM booking_custom_services bcs2
			JOIN custom_services cs ON cs.id = bcs2.custom_service_id
			JOIN bookings b ON b.id = bcs2.booking_id
			WHERE b.created_at > $1 AND b.created_at <= $2
			  AND b.status != 'pending'
		) all_services
		GROUP BY name
		ORDER BY count DESC, name ASC
	`, lastClose, now)

	if err != nil {
		log.Printf("Failed to fetch new booking services since close: %v", err)
		return nil
	}
	defer rows.Close()

	var items []ServiceBookingCount
	for rows.Next() {
		var item ServiceBookingCount
		if err := rows.Scan(&item.ServiceName, &item.Count); err != nil {
			log.Printf("Failed to scan new booking service row: %v", err)
			continue
		}
		items = append(items, item)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error in getNewBookingServiceCounts: %v", err)
	}

	return items
}

func isDayOpen(r *http.Request, date time.Time) bool {
	weekday := int(date.Weekday())
	if weekday == 0 {
		weekday = 6
	} else {
		weekday -= 1
	}
	dateStr := date.Format("2006-01-02")

	// Try exceptional hours first
	var exceptionalOpen sql.NullBool
	err := db.Conn.QueryRow(r.Context(), `
		SELECT ewh.is_open
		FROM exceptional_working_hours ewh
		JOIN exceptional_working_hours_groups ewhg ON ewhg.id = ewh.group_id
		JOIN exceptional_group_applications ega ON ega.group_id = ewhg.id
		WHERE ewh.weekday = $1
		  AND ega.week_start <= $2::date
		  AND ega.week_start + INTERVAL '7 days' > $2::date
		LIMIT 1
	`, weekday, dateStr).Scan(&exceptionalOpen)

	if err == nil {
		return exceptionalOpen.Bool
	}

	// Fall back to default working hours
	var isOpen bool
	err = db.Conn.QueryRow(r.Context(), `
		SELECT is_open FROM working_hours WHERE weekday = $1
	`, weekday).Scan(&isOpen)

	return err == nil && isOpen
}

// findWeekSummaryRange returns the start and end of the working period
// before the current run of consecutive closed days.
// For adjacent closed days (e.g. Sat+Sun), both days show stats for the
// same working period (e.g. Mon-Fri).
func findWeekSummaryRange(r *http.Request, today time.Time) (time.Time, time.Time) {
	// Find the start of this run of consecutive closed days
	closedRunStart := today
	for i := 0; i <= 14; i++ {
		d := today.AddDate(0, 0, -i)
		if !isDayOpen(r, d) {
			closedRunStart = d
		} else {
			break
		}
	}

	// The working period ends the day before the closed run starts
	workingEnd := closedRunStart.AddDate(0, 0, -1)
	workingEndStart := time.Date(workingEnd.Year(), workingEnd.Month(), workingEnd.Day(), 0, 0, 0, 0, londonLocation)

	// Walk back from workingEnd to find where the previous closed run ended
	workingStart := workingEndStart
	for i := 0; i <= 14; i++ {
		d := workingEnd.AddDate(0, 0, -i)
		if !isDayOpen(r, d) {
			workingStart = d.AddDate(0, 0, 1) // day after the closed day
			workingStart = time.Date(workingStart.Year(), workingStart.Month(), workingStart.Day(), 0, 0, 0, 0, londonLocation)
			break
		}
	}

	// workingEndEnd is the end boundary (exclusive)
	workingEndEnd := workingEndStart.Add(24 * time.Hour)

	return workingStart, workingEndEnd
}

func getClosingTime(r *http.Request, date time.Time) string {
	weekday := int(date.Weekday())
	if weekday == 0 {
		weekday = 6
	} else {
		weekday -= 1
	}
	dateStr := date.Format("2006-01-02")

	// Check exceptional hours first
	var exceptionalClose sql.NullString
	err := db.Conn.QueryRow(r.Context(), `
		SELECT ewh.end_time
		FROM exceptional_working_hours ewh
		JOIN exceptional_working_hours_groups ewhg ON ewhg.id = ewh.group_id
		JOIN exceptional_group_applications ega ON ega.group_id = ewhg.id
		WHERE ewh.weekday = $1
		  AND ega.week_start <= $2::date
		  AND ega.week_start + INTERVAL '7 days' > $2::date
		  AND ewh.is_open = true
		LIMIT 1
	`, weekday, dateStr).Scan(&exceptionalClose)

	if err == nil && exceptionalClose.Valid {
		return exceptionalClose.String
	}

	// Fall back to default working hours
	var defaultClose sql.NullString
	err = db.Conn.QueryRow(r.Context(), `
		SELECT end_time FROM working_hours WHERE weekday = $1 AND is_open = true
	`, weekday).Scan(&defaultClose)

	if err == nil && defaultClose.Valid {
		return defaultClose.String
	}

	return ""
}

// Helper function to fetch a single appointment with all details
func fetchAppointment(r *http.Request, query string, args ...any) (*AppointmentInfo, error) {
	var bookingID string
	var startTime time.Time
	var status string
	var notes sql.NullString
	var userID string

	err := db.Conn.QueryRow(r.Context(), query, args...).Scan(
		&bookingID, &startTime, &status, &notes, &userID,
	)
	if err != nil {
		return nil, err
	}

	appointment := &AppointmentInfo{
		ID:        bookingID,
		StartTime: startTime,
		Status:    status,
	}

	if notes.Valid {
		appointment.Notes = &notes.String
	}

	// Fetch user info
	var user UserInfo
	var phone sql.NullString
	var email sql.NullString
	var profilePicURL sql.NullString

	err = db.Conn.QueryRow(r.Context(), `
		SELECT id, fn, phone, email, profile_pic_url
		FROM users
		WHERE id = $1
	`, userID).Scan(&user.ID, &user.FullName, &phone, &email, &profilePicURL)

	if err == nil {
		if phone.Valid {
			user.Phone = &phone.String
		}
		if profilePicURL.Valid {
			user.ProfilePicURL = &profilePicURL.String
		}
		if email.Valid {
			user.Email = &email.String
		}

		// Fetch unconsumed name history for former name display
		var prevFirstName, prevLastName sql.NullString
		err = db.Conn.QueryRow(r.Context(), `
			SELECT nh.previous_first_name, nh.previous_last_name
			FROM name_history nh
			WHERE nh.user_id = $1 AND nh.booking_id IS NULL
			ORDER BY nh.changed_at ASC
			LIMIT 1
		`, userID).Scan(&prevFirstName, &prevLastName)
		if err == nil && prevFirstName.Valid && prevLastName.Valid {
			user.PreviousFirstName = &prevFirstName.String
			user.PreviousLastName = &prevLastName.String
		}

		appointment.User = &user
	} else {
		log.Printf("Failed to fetch user %s: %v", userID, err)
	}

	// Fetch services
	serviceRows, err := db.Conn.Query(r.Context(), `
		SELECT
			s.name,
			s.description,
			COALESCE(bs.override_price, s.price) as price,
			COALESCE(bs.override_duration_minutes, s.duration_minutes) as duration_minutes
		FROM booking_services bs
		LEFT JOIN services s ON bs.service_id = s.id
		WHERE bs.booking_id = $1
		UNION ALL
		SELECT
			cs.name,
			cs.description,
			COALESCE(bcs.override_price, cs.price),
			COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
		FROM booking_custom_services bcs
		LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
		WHERE bcs.booking_id = $1
		ORDER BY name
	`, bookingID)

	if err != nil {
		log.Printf("Failed to fetch services for booking %s: %v", bookingID, err)
		return appointment, nil
	}
	defer serviceRows.Close()

	var totalAmount float64
	var totalDuration int

	for serviceRows.Next() {
		var service ServiceInfo
		var name string
		var description sql.NullString
		var price float64
		var duration int

		if err := serviceRows.Scan(&name, &description, &price, &duration); err != nil {
			log.Printf("Failed to scan service: %v", err)
			continue
		}

		service.ServiceName = &name
		if description.Valid {
			service.ServiceDescription = &description.String
		}
		service.Price = &price
		service.DurationMinutes = &duration

		totalAmount += price
		totalDuration += duration

		appointment.Services = append(appointment.Services, service)
	}

	appointment.TotalAmount = totalAmount
	appointment.DurationMinutes = totalDuration

	return appointment, nil
}

type TodayAppointment struct {
	ID                string   `json:"id"`
	StartTime         string   `json:"start_time"`
	Status            string   `json:"status"`
	UserName          string   `json:"user_name"`
	UserID            string   `json:"user_id"`
	Services          []string `json:"services"`
	DurationMinutes   int      `json:"duration_minutes"`
	PreviousFirstName *string  `json:"previous_first_name,omitempty"`
	PreviousLastName  *string  `json:"previous_last_name,omitempty"`
}

type TodayAppointmentsResponse struct {
	Appointments []TodayAppointment `json:"appointments"`
}

// GET /api/admin/today/appointments
func GetTodayAppointmentsHandler(w http.ResponseWriter, r *http.Request) {
	now := clock.Now()

	// Auto-transition confirmed bookings that have started but not ended to in_progress
	// Auto-transition in_progress bookings that have ended to completed
	tx, err := db.Conn.Begin(r.Context())
	if err != nil {
		log.Printf("Failed to begin transaction: %v", err)
		return
	}
	defer func() {
    if err := tx.Rollback(r.Context()); err != nil {
        slog.Error("failed to rollback transaction", "err", err)
    }
}()

	_, err = tx.Exec(r.Context(), `
		UPDATE bookings 
		SET status = 'in_progress'
		WHERE status = 'confirmed'
		  AND start_time <= $1
		  AND end_time > $1
	`, now)
	if err != nil {
		log.Printf("Failed to auto-transition bookings to in_progress: %v", err)
		return
	}

	_, err = tx.Exec(r.Context(), `
		UPDATE bookings 
		SET status = 'completed'
		WHERE status = 'in_progress'
		  AND end_time <= $1
	`, now)
	if err != nil {
		log.Printf("Failed to auto-transition bookings to completed: %v", err)
		return
	}

	if err := tx.Commit(r.Context()); err != nil {
		log.Printf("Failed to commit transaction: %v", err)
		return
	}

	londonNow := now.In(londonLocation)
	rangeStart := time.Date(londonNow.Year(), londonNow.Month(), londonNow.Day(), 0, 0, 0, 0, londonLocation).UTC()
	todayEnd := rangeStart.Add(24 * time.Hour)

	// Fetch all bookings for today
	rows, err := db.Conn.Query(r.Context(), `
		SELECT
			b.id,
			b.start_time,
			b.status,
			u.fn as user_name,
			u.id as user_id
		FROM bookings b
		LEFT JOIN users u ON b.user_id = u.id
		WHERE b.start_time >= $1
		  AND b.start_time < $2
		ORDER BY b.start_time ASC
	`, rangeStart, todayEnd)

	if err != nil {
		log.Printf("Failed to fetch today's appointments: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	type rawAppt struct {
		ID        string
		StartTime time.Time
		Status    string
		UserName  string
		UserID    string
	}
	var raw []rawAppt

	for rows.Next() {
		var a rawAppt
		if err := rows.Scan(&a.ID, &a.StartTime, &a.Status, &a.UserName, &a.UserID); err != nil {
			log.Printf("Failed to scan appointment row: %v", err)
			continue
		}
		raw = append(raw, a)
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error in GetTodayAppointments: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	rows.Close()

	if len(raw) == 0 {
		_ = json.NewEncoder(w).Encode(TodayAppointmentsResponse{Appointments: []TodayAppointment{}})
		return
	}

	// Batch-fetch services for ALL appointments in a single query.
	batchIDs := make([]string, len(raw))
	for i, a := range raw {
		batchIDs[i] = a.ID
	}
	svcRows, err := db.Conn.Query(r.Context(), `
		SELECT bs.booking_id, s.name, COALESCE(bs.override_duration_minutes, s.duration_minutes)
		FROM booking_services bs
		LEFT JOIN services s ON bs.service_id = s.id
		WHERE bs.booking_id = ANY($1)
		UNION ALL
		SELECT bcs.booking_id, cs.name, COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
		FROM booking_custom_services bcs
		LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
		WHERE bcs.booking_id = ANY($1)
		ORDER BY name
	`, batchIDs)
	if err != nil {
		log.Printf("Failed to batch-fetch services: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	svcMap := make(map[string][]string, len(raw))
	durMap := make(map[string]int, len(raw))
	for svcRows.Next() {
		var bid, name string
		var dur int
		if err := svcRows.Scan(&bid, &name, &dur); err != nil {
			log.Printf("Failed to scan service row: %v", err)
			continue
		}
		svcMap[bid] = append(svcMap[bid], name)
		durMap[bid] += dur
	}
	if err := svcRows.Err(); err != nil {
		log.Printf("Row iteration error in GetTodayAppointments service fetch: %v", err)
	}
	svcRows.Close()

	appointments := make([]TodayAppointment, 0, len(raw))
	for _, a := range raw {
		appointments = append(appointments, TodayAppointment{
			ID:              a.ID,
			StartTime:       a.StartTime.Format(time.RFC3339),
			Status:          a.Status,
			UserName:        a.UserName,
			UserID:          a.UserID,
			Services:        svcMap[a.ID],
			DurationMinutes: durMap[a.ID],
		})
	}

	{
		seenUserIDs := make(map[string]struct{})
		var userIDs []string
		for _, a := range appointments {
			if a.UserID != "" {
				if _, seen := seenUserIDs[a.UserID]; !seen {
					seenUserIDs[a.UserID] = struct{}{}
					userIDs = append(userIDs, a.UserID)
				}
			}
		}
		if len(userIDs) > 0 {
			nhRows, err := db.Conn.Query(r.Context(), `
				SELECT DISTINCT ON (user_id) user_id, previous_first_name, previous_last_name
				FROM name_history
				WHERE user_id = ANY($1) AND booking_id IS NULL
				ORDER BY user_id, changed_at ASC
			`, userIDs)
			if err == nil {
				prevByUser := make(map[string][2]string)
				for nhRows.Next() {
					var uid, pfn, pln string
					if err := nhRows.Scan(&uid, &pfn, &pln); err == nil {
						prevByUser[uid] = [2]string{pfn, pln}
					}
				}
				if err := nhRows.Err(); err != nil {
					log.Printf("Row iteration error in GetTodayAppointments name_history: %v", err)
				}
				nhRows.Close()
				for i := range appointments {
					if prev, ok := prevByUser[appointments[i].UserID]; ok {
						appointments[i].PreviousFirstName = &prev[0]
						appointments[i].PreviousLastName = &prev[1]
					}
				}
			}
		}
	}

	response := TodayAppointmentsResponse{
		Appointments: appointments,
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	if err := json.NewEncoder(w).Encode(response); err != nil {
		log.Printf("Failed to encode today's appointments response: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
}

type PendingApproval struct {
	ID                string   `json:"id"`
	StartTime         string   `json:"start_time"`
	UserID            string   `json:"user_id"`
	UserName          string   `json:"user_name"`
	PreviousFirstName *string  `json:"previous_first_name,omitempty"`
	PreviousLastName  *string  `json:"previous_last_name,omitempty"`
	Services          []string `json:"services"`
	DurationMinutes   int      `json:"duration_minutes"`
	CreatedAt         string   `json:"created_at"`
}

type PendingApprovalsResponse struct {
	Approvals []PendingApproval `json:"approvals"`
}

// GET /api/admin/today/pending-approvals
func GetPendingApprovalsHandler(w http.ResponseWriter, r *http.Request) {
	rows, err := db.Conn.Query(r.Context(), `
		SELECT
			b.id,
			b.start_time,
			b.created_at,
			u.id as user_id,
			u.fn as user_name
		FROM bookings b
		LEFT JOIN users u ON b.user_id = u.id
		WHERE b.status = 'pending'
		ORDER BY b.created_at ASC
	`)

	if err != nil {
		log.Printf("Failed to fetch pending approvals: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
	defer rows.Close()

	type rawApproval struct {
		id, userID, userName string
		startTime, createdAt time.Time
	}
	var raw []rawApproval
	var bookingIDs []string
	var userIDs []string
	seenUsers := make(map[string]struct{})

	for rows.Next() {
		var a rawApproval
		if err := rows.Scan(&a.id, &a.startTime, &a.createdAt, &a.userID, &a.userName); err != nil {
			log.Printf("Failed to scan pending approval row: %v", err)
			continue
		}
		raw = append(raw, a)
		bookingIDs = append(bookingIDs, a.id)
		if _, seen := seenUsers[a.userID]; !seen && a.userID != "" {
			seenUsers[a.userID] = struct{}{}
			userIDs = append(userIDs, a.userID)
		}
	}
	if err := rows.Err(); err != nil {
		log.Printf("Row iteration error in GetPendingApprovals: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}

	// Batch-fetch name_history for all users.
	prevByUser := make(map[string][2]string)
	if len(userIDs) > 0 {
		nhRows, err := db.Conn.Query(r.Context(), `
			SELECT DISTINCT ON (user_id) user_id, previous_first_name, previous_last_name
			FROM name_history
			WHERE user_id = ANY($1) AND booking_id IS NULL
			ORDER BY user_id, changed_at ASC
		`, userIDs)
		if err == nil {
			for nhRows.Next() {
				var uid, pfn, pln string
				if err := nhRows.Scan(&uid, &pfn, &pln); err == nil {
					prevByUser[uid] = [2]string{pfn, pln}
				}
			}
			if err := nhRows.Err(); err != nil {
				log.Printf("Row iteration error in GetPendingApprovals name_history: %v", err)
			}
			nhRows.Close()
		}
	}

	// Batch-fetch services for all bookings.
	svcMap := make(map[string][]string)
	durMap := make(map[string]int)
	if len(bookingIDs) > 0 {
		svcRows, err := db.Conn.Query(r.Context(), `
			SELECT booking_id, name, duration_minutes FROM (
				SELECT bs.booking_id, s.name, COALESCE(bs.override_duration_minutes, s.duration_minutes) AS duration_minutes
				FROM booking_services bs
				LEFT JOIN services s ON bs.service_id = s.id
				WHERE bs.booking_id = ANY($1)
				UNION ALL
				SELECT bcs.booking_id, cs.name, COALESCE(bcs.override_duration_minutes, cs.duration_minutes)
				FROM booking_custom_services bcs
				LEFT JOIN custom_services cs ON bcs.custom_service_id = cs.id
				WHERE bcs.booking_id = ANY($1)
			) sub ORDER BY name
		`, bookingIDs)
		if err == nil {
			for svcRows.Next() {
				var bid, name string
				var dur int
				if err := svcRows.Scan(&bid, &name, &dur); err == nil {
					svcMap[bid] = append(svcMap[bid], name)
					durMap[bid] += dur
				}
			}
			svcRows.Close()
		}
	}

	approvals := make([]PendingApproval, 0, len(raw))
	for _, a := range raw {
		prevFirstName, prevLastName := "", ""
		if p, ok := prevByUser[a.userID]; ok {
			prevFirstName = p[0]
			prevLastName = p[1]
		}
		apt := PendingApproval{
			ID:              a.id,
			StartTime:       a.startTime.Format(time.RFC3339),
			CreatedAt:       a.createdAt.Format(time.RFC3339),
			UserID:          a.userID,
			UserName:        a.userName,
			Services:        svcMap[a.id],
			DurationMinutes: durMap[a.id],
		}
		if prevFirstName != "" {
			apt.PreviousFirstName = &prevFirstName
		}
		if prevLastName != "" {
			apt.PreviousLastName = &prevLastName
		}
		approvals = append(approvals, apt)
	}

	if approvals == nil {
		approvals = []PendingApproval{}
	}

	response := PendingApprovalsResponse{
		Approvals: approvals,
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	if err := json.NewEncoder(w).Encode(response); err != nil {
		log.Printf("Failed to encode pending approvals response: %v", err)
		http.Error(w, "Internal server error", http.StatusInternalServerError)
		return
	}
}
