package controllers

import (
	"crypto/rand"
	"encoding/json"
	"fmt"
	"math"
	"strconv"
	"time"

	"doormile/config"
	"doormile/constants"
	"doormile/db"
	"doormile/dto"
	"doormile/internal/assignment"
	"doormile/models"
	"doormile/utils"

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

func generateBookingNo() string {
	b := make([]byte, 4)
	rand.Read(b)
	return fmt.Sprintf("DM-BK-%X-%d", b, time.Now().Unix()%100000)
}

func calculateDistance(lat1, lon1, lat2, lon2 float64) float64 {
	const R = 6371.0
	dLat := (lat2 - lat1) * math.Pi / 180.0
	dLon := (lon2 - lon1) * math.Pi / 180.0
	a := math.Sin(dLat/2)*math.Sin(dLat/2) +
		math.Cos(lat1*math.Pi/180.0)*math.Cos(lat2*math.Pi/180.0)*
			math.Sin(dLon/2)*math.Sin(dLon/2)
	c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
	return R * c
}

func calculateVolumetricWeight(length, width, height float64) float64 {
	return (length * width * height) / 5000.0
}

func RegisterCustomer(cfg *config.Config) fiber.Handler {
	return func(c *fiber.Ctx) error {
		req := new(dto.CustomerRegisterRequest)
		if err := c.BodyParser(req); err != nil {
			return utils.BadRequest(c, "invalid request body")
		}

		if req.Phone == "" || req.Firstname == "" || req.Pin == "" {
			return utils.BadRequest(c, "phone, firstname, and pin are required")
		}

		pinHash, err := utils.HashPassword(req.Pin)
		if err != nil {
			return utils.Internal(c, "failed to process registration")
		}

		configID := req.Configid
		if configID == 0 {
			configID = 1001
		}

		var existing models.AppCustomer
		if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&existing).Error; err == nil {
			return utils.Conflict(c, "a customer with this phone number already exists")
		}

		customer := models.AppCustomer{
			Firstname:    req.Firstname,
			Lastname:     req.Lastname,
			Phone:        req.Phone,
			Email:        req.Email,
			Loginpinhash: pinHash,
			Status:       "Active",
			Configid:     configID,
		}

		if err := db.DB.Create(&customer).Error; err != nil {
			return utils.Internal(c, "failed to register customer")
		}

		token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
		if err != nil {
			return utils.Internal(c, "registration successful but failed to generate token")
		}

		return c.Status(fiber.StatusCreated).JSON(fiber.Map{
			"success": true,
			"token":   token,
			"user":    customer,
		})
	}
}

func LoginCustomer(c *fiber.Ctx) error {
	req := new(dto.CustomerLoginRequest)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Phone == "" {
		return utils.BadRequest(c, "phone is required")
	}

	configID := req.Configid
	if configID == 0 {
		configID = 1001
	}

	var customer models.AppCustomer
	if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
		return utils.NotFound(c, "no account found for this phone number")
	}

	if customer.Status == "Blocked" {
		return utils.Forbidden(c, "this account has been blocked")
	}

	return c.JSON(fiber.Map{
		"success": true,
		"message": "PIN verification required",
		"phone":   req.Phone,
	})
}

func VerifyCustomerPin(cfg *config.Config) fiber.Handler {
	return func(c *fiber.Ctx) error {
		req := new(dto.CustomerPinVerifyRequest)
		if err := c.BodyParser(req); err != nil {
			return utils.BadRequest(c, "invalid request body")
		}

		if req.Phone == "" || req.Pin == "" {
			return utils.BadRequest(c, "phone and pin are required")
		}

		configID := req.Configid
		if configID == 0 {
			configID = 1001
		}

		var customer models.AppCustomer
		if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
			return utils.NotFound(c, "customer not found")
		}

		if !utils.CheckPasswordHash(req.Pin, customer.Loginpinhash) {
			return utils.Unauthorized(c, "incorrect PIN")
		}

		now := time.Now()
		customer.Lastloginat = &now
		if req.DeviceToken != "" {
			customer.Devicetoken = req.DeviceToken
		}
		db.DB.Save(&customer)

		token, err := utils.GenerateToken(customer.Appcustomerid, customer.Phone, 9, 0, customer.Configid, cfg.JWTSecret)
		if err != nil {
			return utils.Internal(c, "failed to generate token")
		}

		return c.JSON(fiber.Map{
			"success": true,
			"token":   token,
			"user":    customer,
		})
	}
}

