package services

import (
	"bytes"
	"encoding/csv"
	"fmt"
	"io"
	"math"
	"sort"
	"strings"
	"time"

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

type ExportService struct {
	config  config.ExportConfig
	localDB *localdb.LocalDB
}

func NewExportService(cfg config.ExportConfig, local *localdb.LocalDB) *ExportService {
	return &ExportService{
		config:  cfg,
		localDB: local,
	}
}

// ExportItem represents a single component in an export block.
type ExportItem struct {
	LotName     string
	Description string
	Category    string
	Quantity    int
	UnitPrice   float64
	TotalPrice  float64
}

// ConfigExportBlock represents one configuration (server) in the export.
type ConfigExportBlock struct {
	Article     string
	Line        int
	ServerCount int
	UnitPrice   float64 // sum of component prices for one server
	Items       []ExportItem
}

// ProjectExportData holds all configuration blocks for a project-level export.
type ProjectExportData struct {
	Configs   []ConfigExportBlock
	CreatedAt time.Time
}

type ProjectPricingExportOptions struct {
	IncludeLOT        bool     `json:"include_lot"`
	IncludeBOM        bool     `json:"include_bom"`
	IncludeEstimate   bool     `json:"include_estimate"`
	IncludeStock      bool     `json:"include_stock"`
	IncludeCompetitor bool     `json:"include_competitor"`
	Basis             string   `json:"basis"`         // "fob" or "ddp"; empty defaults to "fob"
	SaleMarkup        float64  `json:"sale_markup"`   // DDP multiplier; 0 defaults to 1.3
	ManualPrice       *float64 `json:"manual_price"`  // user-defined total price; distributed proportionally across rows
}

func (o ProjectPricingExportOptions) saleMarkupFactor() float64 {
	if o.SaleMarkup > 0 {
		return o.SaleMarkup
	}
	return 1.3
}

func (o ProjectPricingExportOptions) isDDP() bool {
	return strings.EqualFold(strings.TrimSpace(o.Basis), "ddp")
}

type ProjectPricingExportData struct {
	Configs   []ProjectPricingExportConfig
	CreatedAt time.Time
}

type ProjectPricingExportConfig struct {
	Name        string
	Article     string
	Line        int
	ServerCount int
	Rows        []ProjectPricingExportRow
}

type ProjectPricingExportRow struct {
	LotDisplay   string
	VendorPN     string
	Description  string
	Quantity     int
	BOMTotal     *float64
	Estimate     *float64
	Stock        *float64
	Competitor   *float64
	ManualPrice  *float64 // proportional share of the user-defined total price
}

// ToCSV writes project export data in the new structured CSV format.
//
// Format:
//
//	Line;Type;p/n;Description;Qty (1 pcs.);Qty (total);Price (1 pcs.);Price (total)
//	10;;DL380-ARTICLE;;;10;10470;104 700
//	;;MB_INTEL_...;;1;;2074,5;
//	...
//	(empty row)
//	20;;DL380-ARTICLE-2;;;2;10470;20 940
//	...
func (s *ExportService) ToCSV(w io.Writer, data *ProjectExportData) error {
	// Write UTF-8 BOM for Excel compatibility
	if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
		return fmt.Errorf("failed to write BOM: %w", err)
	}

	csvWriter := csv.NewWriter(w)
	csvWriter.Comma = ';'
	defer csvWriter.Flush()

	// Header
	headers := []string{"Line", "Type", "p/n", "Description", "Qty (1 pcs.)", "Qty (total)", "Price (1 pcs.)", "Price (total)"}
	if err := csvWriter.Write(headers); err != nil {
		return fmt.Errorf("failed to write header: %w", err)
	}

	categoryOrder := defaultCategoryOrder()

	for i, block := range data.Configs {
		lineNo := block.Line
		if lineNo <= 0 {
			lineNo = (i + 1) * 10
		}

		serverCount := block.ServerCount
		if serverCount < 1 {
			serverCount = 1
		}

		totalPrice := block.UnitPrice * float64(serverCount)

		// Server summary row
		serverRow := []string{
			fmt.Sprintf("%d", lineNo),        // Line
			"",                               // Type
			block.Article,                    // p/n
			"",                               // Description
			"",                               // Qty (1 pcs.)
			fmt.Sprintf("%d", serverCount),   // Qty (total)
			formatPriceInt(block.UnitPrice),  // Price (1 pcs.)
			formatPriceWithSpace(totalPrice), // Price (total)
		}
		if err := csvWriter.Write(serverRow); err != nil {
			return fmt.Errorf("failed to write server row: %w", err)
		}

		// Sort items by category display order
		sortedItems := make([]ExportItem, len(block.Items))
		copy(sortedItems, block.Items)
		sortItemsByCategory(sortedItems, categoryOrder)

		// Component rows
		for _, item := range sortedItems {
			componentRow := []string{
				"",                               // Line
				item.Category,                    // Type
				item.LotName,                     // p/n
				"",                               // Description
				fmt.Sprintf("%d", item.Quantity), // Qty (1 pcs.)
				"",                               // Qty (total)
				formatPriceComma(item.UnitPrice), // Price (1 pcs.)
				"",                               // Price (total)
			}
			if err := csvWriter.Write(componentRow); err != nil {
				return fmt.Errorf("failed to write component row: %w", err)
			}
		}

		// Empty separator row between blocks (skip after last)
		if i < len(data.Configs)-1 {
			if err := csvWriter.Write([]string{"", "", "", "", "", "", "", ""}); err != nil {
				return fmt.Errorf("failed to write separator row: %w", err)
			}
		}
	}

	csvWriter.Flush()
	if err := csvWriter.Error(); err != nil {
		return fmt.Errorf("csv writer error: %w", err)
	}

	return nil
}

