package queries

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/lib/pq"
)

// Rule tables:
// - mk_pricing_rule: the "scope" (filters) to which multipliers/roundings apply.
// - mk_pricex: per-currency multipliers (base + 1..6).
// - mk_priceroll: per-currency rounding steps for wholesale (1-5) and retail (6+).

func normalizeRetailMode(v string) string {
	v = strings.ToUpper(strings.TrimSpace(v))
	switch v {
	case "", "STEP":
		return "STEP"
	case "END_99", "END_49", "BAND_99", "BAND_49":
		return v
	default:
		return "STEP"
	}
}

func NormalizeRetailModeForRoute(v string) string {
	return normalizeRetailMode(v)
}

func EnsurePricingRuleTables(pg *sql.DB) error {
	stmts := []string{
		`
CREATE TABLE IF NOT EXISTS mk_pricing_rule (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

  askili_yan     TEXT[] NOT NULL DEFAULT '{}'::text[],
  kategori       TEXT[] NOT NULL DEFAULT '{}'::text[],
  urun_ilk_grubu TEXT[] NOT NULL DEFAULT '{}'::text[],
  urun_ana_grubu TEXT[] NOT NULL DEFAULT '{}'::text[],
  urun_alt_grubu TEXT[] NOT NULL DEFAULT '{}'::text[],
  icerik         TEXT[] NOT NULL DEFAULT '{}'::text[],
  karisim        TEXT[] NOT NULL DEFAULT '{}'::text[],
  marka          TEXT[] NOT NULL DEFAULT '{}'::text[],
  brand_code     TEXT[] NOT NULL DEFAULT '{}'::text[],
  brand_group    TEXT[] NOT NULL DEFAULT '{}'::text[],

  strategy_code TEXT NOT NULL DEFAULT 'CORE',
  anchor_mode TEXT NOT NULL DEFAULT 'USD',
  calc_enabled BOOLEAN NOT NULL DEFAULT TRUE,
  publish_postgres BOOLEAN NOT NULL DEFAULT TRUE,
  publish_nebim BOOLEAN NOT NULL DEFAULT TRUE,
  is_active BOOLEAN NOT NULL DEFAULT TRUE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`,
		`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS strategy_code TEXT NOT NULL DEFAULT 'CORE'`,
		`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS anchor_mode TEXT NOT NULL DEFAULT 'USD'`,
		`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS calc_enabled BOOLEAN NOT NULL DEFAULT TRUE`,
		`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS publish_postgres BOOLEAN NOT NULL DEFAULT TRUE`,
		`ALTER TABLE mk_pricing_rule ADD COLUMN IF NOT EXISTS publish_nebim BOOLEAN NOT NULL DEFAULT TRUE`,
		`UPDATE mk_pricing_rule SET strategy_code='CORE' WHERE COALESCE(NULLIF(BTRIM(strategy_code), ''), '') = ''`,
		`UPDATE mk_pricing_rule SET anchor_mode='USD' WHERE COALESCE(NULLIF(BTRIM(anchor_mode), ''), '') = ''`,
		`ALTER TABLE mk_pricing_rule DROP CONSTRAINT IF EXISTS ck_mk_pricing_rule_strategy_code`,
		`ALTER TABLE mk_pricing_rule ADD CONSTRAINT ck_mk_pricing_rule_strategy_code CHECK (strategy_code IN ('CORE','PREMIUM','SARTORIAL'))`,
		`ALTER TABLE mk_pricing_rule DROP CONSTRAINT IF EXISTS ck_mk_pricing_rule_anchor_mode`,
		`ALTER TABLE mk_pricing_rule ADD CONSTRAINT ck_mk_pricing_rule_anchor_mode CHECK (anchor_mode IN ('TRY','USD'))`,
		`CREATE INDEX IF NOT EXISTS ix_mk_pricing_rule_active ON mk_pricing_rule (is_active)`,
		`
CREATE TABLE IF NOT EXISTS mk_pricex (
  rule_id UUID NOT NULL REFERENCES mk_pricing_rule(id) ON DELETE CASCADE,
  currency TEXT NOT NULL CHECK (currency IN ('TRY','USD','EUR')),
  base_mult NUMERIC(18,6) NOT NULL DEFAULT 0,
  m1 NUMERIC(18,6) NOT NULL DEFAULT 0,
  m2 NUMERIC(18,6) NOT NULL DEFAULT 0,
  m3 NUMERIC(18,6) NOT NULL DEFAULT 0,
  m4 NUMERIC(18,6) NOT NULL DEFAULT 0,
  m5 NUMERIC(18,6) NOT NULL DEFAULT 0,
  m6 NUMERIC(18,6) NOT NULL DEFAULT 0,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (rule_id, currency)
)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_pricex_currency ON mk_pricex (currency)`,
		`
CREATE TABLE IF NOT EXISTS mk_priceroll (
  rule_id UUID NOT NULL REFERENCES mk_pricing_rule(id) ON DELETE CASCADE,
  currency TEXT NOT NULL CHECK (currency IN ('TRY','USD','EUR')),
  step NUMERIC(18,6) NOT NULL DEFAULT 0,
  wholesale_step NUMERIC(18,6) NOT NULL DEFAULT 0,
  retail_step NUMERIC(18,6) NOT NULL DEFAULT 0,
  retail_mode TEXT NOT NULL DEFAULT 'STEP',
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  PRIMARY KEY (rule_id, currency)
		)`,
		`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS wholesale_step NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS retail_step NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_priceroll ADD COLUMN IF NOT EXISTS retail_mode TEXT NOT NULL DEFAULT 'STEP'`,
		`UPDATE mk_priceroll SET wholesale_step = step, retail_step = step WHERE step <> 0 AND wholesale_step = 0 AND retail_step = 0`,
		`UPDATE mk_priceroll SET retail_mode='STEP' WHERE COALESCE(NULLIF(BTRIM(retail_mode), ''), '') = ''`,
		`CREATE INDEX IF NOT EXISTS ix_mk_priceroll_currency ON mk_priceroll (currency)`,
	}
	for _, s := range stmts {
		if _, err := pg.Exec(s); err != nil {
			return err
		}
	}
	return nil
}

