package routes

import (
	"bssapp-backend/auth"
	"bssapp-backend/queries"
	"bssapp-backend/utils"
	"database/sql"
	"encoding/json"
	"fmt"
	"github.com/lib/pq"
	"net/http"
	"sort"
	"strconv"
	"strings"
	"time"
)

// Step-1/2 scope (distinct+cascade) comes from the PostgreSQL parameter cache.
// For now we implement:
// - Postgres tables (bootstrap)
// - List/Save rules (bulk)
// - Options endpoint for cascade (mk_urunpricingprmtr)

type PricingRuleBulkSavePayload struct {
	Items []queries.PricingRuleSaveItem `json:"items"`
}

type PricingRuleImportItem struct {
	AskiliYan        string  `json:"askili_yan"`
	Kategori         string  `json:"kategori"`
	UrunIlkGrubu     string  `json:"urun_ilk_grubu"`
	UrunAnaGrubu     string  `json:"urun_ana_grubu"`
	UrunAltGrubu     string  `json:"urun_alt_grubu"`
	Icerik           string  `json:"icerik"`
	Marka            string  `json:"marka"`
	BrandCode        string  `json:"brand_code"`
	BrandGroupSec    string  `json:"brand_group"`
	StrategyCode     string  `json:"strategy_code"`
	AnchorMode       string  `json:"anchor_mode"`
	CalcEnabled      bool    `json:"calc_enabled"`
	PublishPostgres  bool    `json:"publish_postgres"`
	PublishNebim     bool    `json:"publish_nebim"`
	IsActive         bool    `json:"is_active"`
	TryBase          float64 `json:"try_base"`
	Try1             float64 `json:"try1"`
	Try2             float64 `json:"try2"`
	Try3             float64 `json:"try3"`
	Try4             float64 `json:"try4"`
	Try5             float64 `json:"try5"`
	Try6             float64 `json:"try6"`
	TryWholesaleStep float64 `json:"try_wholesale_step"`
	TryRetailStep    float64 `json:"try_retail_step"`
	TryRetailMode    string  `json:"try_retail_mode"`
	UsdBase          float64 `json:"usd_base"`
	Usd1             float64 `json:"usd1"`
	Usd2             float64 `json:"usd2"`
	Usd3             float64 `json:"usd3"`
	Usd4             float64 `json:"usd4"`
	Usd5             float64 `json:"usd5"`
	Usd6             float64 `json:"usd6"`
	UsdWholesaleStep float64 `json:"usd_wholesale_step"`
	UsdRetailStep    float64 `json:"usd_retail_step"`
	UsdRetailMode    string  `json:"usd_retail_mode"`
	EurBase          float64 `json:"eur_base"`
	Eur1             float64 `json:"eur1"`
	Eur2             float64 `json:"eur2"`
	Eur3             float64 `json:"eur3"`
	Eur4             float64 `json:"eur4"`
	Eur5             float64 `json:"eur5"`
	Eur6             float64 `json:"eur6"`
	EurWholesaleStep float64 `json:"eur_wholesale_step"`
	EurRetailStep    float64 `json:"eur_retail_step"`
	EurRetailMode    string  `json:"eur_retail_mode"`
}

type PricingRuleImportPayload struct {
	Items []PricingRuleImportItem `json:"items"`
}

type PricingRuleImportResult struct {
	Success             bool `json:"success"`
	Processed           int  `json:"processed"`
	Matched             int  `json:"matched"`
	Skipped             int  `json:"skipped"`
	Updated             int  `json:"updated"`
	ActivatedScopeCount int  `json:"activated_scope_count"`
	ErrorCount          int  `json:"error_count"`
}

func normalizePricingStrategyCode(v string) string {
	v = strings.ToUpper(strings.TrimSpace(v))
	if v == "" {
		return "CORE"
	}
	return v
}

func normalizePricingAnchorMode(v string) string {
	v = strings.ToUpper(strings.TrimSpace(v))
	if v == "" {
		return "USD"
	}
	return v
}

func isValidPricingStrategyCode(v string) bool {
	if strings.TrimSpace(v) == "" {
		return true
	}
	switch normalizePricingStrategyCode(v) {
	case "CORE", "PREMIUM", "SARTORIAL":
		return true
	default:
		return false
	}
}