// ToCSVBytes is a backward-compatible wrapper that returns CSV data as bytes.
func (s *ExportService) ToCSVBytes(data *ProjectExportData) ([]byte, error) {
	var buf bytes.Buffer
	if err := s.ToCSV(&buf, data); err != nil {
		return nil, err
	}
	return buf.Bytes(), nil
}

func sortConfigsByLine(configs []models.Configuration) []models.Configuration {
	sorted := make([]models.Configuration, len(configs))
	copy(sorted, configs)
	sort.Slice(sorted, func(i, j int) bool {
		li, lj := sorted[i].Line, sorted[j].Line
		if li <= 0 {
			li = int(^uint(0) >> 1)
		}
		if lj <= 0 {
			lj = int(^uint(0) >> 1)
		}
		if li != lj {
			return li < lj
		}
		if !sorted[i].CreatedAt.Equal(sorted[j].CreatedAt) {
			return sorted[i].CreatedAt.After(sorted[j].CreatedAt)
		}
		return sorted[i].UUID > sorted[j].UUID
	})
	return sorted
}

func (s *ExportService) ProjectToPricingExportData(configs []models.Configuration, opts ProjectPricingExportOptions) (*ProjectPricingExportData, error) {
	sortedConfigs := sortConfigsByLine(configs)

	blocks := make([]ProjectPricingExportConfig, 0, len(sortedConfigs))
	for i := range sortedConfigs {
		block, err := s.buildPricingExportBlock(&sortedConfigs[i], opts)
		if err != nil {
			return nil, err
		}
		blocks = append(blocks, block)
	}

	return &ProjectPricingExportData{
		Configs:   blocks,
		CreatedAt: time.Now(),
	}, nil
}

func (s *ExportService) ToPricingCSV(w io.Writer, data *ProjectPricingExportData, opts ProjectPricingExportOptions) error {
	if _, err := w.Write([]byte{0xEF, 0xBB, 0xBF}); err != nil {
		return fmt.Errorf("failed to write BOM: %w", err)
	}

	csvWriter := csv.NewWriter(w)
	csvWriter.Comma = ';'
	defer csvWriter.Flush()

	headers := pricingCSVHeaders(opts)
	if err := csvWriter.Write(headers); err != nil {
		return fmt.Errorf("failed to write pricing header: %w", err)
	}

	writeRows := opts.IncludeLOT || opts.IncludeBOM
	for _, cfg := range data.Configs {
		if err := csvWriter.Write(pricingConfigSummaryRow(cfg, opts)); err != nil {
			return fmt.Errorf("failed to write config summary row: %w", err)
		}
		if writeRows {
			for _, row := range cfg.Rows {
				if err := csvWriter.Write(pricingCSVRow(row, opts)); err != nil {
					return fmt.Errorf("failed to write pricing row: %w", err)
				}
			}
		}
	}

	csvWriter.Flush()
	if err := csvWriter.Error(); err != nil {
		return fmt.Errorf("csv writer error: %w", err)
	}
	return nil
}