type PricingRuleRow struct {
	ID                 string `json:"id"`
	PricingParameterID int64  `json:"pricing_parameter_id"`

	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"`
	Karisim       []string `json:"karisim"`
	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"`

	// multipliers/rolls are per currency
	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 PricingRuleSaveItem struct {
	ID                 string `json:"id"`
	PricingParameterID int64  `json:"pricing_parameter_id"`

	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"`
	Karisim       []string `json:"karisim"`
	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"`
}

// BulkSavePricingRulesFast persists multipliers + rounding steps in a set-based way.
// This is intentionally "dumb": it updates/creates a mk_pricing_rule row (latest by pricing_parameter_id)
// and upserts mk_pricex/mk_priceroll for TRY/USD/EUR.
func BulkSavePricingRulesFast(ctx context.Context, tx *sql.Tx, items []PricingRuleSaveItem) (int, error) {
	if len(items) == 0 {
		return 0, nil
	}

	raw, err := json.Marshal(items)
	if err != nil {
		return 0, err
	}

	// Notes:
	// - rule_id resolution:
	//   1) explicit id (if provided)
	//   2) latest rule for pricing_parameter_id (if provided)
	//   3) otherwise new UUID
	// - mk_pricing_rule has no unique constraint on pricing_parameter_id by design, so we target "latest" row.
	// - created_at uses default; updated_at is bumped on every save.
	q := `
WITH input AS (
  SELECT *
  FROM jsonb_to_recordset($1::jsonb) AS x(
    id text,
    pricing_parameter_id bigint,
    calc_enabled boolean,
    publish_postgres boolean,
    publish_nebim boolean,
    is_active boolean,
    try_retail_mode text,
    usd_retail_mode text,
    eur_retail_mode text,

    try_base float8, try1 float8, try2 float8, try3 float8, try4 float8, try5 float8, try6 float8,
    try_wholesale_step float8, try_retail_step float8,

    usd_base float8, usd1 float8, usd2 float8, usd3 float8, usd4 float8, usd5 float8, usd6 float8,
    usd_wholesale_step float8, usd_retail_step float8,

    eur_base float8, eur1 float8, eur2 float8, eur3 float8, eur4 float8, eur5 float8, eur6 float8,
    eur_wholesale_step float8, eur_retail_step float8
  )
),
norm AS (
  SELECT
    NULLIF(BTRIM(id), '') AS id_txt,
    COALESCE(pricing_parameter_id, 0) AS pricing_parameter_id,
    COALESCE(calc_enabled, TRUE) AS calc_enabled,
    COALESCE(publish_postgres, TRUE) AS publish_postgres,
    COALESCE(publish_nebim, TRUE) AS publish_nebim,
    COALESCE(is_active, TRUE) AS is_active,
    COALESCE(NULLIF(UPPER(BTRIM(try_retail_mode)), ''), 'STEP') AS try_retail_mode,
    COALESCE(NULLIF(UPPER(BTRIM(usd_retail_mode)), ''), 'STEP') AS usd_retail_mode,
    COALESCE(NULLIF(UPPER(BTRIM(eur_retail_mode)), ''), 'STEP') AS eur_retail_mode,

    COALESCE(try_base, 0) AS try_base, COALESCE(try1, 0) AS try1, COALESCE(try2, 0) AS try2, COALESCE(try3, 0) AS try3, COALESCE(try4, 0) AS try4, COALESCE(try5, 0) AS try5, COALESCE(try6, 0) AS try6,
    COALESCE(try_wholesale_step, 0) AS try_wholesale_step, COALESCE(try_retail_step, 0) AS try_retail_step,

    COALESCE(usd_base, 0) AS usd_base, COALESCE(usd1, 0) AS usd1, COALESCE(usd2, 0) AS usd2, COALESCE(usd3, 0) AS usd3, COALESCE(usd4, 0) AS usd4, COALESCE(usd5, 0) AS usd5, COALESCE(usd6, 0) AS usd6,
    COALESCE(usd_wholesale_step, 0) AS usd_wholesale_step, COALESCE(usd_retail_step, 0) AS usd_retail_step,

    COALESCE(eur_base, 0) AS eur_base, COALESCE(eur1, 0) AS eur1, COALESCE(eur2, 0) AS eur2, COALESCE(eur3, 0) AS eur3, COALESCE(eur4, 0) AS eur4, COALESCE(eur5, 0) AS eur5, COALESCE(eur6, 0) AS eur6,
    COALESCE(eur_wholesale_step, 0) AS eur_wholesale_step, COALESCE(eur_retail_step, 0) AS eur_retail_step
  FROM input
),
resolved AS (
  SELECT
    COALESCE(
      NULLIF(id_txt, '')::uuid,
      latest.id,
      gen_random_uuid()
    ) AS rule_id,
    pricing_parameter_id,
    calc_enabled,
    publish_postgres,
    publish_nebim,
    is_active,
    try_retail_mode,
    usd_retail_mode,
    eur_retail_mode,

    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
  FROM norm n
  LEFT JOIN LATERAL (
    SELECT r.id
    FROM mk_pricing_rule r
    WHERE r.pricing_parameter_id = n.pricing_parameter_id
    ORDER BY r.created_at DESC, r.updated_at DESC, r.id DESC
    LIMIT 1
  ) latest ON (n.id_txt IS NULL AND n.pricing_parameter_id > 0)
),
upsert_rule AS (
  INSERT INTO mk_pricing_rule (
    id,
    pricing_parameter_id,
    calc_enabled,
    publish_postgres,
    publish_nebim,
    is_active,
    updated_at
  )
  SELECT
    rule_id,
    NULLIF(pricing_parameter_id, 0),
    calc_enabled,
    publish_postgres,
    publish_nebim,
    is_active,
    now()
  FROM resolved
  ON CONFLICT (id) DO UPDATE SET
    pricing_parameter_id = EXCLUDED.pricing_parameter_id,
    calc_enabled = EXCLUDED.calc_enabled,
    publish_postgres = EXCLUDED.publish_postgres,
    publish_nebim = EXCLUDED.publish_nebim,
    is_active = EXCLUDED.is_active,
    updated_at = now()
  RETURNING id
),
upsert_pricex AS (
  INSERT INTO mk_pricex (rule_id, currency, base_mult, m1, m2, m3, m4, m5, m6, updated_at)
  SELECT rule_id, 'TRY', try_base, try1, try2, try3, try4, try5, try6, now() FROM resolved
  UNION ALL
  SELECT rule_id, 'USD', usd_base, usd1, usd2, usd3, usd4, usd5, usd6, now() FROM resolved
  UNION ALL
  SELECT rule_id, 'EUR', eur_base, eur1, eur2, eur3, eur4, eur5, eur6, now() FROM resolved
  ON CONFLICT (rule_id, currency) DO UPDATE SET
    base_mult = EXCLUDED.base_mult,
    m1 = EXCLUDED.m1,
    m2 = EXCLUDED.m2,
    m3 = EXCLUDED.m3,
    m4 = EXCLUDED.m4,
    m5 = EXCLUDED.m5,
    m6 = EXCLUDED.m6,
    updated_at = now()
  RETURNING 1
),
upsert_priceroll AS (
  INSERT INTO mk_priceroll (rule_id, currency, wholesale_step, retail_step, retail_mode, updated_at)
  SELECT rule_id, 'TRY', try_wholesale_step, try_retail_step, try_retail_mode, now() FROM resolved
  UNION ALL
  SELECT rule_id, 'USD', usd_wholesale_step, usd_retail_step, usd_retail_mode, now() FROM resolved
  UNION ALL
  SELECT rule_id, 'EUR', eur_wholesale_step, eur_retail_step, eur_retail_mode, now() FROM resolved
  ON CONFLICT (rule_id, currency) DO UPDATE SET
    wholesale_step = EXCLUDED.wholesale_step,
    retail_step = EXCLUDED.retail_step,
    retail_mode = EXCLUDED.retail_mode,
    updated_at = now()
  RETURNING 1
)
SELECT COUNT(*)::int FROM resolved;
`

	var updated int
	if err := tx.QueryRowContext(ctx, q, raw).Scan(&updated); err != nil {
		return 0, err
	}
	return updated, nil
}

func ListPricingRules(ctx context.Context, pg *sql.DB) ([]PricingRuleRow, error) {
	// Use LEFT joins so newly inserted rules show defaults.
	q := `
SELECT
  r.id,
  COALESCE(r.pricing_parameter_id, 0),
  r.askili_yan,
  r.kategori,
  r.urun_ilk_grubu,
  r.urun_ana_grubu,
  r.urun_alt_grubu,
  r.icerik,
  r.karisim,
  r.marka,
  r.brand_code,
  r.brand_group,
  r.strategy_code,
  r.anchor_mode,
  r.calc_enabled,
  r.publish_postgres,
  r.publish_nebim,
  r.is_active,

  COALESCE(tx.base_mult, 0)::float8 AS try_base,
  COALESCE(tx.m1, 0)::float8 AS try1,
  COALESCE(tx.m2, 0)::float8 AS try2,
  COALESCE(tx.m3, 0)::float8 AS try3,
  COALESCE(tx.m4, 0)::float8 AS try4,
  COALESCE(tx.m5, 0)::float8 AS try5,
  COALESCE(tx.m6, 0)::float8 AS try6,
  COALESCE(NULLIF(tr.wholesale_step, 0), tr.step, 0)::float8 AS try_wholesale_step,
  COALESCE(NULLIF(tr.retail_step, 0), tr.step, 0)::float8 AS try_retail_step,
  COALESCE(NULLIF(BTRIM(tr.retail_mode), ''), 'STEP') AS try_retail_mode,

  COALESCE(ux.base_mult, 0)::float8 AS usd_base,
  COALESCE(ux.m1, 0)::float8 AS usd1,
  COALESCE(ux.m2, 0)::float8 AS usd2,
  COALESCE(ux.m3, 0)::float8 AS usd3,
  COALESCE(ux.m4, 0)::float8 AS usd4,
  COALESCE(ux.m5, 0)::float8 AS usd5,
  COALESCE(ux.m6, 0)::float8 AS usd6,
  COALESCE(NULLIF(ur.wholesale_step, 0), ur.step, 0)::float8 AS usd_wholesale_step,
  COALESCE(NULLIF(ur.retail_step, 0), ur.step, 0)::float8 AS usd_retail_step,
  COALESCE(NULLIF(BTRIM(ur.retail_mode), ''), 'STEP') AS usd_retail_mode,

  COALESCE(ex.base_mult, 0)::float8 AS eur_base,
  COALESCE(ex.m1, 0)::float8 AS eur1,
  COALESCE(ex.m2, 0)::float8 AS eur2,
  COALESCE(ex.m3, 0)::float8 AS eur3,
  COALESCE(ex.m4, 0)::float8 AS eur4,
  COALESCE(ex.m5, 0)::float8 AS eur5,
  COALESCE(ex.m6, 0)::float8 AS eur6,
  COALESCE(NULLIF(er.wholesale_step, 0), er.step, 0)::float8 AS eur_wholesale_step,
  COALESCE(NULLIF(er.retail_step, 0), er.step, 0)::float8 AS eur_retail_step,
  COALESCE(NULLIF(BTRIM(er.retail_mode), ''), 'STEP') AS eur_retail_mode
FROM mk_pricing_rule r
LEFT JOIN mk_pricex tx ON tx.rule_id = r.id AND tx.currency='TRY'
LEFT JOIN mk_pricex ux ON ux.rule_id = r.id AND ux.currency='USD'
LEFT JOIN mk_pricex ex ON ex.rule_id = r.id AND ex.currency='EUR'
LEFT JOIN mk_priceroll tr ON tr.rule_id = r.id AND tr.currency='TRY'
LEFT JOIN mk_priceroll ur ON ur.rule_id = r.id AND ur.currency='USD'
LEFT JOIN mk_priceroll er ON er.rule_id = r.id AND er.currency='EUR'
ORDER BY r.created_at DESC;
`
	rows, err := pg.QueryContext(ctx, q)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]PricingRuleRow, 0, 256)
	for rows.Next() {
		var r PricingRuleRow
		if err := rows.Scan(
			&r.ID,
			&r.PricingParameterID,
			pq.Array(&r.AskiliYan),
			pq.Array(&r.Kategori),
			pq.Array(&r.UrunIlkGrubu),
			pq.Array(&r.UrunAnaGrubu),
			pq.Array(&r.UrunAltGrubu),
			pq.Array(&r.Icerik),
			pq.Array(&r.Karisim),
			pq.Array(&r.Marka),
			pq.Array(&r.BrandCode),
			pq.Array(&r.BrandGroupSec),
			&r.StrategyCode,
			&r.AnchorMode,
			&r.CalcEnabled,
			&r.PublishPostgres,
			&r.PublishNebim,
			&r.IsActive,

			&r.TryBase, &r.Try1, &r.Try2, &r.Try3, &r.Try4, &r.Try5, &r.Try6, &r.TryWholesaleStep, &r.TryRetailStep, &r.TryRetailMode,
			&r.UsdBase, &r.Usd1, &r.Usd2, &r.Usd3, &r.Usd4, &r.Usd5, &r.Usd6, &r.UsdWholesaleStep, &r.UsdRetailStep, &r.UsdRetailMode,
			&r.EurBase, &r.Eur1, &r.Eur2, &r.Eur3, &r.Eur4, &r.Eur5, &r.Eur6, &r.EurWholesaleStep, &r.EurRetailStep, &r.EurRetailMode,
		); err != nil {
			return nil, err
		}
		r.TryRetailMode = normalizeRetailMode(r.TryRetailMode)
		r.UsdRetailMode = normalizeRetailMode(r.UsdRetailMode)
		r.EurRetailMode = normalizeRetailMode(r.EurRetailMode)
		out = append(out, r)
	}
	return out, rows.Err()
}