func ResetCustomerPin(c *fiber.Ctx) error {
	req := new(dto.CustomerResetPinRequest)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Phone == "" || req.NewPin == "" {
		return utils.BadRequest(c, "phone and new_pin are required")
	}

	configID := req.Configid
	if configID == 0 {
		configID = 1001
	}

	var customer models.AppCustomer
	if err := db.DB.Where("phone = ? AND configid = ?", req.Phone, configID).First(&customer).Error; err != nil {
		return utils.NotFound(c, "customer not found")
	}

	pinHash, err := utils.HashPassword(req.NewPin)
	if err != nil {
		return utils.Internal(c, "failed to process PIN reset")
	}

	customer.Loginpinhash = pinHash
	if err := db.DB.Save(&customer).Error; err != nil {
		return utils.Internal(c, "failed to reset PIN")
	}

	return utils.Message(c, "PIN reset successfully")
}

func GetCustomerProfile(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var customer models.AppCustomer
	if err := db.DB.First(&customer, customerID).Error; err != nil {
		return utils.NotFound(c, "profile not found")
	}

	return utils.OK(c, customer)
}

func UpdateCustomerProfile(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var customer models.AppCustomer
	if err := db.DB.First(&customer, customerID).Error; err != nil {
		return utils.NotFound(c, "profile not found")
	}

	type ProfileUpdate struct {
		Firstname        string  `json:"firstname"`
		Lastname         string  `json:"lastname"`
		Email            string  `json:"email"`
		Defaultlatitude  float64 `json:"defaultlatitude"`
		Defaultlongitude float64 `json:"defaultlongitude"`
		Defaultpincode   string  `json:"defaultpincode"`
	}

	req := new(ProfileUpdate)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Firstname != "" {
		customer.Firstname = req.Firstname
	}
	customer.Lastname = req.Lastname
	customer.Email = req.Email
	if req.Defaultlatitude != 0 {
		customer.Defaultlatitude = req.Defaultlatitude
	}
	if req.Defaultlongitude != 0 {
		customer.Defaultlongitude = req.Defaultlongitude
	}
	if req.Defaultpincode != "" {
		customer.Defaultpincode = req.Defaultpincode
	}

	if err := db.DB.Save(&customer).Error; err != nil {
		return utils.Internal(c, "failed to update profile")
	}

	return utils.OK(c, customer)
}

func GetCustomerLocations(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var locations []models.AppCustomerLocation
	if err := db.DB.Where("appcustomerid = ? AND status = ?", customerID, "Active").Find(&locations).Error; err != nil {
		return utils.Internal(c, "failed to fetch locations")
	}

	return utils.List(c, locations, int64(len(locations)))
}

func CreateCustomerLocation(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var count int64
	db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ? AND status = ?", customerID, "Active").Count(&count)
	if count >= 10 {
		return utils.BadRequest(c, "maximum of 10 saved locations allowed")
	}

	req := new(dto.LocationCreateRequest)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Address == "" || req.Pincode == "" || req.Latitude == 0 || req.Longitude == 0 {
		return utils.BadRequest(c, "address, pincode, latitude, and longitude are required")
	}

	if req.Isdefault {
		db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
	}

	location := models.AppCustomerLocation{
		Appcustomerid: customerID,
		Label:         req.Label,
		Receivername:  req.Receivername,
		Receiverphone: req.Receiverphone,
		Address:       req.Address,
		Landmark:      req.Landmark,
		City:          req.City,
		State:         req.State,
		Pincode:       req.Pincode,
		Latitude:      req.Latitude,
		Longitude:     req.Longitude,
		Isdefault:     req.Isdefault,
		Status:        "Active",
	}

	if err := db.DB.Create(&location).Error; err != nil {
		return utils.Internal(c, "failed to save location")
	}

	return utils.Created(c, location)
}