// ConfigToExportData converts a single configuration into ProjectExportData.
func (s *ExportService) ConfigToExportData(cfg *models.Configuration) *ProjectExportData {
	block := s.buildExportBlock(cfg)
	return &ProjectExportData{
		Configs:   []ConfigExportBlock{block},
		CreatedAt: cfg.CreatedAt,
	}
}

// ProjectToExportData converts multiple configurations into ProjectExportData.
func (s *ExportService) ProjectToExportData(configs []models.Configuration) *ProjectExportData {
	sortedConfigs := sortConfigsByLine(configs)

	blocks := make([]ConfigExportBlock, 0, len(configs))
	for i := range sortedConfigs {
		blocks = append(blocks, s.buildExportBlock(&sortedConfigs[i]))
	}
	return &ProjectExportData{
		Configs:   blocks,
		CreatedAt: time.Now(),
	}
}

// ConfigToPricingExportData is a single-config variant of ProjectToPricingExportData.
func (s *ExportService) ConfigToPricingExportData(cfg *models.Configuration, opts ProjectPricingExportOptions) (*ProjectPricingExportData, error) {
	block, err := s.buildPricingExportBlock(cfg, opts)
	if err != nil {
		return nil, err
	}
	return &ProjectPricingExportData{
		Configs:   []ProjectPricingExportConfig{block},
		CreatedAt: time.Now(),
	}, nil
}

func (s *ExportService) buildExportBlock(cfg *models.Configuration) ConfigExportBlock {
	// Batch-fetch categories from local data (pricelist items → local_components fallback)
	lotNames := make([]string, len(cfg.Items))
	for i, item := range cfg.Items {
		lotNames[i] = item.LotName
	}
	categories := s.resolveCategories(cfg.PricelistID, lotNames)

	items := make([]ExportItem, len(cfg.Items))
	var unitTotal float64

	for i, item := range cfg.Items {
		itemTotal := item.UnitPrice * float64(item.Quantity)
		items[i] = ExportItem{
			LotName:    item.LotName,
			Category:   categories[item.LotName],
			Quantity:   item.Quantity,
			UnitPrice:  item.UnitPrice,
			TotalPrice: itemTotal,
		}
		unitTotal += itemTotal
	}

	serverCount := cfg.ServerCount
	if serverCount < 1 {
		serverCount = 1
	}

	return ConfigExportBlock{
		Article:     cfg.Article,
		Line:        cfg.Line,
		ServerCount: serverCount,
		UnitPrice:   unitTotal,
		Items:       items,
	}
}

