package services

import (
	"errors"
	"fmt"
	"sync"
	"time"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/models"
	"git.mchus.pro/mchus/quoteforge/internal/repository"
)

var (
	ErrEmptyQuote        = errors.New("quote cannot be empty")
	ErrComponentNotFound = errors.New("component not found")
	ErrNoPriceAvailable  = errors.New("no price available for component")
)

type QuoteService struct {
	componentRepo  *repository.ComponentRepository
	pricelistRepo  *repository.PricelistRepository
	localDB        *localdb.LocalDB
	pricingService priceResolver
	cacheMu        sync.RWMutex
	priceCache     map[string]cachedLotPrice
	cacheTTL       time.Duration
}

type priceResolver interface {
	GetEffectivePrice(lotName string) (*float64, error)
}

func NewQuoteService(
	componentRepo *repository.ComponentRepository,
	pricelistRepo *repository.PricelistRepository,
	localDB *localdb.LocalDB,
	pricingService priceResolver,
) *QuoteService {
	return &QuoteService{
		componentRepo:  componentRepo,
		pricelistRepo:  pricelistRepo,
		localDB:        localDB,
		pricingService: pricingService,
		priceCache:     make(map[string]cachedLotPrice, 4096),
		cacheTTL:       10 * time.Second,
	}
}

type cachedLotPrice struct {
	price     *float64
	expiresAt time.Time
}

type QuoteItem struct {
	LotName     string  `json:"lot_name"`
	Quantity    int     `json:"quantity"`
	UnitPrice   float64 `json:"unit_price"`
	TotalPrice  float64 `json:"total_price"`
	Description string  `json:"description"`
	Category    string  `json:"category"`
	HasPrice    bool    `json:"has_price"`
}

type QuoteValidationResult struct {
	Valid    bool        `json:"valid"`
	Items    []QuoteItem `json:"items"`
	Errors   []string    `json:"errors"`
	Warnings []string    `json:"warnings"`
	Total    float64     `json:"total"`
}

type QuoteRequest struct {
	Items []struct {
		LotName  string `json:"lot_name"`
		Quantity int    `json:"quantity"`
	} `json:"items"`
	PricelistID *uint `json:"pricelist_id,omitempty"` // Optional: use specific pricelist for pricing
}

type PriceLevelsRequest struct {
	Items []struct {
		LotName  string `json:"lot_name"`
		Quantity int    `json:"quantity"`
	} `json:"items"`
	PricelistIDs map[string]uint `json:"pricelist_ids,omitempty"`
	NoCache      bool            `json:"no_cache,omitempty"`
}

type PriceLevelsItem struct {
	LotName              string   `json:"lot_name"`
	Quantity             int      `json:"quantity"`
	EstimatePrice        *float64 `json:"estimate_price"`
	WarehousePrice       *float64 `json:"warehouse_price"`
	CompetitorPrice      *float64 `json:"competitor_price"`
	DeltaWhEstimateAbs   *float64 `json:"delta_wh_estimate_abs"`
	DeltaWhEstimatePct   *float64 `json:"delta_wh_estimate_pct"`
	DeltaCompEstimateAbs *float64 `json:"delta_comp_estimate_abs"`
	DeltaCompEstimatePct *float64 `json:"delta_comp_estimate_pct"`
	DeltaCompWhAbs       *float64 `json:"delta_comp_wh_abs"`
	DeltaCompWhPct       *float64 `json:"delta_comp_wh_pct"`
	PriceMissing         []string `json:"price_missing"`
}

type PriceLevelsResult struct {
	Items                []PriceLevelsItem `json:"items"`
	ResolvedPricelistIDs map[string]uint   `json:"resolved_pricelist_ids"`
}

