package assignment

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

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

// providerResult holds the chosen logistics provider and their pricing details.
type providerResult struct {
	company        string
	estimatedPrice float64
	reliability    float64
}

// AssignCustomerMiler is the B2C goroutine entry point.
// It selects both a miler (first-mile pickup) and a provider (delivery routing),
// then commits the assignment. Retries up to maxRetries times with retryDelay in between.
// Must be called as a goroutine after tx.Commit() in CreateCustomerBooking.
func AssignCustomerMiler(bookingID int) {
	defer func() {
		if r := recover(); r != nil {
			utils.Error("B2CAssignment: panic recovered", "booking_id", bookingID, "error", r)
		}
	}()

	for attempt := 1; attempt <= maxRetries; attempt++ {
		if attempt > 1 {
			time.Sleep(retryDelay)
		}

		utils.Info("B2CAssignment: attempting assignment", "booking_id", bookingID, "attempt", attempt)

		done, err := tryCustomerAssign(bookingID)
		if err != nil {
			utils.Error("B2CAssignment: attempt error", "booking_id", bookingID, "attempt", attempt, "error", err)
			continue
		}
		if done {
			return
		}

		utils.Warn("B2CAssignment: no eligible miler on attempt",
			"booking_id", bookingID,
			"attempt", attempt,
			"remaining", maxRetries-attempt,
		)
	}

	utils.Error("B2CAssignment: NO_MILER_AVAILABLE — all retries exhausted",
		"booking_id", bookingID,
		"max_retries", maxRetries,
	)
}

// tryCustomerAssign performs one full attempt: GEO miler search → provider selection → commit.
// Returns (true, nil) on success or when the booking no longer needs assignment.
// Returns (false, nil) when no eligible miler was found (retry warranted).
// Returns (false, err) on hard errors.
func tryCustomerAssign(bookingID int) (bool, error) {
	var booking models.PickupBooking
	if err := db.DB.Preload("Parcels").Preload("ServiceOptions").First(&booking, bookingID).Error; err != nil {
		return false, fmt.Errorf("load booking: %w", err)
	}

	if booking.Status == constants.BookingCancelled || booking.Assignedmileruserid != nil {
		utils.Info("B2CAssignment: booking no longer needs assignment",
			"booking_id", bookingID,
			"status", booking.Status,
		)
		return true, nil
	}

	if booking.Pickuplatitude == 0 || booking.Pickuplongitude == 0 {
		return false, fmt.Errorf("booking %d has no pickup coordinates", bookingID)
	}

	// Step 1 — Find the best nearby miler via AI decision engine (GEORADIUS unchanged).
	nearby, err := queryNearbyMilers(booking.Pickuplatitude, booking.Pickuplongitude)
	if err != nil {
		utils.Warn("B2CAssignment: GEO query failed", "booking_id", bookingID, "error", err)
		return false, nil
	}
	if len(nearby) == 0 {
		return false, nil
	}

	miler, agentDecisionID, found := selectMilerWithAI(&booking, nearby)
	if !found {
		return false, nil
	}

	// Step 2 — Select the best logistics provider from DoormilePricing.
	zone := b2cResolveZone(booking.Pickuppincode, booking.Deliverypincode)
	category := b2cNormalizePricingCategory(b2cFirstParcelCategory(&booking))
	serviceType := b2cFirstServiceType(&booking)
	weight := b2cChargeableWeight(&booking)

	provider, providerFound := selectBestProvider(zone, category, serviceType, weight)
	if !providerFound {
		utils.Warn("B2CAssignment: no provider pricing match, defaulting to Doormile",
			"booking_id", bookingID,
			"zone", zone,
			"category", category,
		)
		provider = providerResult{company: "Doormile"}
	}

	// Step 3 — ETA based on miler-to-pickup distance.
	etaMinutes := calculateETA(miler.distanceKm)

	// Steps 4–6 — Commit to DB and publish to NATS.
	if err := commitCustomerAssignment(&booking, miler, provider, etaMinutes, agentDecisionID); err != nil {
		return false, fmt.Errorf("commit: %w", err)
	}

	return true, nil
}

// ─── Provider selection ───────────────────────────────────────────────────────