func (s *ExportService) buildPricingExportBlock(cfg *models.Configuration, opts ProjectPricingExportOptions) (ProjectPricingExportConfig, error) {
	block := ProjectPricingExportConfig{
		Name:        cfg.Name,
		Article:     cfg.Article,
		Line:        cfg.Line,
		ServerCount: exportPositiveInt(cfg.ServerCount, 1),
		Rows:        make([]ProjectPricingExportRow, 0),
	}
	if s.localDB == nil {
		for _, item := range cfg.Items {
			block.Rows = append(block.Rows, ProjectPricingExportRow{
				LotDisplay: item.LotName,
				VendorPN:   "—",
				Quantity:   item.Quantity,
				Estimate:   floatPtr(item.UnitPrice * float64(item.Quantity)),
			})
		}
		return block, nil
	}

	localCfg, err := s.localDB.GetConfigurationByUUID(cfg.UUID)
	if err != nil {
		localCfg = nil
	}

	priceMap := s.resolvePricingTotals(cfg, localCfg, opts)
	componentDescriptions := s.resolveLotDescriptions(cfg, localCfg)
	if opts.IncludeBOM && localCfg != nil && len(localCfg.VendorSpec) > 0 {
		coveredLots := make(map[string]struct{})
		for _, row := range localCfg.VendorSpec {
			rowMappings := normalizeLotMappings(row.LotMappings)
			for _, mapping := range rowMappings {
				coveredLots[mapping.LotName] = struct{}{}
			}

			description := strings.TrimSpace(row.Description)
			if description == "" && len(rowMappings) > 0 {
				description = componentDescriptions[rowMappings[0].LotName]
			}

			if len(rowMappings) == 0 {
				block.Rows = append(block.Rows, ProjectPricingExportRow{
					LotDisplay:  "н/д",
					VendorPN:    row.VendorPartnumber,
					Description: description,
					Quantity:    exportPositiveInt(row.Quantity, 1),
					BOMTotal:    vendorRowTotal(row),
				})
				continue
			}

			// One export row per LOT mapping so that bundles (1 PN → N LOTs) appear
			// as separate lines, matching the frontend pricing table layout.
			pnQty := exportPositiveInt(row.Quantity, 1)
			for i, mapping := range rowMappings {
				lotQty := pnQty * mapping.QuantityPerPN
				var bomTotal *float64
				if i == 0 {
					bomTotal = vendorRowTotal(row)
				}
				block.Rows = append(block.Rows, ProjectPricingExportRow{
					LotDisplay:  mapping.LotName,
					VendorPN:    row.VendorPartnumber,
					Description: description,
					Quantity:    lotQty,
					BOMTotal:    bomTotal,
					Estimate:    computeSingleLotTotal(priceMap, mapping.LotName, lotQty, func(p pricingLevels) *float64 { return p.Estimate }),
					Stock:       computeSingleLotTotal(priceMap, mapping.LotName, lotQty, func(p pricingLevels) *float64 { return p.Stock }),
					Competitor:  computeSingleLotTotal(priceMap, mapping.LotName, lotQty, func(p pricingLevels) *float64 { return p.Competitor }),
				})
			}
		}

		for _, item := range cfg.Items {
			if item.LotName == "" {
				continue
			}
			if _, ok := coveredLots[item.LotName]; ok {
				continue
			}
			estimate := estimateOnlyTotal(priceMap[item.LotName].Estimate, item.UnitPrice, item.Quantity)
			block.Rows = append(block.Rows, ProjectPricingExportRow{
				LotDisplay:  item.LotName,
				VendorPN:    "—",
				Description: componentDescriptions[item.LotName],
				Quantity:    exportPositiveInt(item.Quantity, 1),
				Estimate:    estimate,
				Stock:       totalForUnitPrice(priceMap[item.LotName].Stock, item.Quantity),
				Competitor:  totalForUnitPrice(priceMap[item.LotName].Competitor, item.Quantity),
			})
		}
		if opts.isDDP() {
			applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
		}
		if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
			distributeManualPrice(block.Rows, *opts.ManualPrice)
		}
		return block, nil
	}

	catOrder := defaultCategoryOrder()
	lotNames := make([]string, 0, len(cfg.Items))
	for _, item := range cfg.Items {
		if item.LotName != "" {
			lotNames = append(lotNames, item.LotName)
		}
	}
	itemCategories := s.resolveCategories(cfg.PricelistID, lotNames)
	sortedItems := sortConfigItemsByCategoryMap(cfg.Items, catOrder, itemCategories)
	for _, item := range sortedItems {
		if item.LotName == "" {
			continue
		}
		estimate := estimateOnlyTotal(priceMap[item.LotName].Estimate, item.UnitPrice, item.Quantity)
		block.Rows = append(block.Rows, ProjectPricingExportRow{
			LotDisplay:  item.LotName,
			VendorPN:    "—",
			Description: componentDescriptions[item.LotName],
			Quantity:    exportPositiveInt(item.Quantity, 1),
			Estimate:    estimate,
			Stock:       totalForUnitPrice(priceMap[item.LotName].Stock, item.Quantity),
			Competitor:  totalForUnitPrice(priceMap[item.LotName].Competitor, item.Quantity),
		})
	}

	if opts.isDDP() {
		applyDDPMarkup(block.Rows, opts.saleMarkupFactor())
	}
	if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
		distributeManualPrice(block.Rows, *opts.ManualPrice)
	}

	return block, nil
}

