package assignment

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"time"

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

	"github.com/redis/go-redis/v9"
)

// ─── Request / response types ────────────────────────────────────────────────

type aiCandidate struct {
	MilerID        int     `json:"miler_id"`
	DistanceKm     float64 `json:"distance_km"`
	Rating         float64 `json:"rating"`
	ActiveBookings int64   `json:"active_bookings"`
	OnTimeRate30d  float64 `json:"on_time_rate_30d"`
	CompletedToday int64   `json:"completed_today"`
	HubID          int     `json:"hub_id"`
	HubLoad        int64   `json:"hub_load"`
	HubCapacity    int     `json:"hub_capacity"`
}

type aiDecisionRequest struct {
	Booking    aiBookingInfo `json:"booking"`
	Candidates []aiCandidate `json:"candidates"`
	Context    aiRequestCtx  `json:"context"`
}

type aiBookingInfo struct {
	PickupLat    float64 `json:"pickup_lat"`
	PickupLon    float64 `json:"pickup_lon"`
	DeliveryLat  float64 `json:"delivery_lat"`
	DeliveryLon  float64 `json:"delivery_lon"`
	ItemCategory string  `json:"item_category"`
	Weight       float64 `json:"weight"`
	ServiceType  string  `json:"service_type"`
}

type aiRequestCtx struct {
	Hour   int    `json:"hour"`
	IsPeak bool   `json:"is_peak"`
	Zone   string `json:"zone"`
}

type aiDecisionResponse struct {
	ChosenMilerID   int    `json:"chosen_miler_id"`
	Escalate        bool   `json:"escalate"`
	AgentDecisionID uint64 `json:"agent_decision_id"`
	Reasoning       string `json:"reasoning"`
}

// ─── Main entry point ────────────────────────────────────────────────────────

// selectMilerWithAI collects all eligible milers from the GEORADIUS result, calls
// the AI decision engine, and returns the chosen miler plus the decision ID for
// audit. Falls back to the original distance/load/rating formula when the AI
// layer is unreachable or times out. Returns (nil, nil, false) when there are no
// eligible candidates or when the AI layer escalates the booking.
func selectMilerWithAI(booking *models.PickupBooking, nearby []redis.GeoLocation) (*milerCandidate, *uint64, bool) {
	candidates, aiCandidates := collectEligibleCandidates(nearby)
	if len(candidates) == 0 {
		return nil, nil, false
	}

	decision, err := callDecisionEngine(booking, aiCandidates)
	if err != nil {
		utils.Warn("AI_LAYER_FALLBACK: decide-assignment unreachable, using legacy scoring",
			"booking_id", booking.Bookingid, "error", err)
		best := pickBestFromCandidates(candidates)
		return best, nil, best != nil
	}

	utils.Info("Assignment: AI layer responded",
		"booking_id", booking.Bookingid,
		"escalate", decision.Escalate,
		"chosen_miler_id", decision.ChosenMilerID,
		"agent_decision_id", decision.AgentDecisionID,
		"reasoning", decision.Reasoning,
	)

	if decision.Escalate {
		utils.Warn("Assignment: AI layer escalated — skipping assignment this attempt",
			"booking_id", booking.Bookingid,
			"reasoning", decision.Reasoning,
		)
		return nil, nil, false
	}

	var decisionID *uint64
	if decision.AgentDecisionID != 0 {
		id := decision.AgentDecisionID
		decisionID = &id
	}

	for _, c := range candidates {
		if c.profile.Userid == decision.ChosenMilerID {
			return c, decisionID, true
		}
	}

	// AI returned an ID that is not in our eligibility set — fall back safely.
	utils.Warn("AI_LAYER_FALLBACK: chosen miler not in eligible set, using legacy scoring",
		"booking_id", booking.Bookingid,
		"chosen_miler_id", decision.ChosenMilerID,
	)
	best := pickBestFromCandidates(candidates)
	return best, nil, best != nil
}

// ─── Candidate collection ────────────────────────────────────────────────────

// collectEligibleCandidates iterates the GEORADIUS result, applies eligibility
// filters (availability, active-booking cap), and fetches per-miler stats needed
// by the AI layer. Returns parallel slices so callers can use either.
func collectEligibleCandidates(nearby []redis.GeoLocation) ([]*milerCandidate, []aiCandidate) {
	var candidates []*milerCandidate
	var aiCandidates []aiCandidate

	for _, loc := range nearby {
		milerUserID, err := strconv.Atoi(loc.Name)
		if err != nil {
			utils.Warn("Assignment: skipping non-numeric GEO member", "name", loc.Name)
			continue
		}

		var profile models.MilerProfile
		if err := db.DB.Where("userid = ?", milerUserID).First(&profile).Error; err != nil {
			continue
		}

		if profile.Availabilitystatus != constants.MilerAvailable {
			continue
		}

		var activeCount int64
		db.DB.Model(&models.BookingAssignment{}).
			Where("mileruserid = ? AND assignmentstatus IN ?", milerUserID, []string{
				constants.AssignmentAssigned,
				constants.AssignmentAccepted,
			}).
			Count(&activeCount)

		if activeCount >= maxActive {
			continue
		}

		onTimeRate, completedToday := fetchMilerStats(milerUserID)
		hubID, hubLoad, hubCapacity := fetchHubData(profile.Hubid)

		candidates = append(candidates, &milerCandidate{
			profile:        profile,
			distanceKm:     loc.Dist,
			activeBookings: activeCount,
		})
		aiCandidates = append(aiCandidates, aiCandidate{
			MilerID:        milerUserID,
			DistanceKm:     loc.Dist,
			Rating:         profile.Rating,
			ActiveBookings: activeCount,
			OnTimeRate30d:  onTimeRate,
			CompletedToday: completedToday,
			HubID:          hubID,
			HubLoad:        hubLoad,
			HubCapacity:    hubCapacity,
		})
	}

	return candidates, aiCandidates
}