func isValidPricingAnchorMode(v string) bool {
	switch normalizePricingAnchorMode(v) {
	case "TRY", "USD":
		return true
	default:
		return false
	}
}

func isValidPricingRetailMode(v string) bool {
	switch queries.NormalizeRetailModeForRoute(v) {
	case "STEP", "END_99", "END_49", "BAND_99", "BAND_49":
		return true
	default:
		return false
	}
}

func GetPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		traceID := utils.TraceIDFromRequest(r)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)

		rows, err := queries.ListPricingRules(ctx, pg)
		if err != nil {
			http.Error(w, "pricing rules list error", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(rows)
	}
}

// Very small “bulk upsert” for step-1/2: we only need to persist the multipliers+roundings for now.
// Rules are identified by UUID; new rows can be created via empty id (server generates).
func SavePricingRulesBulkHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload PricingRuleBulkSavePayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}

		started := time.Now()
		traceID := utils.TraceIDFromRequest(r)
		w.Header().Set("X-Trace-ID", traceID)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)
		logger := utils.SlogFromContext(ctx).With("handler", "pricing-rules.bulk-save")

		claims, _ := auth.GetClaimsFromContext(ctx)
		if claims != nil {
			logger = logger.With("user", claims.Username, "user_id", claims.ID)
		}
		existingIDCount := 0
		newIDCount := 0
		for _, it := range payload.Items {
			if strings.TrimSpace(it.ID) != "" {
				existingIDCount++
			} else {
				newIDCount++
			}
		}
		logger.Info("bulk-save:start",
			"items", len(payload.Items),
			"existing_id", existingIDCount,
			"new_id", newIDCount,
		)

		tx, err := pg.BeginTx(ctx, nil)
		if err != nil {
			logger.Error("bulk-save:tx-begin:error", "err", err)
			http.Error(w, "pg transaction start error", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		// Serialize writes touching mk_urunpricingprmtr/mk_pricing_rule/mk_pricex/mk_priceroll
		// to avoid deadlocks with pricing-parameter sync and concurrent bulk-saves.
		lockWaitStarted := time.Now()
		if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(1001, 1)`); err != nil {
			logger.Error("bulk-save:advisory-lock:error", "err", err)
			http.Error(w, "pg advisory lock error", http.StatusInternalServerError)
			return
		}
		logger.Info("bulk-save:advisory-lock:acquired", "wait_ms", time.Since(lockWaitStarted).Milliseconds())

		logPgErr := func(msg string, err error, it queries.PricingRuleSaveItem) {
			fields := []any{
				"pricing_parameter_id", it.PricingParameterID,
				"id", strings.TrimSpace(it.ID),
				"err", err,
			}
			if pe, ok := err.(*pq.Error); ok && pe != nil {
				fields = append(fields,
					"sqlstate", string(pe.Code),
					"constraint", pe.Constraint,
					"table", pe.Table,
					"column", pe.Column,
					"detail", pe.Detail,
					"where", pe.Where,
				)
			}
			logger.Error(msg, fields...)
		}

		updated := 0
		for _, it := range payload.Items {
			// Zero means that no rounding rule has been configured yet.
			if it.TryWholesaleStep < 0 || it.TryRetailStep < 0 || it.UsdWholesaleStep < 0 || it.UsdRetailStep < 0 || it.EurWholesaleStep < 0 || it.EurRetailStep < 0 {
				logger.Warn("bulk-save:invalid-rounding-step",
					"pricing_parameter_id", it.PricingParameterID,
					"id", strings.TrimSpace(it.ID),
				)
				http.Error(w, "invalid rounding step", http.StatusBadRequest)
				return
			}
			if !isValidPricingStrategyCode(it.StrategyCode) {
				logger.Warn("bulk-save:invalid-strategy-code",
					"pricing_parameter_id", it.PricingParameterID,
					"id", strings.TrimSpace(it.ID),
					"strategy_code", it.StrategyCode,
				)
				http.Error(w, "invalid strategy_code", http.StatusBadRequest)
				return
			}
			if !isValidPricingAnchorMode(it.AnchorMode) {
				logger.Warn("bulk-save:invalid-anchor-mode",
					"pricing_parameter_id", it.PricingParameterID,
					"id", strings.TrimSpace(it.ID),
					"anchor_mode", it.AnchorMode,
				)
				http.Error(w, "invalid anchor_mode", http.StatusBadRequest)
				return
			}
			if !isValidPricingRetailMode(it.TryRetailMode) || !isValidPricingRetailMode(it.UsdRetailMode) || !isValidPricingRetailMode(it.EurRetailMode) {
				logger.Warn("bulk-save:invalid-retail-mode",
					"pricing_parameter_id", it.PricingParameterID,
					"id", strings.TrimSpace(it.ID),
					"try_retail_mode", it.TryRetailMode,
					"usd_retail_mode", it.UsdRetailMode,
					"eur_retail_mode", it.EurRetailMode,
				)
				http.Error(w, "invalid retail_mode", http.StatusBadRequest)
				return
			}
		}

		dbStarted := time.Now()
		updated, err = queries.BulkSavePricingRulesFast(ctx, tx, payload.Items)
		if err != nil {
			// best-effort: log first item context
			if len(payload.Items) > 0 {
				logPgErr("bulk-save:bulk-fast:error", err, payload.Items[0])
			} else {
				logger.Error("bulk-save:bulk-fast:error", "err", err)
			}
			http.Error(w, "pricing rule save error: "+err.Error(), http.StatusInternalServerError)
			return
		}
		logger.Info("bulk-save:db:done", "updated", updated, "duration_ms", time.Since(dbStarted).Milliseconds())

		if err := tx.Commit(); err != nil {
			logger.Error("bulk-save:commit:error", "err", err)
			http.Error(w, "pg transaction commit error", http.StatusInternalServerError)
			return
		}
		logger.Info("bulk-save:done", "updated", updated, "duration_ms", time.Since(started).Milliseconds())
		_ = json.NewEncoder(w).Encode(map[string]any{"success": true, "updated": updated})
	}
}

func ImportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		var payload PricingRuleImportPayload
		if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
			http.Error(w, "invalid payload", http.StatusBadRequest)
			return
		}
		if len(payload.Items) == 0 {
			_ = json.NewEncoder(w).Encode(PricingRuleImportResult{Success: true})
			return
		}

		traceID := utils.TraceIDFromRequest(r)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)

		tx, err := pg.BeginTx(ctx, nil)
		if err != nil {
			http.Error(w, "pg transaction start error", http.StatusInternalServerError)
			return
		}
		defer tx.Rollback()

		// Same global lock as bulk-save: prevents deadlocks with concurrent updates/sync.
		if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(1001, 1)`); err != nil {
			http.Error(w, "pg advisory lock error", http.StatusInternalServerError)
			return
		}

		updated := 0
		matched := 0
		skipped := 0
		for _, raw := range payload.Items {
			if raw.TryWholesaleStep < 0 || raw.TryRetailStep < 0 || raw.UsdWholesaleStep < 0 || raw.UsdRetailStep < 0 || raw.EurWholesaleStep < 0 || raw.EurRetailStep < 0 {
				http.Error(w, "invalid rounding step", http.StatusBadRequest)
				return
			}
			if !isValidPricingStrategyCode(raw.StrategyCode) {
				http.Error(w, "invalid strategy_code", http.StatusBadRequest)
				return
			}
			if !isValidPricingAnchorMode(raw.AnchorMode) {
				http.Error(w, "invalid anchor_mode", http.StatusBadRequest)
				return
			}
			if !isValidPricingRetailMode(raw.TryRetailMode) || !isValidPricingRetailMode(raw.UsdRetailMode) || !isValidPricingRetailMode(raw.EurRetailMode) {
				http.Error(w, "invalid retail_mode", http.StatusBadRequest)
				return
			}

			pricingParameterID, err := queries.FindActivePricingParameterByScope(ctx, tx, queries.PricingParameterRowForImport(
				raw.AskiliYan,
				raw.Kategori,
				raw.UrunIlkGrubu,
				raw.UrunAnaGrubu,
				raw.UrunAltGrubu,
				raw.Icerik,
				raw.Marka,
				raw.BrandCode,
				raw.BrandGroupSec,
			))
			if err == sql.ErrNoRows {
				skipped++
				continue
			}
			if err != nil {
				http.Error(w, "pricing parameter resolve error", http.StatusInternalServerError)
				return
			}
			matched++

			_, err = queries.UpsertPricingRule(ctx, tx, queries.PricingRuleSaveItem{
				PricingParameterID: pricingParameterID,
				StrategyCode:       normalizePricingStrategyCode(raw.StrategyCode),
				AnchorMode:         normalizePricingAnchorMode(raw.AnchorMode),
				CalcEnabled:        raw.CalcEnabled,
				PublishPostgres:    raw.PublishPostgres,
				PublishNebim:       raw.PublishNebim,
				IsActive:           raw.IsActive,
				TryBase:            raw.TryBase,
				Try1:               raw.Try1,
				Try2:               raw.Try2,
				Try3:               raw.Try3,
				Try4:               raw.Try4,
				Try5:               raw.Try5,
				Try6:               raw.Try6,
				TryWholesaleStep:   raw.TryWholesaleStep,
				TryRetailStep:      raw.TryRetailStep,
				TryRetailMode:      queries.NormalizeRetailModeForRoute(raw.TryRetailMode),
				UsdBase:            raw.UsdBase,
				Usd1:               raw.Usd1,
				Usd2:               raw.Usd2,
				Usd3:               raw.Usd3,
				Usd4:               raw.Usd4,
				Usd5:               raw.Usd5,
				Usd6:               raw.Usd6,
				UsdWholesaleStep:   raw.UsdWholesaleStep,
				UsdRetailStep:      raw.UsdRetailStep,
				UsdRetailMode:      queries.NormalizeRetailModeForRoute(raw.UsdRetailMode),
				EurBase:            raw.EurBase,
				Eur1:               raw.Eur1,
				Eur2:               raw.Eur2,
				Eur3:               raw.Eur3,
				Eur4:               raw.Eur4,
				Eur5:               raw.Eur5,
				Eur6:               raw.Eur6,
				EurWholesaleStep:   raw.EurWholesaleStep,
				EurRetailStep:      raw.EurRetailStep,
				EurRetailMode:      queries.NormalizeRetailModeForRoute(raw.EurRetailMode),
			})
			if err != nil {
				http.Error(w, "pricing rule import error", http.StatusInternalServerError)
				return
			}
			updated++
		}

		if err := tx.Commit(); err != nil {
			http.Error(w, "pg transaction commit error", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(PricingRuleImportResult{
			Success:             true,
			Processed:           len(payload.Items),
			Matched:             matched,
			Skipped:             skipped,
			Updated:             updated,
			ActivatedScopeCount: 0,
			ErrorCount:          0,
		})
	}
}

func GetPricingRuleOptionsHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		field := strings.TrimSpace(r.URL.Query().Get("field"))
		if field == "" {
			http.Error(w, "missing field", http.StatusBadRequest)
			return
		}

		limit := 500
		if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
			if n, err := strconv.Atoi(raw); err == nil && n > 0 && n <= 5000 {
				limit = n
			}
		}

		f := pricingRuleFiltersFromRequest(r)

		traceID := utils.TraceIDFromRequest(r)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)

		opts, err := queries.ListPricingParameterDistinctOptions(ctx, pg, field, f, limit)
		if err != nil {
			http.Error(w, "options lookup error", http.StatusInternalServerError)
			return
		}

		_ = json.NewEncoder(w).Encode(map[string]any{
			"field":   field,
			"options": opts,
		})
	}
}

func GetPricingParameterRulesHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=utf-8")

		traceID := utils.TraceIDFromRequest(r)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)
		rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
		if err != nil {
			http.Error(w, "pricing parameter rules list error", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(rows)
	}
}

func ExportPricingRulesHandler(pg *sql.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		traceID := utils.TraceIDFromRequest(r)
		ctx := utils.ContextWithTraceID(r.Context(), traceID)

		rows, err := queries.ListPricingParameterRules(ctx, pg, pricingRuleFiltersFromRequest(r))
		if err != nil {
			http.Error(w, "pricing parameter rules export error", http.StatusInternalServerError)
			return
		}

		rows = filterPricingRuleExportRows(rows, r)
		sortPricingRuleExportRows(rows, strings.TrimSpace(r.URL.Query().Get("sort_by")), strings.TrimSpace(r.URL.Query().Get("desc")) != "0")

		filename := fmt.Sprintf("pricing_rules_all_%s.csv", time.Now().Format("2006-01-02"))
		w.Header().Set("Content-Type", "text/csv; charset=utf-8")
		w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
		_, _ = w.Write([]byte("\uFEFF"))
		_, _ = w.Write([]byte(buildPricingRuleCSV(rows)))
	}
}

func pricingRuleFiltersFromRequest(r *http.Request) queries.PricingRuleOptionFilters {
	return queries.PricingRuleOptionFilters{
		AskiliYan:     splitCSV(r.URL.Query().Get("askili_yan")),
		Kategori:      splitCSV(r.URL.Query().Get("kategori")),
		UrunIlkGrubu:  splitCSV(r.URL.Query().Get("urun_ilk_grubu")),
		UrunAnaGrubu:  splitCSV(r.URL.Query().Get("urun_ana_grubu")),
		UrunAltGrubu:  splitCSV(r.URL.Query().Get("urun_alt_grubu")),
		Icerik:        splitCSV(r.URL.Query().Get("icerik")),
		Marka:         splitCSV(r.URL.Query().Get("marka")),
		BrandCode:     splitCSV(r.URL.Query().Get("brand_code")),
		BrandGroupSec: splitCSV(r.URL.Query().Get("brand_group")),
	}
}