// sortConfigItemsByCategoryMap returns a copy of items sorted by category display order.
// categories maps lot_name → category code; catOrder maps category code → display order.
func sortConfigItemsByCategoryMap(items models.ConfigItems, catOrder map[string]int, categories map[string]string) models.ConfigItems {
	sorted := make(models.ConfigItems, len(items))
	copy(sorted, items)
	sort.SliceStable(sorted, func(i, j int) bool {
		orderI, hasI := categoryDisplayOrder(catOrder, categories[sorted[i].LotName])
		orderJ, hasJ := categoryDisplayOrder(catOrder, categories[sorted[j].LotName])
		if hasI && hasJ {
			return orderI < orderJ
		}
		return hasI && !hasJ
	})
	return sorted
}

func applyDDPMarkup(rows []ProjectPricingExportRow, factor float64) {
	for i := range rows {
		rows[i].Estimate = scaleFloatPtr(rows[i].Estimate, factor)
		rows[i].Stock = scaleFloatPtr(rows[i].Stock, factor)
		rows[i].Competitor = scaleFloatPtr(rows[i].Competitor, factor)
	}
}

func scaleFloatPtr(v *float64, factor float64) *float64 {
	if v == nil {
		return nil
	}
	result := *v * factor
	return &result
}

// resolveCategories returns lot_name → category map.
// Primary source: pricelist items (lot_category). Fallback: local_components table.
func (s *ExportService) resolveCategories(pricelistID *uint, lotNames []string) map[string]string {
	if len(lotNames) == 0 || s.localDB == nil {
		return map[string]string{}
	}

	categories := make(map[string]string, len(lotNames))

	// Primary: pricelist items
	if pricelistID != nil && *pricelistID > 0 {
		if cats, err := s.localDB.GetLocalLotCategoriesByServerPricelistID(*pricelistID, lotNames); err == nil {
			for lot, cat := range cats {
				if strings.TrimSpace(cat) != "" {
					categories[lot] = cat
				}
			}
		}
	}

	// Fallback: local_components for any still missing
	var missing []string
	for _, lot := range lotNames {
		if categories[lot] == "" {
			missing = append(missing, lot)
		}
	}
	if len(missing) > 0 {
		if fallback, err := s.localDB.GetLocalComponentCategoriesByLotNames(missing); err == nil {
			for lot, cat := range fallback {
				if strings.TrimSpace(cat) != "" {
					categories[lot] = cat
				}
			}
		}
	}

	return categories
}

// defaultCategoryOrder returns an uppercase category code → display_order map from models.DefaultCategories.
func defaultCategoryOrder() map[string]int {
	m := make(map[string]int, len(models.DefaultCategories))
	for _, cat := range models.DefaultCategories {
		m[strings.ToUpper(cat.Code)] = cat.DisplayOrder
	}
	return m
}

func categoryDisplayOrder(categoryOrder map[string]int, category string) (int, bool) {
	order, ok := categoryOrder[strings.ToUpper(strings.TrimSpace(category))]
	return order, ok
}

// sortItemsByCategory sorts items by category display order (items without category go to the end).
func sortItemsByCategory(items []ExportItem, categoryOrder map[string]int) {
	sort.SliceStable(items, func(i, j int) bool {
		orderI, hasI := categoryDisplayOrder(categoryOrder, items[i].Category)
		orderJ, hasJ := categoryDisplayOrder(categoryOrder, items[j].Category)
		if hasI && hasJ {
			return orderI < orderJ
		}
		return hasI && !hasJ
	})
}

type pricingLevels struct {
	Estimate   *float64
	Stock      *float64
	Competitor *float64
}