// selectBestProvider queries all active DoormilePricing rows that cover the given
// zone / serviceType / weight and picks the provider with the lowest composite score.
//
// Scoring (lower = better):
//
//	score = avg_price - reliability_score * 2.0
//
// A reliability point is worth 2 rupees — price dominates for large spreads, but
// a highly reliable provider can edge out a marginally cheaper one.
func selectBestProvider(zone, category, serviceType string, weight float64) (providerResult, bool) {
	pricingServiceType := b2cMapServiceTypeToPricing(serviceType)

	var rules []models.DoormilePricing
	db.DB.Where(
		"zone = ? AND servicetype = ? AND status = ? AND deletedat IS NULL"+
			" AND min_weight <= ? AND max_weight >= ?",
		zone, pricingServiceType, "Active", weight, weight,
	).Find(&rules)

	if len(rules) == 0 {
		return providerResult{}, false
	}

	// Prefer exact category match; fall back to General; use all rows as last resort.
	matched := b2cFilterByCategory(rules, category)
	if len(matched) == 0 && category != "General" {
		matched = b2cFilterByCategory(rules, "General")
	}
	if len(matched) == 0 {
		matched = rules
	}

	var best *providerResult
	bestScore := math.MaxFloat64

	for _, rule := range matched {
		avgPrice := (rule.Minprice + rule.Maxprice) / 2.0
		score := avgPrice - rule.Reliabilityscore*2.0

		if score < bestScore {
			bestScore = score
			r := providerResult{
				company:        rule.Providercompany,
				estimatedPrice: avgPrice,
				reliability:    rule.Reliabilityscore,
			}
			best = &r
		}
	}

	if best == nil {
		return providerResult{}, false
	}
	return *best, true
}

// ─── ETA ─────────────────────────────────────────────────────────────────────

// calculateETA returns the estimated arrival time in minutes.
// Formula: (miler_to_pickup_distance_km / 20.0) * 60 + 10-minute pickup buffer.
func calculateETA(distanceKm float64) float64 {
	return math.Round((distanceKm/20.0)*60.0 + 10.0)
}

// ─── Commit ──────────────────────────────────────────────────────────────────

// commitCustomerAssignment writes the full assignment in a single DB transaction:
//   - BookingAssignment row (status: Assigned)
//   - PickupBooking: assignedmileruserid, status → Miler_Assigned, providercompany (if found)
//   - MilerProfile: availabilitystatus → Assigned
//
// Publishes to NATS after a successful commit.
func commitCustomerAssignment(
	booking *models.PickupBooking,
	miler *milerCandidate,
	provider providerResult,
	etaMinutes float64,
	agentDecisionID *uint64,
) error {
	milerUserID := miler.profile.Userid

	tx := db.DB.Begin()

	ba := models.BookingAssignment{
		Bookingid:        booking.Bookingid,
		Mileruserid:      milerUserID,
		Assignmentstatus: constants.AssignmentAssigned,
		Assignedat:       time.Now(),
		AgentDecisionID:  agentDecisionID,
	}
	if err := tx.Create(&ba).Error; err != nil {
		tx.Rollback()
		return fmt.Errorf("create BookingAssignment: %w", err)
	}

	bookingUpdates := map[string]interface{}{
		"assignedmileruserid": milerUserID,
		"status":              constants.BookingMilerAssigned,
		"updatedat":           time.Now(),
	}
	if provider.company != "" {
		bookingUpdates["providercompany"] = provider.company
	}

	if err := tx.Model(booking).Updates(bookingUpdates).Error; err != nil {
		tx.Rollback()
		return fmt.Errorf("update PickupBooking: %w", err)
	}

	if err := tx.Model(&models.MilerProfile{}).
		Where("userid = ?", milerUserID).
		Update("availabilitystatus", constants.MilerAssigned).Error; err != nil {
		tx.Rollback()
		return fmt.Errorf("update MilerProfile availability: %w", err)
	}

	tx.Commit()

	utils.Info("B2CAssignment: assigned",
		"booking_id", booking.Bookingid,
		"miler_id", milerUserID,
		"provider", provider.company,
		"estimated_price", provider.estimatedPrice,
		"eta_minutes", etaMinutes,
		"miler_distance_km", miler.distanceKm,
		"agent_decision_id", agentDecisionID,
	)

	publishCustomerAssignment(booking, milerUserID, provider, etaMinutes)
	notifyMilerNewAssignment(miler.profile, booking.Bookingid)
	notifyCustomerMilerAssigned(booking, miler.profile.Displayname)

	return nil
}

