package ws

import (
	"context"
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"doormile/constants"
	"doormile/db"
	"doormile/models"
	"doormile/utils"

	"github.com/gofiber/websocket/v2"
)

// trackingFrame is the JSON payload pushed to the client every 2 seconds.
type trackingFrame struct {
	Lat        float64 `json:"lat"`
	Lon        float64 `json:"lon"`
	EtaMinutes float64 `json:"eta_minutes"`
	Status     string  `json:"status"`
	MilerName  string  `json:"miler_name"`
}

// TrackingHandler streams live miler location for a booking over WebSocket.
//
// Route: GET /ws/bookings/:bookingid/track  (no auth — public tracking link)
//
// The handler:
//  1. Validates the booking ID and loads the booking.
//  2. Spawns a reader goroutine that closes `done` on client disconnect.
//  3. Every 2 s: re-fetches booking status, reads miler GPS from Redis,
//     computes ETA (haversine miler→pickup, 20 km/h urban average), and
//     sends a JSON frame.
//  4. Exits (closing the WS) when status is Picked_Up, Converted_To_Consignment,
//     or Cancelled, or when the client disconnects.
func TrackingHandler(c *websocket.Conn) {
	bookingID, err := strconv.Atoi(c.Params("bookingid"))
	if err != nil {
		sendError(c, "invalid booking ID")
		return
	}

	// Initial booking load — fail fast if it doesn't exist.
	var booking models.PickupBooking
	if err := db.DB.First(&booking, bookingID).Error; err != nil {
		sendError(c, "booking not found")
		return
	}

	// Reader goroutine: detect client disconnect via any read error.
	done := make(chan struct{})
	go func() {
		for {
			if _, _, err := c.ReadMessage(); err != nil {
				close(done)
				return
			}
		}
	}()

	// Cache miler display names to avoid redundant DB hits each tick.
	nameCache := make(map[int]string)
	milerName := func(userID int) string {
		if n, ok := nameCache[userID]; ok {
			return n
		}
		var p models.MilerProfile
		if err := db.DB.Where("userid = ?", userID).First(&p).Error; err == nil {
			nameCache[userID] = p.Displayname
			return p.Displayname
		}
		return "Miler"
	}

	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()

	for {
		select {
		case <-done:
			return

		case <-ticker.C:
			// Re-fetch booking on every tick so status changes are reflected.
			if err := db.DB.First(&booking, bookingID).Error; err != nil {
				utils.Warn("WS/Tracking: failed to reload booking", "booking_id", bookingID, "error", err)
				return
			}

			// Terminal state — send one final frame then close.
			if isTerminalStatus(booking.Status) {
				frame := trackingFrame{Status: booking.Status}
				if booking.Assignedmileruserid != nil {
					frame.MilerName = milerName(*booking.Assignedmileruserid)
				}
				sendJSON(c, frame)
				return
			}

			// No miler assigned yet — send status-only frame and keep waiting.
			if booking.Assignedmileruserid == nil {
				if err := sendJSON(c, trackingFrame{Status: booking.Status}); err != nil {
					return
				}
				continue
			}

			milerUserID := *booking.Assignedmileruserid
			lat, lon, gpsOk := readMilerGPS(milerUserID)

			// Redis key expired or not yet set — fall back to last known DB coords.
			if !gpsOk {
				var mp models.MilerProfile
				if db.DB.Where("userid = ?", milerUserID).First(&mp).Error == nil &&
					(mp.Currentlatitude != 0 || mp.Currentlongitude != 0) {
					lat = mp.Currentlatitude
					lon = mp.Currentlongitude
					gpsOk = true
				}
			}

			frame := trackingFrame{
				Status:    booking.Status,
				MilerName: milerName(milerUserID),
			}
			if gpsOk {
				frame.Lat = lat
				frame.Lon = lon
				frame.EtaMinutes = haversineETA(
					lat, lon,
					booking.Pickuplatitude, booking.Pickuplongitude,
				)
			}

			if err := sendJSON(c, frame); err != nil {
				return
			}
		}
	}
}

// ─── helpers ─────────────────────────────────────────────────────────────────

func isTerminalStatus(status string) bool {
	return status == constants.BookingPickedUp ||
		status == constants.BookingConvertedConsignment ||
		status == constants.BookingCancelled
}

// readMilerGPS fetches the miler's last-known position from Redis.
// The key "miler:gps:{id}" is written by UpdateMilerLocation as "{lat},{lon}".
func readMilerGPS(milerUserID int) (lat, lon float64, ok bool) {
	if db.Rdb == nil {
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	val, err := db.Rdb.Get(ctx, fmt.Sprintf("miler:gps:%d", milerUserID)).Result()
	if err != nil {
		return
	}

	parts := strings.SplitN(strings.TrimSpace(val), ",", 2)
	if len(parts) != 2 {
		return
	}

	lat, err = strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)
	if err != nil {
		return
	}
	lon, err = strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)
	if err != nil {
		return
	}

	return lat, lon, true
}

// haversineETA returns the estimated arrival time in minutes from (milerLat, milerLon)
// to (destLat, destLon), assuming 20 km/h average urban speed plus a 2-minute buffer.
func haversineETA(milerLat, milerLon, destLat, destLon float64) float64 {
	const earthRadiusKm = 6371.0
	dLat := (destLat - milerLat) * math.Pi / 180.0
	dLon := (destLon - milerLon) * math.Pi / 180.0
	a := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(milerLat*math.Pi/180.0)*math.Cos(destLat*math.Pi/180.0)*
			math.Sin(dLon/2)*math.Sin(dLon/2)
	distKm := earthRadiusKm * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
	return math.Round((distKm/20.0)*60.0 + 2.0)
}

// sendJSON marshals v and writes it as a text WebSocket frame.
// Returns a non-nil error only when the write fails (client gone).
func sendJSON(c *websocket.Conn, v any) error {
	data, err := json.Marshal(v)
	if err != nil {
		return nil // marshal failure is a code bug, not a client issue
	}
	return c.WriteMessage(websocket.TextMessage, data)
}

// sendError sends a single error frame and ignores the write result
// (the handler is about to return regardless).
func sendError(c *websocket.Conn, msg string) {
	data, _ := json.Marshal(map[string]string{"error": msg})
	c.WriteMessage(websocket.TextMessage, data) //nolint:errcheck
}