func (s *ExportService) resolvePricingTotals(cfg *models.Configuration, localCfg *localdb.LocalConfiguration, opts ProjectPricingExportOptions) map[string]pricingLevels {
	result := map[string]pricingLevels{}
	lots := collectPricingLots(cfg, localCfg, opts.IncludeBOM)
	if len(lots) == 0 || s.localDB == nil {
		return result
	}

	estimateID := cfg.PricelistID
	if estimateID == nil || *estimateID == 0 {
		if latest, err := s.localDB.GetLatestLocalPricelistBySource("estimate"); err == nil && latest != nil {
			estimateID = &latest.ServerID
		}
	}

	var warehouseID *uint
	var competitorID *uint
	if localCfg != nil {
		warehouseID = localCfg.WarehousePricelistID
		competitorID = localCfg.CompetitorPricelistID
	}
	if warehouseID == nil || *warehouseID == 0 {
		if latest, err := s.localDB.GetLatestLocalPricelistBySource("warehouse"); err == nil && latest != nil {
			warehouseID = &latest.ServerID
		}
	}
	if competitorID == nil || *competitorID == 0 {
		if latest, err := s.localDB.GetLatestLocalPricelistBySource("competitor"); err == nil && latest != nil {
			competitorID = &latest.ServerID
		}
	}

	estimatePrices := s.batchLookupPrices(estimateID, lots)
	stockPrices := s.batchLookupPrices(warehouseID, lots)
	competitorPrices := s.batchLookupPrices(competitorID, lots)

	for _, lot := range lots {
		level := pricingLevels{}
		if p, ok := estimatePrices[lot]; ok {
			level.Estimate = floatPtr(p)
		}
		if p, ok := stockPrices[lot]; ok {
			level.Stock = floatPtr(p)
		}
		if p, ok := competitorPrices[lot]; ok {
			level.Competitor = floatPtr(p)
		}
		result[lot] = level
	}
	return result
}

// batchLookupPrices fetches prices for all lots from a pricelist in a single query.
func (s *ExportService) batchLookupPrices(serverPricelistID *uint, lots []string) map[string]float64 {
	if s.localDB == nil || serverPricelistID == nil || *serverPricelistID == 0 || len(lots) == 0 {
		return nil
	}
	localPL, err := s.localDB.GetLocalPricelistByServerID(*serverPricelistID)
	if err != nil {
		return nil
	}
	prices, err := s.localDB.GetLocalPricesForLots(localPL.ID, lots)
	if err != nil {
		return nil
	}
	return prices
}

func (s *ExportService) resolveLotDescriptions(cfg *models.Configuration, localCfg *localdb.LocalConfiguration) map[string]string {
	lots := collectPricingLots(cfg, localCfg, true)
	if s.localDB == nil || len(lots) == 0 {
		return map[string]string{}
	}
	descriptions, err := s.localDB.GetLocalComponentDescriptionsByLotNames(lots)
	if err != nil {
		return map[string]string{}
	}
	return descriptions
}

func collectPricingLots(cfg *models.Configuration, localCfg *localdb.LocalConfiguration, includeBOM bool) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0)
	if includeBOM && localCfg != nil {
		for _, row := range localCfg.VendorSpec {
			for _, mapping := range normalizeLotMappings(row.LotMappings) {
				if _, ok := seen[mapping.LotName]; ok {
					continue
				}
				seen[mapping.LotName] = struct{}{}
				out = append(out, mapping.LotName)
			}
		}
	}
	for _, item := range cfg.Items {
		lot := strings.TrimSpace(item.LotName)
		if lot == "" {
			continue
		}
		if _, ok := seen[lot]; ok {
			continue
		}
		seen[lot] = struct{}{}
		out = append(out, lot)
	}
	return out
}

func normalizeLotMappings(mappings []localdb.VendorSpecLotMapping) []localdb.VendorSpecLotMapping {
	if len(mappings) == 0 {
		return nil
	}
	out := make([]localdb.VendorSpecLotMapping, 0, len(mappings))
	for _, mapping := range mappings {
		lot := strings.TrimSpace(mapping.LotName)
		if lot == "" {
			continue
		}
		qty := mapping.QuantityPerPN
		if qty < 1 {
			qty = 1
		}
		out = append(out, localdb.VendorSpecLotMapping{
			LotName:       lot,
			QuantityPerPN: qty,
		})
	}
	return out
}

func vendorRowTotal(row localdb.VendorSpecItem) *float64 {
	if row.TotalPrice != nil {
		return floatPtr(*row.TotalPrice)
	}
	if row.UnitPrice == nil {
		return nil
	}
	return floatPtr(*row.UnitPrice * float64(exportPositiveInt(row.Quantity, 1)))
}