func (s *QuoteService) ValidateAndCalculate(req *QuoteRequest) (*QuoteValidationResult, error) {
	if len(req.Items) == 0 {
		return nil, ErrEmptyQuote
	}

	// Strict local-first path: calculations use local SQLite snapshot regardless of online status.
	if s.localDB != nil {
		result := &QuoteValidationResult{
			Valid:    true,
			Items:    make([]QuoteItem, 0, len(req.Items)),
			Errors:   make([]string, 0),
			Warnings: make([]string, 0),
		}

		// Determine which pricelist to use for pricing
		pricelistID := req.PricelistID
		if pricelistID == nil || *pricelistID == 0 {
			// By default, use latest estimate pricelist
			latestPricelist, err := s.localDB.GetLatestLocalPricelistBySource("estimate")
			if err == nil && latestPricelist != nil {
				pricelistID = &latestPricelist.ServerID
			}
		}

		var total float64
		for _, reqItem := range req.Items {
			localComp, err := s.localDB.GetLocalComponent(reqItem.LotName)
			if err != nil {
				result.Valid = false
				result.Errors = append(result.Errors, "Component not found: "+reqItem.LotName)
				continue
			}

			item := QuoteItem{
				LotName:     reqItem.LotName,
				Quantity:    reqItem.Quantity,
				Description: localComp.LotDescription,
				Category:    localComp.Category,
				HasPrice:    false,
				UnitPrice:   0,
				TotalPrice:  0,
			}

			// Get price from pricelist_items
			if pricelistID != nil {
				price, found := s.lookupPriceByPricelistID(*pricelistID, reqItem.LotName)
				if found && price > 0 {
					item.UnitPrice = price
					item.TotalPrice = price * float64(reqItem.Quantity)
					item.HasPrice = true
					total += item.TotalPrice
				} else {
					result.Warnings = append(result.Warnings, "No price available for: "+reqItem.LotName)
				}
			} else {
				result.Warnings = append(result.Warnings, "No pricelist available for: "+reqItem.LotName)
			}

			result.Items = append(result.Items, item)
		}

		result.Total = total
		return result, nil
	}

	if s.componentRepo == nil || s.pricingService == nil {
		return nil, errors.New("quote calculation not available")
	}

	result := &QuoteValidationResult{
		Valid:    true,
		Items:    make([]QuoteItem, 0, len(req.Items)),
		Errors:   make([]string, 0),
		Warnings: make([]string, 0),
	}

	lotNames := make([]string, len(req.Items))
	quantities := make(map[string]int)
	for i, item := range req.Items {
		lotNames[i] = item.LotName
		quantities[item.LotName] = item.Quantity
	}

	components, err := s.componentRepo.GetMultiple(lotNames)
	if err != nil {
		return nil, err
	}

	componentMap := make(map[string]*models.LotMetadata)
	for i := range components {
		componentMap[components[i].LotName] = &components[i]
	}

	var total float64

	for _, reqItem := range req.Items {
		comp, exists := componentMap[reqItem.LotName]
		if !exists {
			result.Valid = false
			result.Errors = append(result.Errors, "Component not found: "+reqItem.LotName)
			continue
		}

		item := QuoteItem{
			LotName:  reqItem.LotName,
			Quantity: reqItem.Quantity,
			HasPrice: false,
		}

		if comp.Lot != nil {
			item.Description = comp.Lot.LotDescription
		}
		if comp.Category != nil {
			item.Category = comp.Category.Code
		}

		// Get effective price (override or calculated)
		price, err := s.pricingService.GetEffectivePrice(reqItem.LotName)
		if err == nil && price != nil && *price > 0 {
			item.UnitPrice = *price
			item.TotalPrice = *price * float64(reqItem.Quantity)
			item.HasPrice = true
			total += item.TotalPrice
		} else {
			result.Warnings = append(result.Warnings, "No price available for: "+reqItem.LotName)
		}

		result.Items = append(result.Items, item)
	}

	result.Total = total
	return result, nil
}