// publishCustomerAssignment sends the booking.assigned event to NATS JetStream.
// Non-fatal: logs a warning and returns if NATS is unavailable or publish fails.
func publishCustomerAssignment(
	booking *models.PickupBooking,
	milerUserID int,
	provider providerResult,
	etaMinutes float64,
) {
	if db.Js == nil {
		return
	}

	payload := map[string]interface{}{
		"booking_id":        booking.Bookingid,
		"booking_no":        booking.Bookingno,
		"miler_id":          milerUserID,
		"selected_provider": provider.company,
		"estimated_price":   provider.estimatedPrice,
		"eta_minutes":       etaMinutes,
		"assigned_at":       time.Now().UnixMilli(),
	}

	data, err := json.Marshal(payload)
	if err != nil {
		utils.Warn("B2CAssignment: failed to marshal NATS payload", "booking_id", booking.Bookingid, "error", err)
		return
	}

	if _, err := db.Js.Publish("booking.assigned", data); err != nil {
		utils.Warn("B2CAssignment: NATS publish failed", "booking_id", booking.Bookingid, "error", err)
	}
}

// ─── Booking field extractors ─────────────────────────────────────────────────

func b2cFirstParcelCategory(booking *models.PickupBooking) string {
	if len(booking.Parcels) > 0 {
		return booking.Parcels[0].Itemcategory
	}
	return ""
}

func b2cFirstServiceType(booking *models.PickupBooking) string {
	if len(booking.ServiceOptions) > 0 {
		return booking.ServiceOptions[0].Servicetype
	}
	return "Normal"
}

// b2cChargeableWeight sums max(actual, volumetric) per parcel across the booking.
func b2cChargeableWeight(booking *models.PickupBooking) float64 {
	var total float64
	for _, p := range booking.Parcels {
		volumetric := (p.Length * p.Width * p.Height) / 5000.0
		total += math.Max(p.Weight, volumetric)
	}
	if total == 0 {
		return 0.5 // minimum chargeable to avoid zero-weight lookup
	}
	return total
}

// ─── Zone / category helpers (self-contained; no import of controllers pkg) ──

func b2cPincodeToState(pincode string) string {
	if len(pincode) < 3 {
		return pincode
	}
	p, err := strconv.Atoi(pincode[:3])
	if err != nil {
		if len(pincode) >= 2 {
			return pincode[:2]
		}
		return pincode
	}
	switch {
	case p >= 500 && p <= 535:
		return "AP_TS"
	case p >= 560 && p <= 591:
		return "KA"
	case p >= 600 && p <= 643:
		return "TN"
	case p >= 670 && p <= 695:
		return "KL"
	case p >= 380 && p <= 396:
		return "GJ"
	case p >= 400 && p <= 444:
		return "MH"
	default:
		if len(pincode) >= 2 {
			return pincode[:2]
		}
		return pincode
	}
}

func b2cResolveZone(pickupPincode, deliveryPincode string) string {
	if len(pickupPincode) >= 3 && len(deliveryPincode) >= 3 && pickupPincode[:3] == deliveryPincode[:3] {
		return "Local"
	}
	if b2cPincodeToState(pickupPincode) == b2cPincodeToState(deliveryPincode) {
		return "Regional"
	}
	return "National"
}

func b2cNormalizePricingCategory(category string) string {
	switch category {
	case "General", "Documents", "Electronics", "Clothing", "Fragile", "Medical", "Automotive", "Food":
		return category
	default:
		return "General"
	}
}

func b2cMapServiceTypeToPricing(serviceType string) string {
	if serviceType == "Fast" || serviceType == "Superfast" || serviceType == "Express" {
		return "Express"
	}
	return "Normal"
}

func b2cFilterByCategory(rules []models.DoormilePricing, category string) []models.DoormilePricing {
	out := make([]models.DoormilePricing, 0, len(rules))
	for _, r := range rules {
		if r.Category == category {
			out = append(out, r)
		}
	}
	return out
}