func UpdateCustomerLocation(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)
	locationID, err := strconv.Atoi(c.Params("id"))
	if err != nil {
		return utils.BadRequest(c, "invalid location ID")
	}

	var location models.AppCustomerLocation
	if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
		return utils.NotFound(c, "location not found")
	}

	req := new(dto.LocationCreateRequest)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Label != "" {
		location.Label = req.Label
	}
	location.Receivername = req.Receivername
	location.Receiverphone = req.Receiverphone
	if req.Address != "" {
		location.Address = req.Address
	}
	location.Landmark = req.Landmark
	location.City = req.City
	location.State = req.State
	if req.Pincode != "" {
		location.Pincode = req.Pincode
	}
	if req.Latitude != 0 {
		location.Latitude = req.Latitude
	}
	if req.Longitude != 0 {
		location.Longitude = req.Longitude
	}
	location.Isdefault = req.Isdefault

	if req.Isdefault {
		db.DB.Model(&models.AppCustomerLocation{}).Where("appcustomerid = ?", customerID).Update("isdefault", false)
	}

	if err := db.DB.Save(&location).Error; err != nil {
		return utils.Internal(c, "failed to update location")
	}

	return utils.OK(c, location)
}

func DeleteCustomerLocation(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)
	locationID, err := strconv.Atoi(c.Params("id"))
	if err != nil {
		return utils.BadRequest(c, "invalid location ID")
	}

	var location models.AppCustomerLocation
	if err := db.DB.Where("appcustomerlocationid = ? AND appcustomerid = ?", locationID, customerID).First(&location).Error; err != nil {
		return utils.NotFound(c, "location not found")
	}

	location.Status = "InActive"
	db.DB.Save(&location)

	return utils.Message(c, "location deleted successfully")
}