func (s *QuoteService) CalculatePriceLevels(req *PriceLevelsRequest) (*PriceLevelsResult, error) {
	if len(req.Items) == 0 {
		return nil, ErrEmptyQuote
	}

	lotNames := make([]string, 0, len(req.Items))
	seenLots := make(map[string]struct{}, len(req.Items))
	for _, reqItem := range req.Items {
		if _, ok := seenLots[reqItem.LotName]; ok {
			continue
		}
		seenLots[reqItem.LotName] = struct{}{}
		lotNames = append(lotNames, reqItem.LotName)
	}

	result := &PriceLevelsResult{
		Items:                make([]PriceLevelsItem, 0, len(req.Items)),
		ResolvedPricelistIDs: map[string]uint{},
	}

	type levelState struct {
		id     uint
		prices map[string]float64
	}
	levelBySource := map[models.PricelistSource]*levelState{
		models.PricelistSourceEstimate:   {prices: map[string]float64{}},
		models.PricelistSourceWarehouse:  {prices: map[string]float64{}},
		models.PricelistSourceCompetitor: {prices: map[string]float64{}},
	}

	for source, st := range levelBySource {
		sourceKey := string(source)
		if req.PricelistIDs != nil {
			if explicitID, ok := req.PricelistIDs[sourceKey]; ok && explicitID > 0 {
				st.id = explicitID
				result.ResolvedPricelistIDs[sourceKey] = explicitID
			}
		}
		if st.id == 0 && s.pricelistRepo != nil {
			latest, err := s.pricelistRepo.GetLatestActiveBySource(sourceKey)
			if err == nil {
				st.id = latest.ID
				result.ResolvedPricelistIDs[sourceKey] = latest.ID
			}
		}
		if st.id == 0 && s.localDB != nil {
			localPL, err := s.localDB.GetLatestLocalPricelistBySource(sourceKey)
			if err == nil && localPL != nil {
				st.id = localPL.ServerID
				result.ResolvedPricelistIDs[sourceKey] = localPL.ServerID
			}
		}
		if st.id == 0 {
			continue
		}
		prices, err := s.lookupPricesByPricelistID(st.id, lotNames, req.NoCache)
		if err == nil {
			st.prices = prices
		}
	}

	for _, reqItem := range req.Items {
		item := PriceLevelsItem{
			LotName:      reqItem.LotName,
			Quantity:     reqItem.Quantity,
			PriceMissing: make([]string, 0, 3),
		}

		if p, ok := levelBySource[models.PricelistSourceEstimate].prices[reqItem.LotName]; ok && p > 0 {
			price := p
			item.EstimatePrice = &price
		}
		if p, ok := levelBySource[models.PricelistSourceWarehouse].prices[reqItem.LotName]; ok && p > 0 {
			price := p
			item.WarehousePrice = &price
		}
		if p, ok := levelBySource[models.PricelistSourceCompetitor].prices[reqItem.LotName]; ok && p > 0 {
			price := p
			item.CompetitorPrice = &price
		}

		if item.EstimatePrice == nil {
			item.PriceMissing = append(item.PriceMissing, string(models.PricelistSourceEstimate))
		}
		if item.WarehousePrice == nil {
			item.PriceMissing = append(item.PriceMissing, string(models.PricelistSourceWarehouse))
		}
		if item.CompetitorPrice == nil {
			item.PriceMissing = append(item.PriceMissing, string(models.PricelistSourceCompetitor))
		}

		item.DeltaWhEstimateAbs, item.DeltaWhEstimatePct = calculateDelta(item.WarehousePrice, item.EstimatePrice)
		item.DeltaCompEstimateAbs, item.DeltaCompEstimatePct = calculateDelta(item.CompetitorPrice, item.EstimatePrice)
		item.DeltaCompWhAbs, item.DeltaCompWhPct = calculateDelta(item.CompetitorPrice, item.WarehousePrice)

		result.Items = append(result.Items, item)
	}

	return result, nil
}