// ─── Per-miler stats ─────────────────────────────────────────────────────────

func fetchMilerStats(milerUserID int) (onTimeRate float64, completedToday int64) {
	type statsRow struct {
		Total  int64
		OnTime int64
	}
	var row statsRow
	db.DB.Raw(`
		SELECT
			COUNT(*) AS total,
			COUNT(CASE WHEN assignmentstatus = 'Completed_OnTime' THEN 1 END) AS on_time
		FROM bookingassignments
		WHERE mileruserid = ?
		  AND createdat > NOW() - INTERVAL '30 days'
	`, milerUserID).Scan(&row)

	if row.Total == 0 {
		onTimeRate = 0.85 // neutral assumption for milers with no recent history
	} else {
		onTimeRate = float64(row.OnTime) / float64(row.Total)
	}

	today := time.Now().Truncate(24 * time.Hour)
	db.DB.Model(&models.BookingAssignment{}).
		Where("mileruserid = ? AND createdat >= ? AND assignmentstatus = ?",
			milerUserID, today, constants.AssignmentCompleted).
		Count(&completedToday)

	return
}

// ─── Hub stats ───────────────────────────────────────────────────────────────

// fetchHubData returns the hub_load (active assignments across the hub) and
// hub_capacity for the given hub. Defaults to 50 for capacity when the column
// doesn't exist or the miler has no assigned hub.
func fetchHubData(hubID *int) (resolvedHubID int, hubLoad int64, hubCapacity int) {
	hubCapacity = 50 // safe default — also used if hubs.capacity column is absent
	if hubID == nil {
		return 0, 0, hubCapacity
	}
	resolvedHubID = *hubID

	// hub_load: total active assignments whose miler belongs to this hub
	db.DB.Raw(`
		SELECT COUNT(*) FROM bookingassignments ba
		JOIN milerprofiles mp ON ba.mileruserid = mp.userid
		WHERE mp.hubid = ?
		  AND ba.assignmentstatus IN ('Assigned', 'Accepted', 'Pickup_Scheduled')
	`, resolvedHubID).Scan(&hubLoad)

	// hub_capacity: gracefully handle column not existing yet
	var cap int
	if err := db.DB.Raw(`SELECT capacity FROM hubs WHERE hubid = ?`, resolvedHubID).Scan(&cap).Error; err == nil && cap > 0 {
		hubCapacity = cap
	}

	return
}

// ─── Fallback scorer ─────────────────────────────────────────────────────────

// pickBestFromCandidates applies the original formula to an already-collected
// eligible slice: score = distance_km*1.0 + active_bookings*2.0 - rating*0.5
func pickBestFromCandidates(candidates []*milerCandidate) *milerCandidate {
	var best *milerCandidate
	bestScore := 1e18

	for _, c := range candidates {
		score := c.distanceKm*1.0 + float64(c.activeBookings)*2.0 - c.profile.Rating*0.5
		if score < bestScore {
			bestScore = score
			best = c
		}
	}

	return best
}

// ─── AI layer HTTP call ──────────────────────────────────────────────────────

func callDecisionEngine(booking *models.PickupBooking, candidates []aiCandidate) (aiDecisionResponse, error) {
	baseURL := os.Getenv("AI_LAYER_BASE_URL")
	if baseURL == "" {
		baseURL = "https://routemate.workolik.com"
	}

	now := time.Now()
	hour := now.Hour()
	isPeak := (hour >= 7 && hour <= 10) || (hour >= 17 && hour <= 21)

	reqBody := aiDecisionRequest{
		Booking: aiBookingInfo{
			PickupLat:    booking.Pickuplatitude,
			PickupLon:    booking.Pickuplongitude,
			DeliveryLat:  booking.Deliverylatitude,
			DeliveryLon:  booking.Deliverylongitude,
			ItemCategory: b2cFirstParcelCategory(booking),
			Weight:       b2cChargeableWeight(booking),
			ServiceType:  b2cFirstServiceType(booking),
		},
		Candidates: candidates,
		Context: aiRequestCtx{
			Hour:   hour,
			IsPeak: isPeak,
			Zone:   booking.Deliverycity,
		},
	}

	body, err := json.Marshal(reqBody)
	if err != nil {
		return aiDecisionResponse{}, fmt.Errorf("marshal AI request: %w", err)
	}

	utils.Info("Assignment: AI layer outgoing payload",
		"booking_id", booking.Bookingid,
		"payload", string(body),
	)

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

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		baseURL+"/api/v1/doormile/decide-assignment",
		bytes.NewReader(body))
	if err != nil {
		return aiDecisionResponse{}, fmt.Errorf("build AI request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return aiDecisionResponse{}, fmt.Errorf("AI layer call: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return aiDecisionResponse{}, fmt.Errorf("AI layer HTTP %d", resp.StatusCode)
	}

	var decision aiDecisionResponse
	if err := json.NewDecoder(resp.Body).Decode(&decision); err != nil {
		return aiDecisionResponse{}, fmt.Errorf("decode AI response: %w", err)
	}

	return decision, nil
}
