package controllers

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

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

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

// Valid enum values
var validZones = map[string]bool{
	"Local":    true,
	"Regional": true,
	"National": true,
}

var validCategories = map[string]bool{
	"General":     true,
	"Documents":   true,
	"Electronics": true,
	"Clothing":    true,
	"Fragile":     true,
	"Medical":     true,
	"Automotive":  true,
	"Food":        true,
}

var validServiceTypes = map[string]bool{
	"Normal":  true,
	"Express": true,
}

var categoryLabels = map[string]string{
	"General":     "General Goods",
	"Documents":   "Books & Documents",
	"Electronics": "Electronics & Gadgets",
	"Clothing":    "Clothing & Textiles",
	"Fragile":     "Fragile Items",
	"Medical":     "Medical & Pharma",
	"Automotive":  "Automotive Parts",
	"Food":        "Food & Perishables",
}

// pricingCacheKey returns the Redis key for a zone+servicetype slab.
// Only 6 possible keys — 3 zones × 2 service types.
func pricingCacheKey(zone, servicetype string) string {
	return fmt.Sprintf("doormile:pricing:%s:%s", zone, servicetype)
}

// invalidatePricingCache drops the Redis key for a zone+servicetype so the next
// CheckPrice request re-warms it from Postgres.
func invalidatePricingCache(zone, servicetype string) {
	if db.Rdb == nil {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	key := pricingCacheKey(zone, servicetype)
	if err := db.Rdb.Del(ctx, key).Err(); err != nil {
		utils.Warn("Failed to invalidate pricing cache", "key", key, "error", err)
	} else {
		utils.Info("Pricing cache invalidated", "key", key)
	}
}

// loadFromPostgres fetches all Active rules for a zone+servicetype and writes
// them into Redis. Returns the rules regardless of whether Redis succeeds.
func loadFromPostgres(zone, servicetype string) ([]models.DoormilePricing, error) {
	var rules []models.DoormilePricing
	err := db.DB.
		Where("zone = ? AND servicetype = ? AND status = ? AND deletedat IS NULL", zone, servicetype, "Active").
		Order("category ASC").
		Find(&rules).Error
	if err != nil {
		return nil, err
	}

	// Warm the cache — no TTL because we invalidate explicitly on admin writes
	if db.Rdb != nil && len(rules) > 0 {
		data, merr := json.Marshal(rules)
		if merr == nil {
			ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
			defer cancel()
			if rerr := db.Rdb.Set(ctx, pricingCacheKey(zone, servicetype), data, 0).Err(); rerr != nil {
				utils.Warn("Failed to warm pricing cache", "zone", zone, "servicetype", servicetype, "error", rerr)
			} else {
				utils.Info("Pricing cache warmed", "zone", zone, "servicetype", servicetype, "count", len(rules))
			}
		}
	}

	return rules, nil
}

// applyFilters takes the full slab for a zone+servicetype and applies the
// weight and optional category filters in memory.
func applyFilters(rules []models.DoormilePricing, weight float64, category string) []models.DoormilePricing {
	out := make([]models.DoormilePricing, 0, len(rules))
	for _, r := range rules {
		if r.Minweight > weight || r.Maxweight < weight {
			continue
		}
		if category != "" && r.Category != category {
			continue
		}
		out = append(out, r)
	}
	return out
}

// buildPriceResponse constructs the final JSON response from a filtered rule set.
func buildPriceResponse(zone, servicetype string, weight float64, rules []models.DoormilePricing) fiber.Map {
	if len(rules) == 0 {
		return fiber.Map{
			"found":        false,
			"zone":         zone,
			"service_type": servicetype,
			"weight":       weight,
			"message":      "no pricing configured for this combination",
			"results":      []interface{}{},
			"total":        0,
		}
	}

	results := make([]fiber.Map, 0, len(rules))
	for _, r := range rules {
		results = append(results, fiber.Map{
			"category":       r.Category,
			"category_label": categoryLabels[r.Category],
			"min_price":      r.Minprice,
			"max_price":      r.Maxprice,
		})
	}

	return fiber.Map{
		"found":        true,
		"zone":         zone,
		"service_type": servicetype,
		"weight":       weight,
		"currency":     rules[0].Currency,
		"total":        len(results),
		"results":      results,
	}
}

// CheckPrice is the public endpoint Flutter calls after the customer fills the form.
// Reads from Redis first (full zone+servicetype slab), filters by weight in memory.
// Falls back to Postgres on cache miss and re-warms Redis before returning.
//
// POST /api/v1/pricing/check
// Body: { zone, service_type, weight, category? }
func CheckPrice(c *fiber.Ctx) error {
	type req struct {
		Zone            string  `json:"zone"`
		ServiceType     string  `json:"service_type"`
		Weight          float64 `json:"weight"`
		Category        string  `json:"category"`
		PickupPincode   string  `json:"pickup_pincode"`
		DeliveryPincode string  `json:"delivery_pincode"`
	}

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

	// Auto-resolve zone from pincodes when not explicitly provided.
	if body.Zone == "" && body.PickupPincode != "" && body.DeliveryPincode != "" {
		body.Zone = resolveZone(body.PickupPincode, body.DeliveryPincode)
	}

	if !validZones[body.Zone] {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":     false,
			"message":     "invalid zone",
			"valid_zones": []string{"Local", "Regional", "National"},
		})
	}
	if !validServiceTypes[body.ServiceType] {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":             false,
			"message":             "invalid service_type",
			"valid_service_types": []string{"Normal", "Express"},
		})
	}
	if body.Weight <= 0 {
		return utils.BadRequest(c, "weight must be greater than 0")
	}
	if body.Category != "" && !validCategories[body.Category] {
		return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
			"success":          false,
			"message":          "invalid category",
			"valid_categories": []string{"General", "Documents", "Electronics", "Clothing", "Fragile", "Medical", "Automotive", "Food"},
		})
	}

	var allRules []models.DoormilePricing

	if db.Rdb != nil {
		ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		cached, err := db.Rdb.Get(ctx, pricingCacheKey(body.Zone, body.ServiceType)).Result()
		cancel()

		if err == nil {
			if jerr := json.Unmarshal([]byte(cached), &allRules); jerr != nil {
				utils.Warn("Corrupt pricing cache entry, evicting", "zone", body.Zone, "servicetype", body.ServiceType)
				invalidatePricingCache(body.Zone, body.ServiceType)
				allRules = nil
			}
		}
	}

	if allRules == nil {
		var err error
		allRules, err = loadFromPostgres(body.Zone, body.ServiceType)
		if err != nil {
			return utils.Internal(c, "failed to fetch pricing")
		}
	}

	matched := applyFilters(allRules, body.Weight, body.Category)

	return c.JSON(fiber.Map{
		"success": true,
		"data":    buildPriceResponse(body.Zone, body.ServiceType, body.Weight, matched),
	})
}