func normalizeTextList(in []string) []string {
	out := make([]string, 0, len(in))
	seen := map[string]struct{}{}
	for _, v := range in {
		v = strings.TrimSpace(v)
		if v == "" {
			continue
		}
		if _, ok := seen[v]; ok {
			continue
		}
		seen[v] = struct{}{}
		out = append(out, v)
	}
	return out
}

func deriveStrategyCodeFromBrandGroup(values []string) string {
	for _, value := range values {
		normalized := strings.ToUpper(strings.TrimSpace(value))
		switch normalized {
		case "CORE", "PREMIUM", "SARTORIAL":
			return normalized
		}
	}
	return "CORE"
}

func deriveAnchorModeFromBrandGroup(ctx context.Context, tx *sql.Tx, values []string) string {
	for _, value := range values {
		normalized := strings.TrimSpace(value)
		if normalized == "" {
			continue
		}
		var mode string
		err := tx.QueryRowContext(ctx, `
SELECT anchor_mode
FROM mk_brandgrp
WHERE UPPER(BTRIM(code)) = UPPER(BTRIM($1))
   OR UPPER(BTRIM(title)) = UPPER(BTRIM($1))
ORDER BY id
LIMIT 1
`, normalized).Scan(&mode)
		if err == nil {
			mode = strings.ToUpper(strings.TrimSpace(mode))
			if mode == "TRY" || mode == "USD" {
				return mode
			}
		}
	}
	return "USD"
}