func computeMappingTotal(priceMap map[string]pricingLevels, mappings []localdb.VendorSpecLotMapping, pnQty int, selector func(pricingLevels) *float64) *float64 {
	if len(mappings) == 0 {
		return nil
	}
	total := 0.0
	hasValue := false
	qty := exportPositiveInt(pnQty, 1)
	for _, mapping := range mappings {
		price := selector(priceMap[mapping.LotName])
		if price == nil || *price <= 0 {
			continue
		}
		total += *price * float64(qty*mapping.QuantityPerPN)
		hasValue = true
	}
	if !hasValue {
		return nil
	}
	return floatPtr(total)
}

// distributeManualPrice sets ManualPrice on each row proportionally based on the
// row's Estimate share. The last row with a price absorbs rounding remainder so
// the sum of ManualPrice values always equals manualPrice exactly.
func distributeManualPrice(rows []ProjectPricingExportRow, manualPrice float64) {
	if manualPrice <= 0 || len(rows) == 0 {
		return
	}
	totalEstimate := 0.0
	for _, row := range rows {
		if row.Estimate != nil && *row.Estimate > 0 {
			totalEstimate += *row.Estimate
		}
	}
	if totalEstimate <= 0 {
		return
	}
	lastIdx := -1
	for i, row := range rows {
		if row.Estimate != nil && *row.Estimate > 0 {
			lastIdx = i
		}
	}
	assigned := 0.0
	for i, row := range rows {
		if row.Estimate == nil || *row.Estimate <= 0 {
			continue
		}
		var share float64
		if i == lastIdx {
			share = math.Round((manualPrice-assigned)*100) / 100
		} else {
			share = math.Round((*row.Estimate/totalEstimate)*manualPrice*100) / 100
			assigned += share
		}
		rows[i].ManualPrice = floatPtr(share)
	}
}

func computeSingleLotTotal(priceMap map[string]pricingLevels, lotName string, qty int, selector func(pricingLevels) *float64) *float64 {
	price := selector(priceMap[lotName])
	if price == nil || *price <= 0 {
		return nil
	}
	return floatPtr(*price * float64(qty))
}

func totalForUnitPrice(unitPrice *float64, quantity int) *float64 {
	if unitPrice == nil || *unitPrice <= 0 {
		return nil
	}
	total := *unitPrice * float64(exportPositiveInt(quantity, 1))
	return &total
}

func estimateOnlyTotal(estimatePrice *float64, fallbackUnitPrice float64, quantity int) *float64 {
	if estimatePrice != nil && *estimatePrice > 0 {
		return totalForUnitPrice(estimatePrice, quantity)
	}
	if fallbackUnitPrice <= 0 {
		return nil
	}
	total := fallbackUnitPrice * float64(maxInt(quantity, 1))
	return &total
}

func pricingCSVHeaders(opts ProjectPricingExportOptions) []string {
	headers := make([]string, 0, 9)
	headers = append(headers, "Line Item")
	if opts.IncludeLOT {
		headers = append(headers, "LOT")
	}
	headers = append(headers, "PN вендора", "Описание", "Кол-во")
	if opts.IncludeBOM {
		headers = append(headers, "BOM")
	}
	if opts.IncludeEstimate {
		headers = append(headers, "Estimate")
	}
	if opts.IncludeStock {
		headers = append(headers, "Stock")
	}
	if opts.IncludeCompetitor {
		headers = append(headers, "Конкуренты")
	}
	if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
		headers = append(headers, "Ручная цена")
	}
	return headers
}

func pricingCSVRow(row ProjectPricingExportRow, opts ProjectPricingExportOptions) []string {
	record := make([]string, 0, 9)
	record = append(record, "")
	if opts.IncludeLOT {
		record = append(record, emptyDash(row.LotDisplay))
	}
	record = append(record,
		emptyDash(row.VendorPN),
		emptyDash(row.Description),
		fmt.Sprintf("%d", exportPositiveInt(row.Quantity, 1)),
	)
	if opts.IncludeBOM {
		record = append(record, formatMoneyValue(row.BOMTotal))
	}
	if opts.IncludeEstimate {
		record = append(record, formatMoneyValue(row.Estimate))
	}
	if opts.IncludeStock {
		record = append(record, formatMoneyValue(row.Stock))
	}
	if opts.IncludeCompetitor {
		record = append(record, formatMoneyValue(row.Competitor))
	}
	if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
		record = append(record, formatMoneyValue(row.ManualPrice))
	}
	return record
}