// GetDoormilePricing returns all pricing rules for admin review.
// Always reads from Postgres — admins need the authoritative view.
// GET /api/v1/admin/doormile-pricing
func GetDoormilePricing(c *fiber.Ctx) error {
	query := db.DB.Where("deletedat IS NULL")

	if z := c.Query("zone"); z != "" {
		query = query.Where("zone = ?", z)
	}
	if cat := c.Query("category"); cat != "" {
		query = query.Where("category = ?", cat)
	}
	if st := c.Query("service_type"); st != "" {
		query = query.Where("servicetype = ?", st)
	}

	var rules []models.DoormilePricing
	if err := query.Order("zone, category, servicetype, min_weight").Find(&rules).Error; err != nil {
		return utils.Internal(c, "failed to fetch pricing rules")
	}

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

// CreateDoormilePricing adds a new pricing band and invalidates the relevant cache.
// POST /api/v1/admin/doormile-pricing
func CreateDoormilePricing(c *fiber.Ctx) error {
	type req struct {
		Zone        string  `json:"zone"`
		Category    string  `json:"category"`
		ServiceType string  `json:"service_type"`
		MinWeight   float64 `json:"min_weight"`
		MaxWeight   float64 `json:"max_weight"`
		MinPrice    float64 `json:"min_price"`
		MaxPrice    float64 `json:"max_price"`
		Currency    string  `json:"currency"`
	}

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

	if !validZones[body.Zone] || !validCategories[body.Category] || !validServiceTypes[body.ServiceType] {
		return utils.BadRequest(c, "invalid zone, category, or service_type")
	}
	if body.MinWeight < 0 || body.MaxWeight <= body.MinWeight {
		return utils.BadRequest(c, "max_weight must be greater than min_weight")
	}
	if body.MinPrice <= 0 || body.MaxPrice <= body.MinPrice {
		return utils.BadRequest(c, "max_price must be greater than min_price")
	}

	currency := body.Currency
	if currency == "" {
		currency = "INR"
	}

	rule := models.DoormilePricing{
		Zone:        body.Zone,
		Category:    body.Category,
		Servicetype: body.ServiceType,
		Minweight:   body.MinWeight,
		Maxweight:   body.MaxWeight,
		Minprice:    body.MinPrice,
		Maxprice:    body.MaxPrice,
		Currency:    currency,
		Status:      "Active",
	}

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

	invalidatePricingCache(rule.Zone, rule.Servicetype)

	return utils.Created(c, rule)
}

// UpdateDoormilePricing updates a pricing band and invalidates affected cache keys.
// If zone or service_type changes, both the old and new cache keys are invalidated.
// PUT /api/v1/admin/doormile-pricing/:id
func UpdateDoormilePricing(c *fiber.Ctx) error {
	id, err := strconv.Atoi(c.Params("id"))
	if err != nil {
		return utils.BadRequest(c, "invalid pricing rule ID")
	}

	var rule models.DoormilePricing
	if err := db.DB.Where("doormile_pricing_id = ? AND deletedat IS NULL", id).First(&rule).Error; err != nil {
		return utils.NotFound(c, "pricing rule not found")
	}

	// Capture old keys before any mutation
	oldZone := rule.Zone
	oldServicetype := rule.Servicetype

	type req struct {
		Zone        string  `json:"zone"`
		Category    string  `json:"category"`
		ServiceType string  `json:"service_type"`
		MinWeight   float64 `json:"min_weight"`
		MaxWeight   float64 `json:"max_weight"`
		MinPrice    float64 `json:"min_price"`
		MaxPrice    float64 `json:"max_price"`
		Currency    string  `json:"currency"`
		Status      string  `json:"status"`
	}

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

	if body.Zone != "" {
		if !validZones[body.Zone] {
			return utils.BadRequest(c, "invalid zone")
		}
		rule.Zone = body.Zone
	}
	if body.Category != "" {
		if !validCategories[body.Category] {
			return utils.BadRequest(c, "invalid category")
		}
		rule.Category = body.Category
	}
	if body.ServiceType != "" {
		if !validServiceTypes[body.ServiceType] {
			return utils.BadRequest(c, "invalid service_type")
		}
		rule.Servicetype = body.ServiceType
	}
	if body.MinWeight >= 0 {
		rule.Minweight = body.MinWeight
	}
	if body.MaxWeight > 0 {
		rule.Maxweight = body.MaxWeight
	}
	if body.MinPrice > 0 {
		rule.Minprice = body.MinPrice
	}
	if body.MaxPrice > 0 {
		rule.Maxprice = body.MaxPrice
	}
	if body.Currency != "" {
		rule.Currency = body.Currency
	}
	if body.Status != "" {
		rule.Status = body.Status
	}
	rule.Updatedat = time.Now()

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

	invalidatePricingCache(oldZone, oldServicetype)
	if rule.Zone != oldZone || rule.Servicetype != oldServicetype {
		invalidatePricingCache(rule.Zone, rule.Servicetype)
	}

	return utils.OK(c, rule)
}

// DeleteDoormilePricing soft-deletes a pricing band and invalidates its cache key.
// DELETE /api/v1/admin/doormile-pricing/:id
func DeleteDoormilePricing(c *fiber.Ctx) error {
	id, err := strconv.Atoi(c.Params("id"))
	if err != nil {
		return utils.BadRequest(c, "invalid pricing rule ID")
	}

	var rule models.DoormilePricing
	if err := db.DB.Where("doormile_pricing_id = ? AND deletedat IS NULL", id).First(&rule).Error; err != nil {
		return utils.NotFound(c, "pricing rule not found")
	}

	now := time.Now()
	rule.Deletedat = &now
	db.DB.Save(&rule)

	invalidatePricingCache(rule.Zone, rule.Servicetype)

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

// WarmPricingCache loads every Active pricing rule from Postgres into Redis on
// server startup. Call this once from main.go after DB and Redis are ready.
// If Redis is unavailable it logs a warning and returns — the lazy-load fallback
// in CheckPrice will handle individual misses at request time.
func WarmPricingCache() {
	if db.Rdb == nil {
		utils.Warn("WarmPricingCache: Redis not available, skipping pre-warm")
		return
	}

	utils.Info("WarmPricingCache: warming pricing cache from Postgres...")

	// Iterate every zone × servicetype combination
	zones := []string{"Local", "Regional", "National"}
	serviceTypes := []string{"Normal", "Express"}

	warmed := 0
	for _, zone := range zones {
		for _, st := range serviceTypes {
			rules, err := loadFromPostgres(zone, st)
			if err != nil {
				utils.Error("WarmPricingCache: failed to load", "zone", zone, "servicetype", st, "error", err)
				continue
			}
			if len(rules) > 0 {
				warmed++
			}
		}
	}

	utils.Info("WarmPricingCache: done", "slabs_warmed", warmed)
}

// GetPricingMeta returns the valid enum values so Flutter can populate its dropdowns.
// GET /api/v1/pricing/meta
func GetPricingMeta(c *fiber.Ctx) error {
	return utils.OK(c, fiber.Map{
		"zones": []fiber.Map{
			{"value": "Local", "label": "Local / Same City"},
			{"value": "Regional", "label": "Regional / Same State"},
			{"value": "National", "label": "National / Other State"},
		},
		"categories": []fiber.Map{
			{"value": "General", "label": "General Goods"},
			{"value": "Documents", "label": "Books & Documents"},
			{"value": "Electronics", "label": "Electronics & Gadgets"},
			{"value": "Clothing", "label": "Clothing & Textiles"},
			{"value": "Fragile", "label": "Fragile Items"},
			{"value": "Medical", "label": "Medical & Pharma"},
			{"value": "Automotive", "label": "Automotive Parts"},
			{"value": "Food", "label": "Food & Perishables"},
		},
		"service_types": []fiber.Map{
			{"value": "Normal", "label": "Normal"},
			{"value": "Express", "label": "Express"},
		},
	})
}