// UpsertPricingRule persists rule scope + per-currency multipliers/roundings.
// Parameter-backed worksheet saves append a new rule version so older prices
// remain queryable. Legacy rules without a parameter id keep update behavior.
func UpsertPricingRule(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem) (string, error) {
	if item.PricingParameterID > 0 {
		versionedParameterID, err := VersionPricingParameterForRule(ctx, tx, item.PricingParameterID)
		if err != nil {
			return "", err
		}
		item.PricingParameterID = versionedParameterID
	}
	if err := FillPricingRuleScopeFromParameter(ctx, tx, &item); err != nil {
		return "", err
	}
	item.AskiliYan = normalizeTextList(item.AskiliYan)
	item.Kategori = normalizeTextList(item.Kategori)
	item.UrunIlkGrubu = normalizeTextList(item.UrunIlkGrubu)
	item.UrunAnaGrubu = normalizeTextList(item.UrunAnaGrubu)
	item.UrunAltGrubu = normalizeTextList(item.UrunAltGrubu)
	item.Icerik = normalizeTextList(item.Icerik)
	item.Karisim = normalizeTextList(item.Karisim)
	item.Marka = normalizeTextList(item.Marka)
	item.BrandCode = normalizeTextList(item.BrandCode)
	item.BrandGroupSec = normalizeTextList(item.BrandGroupSec)
	item.StrategyCode = deriveStrategyCodeFromBrandGroup(item.BrandGroupSec)
	item.AnchorMode = deriveAnchorModeFromBrandGroup(ctx, tx, item.BrandGroupSec)
	item.TryRetailMode = normalizeRetailMode(item.TryRetailMode)
	item.UsdRetailMode = normalizeRetailMode(item.UsdRetailMode)
	item.EurRetailMode = normalizeRetailMode(item.EurRetailMode)

	id := strings.TrimSpace(item.ID)
	if item.PricingParameterID > 0 {
		id = ""
	}
	if id == "" {
		// create
		if err := tx.QueryRowContext(ctx, `
INSERT INTO mk_pricing_rule (
  pricing_parameter_id,
  askili_yan,kategori,urun_ilk_grubu,urun_ana_grubu,urun_alt_grubu,
  icerik,karisim,marka,brand_code,brand_group,
  strategy_code,anchor_mode,calc_enabled,publish_postgres,publish_nebim,
  is_active,created_at,updated_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,now(),now())
RETURNING id
`, nullablePricingParameterID(item.PricingParameterID), pq.Array(item.AskiliYan), pq.Array(item.Kategori), pq.Array(item.UrunIlkGrubu), pq.Array(item.UrunAnaGrubu), pq.Array(item.UrunAltGrubu),
			pq.Array(item.Icerik), pq.Array(item.Karisim), pq.Array(item.Marka), pq.Array(item.BrandCode), pq.Array(item.BrandGroupSec),
			item.StrategyCode, item.AnchorMode, item.CalcEnabled, item.PublishPostgres, item.PublishNebim,
			item.IsActive,
		).Scan(&id); err != nil {
			return "", err
		}
	} else {
		if _, err := tx.ExecContext(ctx, `
UPDATE mk_pricing_rule SET
  pricing_parameter_id=$2,
  askili_yan=$3,
  kategori=$4,
  urun_ilk_grubu=$5,
  urun_ana_grubu=$6,
  urun_alt_grubu=$7,
  icerik=$8,
  karisim=$9,
  marka=$10,
  brand_code=$11,
  brand_group=$12,
  strategy_code=$13,
  anchor_mode=$14,
  calc_enabled=$15,
  publish_postgres=$16,
  publish_nebim=$17,
  is_active=$18,
  updated_at=now()
WHERE id=$1
`, id,
			nullablePricingParameterID(item.PricingParameterID),
			pq.Array(item.AskiliYan), pq.Array(item.Kategori), pq.Array(item.UrunIlkGrubu), pq.Array(item.UrunAnaGrubu), pq.Array(item.UrunAltGrubu),
			pq.Array(item.Icerik), pq.Array(item.Karisim), pq.Array(item.Marka), pq.Array(item.BrandCode), pq.Array(item.BrandGroupSec),
			item.StrategyCode, item.AnchorMode, item.CalcEnabled, item.PublishPostgres, item.PublishNebim,
			item.IsActive,
		); err != nil {
			return "", err
		}
	}

	// multipliers upsert helper
	upsertX := func(cur string, base, m1, m2, m3, m4, m5, m6 float64) error {
		_, err := tx.ExecContext(ctx, `
INSERT INTO mk_pricex (rule_id, currency, base_mult, m1, m2, m3, m4, m5, m6, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
  base_mult=EXCLUDED.base_mult,
  m1=EXCLUDED.m1,
  m2=EXCLUDED.m2,
  m3=EXCLUDED.m3,
  m4=EXCLUDED.m4,
  m5=EXCLUDED.m5,
  m6=EXCLUDED.m6,
  updated_at=now()
`, id, cur, base, m1, m2, m3, m4, m5, m6)
		return err
	}
	upsertRoll := func(cur string, wholesaleStep, retailStep float64, retailMode string) error {
		_, err := tx.ExecContext(ctx, `
INSERT INTO mk_priceroll (rule_id, currency, step, wholesale_step, retail_step, retail_mode, created_at, updated_at)
VALUES ($1,$2,$3,$4,$5,$6,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
  step=EXCLUDED.step,
  wholesale_step=EXCLUDED.wholesale_step,
  retail_step=EXCLUDED.retail_step,
  retail_mode=EXCLUDED.retail_mode,
  updated_at=now()
`, id, cur, wholesaleStep, wholesaleStep, retailStep, retailMode)
		return err
	}

	if err := upsertX("TRY", item.TryBase, item.Try1, item.Try2, item.Try3, item.Try4, item.Try5, item.Try6); err != nil {
		return "", err
	}
	if err := upsertRoll("TRY", item.TryWholesaleStep, item.TryRetailStep, item.TryRetailMode); err != nil {
		return "", err
	}
	if err := upsertX("USD", item.UsdBase, item.Usd1, item.Usd2, item.Usd3, item.Usd4, item.Usd5, item.Usd6); err != nil {
		return "", err
	}
	if err := upsertRoll("USD", item.UsdWholesaleStep, item.UsdRetailStep, item.UsdRetailMode); err != nil {
		return "", err
	}
	if err := upsertX("EUR", item.EurBase, item.Eur1, item.Eur2, item.Eur3, item.Eur4, item.Eur5, item.Eur6); err != nil {
		return "", err
	}
	if err := upsertRoll("EUR", item.EurWholesaleStep, item.EurRetailStep, item.EurRetailMode); err != nil {
		return "", err
	}

	return id, nil
}