func pricingConfigSummaryRow(cfg ProjectPricingExportConfig, opts ProjectPricingExportOptions) []string {
	record := make([]string, 0, 9)
	record = append(record, fmt.Sprintf("%d", cfg.Line))
	if opts.IncludeLOT {
		record = append(record, "")
	}
	record = append(record,
		emptyDash(cfg.Article),
		emptyDash(cfg.Name),
		fmt.Sprintf("%d", exportPositiveInt(cfg.ServerCount, 1)),
	)
	if opts.IncludeBOM {
		record = append(record, formatMoneyValue(sumPricingColumn(cfg.Rows, func(row ProjectPricingExportRow) *float64 { return row.BOMTotal })))
	}
	if opts.IncludeEstimate {
		record = append(record, formatMoneyValue(sumPricingColumn(cfg.Rows, func(row ProjectPricingExportRow) *float64 { return row.Estimate })))
	}
	if opts.IncludeStock {
		record = append(record, formatMoneyValue(sumPricingColumn(cfg.Rows, func(row ProjectPricingExportRow) *float64 { return row.Stock })))
	}
	if opts.IncludeCompetitor {
		record = append(record, formatMoneyValue(sumPricingColumn(cfg.Rows, func(row ProjectPricingExportRow) *float64 { return row.Competitor })))
	}
	if opts.ManualPrice != nil && *opts.ManualPrice > 0 {
		record = append(record, formatMoneyValue(opts.ManualPrice))
	}
	return record
}


func formatMoneyValue(value *float64) string {
	if value == nil {
		return "—"
	}
	n := math.Round(*value*100) / 100
	sign := ""
	if n < 0 {
		sign = "-"
		n = -n
	}
	whole := int64(n)
	fraction := int(math.Round((n - float64(whole)) * 100))
	if fraction == 100 {
		whole++
		fraction = 0
	}
	return fmt.Sprintf("%s%s,%02d", sign, formatIntWithSpace(whole), fraction)
}

func emptyDash(value string) string {
	if strings.TrimSpace(value) == "" {
		return "—"
	}
	return value
}

func sumPricingColumn(rows []ProjectPricingExportRow, selector func(ProjectPricingExportRow) *float64) *float64 {
	total := 0.0
	hasValue := false
	for _, row := range rows {
		value := selector(row)
		if value == nil {
			continue
		}
		total += *value
		hasValue = true
	}
	if !hasValue {
		return nil
	}
	return floatPtr(total)
}

func floatPtr(value float64) *float64 {
	v := value
	return &v
}

func exportPositiveInt(value, fallback int) int {
	if value < 1 {
		return fallback
	}
	return value
}

// formatPriceComma formats a price with comma as decimal separator (e.g., "2074,5").
// Trailing zeros after the comma are trimmed, and if the value is an integer, no comma is shown.
func formatPriceComma(value float64) string {
	if value == math.Trunc(value) {
		return fmt.Sprintf("%.0f", value)
	}
	s := fmt.Sprintf("%.2f", value)
	s = strings.ReplaceAll(s, ".", ",")
	// Trim trailing zero: "2074,50" -> "2074,5"
	s = strings.TrimRight(s, "0")
	s = strings.TrimRight(s, ",")
	return s
}

// formatPriceInt formats price as integer (rounded), no decimal.
func formatPriceInt(value float64) string {
	return fmt.Sprintf("%.0f", math.Round(value))
}

// formatPriceWithSpace formats a price as an integer with space as thousands separator (e.g., "104 700").
func formatPriceWithSpace(value float64) string {
	intVal := int64(math.Round(value))
	if intVal < 0 {
		return "-" + formatIntWithSpace(-intVal)
	}
	return formatIntWithSpace(intVal)
}

func formatIntWithSpace(n int64) string {
	s := fmt.Sprintf("%d", n)
	if len(s) <= 3 {
		return s
	}

	var result strings.Builder
	remainder := len(s) % 3
	if remainder > 0 {
		result.WriteString(s[:remainder])
	}
	for i := remainder; i < len(s); i += 3 {
		if result.Len() > 0 {
			result.WriteByte(' ')
		}
		result.WriteString(s[i : i+3])
	}
	return result.String()
}