func filterPricingRuleExportRows(rows []queries.PricingParameterRuleRow, r *http.Request) []queries.PricingParameterRuleRow {
	rangeFilter := func(prefix string) (*float64, *float64) {
		parse := func(raw string) *float64 {
			raw = strings.TrimSpace(raw)
			if raw == "" {
				return nil
			}
			v, err := strconv.ParseFloat(strings.ReplaceAll(raw, ",", "."), 64)
			if err != nil {
				return nil
			}
			return &v
		}
		return parse(r.URL.Query().Get(prefix + "_min")), parse(r.URL.Query().Get(prefix + "_max"))
	}

	fields := []string{
		"try_base", "try1", "try2", "try3", "try4", "try5", "try6", "try_wholesale_step", "try_retail_step",
		"usd_base", "usd1", "usd2", "usd3", "usd4", "usd5", "usd6", "usd_wholesale_step", "usd_retail_step",
		"eur_base", "eur1", "eur2", "eur3", "eur4", "eur5", "eur6", "eur_wholesale_step", "eur_retail_step",
	}
	minMap := map[string]*float64{}
	maxMap := map[string]*float64{}
	for _, field := range fields {
		minMap[field], maxMap[field] = rangeFilter(field)
	}

	out := make([]queries.PricingParameterRuleRow, 0, len(rows))
	for _, row := range rows {
		ok := true
		for _, field := range fields {
			value := pricingRuleNumericValue(row, field)
			if minMap[field] != nil && value < *minMap[field] {
				ok = false
				break
			}
			if maxMap[field] != nil && value > *maxMap[field] {
				ok = false
				break
			}
		}
		if ok {
			out = append(out, row)
		}
	}
	return out
}