// UpdatePricingRuleByIDFast updates an existing rule without parameter versioning/scope fill.
// This is the fast path for worksheet saves where rule_id is already known.
func UpdatePricingRuleByIDFast(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem) error {
	if tx == nil {
		return fmt.Errorf("nil tx")
	}
	ruleID := strings.TrimSpace(item.ID)
	if ruleID == "" {
		return fmt.Errorf("missing rule id")
	}

	item.TryRetailMode = normalizeRetailMode(item.TryRetailMode)
	item.UsdRetailMode = normalizeRetailMode(item.UsdRetailMode)
	item.EurRetailMode = normalizeRetailMode(item.EurRetailMode)

	if _, err := tx.ExecContext(ctx, `
UPDATE mk_pricing_rule SET
  calc_enabled=$2,
  publish_postgres=$3,
  publish_nebim=$4,
  is_active=$5,
  updated_at=now()
WHERE id=$1
`, ruleID, item.CalcEnabled, item.PublishPostgres, item.PublishNebim, item.IsActive); err != nil {
		return err
	}

	upsertX := func(cur string, base, m1, m2, m3, m4, m5, m6 float64) error {
		_, err := tx.ExecContext(ctx, `
INSERT INTO mk_pricex (rule_id, currency, base_mult, m1, m2, m3, m4, m5, m6, created_at, updated_at)
VALUES (NULLIF($1,'')::uuid,$2,$3,$4,$5,$6,$7,$8,$9,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
  base_mult=EXCLUDED.base_mult,
  m1=EXCLUDED.m1,
  m2=EXCLUDED.m2,
  m3=EXCLUDED.m3,
  m4=EXCLUDED.m4,
  m5=EXCLUDED.m5,
  m6=EXCLUDED.m6,
  updated_at=now()
`, ruleID, cur, base, m1, m2, m3, m4, m5, m6)
		return err
	}

	upsertRoll := func(cur string, wholesaleStep, retailStep float64, retailMode string) error {
		_, err := tx.ExecContext(ctx, `
INSERT INTO mk_priceroll (rule_id, currency, step, wholesale_step, retail_step, retail_mode, created_at, updated_at)
VALUES (NULLIF($1,'')::uuid,$2,$3,$4,$5,$6,now(),now())
ON CONFLICT (rule_id, currency) DO UPDATE SET
  step=EXCLUDED.step,
  wholesale_step=EXCLUDED.wholesale_step,
  retail_step=EXCLUDED.retail_step,
  retail_mode=EXCLUDED.retail_mode,
  updated_at=now()
`, ruleID, cur, wholesaleStep, wholesaleStep, retailStep, retailMode)
		return err
	}

	if err := upsertX("TRY", item.TryBase, item.Try1, item.Try2, item.Try3, item.Try4, item.Try5, item.Try6); err != nil {
		return err
	}
	if err := upsertRoll("TRY", item.TryWholesaleStep, item.TryRetailStep, item.TryRetailMode); err != nil {
		return err
	}
	if err := upsertX("USD", item.UsdBase, item.Usd1, item.Usd2, item.Usd3, item.Usd4, item.Usd5, item.Usd6); err != nil {
		return err
	}
	if err := upsertRoll("USD", item.UsdWholesaleStep, item.UsdRetailStep, item.UsdRetailMode); err != nil {
		return err
	}
	if err := upsertX("EUR", item.EurBase, item.Eur1, item.Eur2, item.Eur3, item.Eur4, item.Eur5, item.Eur6); err != nil {
		return err
	}
	if err := upsertRoll("EUR", item.EurWholesaleStep, item.EurRetailStep, item.EurRetailMode); err != nil {
		return err
	}

	return nil
}

