package handlers

import (
	"errors"
	"log/slog"
	"net/http"
	"strings"

	"git.mchus.pro/mchus/quoteforge/internal/localdb"
	"git.mchus.pro/mchus/quoteforge/internal/repository"
	"git.mchus.pro/mchus/quoteforge/internal/services"
	syncsvc "git.mchus.pro/mchus/quoteforge/internal/services/sync"
	"github.com/gin-gonic/gin"
)

// VendorSpecHandler handles vendor BOM spec operations for a configuration.
type VendorSpecHandler struct {
	localDB       *localdb.LocalDB
	configService *services.LocalConfigurationService
	syncService   *syncsvc.Service // optional; nil = no server push
}

func NewVendorSpecHandler(localDB *localdb.LocalDB, syncService *syncsvc.Service) *VendorSpecHandler {
	return &VendorSpecHandler{
		localDB:       localDB,
		configService: services.NewLocalConfigurationService(localDB, nil, nil, func() bool { return false }),
		syncService:   syncService,
	}
}

// lookupConfig finds an active configuration by UUID using the standard localDB method.
func (h *VendorSpecHandler) lookupConfig(uuid string) (*localdb.LocalConfiguration, error) {
	cfg, err := h.localDB.GetConfigurationByUUID(uuid)
	if err != nil {
		return nil, err
	}
	if !cfg.IsActive {
		return nil, errors.New("not active")
	}
	return cfg, nil
}