func CreateCustomerBooking(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	req := new(dto.PickupBookingRequest)
	if err := c.BodyParser(req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}

	if req.Pickupaddress == "" || req.Pickuppincode == "" {
		return utils.BadRequest(c, "pickup address and pincode are required")
	}

	if len(req.Parcels) == 0 {
		return utils.BadRequest(c, "at least one parcel is required")
	}

	// Geocode delivery pincode to lat/lon when the app doesn't supply coordinates.
	if req.Deliverylatitude == 0 && req.Deliverylongitude == 0 && req.Deliverypincode != "" {
		if lat, lon, ok := pincodeToLatLon(req.Deliverypincode); ok {
			req.Deliverylatitude = lat
			req.Deliverylongitude = lon
		}
	}

	tx := db.DB.Begin()

	booking := models.PickupBooking{
		Bookingno:           generateBookingNo(),
		Appcustomerid:       customerID,
		Pickuplocationid:    req.Pickuplocationid,
		Pickupaddress:       req.Pickupaddress,
		Pickuppincode:       req.Pickuppincode,
		Pickuplatitude:      req.Pickuplatitude,
		Pickuplongitude:     req.Pickuplongitude,
		Deliveryaddress:     req.Deliveryaddress,
		Deliverypincode:     req.Deliverypincode,
		Deliverylatitude:    req.Deliverylatitude,
		Deliverylongitude:   req.Deliverylongitude,
		Bookingsource:       "Customer_App",
		Status:              constants.BookingPendingPickup,
		Preferredpickupfrom: req.Preferredpickupfrom,
		Preferredpickupto:   req.Preferredpickupto,
	}

	if err := tx.Create(&booking).Error; err != nil {
		tx.Rollback()
		return utils.Internal(c, "failed to create booking")
	}

	var totalWeight float64
	var totalVolume float64
	var requiresLargeVehicle bool
	for _, p := range req.Parcels {
		volumetric := calculateVolumetricWeight(p.Length, p.Width, p.Height)
		totalWeight += math.Max(p.Weight, volumetric)
		totalVolume += p.Length * p.Width * p.Height

		parcel := models.BookingParcel{
			Bookingid:            booking.Bookingid,
			Itemcategory:         p.Itemcategory,
			Itemdescription:      p.Itemdescription,
			Declaredvalue:        p.Declaredvalue,
			Weight:               p.Weight,
			Length:               p.Length,
			Width:                p.Width,
			Height:               p.Height,
			Isfragile:            p.Isfragile,
			Needsinsurance:       p.Needsinsurance,
			Requireslargevehicle: p.Requireslargevehicle,
		}

		if p.Requireslargevehicle {
			requiresLargeVehicle = true
		}

		if p.Needsinsurance {
			parcel.Insuranceamount = p.Declaredvalue * 0.01
		}

		if err := tx.Create(&parcel).Error; err != nil {
			tx.Rollback()
			return utils.Internal(c, "failed to save parcel details")
		}
	}

	serviceType := req.ServiceOption
	if serviceType == "" {
		serviceType = "Normal"
	}

	zone := resolveZone(req.Pickuppincode, req.Deliverypincode)
	itemCategory := normalizePricingCategory(req.Parcels[0].Itemcategory)

	var estimatedPrice float64
	var pricingID *int
	if price, pid, found := lookupDoormilePrice(zone, mapServiceTypeToPricing(serviceType), totalWeight, itemCategory); found {
		estimatedPrice = price
		pricingID = pid
	} else {
		var distance float64
		if booking.Deliverylatitude != 0 && booking.Deliverylongitude != 0 {
			distance = calculateDistance(booking.Pickuplatitude, booking.Pickuplongitude, booking.Deliverylatitude, booking.Deliverylongitude)
		}
		estimatedPrice = 50.0 + (distance * 5.0) + (totalWeight * 10.0)
	}

	now := time.Now()
	estDelivery := now.Add(24 * time.Hour)
	slaDue := now.Add(36 * time.Hour)
	if serviceType == "Fast" {
		estDelivery = now.Add(12 * time.Hour)
		slaDue = now.Add(18 * time.Hour)
	} else if serviceType == "Superfast" {
		estDelivery = now.Add(6 * time.Hour)
		slaDue = now.Add(9 * time.Hour)
	}

	srvOption := models.BookingServiceOption{
		Bookingid:           booking.Bookingid,
		Servicetype:         serviceType,
		Estimatedprice:      estimatedPrice,
		Pricingid:           pricingID,
		Estimateddeliveryat: &estDelivery,
		Sladueat:            &slaDue,
	}

	if err := tx.Create(&srvOption).Error; err != nil {
		tx.Rollback()
		return utils.Internal(c, "failed to save service option")
	}

	if requiresLargeVehicle || totalVolume > 0 && totalWeight > 20.0 {
		reqVeh := models.BookingVehicleRequirement{
			Bookingid:           booking.Bookingid,
			Requiredvehicletype: "truck",
			Reason:              "Oversized package / heavy weight",
			Status:              "Required",
		}
		tx.Create(&reqVeh)
	}

	tx.Commit()

	go assignment.AssignCustomerMiler(booking.Bookingid)

	if db.Js != nil {
		payload := map[string]interface{}{
			"booking_id":       booking.Bookingid,
			"booking_no":       booking.Bookingno,
			"customer_id":      booking.Appcustomerid,
			"pickup_address":   booking.Pickupaddress,
			"pickup_pincode":   booking.Pickuppincode,
			"delivery_address": booking.Deliveryaddress,
			"delivery_pincode": booking.Deliverypincode,
			"status":           constants.BookingPendingPickup,
			"created_at":       time.Now().UnixMilli(),
		}
		if data, err := json.Marshal(payload); err == nil {
			if _, err := db.Js.Publish("api.v1.bookings.create", data); err != nil {
				utils.Warn("Failed to publish booking.create to NATS", "booking_id", booking.Bookingid, "error", err)
			}
		}
	}

	db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, booking.Bookingid)

	return utils.Created(c, booking)
}