// UpsertPricingRuleByParameterIDFast ensures there is a rule row for a pricing_parameter_id and
// updates its multipliers/roundings in place. This avoids expensive parameter versioning and
// scope fill during worksheet-style bulk saves.
func UpsertPricingRuleByParameterIDFast(ctx context.Context, tx *sql.Tx, item PricingRuleSaveItem) (string, error) {
	if tx == nil {
		return "", fmt.Errorf("nil tx")
	}
	if item.PricingParameterID <= 0 {
		return "", fmt.Errorf("missing pricing_parameter_id")
	}

	// Find latest rule for this parameter id (if any).
	var ruleID string
	_ = tx.QueryRowContext(ctx, `
SELECT id::text
FROM mk_pricing_rule
WHERE pricing_parameter_id = $1
ORDER BY created_at DESC, updated_at DESC, id DESC
LIMIT 1
FOR UPDATE
`, item.PricingParameterID).Scan(&ruleID)
	ruleID = strings.TrimSpace(ruleID)

	if ruleID == "" {
		// Create minimal rule row; other fields have defaults and parameter scope is read from mk_urunpricingprmtr.
		if err := tx.QueryRowContext(ctx, `
INSERT INTO mk_pricing_rule (
  pricing_parameter_id,
  calc_enabled,
  publish_postgres,
  publish_nebim,
  is_active,
  created_at,
  updated_at
)
VALUES ($1,$2,$3,$4,$5,now(),now())
RETURNING id::text
`, item.PricingParameterID, item.CalcEnabled, item.PublishPostgres, item.PublishNebim, item.IsActive).Scan(&ruleID); err != nil {
			return "", err
		}
		ruleID = strings.TrimSpace(ruleID)
	}
	if ruleID == "" {
		return "", fmt.Errorf("failed to resolve rule id")
	}

	// Reuse the ID-fast updater now that we have an id.
	item.ID = ruleID
	if err := UpdatePricingRuleByIDFast(ctx, tx, item); err != nil {
		return "", err
	}
	return ruleID, nil
}