func pricingRuleNumericValue(row queries.PricingParameterRuleRow, field string) float64 {
	if row.Rule == nil {
		return 0
	}
	switch field {
	case "try_base":
		return row.Rule.TryBase
	case "try1":
		return row.Rule.Try1
	case "try2":
		return row.Rule.Try2
	case "try3":
		return row.Rule.Try3
	case "try4":
		return row.Rule.Try4
	case "try5":
		return row.Rule.Try5
	case "try6":
		return row.Rule.Try6
	case "try_wholesale_step":
		return row.Rule.TryWholesaleStep
	case "try_retail_step":
		return row.Rule.TryRetailStep
	case "usd_base":
		return row.Rule.UsdBase
	case "usd1":
		return row.Rule.Usd1
	case "usd2":
		return row.Rule.Usd2
	case "usd3":
		return row.Rule.Usd3
	case "usd4":
		return row.Rule.Usd4
	case "usd5":
		return row.Rule.Usd5
	case "usd6":
		return row.Rule.Usd6
	case "usd_wholesale_step":
		return row.Rule.UsdWholesaleStep
	case "usd_retail_step":
		return row.Rule.UsdRetailStep
	case "eur_base":
		return row.Rule.EurBase
	case "eur1":
		return row.Rule.Eur1
	case "eur2":
		return row.Rule.Eur2
	case "eur3":
		return row.Rule.Eur3
	case "eur4":
		return row.Rule.Eur4
	case "eur5":
		return row.Rule.Eur5
	case "eur6":
		return row.Rule.Eur6
	case "eur_wholesale_step":
		return row.Rule.EurWholesaleStep
	case "eur_retail_step":
		return row.Rule.EurRetailStep
	default:
		return 0
	}
}