func (s *QuoteService) lookupPricesByPricelistID(pricelistID uint, lotNames []string, noCache bool) (map[string]float64, error) {
	result := make(map[string]float64, len(lotNames))
	if pricelistID == 0 || len(lotNames) == 0 {
		return result, nil
	}

	missing := make([]string, 0, len(lotNames))
	if noCache {
		missing = append(missing, lotNames...)
	} else {
		now := time.Now()
		s.cacheMu.RLock()
		for _, lotName := range lotNames {
			if entry, ok := s.priceCache[s.cacheKey(pricelistID, lotName)]; ok && entry.expiresAt.After(now) {
				if entry.price != nil && *entry.price > 0 {
					result[lotName] = *entry.price
				}
				continue
			}
			missing = append(missing, lotName)
		}
		s.cacheMu.RUnlock()
	}

	if len(missing) == 0 {
		return result, nil
	}

	loaded := make(map[string]float64, len(missing))
	if s.pricelistRepo != nil {
		prices, err := s.pricelistRepo.GetPricesForLots(pricelistID, missing)
		if err == nil {
			for lotName, price := range prices {
				if price > 0 {
					result[lotName] = price
					loaded[lotName] = price
				}
			}
			s.updateCache(pricelistID, missing, loaded)
			return result, nil
		}
	}

	// Fallback path (usually offline): batch local lookup (single query via index).
	if s.localDB != nil {
		if localPL, err := s.localDB.GetLocalPricelistByServerID(pricelistID); err == nil {
			if batchPrices, err := s.localDB.GetLocalPricesForLots(localPL.ID, missing); err == nil {
				for lotName, price := range batchPrices {
					result[lotName] = price
					loaded[lotName] = price
				}
			}
		}
		s.updateCache(pricelistID, missing, loaded)
		return result, nil
	}

	return result, fmt.Errorf("price lookup unavailable for pricelist %d", pricelistID)
}

func (s *QuoteService) updateCache(pricelistID uint, requested []string, loaded map[string]float64) {
	if len(requested) == 0 {
		return
	}
	expiresAt := time.Now().Add(s.cacheTTL)
	s.cacheMu.Lock()
	defer s.cacheMu.Unlock()

	for _, lotName := range requested {
		if price, ok := loaded[lotName]; ok && price > 0 {
			priceCopy := price
			s.priceCache[s.cacheKey(pricelistID, lotName)] = cachedLotPrice{
				price:     &priceCopy,
				expiresAt: expiresAt,
			}
			continue
		}
		s.priceCache[s.cacheKey(pricelistID, lotName)] = cachedLotPrice{
			price:     nil,
			expiresAt: expiresAt,
		}
	}
}

func (s *QuoteService) cacheKey(pricelistID uint, lotName string) string {
	return fmt.Sprintf("%d|%s", pricelistID, lotName)
}

func calculateDelta(target, base *float64) (*float64, *float64) {
	if target == nil || base == nil {
		return nil, nil
	}
	abs := *target - *base
	if *base == 0 {
		return &abs, nil
	}
	pct := (abs / *base) * 100
	return &abs, &pct
}

func (s *QuoteService) lookupLevelPrice(source models.PricelistSource, lotName string, pricelistIDs map[string]uint) (*float64, uint) {
	sourceKey := string(source)
	if id, ok := pricelistIDs[sourceKey]; ok && id > 0 {
		price, found := s.lookupPriceByPricelistID(id, lotName)
		if found {
			return &price, id
		}
		return nil, id
	}

	if s.pricelistRepo != nil {
		price, id, err := s.pricelistRepo.GetPriceForLotBySource(sourceKey, lotName)
		if err == nil && price > 0 {
			return &price, id
		}

		latest, latestErr := s.pricelistRepo.GetLatestActiveBySource(sourceKey)
		if latestErr == nil {
			return nil, latest.ID
		}
	}

	if s.localDB != nil {
		localPL, err := s.localDB.GetLatestLocalPricelistBySource(sourceKey)
		if err != nil {
			return nil, 0
		}
		price, err := s.localDB.GetLocalPriceForLot(localPL.ID, lotName)
		if err != nil || price <= 0 {
			return nil, localPL.ServerID
		}
		return &price, localPL.ServerID
	}

	return nil, 0
}

func (s *QuoteService) lookupPriceByPricelistID(pricelistID uint, lotName string) (float64, bool) {
	if s.pricelistRepo != nil {
		price, err := s.pricelistRepo.GetPriceForLot(pricelistID, lotName)
		if err == nil && price > 0 {
			return price, true
		}
	}

	if s.localDB != nil {
		localPL, err := s.localDB.GetLocalPricelistByServerID(pricelistID)
		if err != nil {
			return 0, false
		}
		price, err := s.localDB.GetLocalPriceForLot(localPL.ID, lotName)
		if err == nil && price > 0 {
			return price, true
		}
	}

	return 0, false
}