func nullablePricingParameterID(id int64) any {
	if id <= 0 {
		return nil
	}
	return id
}

type PricingRuleOptionFilters struct {
	AskiliYan     []string
	Kategori      []string
	UrunIlkGrubu  []string
	UrunAnaGrubu  []string
	UrunAltGrubu  []string
	Icerik        []string
	Marka         []string
	BrandCode     []string
	BrandGroupSec []string
}

// ListPricingRuleDistinctOptions returns distinct values for the requested field, applying cascade filters.
// Source is MSSQL ProductFilterWithDescription('TR').
func ListPricingRuleDistinctOptions(ctx context.Context, mssql *sql.DB, field string, f PricingRuleOptionFilters, limit int) ([]string, error) {
	field = strings.TrimSpace(field)
	if limit <= 0 {
		limit = 500
	}

	// Map API field -> MSSQL expression (TR descriptions + raw codes where needed)
	type fieldExpr struct {
		Expr string
		// For BrandGroupSec we need to compute from Postgres later; for now it comes from ProductPricing list,
		// so we only expose BrandCode/Marka cascades from MSSQL.
	}
	fieldMap := map[string]string{
		"askili_yan":     "COALESCE(LTRIM(RTRIM(ProductAtt45Desc)), '')",
		"kategori":       "COALESCE(LTRIM(RTRIM(ProductAtt44Desc)), '')",
		"urun_ilk_grubu": "COALESCE(LTRIM(RTRIM(ProductAtt42Desc)), '')",
		"urun_ana_grubu": "COALESCE(LTRIM(RTRIM(ProductAtt01Desc)), '')",
		"urun_alt_grubu": "COALESCE(LTRIM(RTRIM(ProductAtt02Desc)), '')",
		"icerik":         "COALESCE(LTRIM(RTRIM(ProductAtt41Desc)), '')",
		"marka":          "COALESCE(LTRIM(RTRIM(ProductAtt10Desc)), '')",
		"brand_code":     "COALESCE(LTRIM(RTRIM(ProductAtt10)), '')",
		// "brand_group" is not MSSQL-backed (comes from mk_brandgrpmatch); handled later.
	}
	expr, ok := fieldMap[field]
	if !ok {
		return nil, fmt.Errorf("invalid field")
	}

	// Build WHERE with OR lists like other endpoints
	paramIndex := 1
	args := make([]any, 0, 64)
	nextParam := func() string {
		p := "@p" + strconv.Itoa(paramIndex)
		paramIndex++
		return p
	}
	whereParts := []string{
		"ProductAtt42 IN ('SERI', 'AKSESUAR')",
		"IsBlocked = 0",
		"LEN(LTRIM(RTRIM(ProductCode))) = 13",
	}
	addIn := func(expr string, values []string) {
		clean := make([]string, 0, len(values))
		for _, v := range values {
			v = strings.TrimSpace(v)
			if v != "" {
				clean = append(clean, v)
			}
		}
		if len(clean) == 0 {
			return
		}
		ors := make([]string, 0, len(clean))
		for _, v := range clean {
			p := nextParam()
			ors = append(ors, expr+" = "+p)
			args = append(args, v)
		}
		whereParts = append(whereParts, "("+strings.Join(ors, " OR ")+")")
	}

	addIn(fieldMap["askili_yan"], f.AskiliYan)
	addIn(fieldMap["kategori"], f.Kategori)
	addIn(fieldMap["urun_ilk_grubu"], f.UrunIlkGrubu)
	addIn(fieldMap["urun_ana_grubu"], f.UrunAnaGrubu)
	addIn(fieldMap["urun_alt_grubu"], f.UrunAltGrubu)
	addIn(fieldMap["icerik"], f.Icerik)
	addIn(fieldMap["marka"], f.Marka)
	addIn(fieldMap["brand_code"], f.BrandCode)

	whereSQL := strings.Join(whereParts, " AND ")
	q := `
SELECT TOP (` + strconv.Itoa(limit) + `)
  v
FROM (
  SELECT DISTINCT ` + expr + ` AS v
  FROM ProductFilterWithDescription('TR')
  WHERE ` + whereSQL + `
) t
WHERE ISNULL(LTRIM(RTRIM(v)), '') <> ''
ORDER BY v;
`
	rows, err := mssql.QueryContext(ctx, q, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]string, 0, limit)
	for rows.Next() {
		var v string
		if err := rows.Scan(&v); err != nil {
			return nil, err
		}
		v = strings.TrimSpace(v)
		if v != "" {
			out = append(out, v)
		}
	}
	return out, rows.Err()
}