func GetCustomerBookings(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var bookings []models.PickupBooking
	if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Where("appcustomerid = ?", customerID).Order("createdat DESC").Find(&bookings).Error; err != nil {
		return utils.Internal(c, "failed to fetch bookings")
	}

	return utils.List(c, bookings, int64(len(bookings)))
}

func GetCustomerBookingDetails(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)
	bookingID, err := strconv.Atoi(c.Params("bookingid"))
	if err != nil {
		return utils.BadRequest(c, "invalid booking ID")
	}

	var booking models.PickupBooking
	if err := db.DB.Preload("Parcels").Preload("ServiceOptions").Preload("Payments").Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
		return utils.NotFound(c, "booking not found")
	}

	return utils.OK(c, booking)
}

func CancelCustomerBooking(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)
	bookingID, err := strconv.Atoi(c.Params("bookingid"))
	if err != nil {
		return utils.BadRequest(c, "invalid booking ID")
	}

	var booking models.PickupBooking
	if err := db.DB.Where("bookingid = ? AND appcustomerid = ?", bookingID, customerID).First(&booking).Error; err != nil {
		return utils.NotFound(c, "booking not found")
	}

	if booking.Status == constants.BookingPickedUp || booking.Status == constants.BookingConvertedConsignment {
		return utils.BadRequest(c, "booking cannot be cancelled after the package has been picked up")
	}

	booking.Status = constants.BookingCancelled
	booking.Updatedat = time.Now()
	db.DB.Save(&booking)

	if db.Js != nil {
		payload := map[string]interface{}{
			"booking_id":   booking.Bookingid,
			"booking_no":   booking.Bookingno,
			"customer_id":  booking.Appcustomerid,
			"status":       "Cancelled",
			"cancelled_at": time.Now().UnixMilli(),
		}
		if data, err := json.Marshal(payload); err == nil {
			if _, err := db.Js.Publish("api.v1.bookings.cancel", data); err != nil {
				utils.Warn("Failed to publish booking.cancel to NATS", "booking_id", booking.Bookingid, "error", err)
			}
		}
	}

	return utils.OK(c, booking)
}

func GetCustomerBookingQuote(c *fiber.Ctx) error {
	bookingID, err := strconv.Atoi(c.Params("bookingid"))
	if err != nil {
		return utils.BadRequest(c, "invalid booking ID")
	}

	var serviceOpt models.BookingServiceOption
	if err := db.DB.Where("bookingid = ?", bookingID).Order("createdat DESC").First(&serviceOpt).Error; err != nil {
		return utils.NotFound(c, "price quote not found for this booking")
	}

	return utils.OK(c, serviceOpt)
}

func TrackConsignment(c *fiber.Ctx) error {
	trackingNo := c.Params("trackingno")
	if trackingNo == "" {
		return utils.BadRequest(c, "tracking number is required")
	}

	var consignment models.Consignment
	if err := db.DB.Where("trackingno = ?", trackingNo).First(&consignment).Error; err != nil {
		return utils.NotFound(c, "no shipment found for this tracking number")
	}

	var history []models.ConsignmentHistory
	db.DB.Where("consignmentid = ?", consignment.Consignmentid).Order("createdat DESC").Find(&history)

	return utils.OK(c, fiber.Map{
		"consignment": consignment,
		"history":     history,
	})
}

func SaveCustomerDeviceToken(c *fiber.Ctx) error {
	customerID := c.Locals("userid").(int)

	var req struct {
		DeviceToken string `json:"device_token"`
	}
	if err := c.BodyParser(&req); err != nil {
		return utils.BadRequest(c, "invalid request body")
	}
	if req.DeviceToken == "" {
		return utils.BadRequest(c, "device_token is required")
	}

	if err := db.DB.Model(&models.AppCustomer{}).
		Where("appcustomerid = ?", customerID).
		Update("device_token", req.DeviceToken).Error; err != nil {
		return utils.Internal(c, "failed to save device token")
	}

	return utils.Message(c, "device token saved")
}