func sortPricingRuleExportRows(rows []queries.PricingParameterRuleRow, sortBy string, desc bool) {
	sortBy = strings.TrimSpace(sortBy)
	if sortBy == "" {
		return
	}
	sort.SliceStable(rows, func(i, j int) bool {
		li, lj := rows[i], rows[j]
		switch sortBy {
		case "has_rule":
			if desc {
				return boolRank(li.HasRule) > boolRank(lj.HasRule)
			}
			return boolRank(li.HasRule) < boolRank(lj.HasRule)
		case "is_active":
			liActive, ljActive := false, false
			if li.Rule != nil {
				liActive = li.Rule.IsActive
			}
			if lj.Rule != nil {
				ljActive = lj.Rule.IsActive
			}
			if desc {
				return boolRank(liActive) > boolRank(ljActive)
			}
			return boolRank(liActive) < boolRank(ljActive)
		case "calc_enabled", "publish_postgres", "publish_nebim":
			liValue, ljValue := false, false
			if li.Rule != nil {
				switch sortBy {
				case "calc_enabled":
					liValue = li.Rule.CalcEnabled
				case "publish_postgres":
					liValue = li.Rule.PublishPostgres
				case "publish_nebim":
					liValue = li.Rule.PublishNebim
				}
			}
			if lj.Rule != nil {
				switch sortBy {
				case "calc_enabled":
					ljValue = lj.Rule.CalcEnabled
				case "publish_postgres":
					ljValue = lj.Rule.PublishPostgres
				case "publish_nebim":
					ljValue = lj.Rule.PublishNebim
				}
			}
			if desc {
				return boolRank(liValue) > boolRank(ljValue)
			}
			return boolRank(liValue) < boolRank(ljValue)
		case "askili_yan", "kategori", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu", "icerik", "marka", "brand_code", "brand_group", "anchor_mode",
			"try_retail_mode", "usd_retail_mode", "eur_retail_mode":
			vi := pricingRuleStringValue(li, sortBy)
			vj := pricingRuleStringValue(lj, sortBy)
			if desc {
				return strings.Compare(vi, vj) > 0
			}
			return strings.Compare(vi, vj) < 0
		default:
			vi := pricingRuleNumericValue(li, sortBy)
			vj := pricingRuleNumericValue(lj, sortBy)
			if desc {
				return vi > vj
			}
			return vi < vj
		}
	})
}

func boolRank(v bool) int {
	if v {
		return 1
	}
	return 0
}

func pricingRuleStringValue(row queries.PricingParameterRuleRow, field string) string {
	switch field {
	case "askili_yan":
		return row.AskiliYan
	case "kategori":
		return row.Kategori
	case "urun_ilk_grubu":
		return row.UrunIlkGrubu
	case "urun_ana_grubu":
		return row.UrunAnaGrubu
	case "urun_alt_grubu":
		return row.UrunAltGrubu
	case "icerik":
		return row.Icerik
	case "marka":
		return row.Marka
	case "brand_code":
		return row.BrandCode
	case "brand_group":
		return row.BrandGroupSec
	case "anchor_mode":
		if row.Rule == nil {
			return "USD"
		}
		return row.Rule.AnchorMode
	case "try_retail_mode":
		if row.Rule == nil {
			return "STEP"
		}
		return queries.NormalizeRetailModeForRoute(row.Rule.TryRetailMode)
	case "usd_retail_mode":
		if row.Rule == nil {
			return "STEP"
		}
		return queries.NormalizeRetailModeForRoute(row.Rule.UsdRetailMode)
	case "eur_retail_mode":
		if row.Rule == nil {
			return "STEP"
		}
		return queries.NormalizeRetailModeForRoute(row.Rule.EurRetailMode)
	default:
		return ""
	}
}