// ParseText parses a pasted single-column text BOM (Inspur or Russian text BOM)
// using the same parsers as the vendor file-import path. It is stateless: no
// configuration is required. Returns the parsed rows and the detected format, or
// an empty result when the text is not a recognized single-column format (the
// client then falls back to manual column mapping).
// POST /api/vendor-spec/parse-text
func (h *VendorSpecHandler) ParseText(c *gin.Context) {
	var body struct {
		Text string `json:"text"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
		return
	}

	rows, format := services.ParsePastedBOMText(body.Text)
	if rows == nil {
		rows = []localdb.VendorSpecItem{}
	}
	c.JSON(http.StatusOK, gin.H{"rows": rows, "format": format})
}

// GetVendorSpec returns the vendor spec (BOM) for a configuration.
// GET /api/configs/:uuid/vendor-spec
func (h *VendorSpecHandler) GetVendorSpec(c *gin.Context) {
	cfg, err := h.lookupConfig(c.Param("uuid"))
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
		return
	}

	spec := cfg.VendorSpec
	if spec == nil {
		spec = localdb.VendorSpec{}
	}
	c.JSON(http.StatusOK, gin.H{"vendor_spec": spec})
}

// PutVendorSpec saves (replaces) the vendor spec for a configuration.
// PUT /api/configs/:uuid/vendor-spec
func (h *VendorSpecHandler) PutVendorSpec(c *gin.Context) {
	cfg, err := h.lookupConfig(c.Param("uuid"))
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
		return
	}

	var body struct {
		VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
		return
	}

	for i := range body.VendorSpec {
		if body.VendorSpec[i].SortOrder == 0 {
			body.VendorSpec[i].SortOrder = (i + 1) * 10
		}
		// Persist canonical LOT mapping only.
		body.VendorSpec[i].LotMappings = normalizeLotMappings(body.VendorSpec[i].LotMappings)
		body.VendorSpec[i].ResolvedLotName = ""
		body.VendorSpec[i].ResolutionSource = ""
		body.VendorSpec[i].ManualLotSuggestion = ""
		body.VendorSpec[i].LotQtyPerPN = 0
		body.VendorSpec[i].LotAllocations = nil
	}

	spec := localdb.VendorSpec(body.VendorSpec)
	if _, err := h.configService.UpdateVendorSpecNoAuth(cfg.UUID, spec); err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	// Push manual lot mappings as suggestions to the server (best-effort, non-blocking).
	h.pushLotSuggestions(body.VendorSpec)

	c.JSON(http.StatusOK, gin.H{"vendor_spec": spec})
}

// pushLotSuggestions sends manual PN→LOT mappings to qt_vendor_partnumber_seen.lot_suggestion.
// Errors are logged and silently dropped — they must not affect the HTTP response.
func (h *VendorSpecHandler) pushLotSuggestions(spec []localdb.VendorSpecItem) {
	if h.syncService == nil {
		return
	}

	var items []syncsvc.SeenPartnumber
	for _, row := range spec {
		if row.VendorPartnumber == "" || len(row.LotMappings) == 0 {
			continue
		}
		suggestion := make([]syncsvc.LotSuggestionEntry, 0, len(row.LotMappings))
		for _, m := range row.LotMappings {
			if m.LotName == "" {
				continue
			}
			qty := m.QuantityPerPN
			if qty < 1 {
				qty = 1
			}
			suggestion = append(suggestion, syncsvc.LotSuggestionEntry{
				LotName: m.LotName,
				Qty:     qty,
			})
		}
		if len(suggestion) == 0 {
			continue
		}
		items = append(items, syncsvc.SeenPartnumber{
			Partnumber:    row.VendorPartnumber,
			Description:   row.Description,
			LotSuggestion: suggestion,
		})
	}

	if len(items) == 0 {
		return
	}

	if err := h.syncService.PushPartnumberSeen(items); err != nil {
		slog.Warn("vendor_spec: failed to push lot suggestions to server", "error", err)
	}
}

func normalizeLotMappings(in []localdb.VendorSpecLotMapping) []localdb.VendorSpecLotMapping {
	if len(in) == 0 {
		return nil
	}
	merged := make(map[string]int, len(in))
	order := make([]string, 0, len(in))
	for _, m := range in {
		lot := strings.TrimSpace(m.LotName)
		if lot == "" {
			continue
		}
		qty := m.QuantityPerPN
		if qty < 1 {
			qty = 1
		}
		if _, exists := merged[lot]; !exists {
			order = append(order, lot)
		}
		merged[lot] += qty
	}
	out := make([]localdb.VendorSpecLotMapping, 0, len(order))
	for _, lot := range order {
		out = append(out, localdb.VendorSpecLotMapping{
			LotName:       lot,
			QuantityPerPN: merged[lot],
		})
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

// ResolveVendorSpec resolves vendor PN → LOT without modifying the cart.
// POST /api/configs/:uuid/vendor-spec/resolve
func (h *VendorSpecHandler) ResolveVendorSpec(c *gin.Context) {
	if _, err := h.lookupConfig(c.Param("uuid")); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
		return
	}

	var body struct {
		VendorSpec []localdb.VendorSpecItem `json:"vendor_spec"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
		return
	}

	bookRepo := repository.NewPartnumberBookRepository(h.localDB.DB())
	resolver := services.NewVendorSpecResolver(bookRepo)

	resolved, err := resolver.Resolve(body.VendorSpec)
	if err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	book, err := bookRepo.GetActiveBook()
	if err != nil {
		slog.Warn("vendor spec resolve: no active partnumber book", "err", err)
		book = nil
	}
	aggregated, err := services.AggregateLOTs(resolved, book, bookRepo)
	if err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"resolved":   resolved,
		"aggregated": aggregated,
	})
}

// ApplyVendorSpec applies the resolved BOM to the cart (Estimate items).
// POST /api/configs/:uuid/vendor-spec/apply
func (h *VendorSpecHandler) ApplyVendorSpec(c *gin.Context) {
	cfg, err := h.lookupConfig(c.Param("uuid"))
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "configuration not found"})
		return
	}

	var body struct {
		Items []struct {
			LotName   string  `json:"lot_name"`
			Quantity  int     `json:"quantity"`
			UnitPrice float64 `json:"unit_price"`
		} `json:"items"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		RespondError(c, http.StatusUnprocessableEntity, "invalid request", err)
		return
	}

	newItems := make(localdb.LocalConfigItems, 0, len(body.Items))
	for _, it := range body.Items {
		newItems = append(newItems, localdb.LocalConfigItem{
			LotName:   it.LotName,
			Quantity:  it.Quantity,
			UnitPrice: it.UnitPrice,
		})
	}

	if _, err := h.configService.ApplyVendorSpecItemsNoAuth(cfg.UUID, newItems); err != nil {
		RespondError(c, http.StatusInternalServerError, "internal server error", err)
		return
	}

	c.JSON(http.StatusOK, gin.H{"items": newItems})
}
