package services

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

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

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

func NewExportService(cfg config.ExportConfig, categoryRepo *repository.CategoryRepository, local *localdb.LocalDB) *ExportService {
	return &ExportService{
		config:       cfg,
		categoryRepo: categoryRepo,
		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
	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
}

// 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)
	}

	// Get category hierarchy for sorting
	categoryOrder := make(map[string]int)
	if s.categoryRepo != nil {
		categories, err := s.categoryRepo.GetAll()
		if err == nil {
			for _, cat := range categories {
				categoryOrder[cat.Code] = cat.DisplayOrder
			}
		}
	}

	for i, block := range data.Configs {
		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
}

// 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 {
	blocks := make([]ConfigExportBlock, 0, len(configs))
	for i := range configs {
		blocks = append(blocks, s.buildExportBlock(&configs[i]))
	}
	return &ProjectExportData{
		Configs:   blocks,
		CreatedAt: time.Now(),
	}
}

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,
		ServerCount: serverCount,
		UnitPrice:   unitTotal,
		Items:       items,
	}
}

// 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
}

// sortItemsByCategory sorts items by category display order (items without category go to the end).
func sortItemsByCategory(items []ExportItem, categoryOrder map[string]int) {
	for i := 0; i < len(items)-1; i++ {
		for j := i + 1; j < len(items); j++ {
			orderI, hasI := categoryOrder[items[i].Category]
			orderJ, hasJ := categoryOrder[items[j].Category]

			if !hasI && hasJ {
				items[i], items[j] = items[j], items[i]
			} else if hasI && hasJ && orderI > orderJ {
				items[i], items[j] = items[j], items[i]
			}
		}
	}
}

// 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()
}