func buildPricingRuleCSV(rows []queries.PricingParameterRuleRow) string {
	headers := []string{
		"DURUM", "AKTIF", "ASKILI YAN", "KATEGORI", "URUN ILK GRUBU", "URUN ANA GRUBU", "URUN ALT GRUBU",
		"ICERIK", "MARKA", "BRAND CODE", "MARKA GRUBU", "ANCHOR MODE", "HESAP AKTIF", "PG YAYIN", "NEBIM YAYIN",
		"TRY TOPTAN YUVARLAMA", "TRY PERAKENDE MODU", "TRY PERAKENDE DEGERI", "TRY TABAN", "TRY 1", "TRY 2", "TRY 3", "TRY 4", "TRY 5", "TRY 6",
		"USD TOPTAN YUVARLAMA", "USD PERAKENDE MODU", "USD PERAKENDE DEGERI", "USD TABAN", "USD 1", "USD 2", "USD 3", "USD 4", "USD 5", "USD 6",
		"EUR TOPTAN YUVARLAMA", "EUR PERAKENDE MODU", "EUR PERAKENDE DEGERI", "EUR TABAN", "EUR 1", "EUR 2", "EUR 3", "EUR 4", "EUR 5", "EUR 6",
	}
	var b strings.Builder
	for i, h := range headers {
		b.WriteString(csvEscapeValue(h))
		if i == len(headers)-1 {
			b.WriteString("\n")
		} else {
			b.WriteString(";")
		}
	}
	for _, row := range rows {
		active := "Pasif"
		if row.Rule == nil || row.Rule.IsActive {
			active = "Aktif"
		}
		values := []string{
			map[bool]string{true: "Tanimli", false: "Yeni"}[row.HasRule],
			active,
			row.AskiliYan,
			row.Kategori,
			row.UrunIlkGrubu,
			row.UrunAnaGrubu,
			row.UrunAltGrubu,
			row.Icerik,
			csvExcelTextValue(row.Marka),
			csvExcelTextValue(row.BrandCode),
			row.BrandGroupSec,
			pricingRuleStringValue(row, "anchor_mode"),
			map[bool]string{true: "Aktif", false: "Pasif"}[row.Rule == nil || row.Rule.CalcEnabled],
			map[bool]string{true: "Evet", false: "Hayir"}[row.Rule == nil || row.Rule.PublishPostgres],
			map[bool]string{true: "Evet", false: "Hayir"}[row.Rule == nil || row.Rule.PublishNebim],
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_wholesale_step")),
			pricingRuleStringValue(row, "try_retail_mode"),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_retail_step")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try_base")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try1")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try2")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try3")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try4")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try5")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "try6")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_wholesale_step")),
			pricingRuleStringValue(row, "usd_retail_mode"),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_retail_step")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd_base")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd1")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd2")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd3")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd4")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd5")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "usd6")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_wholesale_step")),
			pricingRuleStringValue(row, "eur_retail_mode"),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_retail_step")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur_base")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur1")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur2")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur3")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur4")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur5")),
			fmt.Sprintf("%.2f", pricingRuleNumericValue(row, "eur6")),
		}
		for i, value := range values {
			b.WriteString(csvEscapeValue(value))
			if i == len(values)-1 {
				b.WriteString("\n")
			} else {
				b.WriteString(";")
			}
		}
	}
	return b.String()
}

func csvEscapeValue(value string) string {
	text := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(value), "\r", " "), "\n", " ")
	if strings.Contains(text, ";") || strings.Contains(text, "\"") {
		text = `"` + strings.ReplaceAll(text, `"`, `""`) + `"`
	}
	return text
}

func csvExcelTextValue(value string) string {
	text := strings.TrimSpace(value)
	if text == "" {
		return ""
	}
	return `="` + strings.ReplaceAll(text, `"`, `""`) + `"`
}

func splitCSV(raw string) []string {
	raw = strings.TrimSpace(raw)
	if raw == "" {
		return nil
	}
	parts := strings.Split(raw, ",")
	out := make([]string, 0, len(parts))
	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p != "" {
			out = append(out, p)
		}
	}
	return out
}
