package queries

import (
	"bssapp-backend/db"
	"bssapp-backend/models"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"math"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/lib/pq"
)

type ProductPerformanceFilters struct {
	Search      string
	ProductCode string
	MarketKey   string
	Kategori    string
	Seri        string
	Bucket      string
	SortBy      string
	Descending  bool
	Limit       int
	Page        int
}

type ProductPerformanceRefreshRequest struct {
	Mode          string
	Stage         string
	ResumeAfter   int
	SkipDelete    bool
	ProductPrefix string
	StartDate     time.Time
	EndDate       time.Time
}

type ProductPerformanceRefreshResult struct {
	Mode         string `json:"mode"`
	Stage        string `json:"stage"`
	StartDate    string `json:"start_date"`
	EndDate      string `json:"end_date"`
	SalesRows    int    `json:"sales_rows"`
	StockRows    int    `json:"stock_rows"`
	KpiRows      int    `json:"kpi_rows"`
	SnapshotRows int    `json:"snapshot_rows"`
	DurationMS   int64  `json:"duration_ms"`
}

type productPerformanceSnapshotBypassKey struct{}

func productPerformanceStockQuery() string {
	return `
;WITH ActiveWarehouses AS (
    SELECT WarehouseCode
    FROM (VALUES
        ('1-0-14'),('1-0-10'),('1-0-8'),('1-2-5'),('1-2-4'),('1-0-12'),('100'),('1-0-28'),
        ('1-0-24'),('1-2-6'),('1-1-14'),('1-0-2'),('1-0-52'),('1-1-2'),('1-0-21'),('1-1-3'),
        ('1-0-33'),('101'),('1-014'),('1-0-49'),('1-0-36'),('1-0-4'),('1-0-29')
    ) W(WarehouseCode)
),
Raw AS (
    SELECT
        ProductCode = UPPER(LTRIM(RTRIM(S.ItemCode))),
        ColorCode = UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))),
        YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))),
        MovementDate = CAST(S.DocumentDate AS date),
        ProcessCode = CASE
            WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
            ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
        END,
        InQty = SUM(S.In_Qty1),
        OutQty = SUM(S.Out_Qty1),
        NetQty = SUM(S.In_Qty1 - S.Out_Qty1)
    FROM trStock S WITH(NOLOCK)
    INNER JOIN ActiveWarehouses W
        ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
    WHERE S.ItemTypeCode = 1
      AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
      AND (@p3 = '' OR UPPER(LTRIM(RTRIM(S.ItemCode))) LIKE @p3 + '%')
      AND S.DocumentDate < DATEADD(DAY, 1, @p2)
    GROUP BY
        S.ItemCode,
        S.ColorCode,
        S.ItemDim2Code,
        CAST(S.DocumentDate AS date),
        CASE
            WHEN LTRIM(RTRIM(ISNULL(S.ProcessCode, ''))) <> '' THEN LTRIM(RTRIM(S.ProcessCode))
            ELSE LTRIM(RTRIM(ISNULL(S.InnerProcessCode, '')))
        END
),
Opening AS (
    SELECT
        StockDate = @p1,
        ProductCode,
        ColorCode,
        YakaKodu,
        NetQty = SUM(NetQty),
        InQty = CAST(0 AS decimal(18,4)),
        OutQty = CAST(0 AS decimal(18,4)),
        KpiInQty = CAST(0 AS decimal(18,4)),
        KpiOutQty = CAST(0 AS decimal(18,4)),
        SalesMovementQty = CAST(0 AS decimal(18,4)),
        ProductionInQty = CAST(0 AS decimal(18,4)),
        PurchaseInQty = CAST(0 AS decimal(18,4)),
        ConsumptionOutQty = CAST(0 AS decimal(18,4)),
        CountDiffQty = CAST(0 AS decimal(18,4))
    FROM Raw
    WHERE MovementDate < @p1
    GROUP BY ProductCode, ColorCode, YakaKodu
),
Daily AS (
    SELECT
        StockDate = MovementDate,
        ProductCode,
        ColorCode,
        YakaKodu,
        NetQty = SUM(NetQty),
        InQty = SUM(InQty),
        OutQty = SUM(OutQty),
        KpiInQty = SUM(CASE WHEN ProcessCode IN ('OP','BP','CI') THEN InQty ELSE 0 END),
        KpiOutQty = SUM(CASE WHEN ProcessCode IN ('R','WS','OC','CO') THEN OutQty ELSE 0 END),
        SalesMovementQty = SUM(CASE WHEN ProcessCode IN ('R','WS') THEN OutQty ELSE 0 END),
        ProductionInQty = SUM(CASE WHEN ProcessCode = 'OP' THEN InQty ELSE 0 END),
        PurchaseInQty = SUM(CASE WHEN ProcessCode = 'BP' THEN InQty ELSE 0 END),
        ConsumptionOutQty = SUM(CASE WHEN ProcessCode = 'OC' THEN OutQty ELSE 0 END),
        CountDiffQty = SUM(CASE WHEN ProcessCode IN ('CO','CI') THEN NetQty ELSE 0 END)
    FROM Raw
    WHERE MovementDate BETWEEN @p1 AND @p2
    GROUP BY MovementDate, ProductCode, ColorCode, YakaKodu
),
Series AS (
    SELECT * FROM Opening
    UNION ALL
    SELECT * FROM Daily
),
Collapsed AS (
    SELECT
        StockDate,
        ProductCode,
        ColorCode,
        YakaKodu,
        NetQty = SUM(NetQty),
        InQty = SUM(InQty),
        OutQty = SUM(OutQty),
        KpiInQty = SUM(KpiInQty),
        KpiOutQty = SUM(KpiOutQty),
        SalesMovementQty = SUM(SalesMovementQty),
        ProductionInQty = SUM(ProductionInQty),
        PurchaseInQty = SUM(PurchaseInQty),
        ConsumptionOutQty = SUM(ConsumptionOutQty),
        CountDiffQty = SUM(CountDiffQty)
    FROM Series
    GROUP BY StockDate, ProductCode, ColorCode, YakaKodu
),
Running AS (
    SELECT
        StockDate,
        ProductCode,
        ColorCode,
        YakaKodu,
        StockQty = SUM(NetQty) OVER (
            PARTITION BY ProductCode, ColorCode, YakaKodu
            ORDER BY StockDate
            ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
        ),
        InQty,
        OutQty,
        KpiInQty,
        KpiOutQty,
        SalesMovementQty,
        ProductionInQty,
        PurchaseInQty,
        ConsumptionOutQty,
        CountDiffQty
    FROM Collapsed
),
CurrentSource AS (
    SELECT
        SourceTable = 'PickingStates',
        WarehouseCode = LTRIM(RTRIM(P.WarehouseCode)),
        ProductCode = UPPER(LTRIM(RTRIM(P.ItemCode))),
        ColorCode = UPPER(LTRIM(RTRIM(ISNULL(P.ColorCode, '')))),
        SizeCode = UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim1Code, '')))),
        YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim2Code, '')))),
        Dim3Code = UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim3Code, '')))),
        PickingQty1 = SUM(P.Qty1),
        ReserveQty1 = CAST(0 AS decimal(18,4)),
        DispOrderQty1 = CAST(0 AS decimal(18,4)),
        InventoryQty1 = CAST(0 AS decimal(18,4))
    FROM PickingStates P WITH(NOLOCK)
    INNER JOIN ActiveWarehouses W ON W.WarehouseCode = LTRIM(RTRIM(P.WarehouseCode))
    WHERE P.ItemTypeCode = 1
      AND LEN(LTRIM(RTRIM(P.ItemCode))) = 13
      AND (@p3 = '' OR UPPER(LTRIM(RTRIM(P.ItemCode))) LIKE @p3 + '%')
    GROUP BY P.WarehouseCode, P.ItemCode, P.ColorCode, P.ItemDim1Code, P.ItemDim2Code, P.ItemDim3Code
    UNION ALL
    SELECT
        SourceTable = 'ReserveStates',
        WarehouseCode = LTRIM(RTRIM(R.WarehouseCode)),
        ProductCode = UPPER(LTRIM(RTRIM(R.ItemCode))),
        ColorCode = UPPER(LTRIM(RTRIM(ISNULL(R.ColorCode, '')))),
        SizeCode = UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim1Code, '')))),
        YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim2Code, '')))),
        Dim3Code = UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim3Code, '')))),
        PickingQty1 = CAST(0 AS decimal(18,4)),
        ReserveQty1 = SUM(R.Qty1),
        DispOrderQty1 = CAST(0 AS decimal(18,4)),
        InventoryQty1 = CAST(0 AS decimal(18,4))
    FROM ReserveStates R WITH(NOLOCK)
    INNER JOIN ActiveWarehouses W ON W.WarehouseCode = LTRIM(RTRIM(R.WarehouseCode))
    WHERE R.ItemTypeCode = 1
      AND LEN(LTRIM(RTRIM(R.ItemCode))) = 13
      AND (@p3 = '' OR UPPER(LTRIM(RTRIM(R.ItemCode))) LIKE @p3 + '%')
    GROUP BY R.WarehouseCode, R.ItemCode, R.ColorCode, R.ItemDim1Code, R.ItemDim2Code, R.ItemDim3Code
    UNION ALL
    SELECT
        SourceTable = 'DispOrderStates',
        WarehouseCode = LTRIM(RTRIM(D.WarehouseCode)),
        ProductCode = UPPER(LTRIM(RTRIM(D.ItemCode))),
        ColorCode = UPPER(LTRIM(RTRIM(ISNULL(D.ColorCode, '')))),
        SizeCode = UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim1Code, '')))),
        YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim2Code, '')))),
        Dim3Code = UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim3Code, '')))),
        PickingQty1 = CAST(0 AS decimal(18,4)),
        ReserveQty1 = CAST(0 AS decimal(18,4)),
        DispOrderQty1 = SUM(D.Qty1),
        InventoryQty1 = CAST(0 AS decimal(18,4))
    FROM DispOrderStates D WITH(NOLOCK)
    INNER JOIN ActiveWarehouses W ON W.WarehouseCode = LTRIM(RTRIM(D.WarehouseCode))
    WHERE D.ItemTypeCode = 1
      AND LEN(LTRIM(RTRIM(D.ItemCode))) = 13
      AND (@p3 = '' OR UPPER(LTRIM(RTRIM(D.ItemCode))) LIKE @p3 + '%')
    GROUP BY D.WarehouseCode, D.ItemCode, D.ColorCode, D.ItemDim1Code, D.ItemDim2Code, D.ItemDim3Code
    UNION ALL
    SELECT
        SourceTable = 'trStock',
        WarehouseCode = LTRIM(RTRIM(S.WarehouseCode)),
        ProductCode = UPPER(LTRIM(RTRIM(S.ItemCode))),
        ColorCode = UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))),
        SizeCode = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))),
        YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))),
        Dim3Code = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim3Code, '')))),
        PickingQty1 = CAST(0 AS decimal(18,4)),
        ReserveQty1 = CAST(0 AS decimal(18,4)),
        DispOrderQty1 = CAST(0 AS decimal(18,4)),
        InventoryQty1 = SUM(S.In_Qty1 - S.Out_Qty1)
    FROM trStock S WITH(NOLOCK)
    INNER JOIN ActiveWarehouses W ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
    WHERE S.ItemTypeCode = 1
      AND LEN(LTRIM(RTRIM(S.ItemCode))) = 13
      AND (@p3 = '' OR UPPER(LTRIM(RTRIM(S.ItemCode))) LIKE @p3 + '%')
      AND S.DocumentDate < DATEADD(DAY, 1, @p2)
    GROUP BY S.WarehouseCode, S.ItemCode, S.ColorCode, S.ItemDim1Code, S.ItemDim2Code, S.ItemDim3Code
),
CurrentInventory AS (
    SELECT
        ProductCode,
        ColorCode,
        SizeCode,
        YakaKodu,
        Dim3Code,
        WarehouseCode,
        PickingQty1 = SUM(PickingQty1),
        ReserveQty1 = SUM(ReserveQty1),
        DispOrderQty1 = SUM(DispOrderQty1),
        InventoryQty1 = SUM(InventoryQty1)
    FROM CurrentSource
    GROUP BY WarehouseCode, ProductCode, ColorCode, SizeCode, YakaKodu, Dim3Code
),
CurrentAvailable AS (
    SELECT
        StockDate = @p2,
        I.ProductCode,
        I.ColorCode,
        I.YakaKodu,
        StockQty = CAST(ROUND(SUM(
            ISNULL(I.InventoryQty1, 0)
            - ISNULL(I.PickingQty1, 0)
            - ISNULL(I.ReserveQty1, 0)
            - ISNULL(I.DispOrderQty1, 0)),
            2
        ) AS decimal(18,4)),
        InQty = CAST(0 AS decimal(18,4)),
        OutQty = CAST(0 AS decimal(18,4)),
        KpiInQty = CAST(0 AS decimal(18,4)),
        KpiOutQty = CAST(0 AS decimal(18,4)),
        SalesMovementQty = CAST(0 AS decimal(18,4)),
        ProductionInQty = CAST(0 AS decimal(18,4)),
        PurchaseInQty = CAST(0 AS decimal(18,4)),
        ConsumptionOutQty = CAST(0 AS decimal(18,4)),
        CountDiffQty = CAST(0 AS decimal(18,4))
    FROM CurrentInventory I
    INNER JOIN cdItem WITH(NOLOCK)
      ON cdItem.ItemTypeCode = 1
     AND UPPER(LTRIM(RTRIM(cdItem.ItemCode))) = I.ProductCode
     AND cdItem.IsBlocked = 0
    WHERE I.InventoryQty1 >= 0
      AND LEN(I.ProductCode) = 13
    GROUP BY I.ProductCode, I.ColorCode, I.YakaKodu
)
SELECT
    StockDate,
    ProductCode,
    ColorCode,
    YakaKodu,
    StockQty,
    InQty,
    OutQty,
    KpiInQty,
    KpiOutQty,
    SalesMovementQty,
    ProductionInQty,
    PurchaseInQty,
    ConsumptionOutQty,
    CountDiffQty
FROM (
    SELECT * FROM Running WHERE StockDate BETWEEN @p1 AND DATEADD(DAY, -1, @p2)
    UNION ALL
    SELECT * FROM CurrentAvailable
) X
ORDER BY StockDate, ProductCode, ColorCode, YakaKodu
`
}

func EnsureProductPerformanceTables(pg *sql.DB) error {
	stmts := []string{
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_sales_daily (
  sales_date        DATE NOT NULL,
  product_code      TEXT NOT NULL,
  color_code        TEXT NOT NULL DEFAULT '',
  yaka_kodu         TEXT NOT NULL DEFAULT '',
  item_description  TEXT NOT NULL DEFAULT '',
  kategori          TEXT NOT NULL DEFAULT '',
  seri              TEXT NOT NULL DEFAULT '',
  yas_grubu         TEXT NOT NULL DEFAULT '',
  askili_yan        TEXT NOT NULL DEFAULT '',
  urun_ilk_grubu    TEXT NOT NULL DEFAULT '',
  urun_ana_grubu    TEXT NOT NULL DEFAULT '',
  urun_alt_grubu    TEXT NOT NULL DEFAULT '',
  market_key        TEXT NOT NULL DEFAULT '',
  channel_code      TEXT NOT NULL DEFAULT '',
  customer_country  TEXT NOT NULL DEFAULT '',
  customer_segment  TEXT NOT NULL DEFAULT '',
  customer_code     TEXT NOT NULL DEFAULT '',
  customer_name     TEXT NOT NULL DEFAULT '',
  sales_qty         NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_tl          NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_usd         NUMERIC(18,4) NOT NULL DEFAULT 0,
  avg_price_usd     NUMERIC(18,6) NOT NULL DEFAULT 0,
  invoice_line_count INTEGER NOT NULL DEFAULT 0,
  invoice_count     INTEGER NOT NULL DEFAULT 0,
  customer_count    INTEGER NOT NULL DEFAULT 0,
  last_ref_number   TEXT NOT NULL DEFAULT '',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT pk_mk_product_performance_sales_daily PRIMARY KEY
    (sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code)
)`,
		`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS customer_code TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS customer_name TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_sales_daily ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
		`
UPDATE mk_product_performance_sales_daily
SET urun_ilk_grubu = '', updated_at = now()
WHERE btrim(urun_ilk_grubu) = '-'
   OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
		`
DELETE FROM mk_product_performance_sales_daily
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
		`
UPDATE mk_product_performance_sales_daily
SET askili_yan = '', updated_at = now()
WHERE btrim(askili_yan) = '-'`,
		`
DO $$
BEGIN
  IF EXISTS (
    SELECT 1
    FROM pg_constraint
    WHERE conname = 'pk_mk_product_performance_sales_daily'
      AND conrelid = 'mk_product_performance_sales_daily'::regclass
  ) THEN
    ALTER TABLE mk_product_performance_sales_daily DROP CONSTRAINT pk_mk_product_performance_sales_daily;
  END IF;
  ALTER TABLE mk_product_performance_sales_daily
    ADD CONSTRAINT pk_mk_product_performance_sales_daily PRIMARY KEY
    (sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code);
END $$`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_product_date ON mk_product_performance_sales_daily (product_code, sales_date DESC)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_market ON mk_product_performance_sales_daily (market_key, sales_date DESC)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_sales_customer ON mk_product_performance_sales_daily (customer_code, sales_date DESC)`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_stock_daily (
  stock_date        DATE NOT NULL,
  product_code      TEXT NOT NULL,
  color_code        TEXT NOT NULL DEFAULT '',
  yaka_kodu         TEXT NOT NULL DEFAULT '',
  stock_qty         NUMERIC(18,4) NOT NULL DEFAULT 0,
  in_qty            NUMERIC(18,4) NOT NULL DEFAULT 0,
  out_qty           NUMERIC(18,4) NOT NULL DEFAULT 0,
  kpi_in_qty        NUMERIC(18,4) NOT NULL DEFAULT 0,
  kpi_out_qty       NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_movement_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
  production_in_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
  purchase_in_qty   NUMERIC(18,4) NOT NULL DEFAULT 0,
  consumption_out_qty NUMERIC(18,4) NOT NULL DEFAULT 0,
  count_diff_qty    NUMERIC(18,4) NOT NULL DEFAULT 0,
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT pk_mk_product_performance_stock_daily PRIMARY KEY
    (stock_date, product_code, color_code, yaka_kodu)
)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_stock_product_date ON mk_product_performance_stock_daily (product_code, stock_date DESC)`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_price_dim (
  product_code      TEXT PRIMARY KEY,
  item_description  TEXT NOT NULL DEFAULT '',
  kategori          TEXT NOT NULL DEFAULT '',
  askili_yan        TEXT NOT NULL DEFAULT '',
  urun_ilk_grubu    TEXT NOT NULL DEFAULT '',
  urun_ana_grubu    TEXT NOT NULL DEFAULT '',
  urun_alt_grubu    TEXT NOT NULL DEFAULT '',
  cost_price_usd    NUMERIC(18,6) NOT NULL DEFAULT 0,
  base_price_usd    NUMERIC(18,6) NOT NULL DEFAULT 0,
  base_price_try    NUMERIC(18,6) NOT NULL DEFAULT 0,
  last_pricing_date DATE,
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now()
)`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS item_description TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS kategori TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS askili_yan TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_price_dim ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
		`
UPDATE mk_product_performance_price_dim
SET
  cost_price_usd = LEAST(cost_price_usd, base_price_usd),
  base_price_usd = GREATEST(cost_price_usd, base_price_usd),
  updated_at = now()
WHERE cost_price_usd > 0
  AND base_price_usd > 0
  AND cost_price_usd > base_price_usd`,
		`
DELETE FROM mk_product_performance_price_dim
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_kpi_daily (
  kpi_date          DATE NOT NULL,
  product_code      TEXT NOT NULL,
  color_code        TEXT NOT NULL DEFAULT '',
  yaka_kodu         TEXT NOT NULL DEFAULT '',
  item_description  TEXT NOT NULL DEFAULT '',
  kategori          TEXT NOT NULL DEFAULT '',
  seri              TEXT NOT NULL DEFAULT '',
  yas_grubu         TEXT NOT NULL DEFAULT '',
  askili_yan        TEXT NOT NULL DEFAULT '',
  urun_ilk_grubu    TEXT NOT NULL DEFAULT '',
  urun_ana_grubu    TEXT NOT NULL DEFAULT '',
  urun_alt_grubu    TEXT NOT NULL DEFAULT '',
  market_key        TEXT NOT NULL DEFAULT '',
  stock_qty         NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_qty_30d     NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_qty_90d     NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_qty_180d    NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_qty_365d    NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_qty_730d    NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_usd_30d     NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_usd_90d     NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_usd_180d    NUMERIC(18,4) NOT NULL DEFAULT 0,
  sales_usd_365d    NUMERIC(18,4) NOT NULL DEFAULT 0,
  avg_daily_sales_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_stock_90d     NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_stock_180d    NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_stock_365d    NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_stock_total   NUMERIC(18,6) NOT NULL DEFAULT 0,
  stock_days_90d    NUMERIC(18,6) NOT NULL DEFAULT 0,
  stock_days_180d   NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_price_usd_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
  avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
  cost_price_usd    NUMERIC(18,6) NOT NULL DEFAULT 0,
  base_price_usd    NUMERIC(18,6) NOT NULL DEFAULT 0,
  base_price_try    NUMERIC(18,6) NOT NULL DEFAULT 0,
  gross_profit_usd_90d NUMERIC(18,4) NOT NULL DEFAULT 0,
  gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0,
  gross_margin_90d  NUMERIC(18,6) NOT NULL DEFAULT 0,
  gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
  unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
  unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
  unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0,
  unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0,
  market_count_90d INTEGER NOT NULL DEFAULT 0,
  market_count_180d INTEGER NOT NULL DEFAULT 0,
  market_count_365d INTEGER NOT NULL DEFAULT 0,
  market_count_total INTEGER NOT NULL DEFAULT 0,
  customer_count_90d INTEGER NOT NULL DEFAULT 0,
  customer_count_180d INTEGER NOT NULL DEFAULT 0,
  customer_count_365d INTEGER NOT NULL DEFAULT 0,
  customer_count_total INTEGER NOT NULL DEFAULT 0,
  sales_index_90d   NUMERIC(18,6) NOT NULL DEFAULT 0,
  sales_index_180d  NUMERIC(18,6) NOT NULL DEFAULT 0,
  sales_index_365d  NUMERIC(18,6) NOT NULL DEFAULT 0,
  sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0,
  price_index_90d   NUMERIC(18,6) NOT NULL DEFAULT 0,
  margin_index_90d  NUMERIC(18,6) NOT NULL DEFAULT 0,
  performance_score NUMERIC(18,6) NOT NULL DEFAULT 0,
  performance_bucket TEXT NOT NULL DEFAULT '',
  recommendation    TEXT NOT NULL DEFAULT '',
  last_sale_date    DATE,
  last_ref_number   TEXT NOT NULL DEFAULT '',
  created_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT pk_mk_product_performance_kpi_daily PRIMARY KEY
    (kpi_date, product_code, color_code, yaka_kodu, market_key)
)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_kpi_bucket ON mk_product_performance_kpi_daily (kpi_date DESC, performance_bucket)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_kpi_market ON mk_product_performance_kpi_daily (kpi_date DESC, market_key, sales_index_90d DESC)`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ilk_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_ana_grubu TEXT NOT NULL DEFAULT ''`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS urun_alt_grubu TEXT NOT NULL DEFAULT ''`,
		`
UPDATE mk_product_performance_kpi_daily
SET urun_ilk_grubu = '', updated_at = now()
WHERE btrim(urun_ilk_grubu) = '-'
   OR upper(translate(btrim(urun_ilk_grubu), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON')`,
		`
DELETE FROM mk_product_performance_kpi_daily
WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')`,
		`
UPDATE mk_product_performance_kpi_daily
SET
  cost_price_usd = LEAST(cost_price_usd, base_price_usd),
  base_price_usd = GREATEST(cost_price_usd, base_price_usd),
  gross_profit_usd_90d = COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd)),
  gross_profit_usd_180d = COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd)),
  gross_margin_90d = CASE WHEN COALESCE(sales_usd_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) - (COALESCE(sales_qty_90d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_90d,0) END,
  gross_margin_180d = CASE WHEN COALESCE(sales_usd_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) - (COALESCE(sales_qty_180d,0) * LEAST(cost_price_usd, base_price_usd))) / NULLIF(sales_usd_180d,0) END,
  unit_profit_cost_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - LEAST(cost_price_usd, base_price_usd) END,
  unit_profit_cost_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - LEAST(cost_price_usd, base_price_usd) END,
  unit_profit_base_90d = CASE WHEN COALESCE(sales_qty_90d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_90d,0) / NULLIF(sales_qty_90d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
  unit_profit_base_180d = CASE WHEN COALESCE(sales_qty_180d,0) = 0 THEN 0 ELSE (COALESCE(sales_usd_180d,0) / NULLIF(sales_qty_180d,0)) - GREATEST(cost_price_usd, base_price_usd) END,
  updated_at = now()
WHERE cost_price_usd > 0
  AND base_price_usd > 0
  AND cost_price_usd > base_price_usd`,
		`
UPDATE mk_product_performance_kpi_daily
SET askili_yan = '', updated_at = now()
WHERE btrim(askili_yan) = '-'`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_qty_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_qty_total NUMERIC(18,4) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_usd_total NUMERIC(18,4) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_daily_sales_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_stock_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_days_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_turnover_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_turnover_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_turnover_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS stock_turnover_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS avg_price_usd_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_profit_usd_180d NUMERIC(18,4) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS gross_margin_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_cost_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_90d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS unit_profit_base_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_90d INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_180d INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_365d INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS market_count_total INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_180d INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_365d INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS customer_count_total INTEGER NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_180d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_365d NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`ALTER TABLE mk_product_performance_kpi_daily ADD COLUMN IF NOT EXISTS sales_index_total NUMERIC(18,6) NOT NULL DEFAULT 0`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot (
  report_key TEXT NOT NULL,
  row_order  INTEGER NOT NULL,
  payload    JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT pk_mk_product_performance_report_snapshot PRIMARY KEY (report_key, row_order)
)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_report_snapshot_key ON mk_product_performance_report_snapshot (report_key, row_order)`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_report_snapshot_meta (
  report_key TEXT PRIMARY KEY,
  row_count  INTEGER NOT NULL DEFAULT 0,
  refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  duration_ms BIGINT NOT NULL DEFAULT 0
)`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot (
  report_key TEXT NOT NULL,
  row_order INTEGER NOT NULL,
  mode TEXT NOT NULL,
  group_levels_key TEXT NOT NULL,
  main_group TEXT NOT NULL DEFAULT '',
  group_level INTEGER NOT NULL DEFAULT 0,
  group_key TEXT NOT NULL DEFAULT '',
  parent_key TEXT NOT NULL DEFAULT '',
  group_field TEXT NOT NULL DEFAULT '',
  group_value TEXT NOT NULL DEFAULT '',
  payload JSONB NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  CONSTRAINT pk_mk_product_performance_grouped_snapshot PRIMARY KEY (report_key, row_order)
)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_key ON mk_product_performance_grouped_snapshot (report_key, row_order)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_level ON mk_product_performance_grouped_snapshot (report_key, group_level, row_order)`,
		`CREATE INDEX IF NOT EXISTS ix_mk_product_perf_grouped_snapshot_field ON mk_product_performance_grouped_snapshot (report_key, group_field, group_value)`,
		`
CREATE TABLE IF NOT EXISTS mk_product_performance_grouped_snapshot_meta (
  report_key TEXT PRIMARY KEY,
  mode TEXT NOT NULL,
  group_levels_key TEXT NOT NULL,
  main_group TEXT NOT NULL DEFAULT '',
  row_count INTEGER NOT NULL DEFAULT 0,
  refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  duration_ms BIGINT NOT NULL DEFAULT 0
)`,
	}
	for _, stmt := range stmts {
		if _, err := pg.Exec(stmt); err != nil {
			return err
		}
	}
	return nil
}

func productPerformanceSnapshotBypass(ctx context.Context) context.Context {
	return context.WithValue(ctx, productPerformanceSnapshotBypassKey{}, true)
}

func productPerformanceUseSnapshot(ctx context.Context) bool {
	v, _ := ctx.Value(productPerformanceSnapshotBypassKey{}).(bool)
	return !v
}

func productPerformanceLiveFallbackEnabled() bool {
	raw := strings.TrimSpace(strings.ToLower(os.Getenv("PRODUCT_PERFORMANCE_LIVE_FALLBACK")))
	return raw == "1" || raw == "true" || raw == "on" || raw == "yes"
}

func productPerformanceSnapshotKey(parts ...string) string {
	cleaned := make([]string, 0, len(parts))
	for _, part := range parts {
		part = strings.ToLower(strings.TrimSpace(part))
		if part != "" {
			cleaned = append(cleaned, part)
		}
	}
	return strings.Join(cleaned, ":")
}

func loadProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]T, bool, error) {
	if !productPerformanceUseSnapshot(ctx) || pg == nil || strings.TrimSpace(reportKey) == "" {
		return nil, false, nil
	}
	if limit <= 0 || limit > 50000 {
		limit = 50000
	}
	rows, err := pg.QueryContext(ctx, `
SELECT payload
FROM mk_product_performance_report_snapshot
WHERE report_key = $1
ORDER BY row_order
LIMIT $2
`, reportKey, limit)
	if err != nil {
		return nil, false, err
	}
	defer rows.Close()

	out := make([]T, 0, limit)
	for rows.Next() {
		var raw []byte
		if err := rows.Scan(&raw); err != nil {
			return nil, false, err
		}
		var item T
		if err := json.Unmarshal(raw, &item); err != nil {
			return nil, false, err
		}
		out = append(out, item)
	}
	if err := rows.Err(); err != nil {
		return nil, false, err
	}
	if len(out) > 0 {
		return out, true, nil
	}
	var exists bool
	if err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM mk_product_performance_report_snapshot_meta
  WHERE report_key = $1
)
`, reportKey).Scan(&exists); err != nil {
		return nil, false, err
	}
	if !exists && !productPerformanceLiveFallbackEnabled() {
		return out, true, nil
	}
	return out, exists, nil
}

func loadProductPerformanceSnapshotMapRows(ctx context.Context, pg *sql.DB, reportKey string, limit int) ([]map[string]any, bool, error) {
	if !productPerformanceUseSnapshot(ctx) || pg == nil || strings.TrimSpace(reportKey) == "" {
		return nil, false, nil
	}
	if limit <= 0 || limit > 50000 {
		limit = 50000
	}
	rows, err := pg.QueryContext(ctx, `
SELECT payload
FROM mk_product_performance_report_snapshot
WHERE report_key = $1
ORDER BY row_order
LIMIT $2
`, reportKey, limit)
	if err != nil {
		return nil, false, err
	}
	defer rows.Close()

	out := make([]map[string]any, 0, limit)
	for rows.Next() {
		var raw []byte
		if err := rows.Scan(&raw); err != nil {
			return nil, false, err
		}
		var item map[string]any
		if err := json.Unmarshal(raw, &item); err != nil {
			return nil, false, err
		}
		if item == nil {
			item = map[string]any{}
		}
		out = append(out, item)
	}
	if err := rows.Err(); err != nil {
		return nil, false, err
	}
	if len(out) > 0 {
		return out, true, nil
	}
	var exists bool
	if err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM mk_product_performance_report_snapshot_meta
  WHERE report_key = $1
)
`, reportKey).Scan(&exists); err != nil {
		return nil, false, err
	}
	if !exists && !productPerformanceLiveFallbackEnabled() {
		return out, true, nil
	}
	return out, exists, nil
}

func loadProductPerformanceSnapshotItem[T any](ctx context.Context, pg *sql.DB, reportKey string) (T, bool, error) {
	var zero T
	rows, ok, err := loadProductPerformanceSnapshotRows[T](ctx, pg, reportKey, 1)
	if err != nil || !ok || len(rows) == 0 {
		return zero, ok, err
	}
	return rows[0], true, nil
}

func saveProductPerformanceSnapshotRows[T any](ctx context.Context, pg *sql.DB, reportKey string, rows []T, started time.Time) error {
	if pg == nil || strings.TrimSpace(reportKey) == "" {
		return nil
	}
	tx, err := pg.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_report_snapshot WHERE report_key = $1`, reportKey); err != nil {
		return err
	}
	stmt, err := tx.PrepareContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot (report_key, row_order, payload, updated_at)
VALUES ($1, $2, $3, now())
`)
	if err != nil {
		return err
	}
	defer stmt.Close()
	for i, row := range rows {
		raw, err := json.Marshal(row)
		if err != nil {
			return err
		}
		if _, err := stmt.ExecContext(ctx, reportKey, i, raw); err != nil {
			return err
		}
	}
	if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_report_snapshot_meta (report_key, row_count, refreshed_at, duration_ms)
VALUES ($1, $2, now(), $3)
ON CONFLICT (report_key) DO UPDATE SET
  row_count = EXCLUDED.row_count,
  refreshed_at = EXCLUDED.refreshed_at,
  duration_ms = EXCLUDED.duration_ms
`, reportKey, len(rows), time.Since(started).Milliseconds()); err != nil {
		return err
	}
	return tx.Commit()
}

func saveProductPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, reportKey string, def productPerformanceGroupedSnapshotDefinition, rows []map[string]any, started time.Time) error {
	if pg == nil || strings.TrimSpace(reportKey) == "" {
		return nil
	}
	tx, err := pg.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key = $1`, reportKey); err != nil {
		return err
	}
	stmt, err := tx.PrepareContext(ctx, `
INSERT INTO mk_product_performance_grouped_snapshot (
  report_key, row_order, mode, group_levels_key, main_group,
  group_level, group_key, parent_key, group_field, group_value, payload, updated_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now())
`)
	if err != nil {
		return err
	}
	defer stmt.Close()

	levelsKey := productPerformanceGroupedLevelsKey(def.Levels)
	for i, row := range rows {
		raw, err := json.Marshal(row)
		if err != nil {
			return err
		}
		key := stringFromMap(row, "key")
		if _, err := stmt.ExecContext(
			ctx,
			reportKey,
			i,
			strings.TrimSpace(def.Mode),
			levelsKey,
			strings.TrimSpace(def.MainGroup),
			intFromMap(row, "level"),
			key,
			productPerformanceParentGroupKey(key),
			stringFromMap(row, "group_field"),
			stringFromMap(row, "group_value"),
			raw,
		); err != nil {
			return err
		}
	}
	if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_grouped_snapshot_meta (
  report_key, mode, group_levels_key, main_group, row_count, refreshed_at, duration_ms
)
VALUES ($1,$2,$3,$4,$5,now(),$6)
ON CONFLICT (report_key) DO UPDATE SET
  mode = EXCLUDED.mode,
  group_levels_key = EXCLUDED.group_levels_key,
  main_group = EXCLUDED.main_group,
  row_count = EXCLUDED.row_count,
  refreshed_at = EXCLUDED.refreshed_at,
  duration_ms = EXCLUDED.duration_ms
`, reportKey, strings.TrimSpace(def.Mode), levelsKey, strings.TrimSpace(def.MainGroup), len(rows), time.Since(started).Milliseconds()); err != nil {
		return err
	}
	return tx.Commit()
}

func RebuildProductPerformanceReportSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return 0, err
	}
	snapshotReadCtx := ctx
	ctx = productPerformanceSnapshotBypass(ctx)
	total := 0

	save := func(reportKey string, rows any, started time.Time) error {
		rowCount := productPerformanceSnapshotPayloadLen(rows)
		log.Printf("[ProductPerformanceRefresh] snapshot save start key=%s rows=%d", reportKey, rowCount)
		defer func() {
			log.Printf("[ProductPerformanceRefresh] snapshot save done key=%s rows=%d elapsed=%s", reportKey, rowCount, time.Since(started).Round(time.Second))
		}()
		switch v := rows.(type) {
		case []models.ProductPerformanceSummary:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceGeneralRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceOrderAnalysisRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceOrderGroupRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceOrderProductCustomerRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceOrderMarketDetailRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceMarketRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceCountryRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceCustomerRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		case []models.ProductPerformanceSalesBreakdownRow:
			total += len(v)
			return saveProductPerformanceSnapshotRows(ctx, pg, reportKey, v, started)
		default:
			return fmt.Errorf("unsupported product performance snapshot payload %s", reportKey)
		}
	}

	started := time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("summary"))
	if summary, err := GetProductPerformanceSummary(ctx, pg); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("summary"), []models.ProductPerformanceSummary{summary}, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("products"))
	if rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: 50000, Page: 1, SortBy: "performance_score", Descending: true}); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("products"), rows, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("general"))
	if rows, err := ListProductPerformanceGeneral(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("general"), rows, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("orders"))
	if rows, err := ListProductPerformanceOrderAnalysis(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("orders"), rows, started); err != nil {
		return total, err
	}
	for _, mode := range []string{"market", "customer"} {
		started = time.Now()
		log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-groups", mode))
		rows, err := ListProductPerformanceOrderGroups(ctx, pg, mode, 50000)
		if err != nil {
			return total, err
		}
		if err := save(productPerformanceSnapshotKey("order-groups", mode), rows, started); err != nil {
			return total, err
		}
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-product-customers"))
	if rows, err := ListProductPerformanceOrderProductCustomers(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("order-product-customers"), rows, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("order-market-details"))
	if rows, err := ListProductPerformanceOrderMarketDetails(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("order-market-details"), rows, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("markets"))
	if rows, err := ListProductPerformanceMarkets(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("markets"), rows, started); err != nil {
		return total, err
	}
	started = time.Now()
	log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("countries"))
	if rows, err := ListProductPerformanceCountries(ctx, pg, 50000); err != nil {
		return total, err
	} else if err := save(productPerformanceSnapshotKey("countries"), rows, started); err != nil {
		return total, err
	}
	for _, mode := range []string{"market_customer", "country_customer", "market_country_customer"} {
		started = time.Now()
		log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("customers", mode))
		rows, err := ListProductPerformanceCustomers(ctx, pg, mode, 50000)
		if err != nil {
			return total, err
		}
		if err := save(productPerformanceSnapshotKey("customers", mode), rows, started); err != nil {
			return total, err
		}
	}
	for _, mode := range []string{"color_yaka_market_customer", "product_country_segment_market_customer", "market_customer_product", "country_segment_market_customer_product"} {
		started = time.Now()
		log.Printf("[ProductPerformanceRefresh] snapshot build start key=%s", productPerformanceSnapshotKey("sales-breakdown", mode))
		rows, err := ListProductPerformanceSalesBreakdown(ctx, pg, mode, 50000)
		if err != nil {
			return total, err
		}
		if err := save(productPerformanceSnapshotKey("sales-breakdown", mode), rows, started); err != nil {
			return total, err
		}
	}
	log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild start")
	groupedRows, err := RebuildProductPerformanceGroupedSnapshots(snapshotReadCtx, pg)
	if err != nil {
		return total, err
	}
	log.Printf("[ProductPerformanceRefresh] grouped snapshots rebuild done rows=%d", groupedRows)
	total += groupedRows
	return total, nil
}

func productPerformanceSnapshotPayloadLen(rows any) int {
	switch v := rows.(type) {
	case []models.ProductPerformanceSummary:
		return len(v)
	case []models.ProductPerformanceRow:
		return len(v)
	case []models.ProductPerformanceGeneralRow:
		return len(v)
	case []models.ProductPerformanceOrderAnalysisRow:
		return len(v)
	case []models.ProductPerformanceOrderGroupRow:
		return len(v)
	case []models.ProductPerformanceOrderProductCustomerRow:
		return len(v)
	case []models.ProductPerformanceOrderMarketDetailRow:
		return len(v)
	case []models.ProductPerformanceMarketRow:
		return len(v)
	case []models.ProductPerformanceCountryRow:
		return len(v)
	case []models.ProductPerformanceCustomerRow:
		return len(v)
	case []models.ProductPerformanceSalesBreakdownRow:
		return len(v)
	default:
		return 0
	}
}

type productPerformanceGroupedSnapshotDefinition struct {
	Mode      string
	Levels    []string
	MainGroup string
}

func RebuildProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB) (int, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return 0, err
	}
	defs, err := productPerformanceGroupedSnapshotDefinitions(ctx, pg)
	if err != nil {
		return 0, err
	}
	log.Printf("[ProductPerformanceRefresh] grouped snapshot definitions ready count=%d", len(defs))
	if err := deleteStaleProductPerformanceGroupedSnapshots(ctx, pg, productPerformanceSnapshotKey("grouped", "product_detail")+":%"); err != nil {
		return 0, err
	}
	total := 0
	for i, def := range defs {
		started := time.Now()
		levels := sanitizeProductPerformanceGroupLevels(def.Levels)
		if len(levels) == 0 {
			levels = defaultProductPerformanceGroupLevels(def.Mode)
		}
		reportKey := productPerformanceGroupedSnapshotReportKey(def.Mode, levels, def.MainGroup)
		log.Printf("[ProductPerformanceRefresh] grouped snapshot start %d/%d key=%s mode=%s main_group=%s levels=%s", i+1, len(defs), reportKey, def.Mode, def.MainGroup, productPerformanceGroupedLevelsKey(levels))
		sourceRows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, def.Mode, 50000)
		if err != nil {
			return total, err
		}
		log.Printf("[ProductPerformanceRefresh] grouped snapshot source loaded key=%s source_rows=%d elapsed=%s", reportKey, len(sourceRows), time.Since(started).Round(time.Second))
		req := ProductPerformanceGroupedRequest{
			Mode:      def.Mode,
			MainGroup: def.MainGroup,
		}
		sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
		groupStarted := time.Now()
		rows := buildProductPerformanceGroupedSnapshotRows(sourceRows, levels, def.Mode)
		log.Printf("[ProductPerformanceRefresh] grouped snapshot tree built key=%s rows=%d elapsed=%s total_elapsed=%s", reportKey, len(rows), time.Since(groupStarted).Round(time.Second), time.Since(started).Round(time.Second))
		if err := saveProductPerformanceGroupedSnapshotRows(ctx, pg, reportKey, productPerformanceGroupedSnapshotDefinition{
			Mode:      def.Mode,
			Levels:    levels,
			MainGroup: def.MainGroup,
		}, rows, started); err != nil {
			return total, err
		}
		total += len(rows)
		log.Printf("[ProductPerformanceRefresh] grouped snapshot done key=%s rows=%d cumulative_rows=%d elapsed=%s", reportKey, len(rows), total, time.Since(started).Round(time.Second))
	}
	return total, nil
}

func deleteStaleProductPerformanceGroupedSnapshots(ctx context.Context, pg *sql.DB, reportKeyLike string) error {
	reportKeyLike = strings.TrimSpace(reportKeyLike)
	if reportKeyLike == "" {
		return nil
	}
	if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot WHERE report_key LIKE $1`, reportKeyLike); err != nil {
		return err
	}
	if _, err := pg.ExecContext(ctx, `DELETE FROM mk_product_performance_grouped_snapshot_meta WHERE report_key LIKE $1`, reportKeyLike); err != nil {
		return err
	}
	return nil
}

func productPerformanceGroupedSnapshotDefinitions(ctx context.Context, pg *sql.DB) ([]productPerformanceGroupedSnapshotDefinition, error) {
	_ = ctx
	_ = pg
	defs := []productPerformanceGroupedSnapshotDefinition{
		{Mode: "products", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}},
		{Mode: "products", Levels: []string{"urun_ana_grubu"}},
		{Mode: "idle", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
		{Mode: "sales_color_yaka_market_customer", Levels: []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}},
		{Mode: "sales_product_country_segment_market_customer", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}},
		{Mode: "sales_country_segment_market_customer_product", Levels: []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
		{Mode: "order_product_customers", Levels: []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}},
		{Mode: "order_market_details", Levels: []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}},
	}
	return defs, nil
}

func RefreshProductPerformance(ctx context.Context, pg *sql.DB, req ProductPerformanceRefreshRequest) (ProductPerformanceRefreshResult, error) {
	started := time.Now()
	stage := productPerformanceRefreshStage(req.Stage)
	productPrefix := productPerformanceProductPrefix(req.ProductPrefix)
	log.Printf("[ProductPerformanceRefresh] start mode=%s stage=%s prefix=%s start=%s end=%s", req.Mode, stage, productPrefix, req.StartDate.Format("2006-01-02"), req.EndDate.Format("2006-01-02"))
	if pg == nil {
		return ProductPerformanceRefreshResult{}, fmt.Errorf("postgres db nil")
	}
	if db.MssqlDB == nil {
		return ProductPerformanceRefreshResult{}, fmt.Errorf("mssql db nil")
	}
	log.Printf("[ProductPerformanceRefresh] ensure tables start")
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return ProductPerformanceRefreshResult{}, err
	}
	log.Printf("[ProductPerformanceRefresh] ensure tables done")
	mode := strings.ToLower(strings.TrimSpace(req.Mode))
	if mode == "" {
		mode = "delta"
	}
	if req.EndDate.IsZero() {
		req.EndDate = time.Now()
	}
	if req.StartDate.IsZero() {
		if mode == "full" {
			req.StartDate = time.Date(2022, 1, 1, 0, 0, 0, 0, req.EndDate.Location())
		} else {
			req.StartDate = req.EndDate.AddDate(0, 0, -45)
		}
	}
	req.StartDate = dateOnly(req.StartDate)
	req.EndDate = dateOnly(req.EndDate)
	shouldRun := func(name string) bool {
		if stage == "all" {
			return true
		}
		order := map[string]int{
			"sales": 1,
			"stock": 2,
			"price": 3,
			"kpi":   4,
		}
		return order[name] >= order[stage]
	}

	runStage := func(stage string, fn func(*sql.Tx) error) error {
		log.Printf("[ProductPerformanceRefresh] %s tx begin", stage)
		tx, err := pg.BeginTx(ctx, nil)
		if err != nil {
			return err
		}
		defer tx.Rollback()
		if err := fn(tx); err != nil {
			return err
		}
		log.Printf("[ProductPerformanceRefresh] %s tx commit start", stage)
		if err := tx.Commit(); err != nil {
			return err
		}
		log.Printf("[ProductPerformanceRefresh] %s tx commit done elapsed=%s", stage, time.Since(started).Round(time.Second))
		return nil
	}

	var salesRows int
	if shouldRun("sales") {
		if err := runStage("sales", func(tx *sql.Tx) error {
			log.Printf("[ProductPerformanceRefresh] delete sales cache start")
			deleteSQL := `DELETE FROM mk_product_performance_sales_daily WHERE sales_date BETWEEN $1 AND $2`
			deleteArgs := []any{req.StartDate, req.EndDate}
			if productPrefix != "" {
				deleteSQL += ` AND product_code ILIKE $3 || '%'`
				deleteArgs = append(deleteArgs, productPrefix)
			}
			if _, err := tx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
				return err
			}
			log.Printf("[ProductPerformanceRefresh] delete sales cache done")
			log.Printf("[ProductPerformanceRefresh] sales refresh start")
			rows, err := refreshProductPerformanceSales(ctx, tx, req.StartDate, req.EndDate, productPrefix)
			if err != nil {
				return err
			}
			salesRows = rows
			log.Printf("[ProductPerformanceRefresh] sales refresh done rows=%d elapsed=%s", salesRows, time.Since(started).Round(time.Second))
			return nil
		}); err != nil {
			return ProductPerformanceRefreshResult{}, err
		}
	} else {
		log.Printf("[ProductPerformanceRefresh] sales skipped stage=%s", stage)
	}

	var stockRows int
	if shouldRun("stock") {
		rows, err := refreshProductPerformanceStockChunked(ctx, pg, req.StartDate, req.EndDate, started, req.SkipDelete || req.ResumeAfter > 0, req.ResumeAfter, productPrefix)
		if err != nil {
			return ProductPerformanceRefreshResult{}, err
		}
		stockRows = rows
	} else {
		log.Printf("[ProductPerformanceRefresh] stock skipped stage=%s", stage)
	}

	if shouldRun("price") {
		if err := runStage("price", func(tx *sql.Tx) error {
			log.Printf("[ProductPerformanceRefresh] price refresh start")
			if err := refreshProductPerformancePrices(ctx, tx, productPrefix); err != nil {
				return err
			}
			log.Printf("[ProductPerformanceRefresh] price refresh done elapsed=%s", time.Since(started).Round(time.Second))
			return nil
		}); err != nil {
			return ProductPerformanceRefreshResult{}, err
		}
	} else {
		log.Printf("[ProductPerformanceRefresh] price skipped stage=%s", stage)
	}

	var kpiRows int
	var snapshotRows int
	if shouldRun("kpi") {
		if err := runStage("kpi", func(tx *sql.Tx) error {
			log.Printf("[ProductPerformanceRefresh] kpi rebuild start")
			rows, err := RebuildProductPerformanceKPI(ctx, tx, req.EndDate)
			if err != nil {
				return err
			}
			kpiRows = rows
			log.Printf("[ProductPerformanceRefresh] kpi rebuild done rows=%d elapsed=%s", kpiRows, time.Since(started).Round(time.Second))
			return nil
		}); err != nil {
			return ProductPerformanceRefreshResult{}, err
		}
		log.Printf("[ProductPerformanceRefresh] report snapshots rebuild start")
		rows, err := RebuildProductPerformanceReportSnapshots(ctx, pg)
		if err != nil {
			return ProductPerformanceRefreshResult{}, err
		}
		snapshotRows = rows
		log.Printf("[ProductPerformanceRefresh] report snapshots rebuild done rows=%d elapsed=%s", snapshotRows, time.Since(started).Round(time.Second))
	} else {
		log.Printf("[ProductPerformanceRefresh] kpi skipped stage=%s", stage)
	}

	log.Printf("[ProductPerformanceRefresh] refresh done total_elapsed=%s", time.Since(started).Round(time.Second))
	return ProductPerformanceRefreshResult{
		Mode:         mode,
		Stage:        stage,
		StartDate:    req.StartDate.Format("2006-01-02"),
		EndDate:      req.EndDate.Format("2006-01-02"),
		SalesRows:    salesRows,
		StockRows:    stockRows,
		KpiRows:      kpiRows,
		SnapshotRows: snapshotRows,
		DurationMS:   time.Since(started).Milliseconds(),
	}, nil
}

func productPerformanceRefreshStage(raw string) string {
	switch strings.ToLower(strings.TrimSpace(raw)) {
	case "", "all", "full":
		return "all"
	case "sales", "stock", "price", "kpi":
		return strings.ToLower(strings.TrimSpace(raw))
	default:
		return "all"
	}
}

func productPerformanceProductPrefix(raw string) string {
	return strings.ToUpper(strings.TrimSpace(raw))
}

func normalizeProductPerformanceProductCode(raw string) string {
	return strings.ToUpper(strings.TrimSpace(raw))
}

func normalizeProductPerformanceCode(raw string) string {
	return strings.ToUpper(strings.TrimSpace(raw))
}

func normalizeProductPerformanceVariantCodes(productCode, colorCode, yakaKodu *string) {
	if productCode != nil {
		*productCode = normalizeProductPerformanceProductCode(*productCode)
	}
	if colorCode != nil {
		*colorCode = normalizeProductPerformanceCode(*colorCode)
	}
	if yakaKodu != nil {
		*yakaKodu = normalizeProductPerformanceCode(*yakaKodu)
	}
}

func productPerformanceColorDescriptions(ctx context.Context, colorCodes []string) map[string]string {
	out := map[string]string{}
	if db.MssqlDB == nil || len(colorCodes) == 0 {
		return out
	}
	seen := map[string]bool{}
	codes := make([]string, 0, len(colorCodes))
	for _, raw := range colorCodes {
		code := normalizeProductPerformanceCode(raw)
		if code == "" || seen[code] {
			continue
		}
		seen[code] = true
		codes = append(codes, code)
	}
	const batchSize = 500
	for start := 0; start < len(codes); start += batchSize {
		end := start + batchSize
		if end > len(codes) {
			end = len(codes)
		}
		batch := codes[start:end]
		placeholders := make([]string, len(batch))
		args := make([]any, len(batch))
		for i, code := range batch {
			placeholders[i] = fmt.Sprintf("@p%d", i+1)
			args[i] = code
		}
		query := fmt.Sprintf(`
SELECT
  UPPER(LTRIM(RTRIM(ColorCode))) AS color_code,
  MAX(LTRIM(RTRIM(ISNULL(ColorDescription, '')))) AS color_description
FROM cdColorDesc WITH(NOLOCK)
WHERE LangCode = 'TR'
  AND UPPER(LTRIM(RTRIM(ColorCode))) IN (%s)
GROUP BY UPPER(LTRIM(RTRIM(ColorCode)))
`, strings.Join(placeholders, ","))
		rows, err := db.MssqlDB.QueryContext(ctx, query, args...)
		if err != nil {
			log.Printf("[ProductPerformance] color descriptions failed: %v", err)
			return out
		}
		for rows.Next() {
			var code, desc string
			if err := rows.Scan(&code, &desc); err != nil {
				rows.Close()
				log.Printf("[ProductPerformance] color description scan failed: %v", err)
				return out
			}
			out[normalizeProductPerformanceCode(code)] = strings.TrimSpace(desc)
		}
		if err := rows.Err(); err != nil {
			rows.Close()
			log.Printf("[ProductPerformance] color description rows failed: %v", err)
			return out
		}
		rows.Close()
	}
	return out
}

func refreshProductPerformanceSales(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
	log.Printf("[ProductPerformanceRefresh] sales mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
	rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceSalesSQL(), startDate, endDate, productPrefix)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	log.Printf("[ProductPerformanceRefresh] sales mssql query returned, postgres insert start")
	count := 0
	for rows.Next() {
		var r productPerformanceSalesDaily
		if err := rows.Scan(
			&r.SalesDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
			&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.ChannelCode,
			&r.CustomerCountry, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName, &r.SalesQty, &r.SalesTL, &r.SalesUSD,
			&r.AvgPriceUSD, &r.InvoiceLineCount, &r.InvoiceCount, &r.CustomerCount, &r.LastRefNumber,
		); err != nil {
			return count, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan)
		r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
		if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_sales_daily (
  sales_date, product_code, color_code, yaka_kodu, item_description,
  kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, channel_code,
  customer_country, customer_segment, customer_code, customer_name, sales_qty, sales_tl, sales_usd,
  avg_price_usd, invoice_line_count, invoice_count, customer_count, last_ref_number, updated_at
) VALUES (
  $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,now()
)
ON CONFLICT (sales_date, product_code, color_code, yaka_kodu, market_key, customer_country, customer_segment, customer_code)
DO UPDATE SET
  item_description=EXCLUDED.item_description,
  kategori=EXCLUDED.kategori,
  seri=EXCLUDED.seri,
  yas_grubu=EXCLUDED.yas_grubu,
  askili_yan=EXCLUDED.askili_yan,
  urun_ilk_grubu=EXCLUDED.urun_ilk_grubu,
  urun_ana_grubu=EXCLUDED.urun_ana_grubu,
  urun_alt_grubu=EXCLUDED.urun_alt_grubu,
  channel_code=EXCLUDED.channel_code,
  customer_name=EXCLUDED.customer_name,
  sales_qty=EXCLUDED.sales_qty,
  sales_tl=EXCLUDED.sales_tl,
  sales_usd=EXCLUDED.sales_usd,
  avg_price_usd=EXCLUDED.avg_price_usd,
  invoice_line_count=EXCLUDED.invoice_line_count,
  invoice_count=EXCLUDED.invoice_count,
  customer_count=EXCLUDED.customer_count,
  last_ref_number=EXCLUDED.last_ref_number,
  updated_at=now()
`, r.SalesDate, r.ProductCode, r.ColorCode, r.YakaKodu, r.ItemDescription,
			r.Kategori, r.Seri, r.YasGrubu, r.AskiliYan, r.UrunIlkGrubu, r.UrunAnaGrubu, r.UrunAltGrubu, r.MarketKey, r.ChannelCode,
			r.CustomerCountry, r.CustomerSegment, r.CustomerCode, r.CustomerName, r.SalesQty, r.SalesTL, r.SalesUSD,
			r.AvgPriceUSD, r.InvoiceLineCount, r.InvoiceCount, r.CustomerCount, r.LastRefNumber); err != nil {
			return count, err
		}
		count++
		if count%5000 == 0 {
			log.Printf("[ProductPerformanceRefresh] sales inserted rows=%d", count)
		}
	}
	return count, rows.Err()
}

func refreshProductPerformanceStock(ctx context.Context, tx *sql.Tx, startDate, endDate time.Time, productPrefix string) (int, error) {
	log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
	rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockQuery(), startDate, endDate, productPrefix)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	log.Printf("[ProductPerformanceRefresh] stock mssql query returned, postgres insert start")
	count := 0
	for rows.Next() {
		var r productPerformanceStockDaily
		if err := rows.Scan(
			&r.StockDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.StockQty,
			&r.InQty, &r.OutQty, &r.KpiInQty, &r.KpiOutQty, &r.SalesMovementQty,
			&r.ProductionInQty, &r.PurchaseInQty, &r.ConsumptionOutQty, &r.CountDiffQty,
		); err != nil {
			return count, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		if err := insertProductPerformanceStock(ctx, tx, r); err != nil {
			return count, err
		}
		count++
		if count%5000 == 0 {
			log.Printf("[ProductPerformanceRefresh] stock inserted rows=%d", count)
		}
	}
	return count, rows.Err()
}

func refreshProductPerformanceStockChunked(ctx context.Context, pg *sql.DB, startDate, endDate, started time.Time, skipDelete bool, resumeAfter int, productPrefix string) (int, error) {
	if skipDelete {
		log.Printf("[ProductPerformanceRefresh] stock delete skipped resume_after=%d", resumeAfter)
	} else {
		log.Printf("[ProductPerformanceRefresh] stock delete tx begin")
		deleteTx, err := pg.BeginTx(ctx, nil)
		if err != nil {
			return 0, err
		}
		defer deleteTx.Rollback()
		log.Printf("[ProductPerformanceRefresh] delete stock cache start")
		deleteSQL := `DELETE FROM mk_product_performance_stock_daily WHERE stock_date BETWEEN $1 AND $2`
		deleteArgs := []any{startDate, endDate}
		if productPrefix != "" {
			deleteSQL += ` AND product_code ILIKE $3 || '%'`
			deleteArgs = append(deleteArgs, productPrefix)
		}
		if _, err := deleteTx.ExecContext(ctx, deleteSQL, deleteArgs...); err != nil {
			return 0, err
		}
		log.Printf("[ProductPerformanceRefresh] delete stock cache done")
		log.Printf("[ProductPerformanceRefresh] stock delete tx commit start")
		if err := deleteTx.Commit(); err != nil {
			return 0, err
		}
		log.Printf("[ProductPerformanceRefresh] stock delete tx commit done elapsed=%s", time.Since(started).Round(time.Second))
	}

	log.Printf("[ProductPerformanceRefresh] stock refresh start")
	log.Printf("[ProductPerformanceRefresh] stock mssql query start start=%s end=%s prefix=%s", startDate.Format("2006-01-02"), endDate.Format("2006-01-02"), productPrefix)
	rows, err := db.MssqlDB.QueryContext(ctx, productPerformanceStockQuery(), startDate, endDate, productPrefix)
	if err != nil {
		return 0, err
	}
	defer rows.Close()
	log.Printf("[ProductPerformanceRefresh] stock mssql query returned, postgres chunk upsert start")

	const chunkSize = 5000
	count := 0
	seen := 0
	chunkRows := 0
	tx, err := pg.BeginTx(ctx, nil)
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	for rows.Next() {
		var r productPerformanceStockDaily
		if err := rows.Scan(
			&r.StockDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.StockQty,
			&r.InQty, &r.OutQty, &r.KpiInQty, &r.KpiOutQty, &r.SalesMovementQty,
			&r.ProductionInQty, &r.PurchaseInQty, &r.ConsumptionOutQty, &r.CountDiffQty,
		); err != nil {
			return count, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		seen++
		if resumeAfter > 0 && seen <= resumeAfter {
			if seen%50000 == 0 || seen == resumeAfter {
				log.Printf("[ProductPerformanceRefresh] stock resume skipped rows=%d", seen)
			}
			continue
		}
		if err := insertProductPerformanceStock(ctx, tx, r); err != nil {
			return count, err
		}
		count++
		chunkRows++
		if chunkRows >= chunkSize {
			log.Printf("[ProductPerformanceRefresh] stock chunk commit start upserted_rows=%d scanned_rows=%d", count, seen)
			if err := tx.Commit(); err != nil {
				return count, err
			}
			log.Printf("[ProductPerformanceRefresh] stock chunk commit done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
			tx, err = pg.BeginTx(ctx, nil)
			if err != nil {
				return count, err
			}
			chunkRows = 0
		}
	}
	if err := rows.Err(); err != nil {
		return count, err
	}
	if chunkRows > 0 {
		log.Printf("[ProductPerformanceRefresh] stock final chunk commit start upserted_rows=%d scanned_rows=%d", count, seen)
		if err := tx.Commit(); err != nil {
			return count, err
		}
		log.Printf("[ProductPerformanceRefresh] stock final chunk commit done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
	} else {
		_ = tx.Rollback()
	}
	log.Printf("[ProductPerformanceRefresh] stock refresh done upserted_rows=%d scanned_rows=%d elapsed=%s", count, seen, time.Since(started).Round(time.Second))
	return count, nil
}

type productPerformanceStockExec interface {
	ExecContext(context.Context, string, ...any) (sql.Result, error)
}

func prepareProductPerformanceStockCopy(ctx context.Context, tx *sql.Tx) (*sql.Stmt, error) {
	return tx.PrepareContext(ctx, pq.CopyIn(
		"mk_product_performance_stock_daily",
		"stock_date",
		"product_code",
		"color_code",
		"yaka_kodu",
		"stock_qty",
		"in_qty",
		"out_qty",
		"kpi_in_qty",
		"kpi_out_qty",
		"sales_movement_qty",
		"production_in_qty",
		"purchase_in_qty",
		"consumption_out_qty",
		"count_diff_qty",
	))
}

func insertProductPerformanceStock(ctx context.Context, exec productPerformanceStockExec, r productPerformanceStockDaily) error {
	normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
	_, err := exec.ExecContext(ctx, `
INSERT INTO mk_product_performance_stock_daily (
  stock_date, product_code, color_code, yaka_kodu, stock_qty, in_qty, out_qty,
  kpi_in_qty, kpi_out_qty, sales_movement_qty, production_in_qty, purchase_in_qty,
  consumption_out_qty, count_diff_qty, updated_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,now())
ON CONFLICT (stock_date, product_code, color_code, yaka_kodu)
DO UPDATE SET
  stock_qty=EXCLUDED.stock_qty,
  in_qty=EXCLUDED.in_qty,
  out_qty=EXCLUDED.out_qty,
  kpi_in_qty=EXCLUDED.kpi_in_qty,
  kpi_out_qty=EXCLUDED.kpi_out_qty,
  sales_movement_qty=EXCLUDED.sales_movement_qty,
  production_in_qty=EXCLUDED.production_in_qty,
  purchase_in_qty=EXCLUDED.purchase_in_qty,
  consumption_out_qty=EXCLUDED.consumption_out_qty,
  count_diff_qty=EXCLUDED.count_diff_qty,
  updated_at=now()
`, r.StockDate, r.ProductCode, r.ColorCode, r.YakaKodu, r.StockQty,
		r.InQty, r.OutQty, r.KpiInQty, r.KpiOutQty, r.SalesMovementQty,
		r.ProductionInQty, r.PurchaseInQty, r.ConsumptionOutQty, r.CountDiffQty)
	return err
}

func refreshProductPerformancePrices(ctx context.Context, tx *sql.Tx, productPrefix string) error {
	log.Printf("[ProductPerformanceRefresh] price mssql query start prefix=%s", productPrefix)
	rows, err := db.MssqlDB.QueryContext(ctx, productPerformancePriceSQL(), productPrefix)
	if err != nil {
		return err
	}
	defer rows.Close()
	log.Printf("[ProductPerformanceRefresh] price mssql query returned, postgres upsert start")
	count := 0
	for rows.Next() {
		var code string
		var itemDescription, kategori, askiliYan, urunIlkGrubu, urunAnaGrubu, urunAltGrubu string
		var cost, usd, tryPrice float64
		var lastPricing sql.NullTime
		if err := rows.Scan(&code, &itemDescription, &kategori, &askiliYan, &urunIlkGrubu, &urunAnaGrubu, &urunAltGrubu, &cost, &usd, &tryPrice, &lastPricing); err != nil {
			return err
		}
		code = normalizeProductPerformanceProductCode(code)
		askiliYan = cleanProductPerformanceOptionalAttr(askiliYan)
		urunIlkGrubu = cleanProductPerformanceFirstGroup(urunIlkGrubu)
		cost, usd = normalizeProductPerformanceCostPair(cost, usd)
		var lp any
		if lastPricing.Valid {
			lp = lastPricing.Time
		}
		if _, err := tx.ExecContext(ctx, `
INSERT INTO mk_product_performance_price_dim (
  product_code, item_description, kategori, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu,
  cost_price_usd, base_price_usd, base_price_try, last_pricing_date, updated_at
)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,now())
ON CONFLICT (product_code) DO UPDATE SET
  item_description=EXCLUDED.item_description,
  kategori=EXCLUDED.kategori,
  askili_yan=EXCLUDED.askili_yan,
  urun_ilk_grubu=EXCLUDED.urun_ilk_grubu,
  urun_ana_grubu=EXCLUDED.urun_ana_grubu,
  urun_alt_grubu=EXCLUDED.urun_alt_grubu,
  cost_price_usd=EXCLUDED.cost_price_usd,
  base_price_usd=EXCLUDED.base_price_usd,
  base_price_try=EXCLUDED.base_price_try,
  last_pricing_date=EXCLUDED.last_pricing_date,
  updated_at=now()
`, code, strings.TrimSpace(itemDescription), strings.TrimSpace(kategori), askiliYan, urunIlkGrubu, strings.TrimSpace(urunAnaGrubu), strings.TrimSpace(urunAltGrubu), cost, usd, tryPrice, lp); err != nil {
			return err
		}
		count++
		if count%5000 == 0 {
			log.Printf("[ProductPerformanceRefresh] price upserted rows=%d", count)
		}
	}
	log.Printf("[ProductPerformanceRefresh] price upsert done rows=%d", count)
	return rows.Err()
}

func RebuildProductPerformanceKPI(ctx context.Context, exec interface {
	ExecContext(context.Context, string, ...any) (sql.Result, error)
}, kpiDate time.Time) (int, error) {
	kpiDate = dateOnly(kpiDate)
	if _, err := exec.ExecContext(ctx, `DELETE FROM mk_product_performance_kpi_daily WHERE kpi_date=$1`, kpiDate); err != nil {
		return 0, err
	}
	res, err := exec.ExecContext(ctx, productPerformanceKPISQL(), kpiDate)
	if err != nil {
		return 0, err
	}
	if _, err := exec.ExecContext(ctx, `
UPDATE mk_product_performance_kpi_daily k
SET
  item_description = CASE WHEN btrim(COALESCE(k.item_description,'')) = '' THEN COALESCE(NULLIF(p.item_description,''), k.item_description) ELSE k.item_description END,
  kategori = CASE WHEN btrim(COALESCE(k.kategori,'')) = '' OR btrim(COALESCE(k.kategori,'')) = '-' THEN COALESCE(NULLIF(p.kategori,''), '') ELSE k.kategori END,
  askili_yan = CASE WHEN btrim(COALESCE(k.askili_yan,'')) = '' OR btrim(COALESCE(k.askili_yan,'')) = '-' THEN COALESCE(NULLIF(NULLIF(p.askili_yan,''), '-'), '') ELSE k.askili_yan END,
  urun_ilk_grubu = CASE WHEN btrim(COALESCE(k.urun_ilk_grubu,'')) = '' OR btrim(COALESCE(k.urun_ilk_grubu,'')) = '-' THEN COALESCE(NULLIF(p.urun_ilk_grubu,''), '') ELSE k.urun_ilk_grubu END,
  urun_ana_grubu = CASE WHEN btrim(COALESCE(k.urun_ana_grubu,'')) = '' THEN COALESCE(NULLIF(p.urun_ana_grubu,''), k.urun_ana_grubu) ELSE k.urun_ana_grubu END,
  urun_alt_grubu = CASE WHEN btrim(COALESCE(k.urun_alt_grubu,'')) = '' THEN COALESCE(NULLIF(p.urun_alt_grubu,''), k.urun_alt_grubu) ELSE k.urun_alt_grubu END,
  updated_at = now()
FROM mk_product_performance_price_dim p
WHERE k.kpi_date = $1
  AND p.product_code = k.product_code
  AND (
    btrim(COALESCE(k.item_description,'')) = ''
    OR btrim(COALESCE(k.kategori,'')) IN ('', '-')
    OR btrim(COALESCE(k.askili_yan,'')) IN ('', '-')
    OR btrim(COALESCE(k.urun_ilk_grubu,'')) IN ('', '-')
    OR btrim(COALESCE(k.urun_ana_grubu,'')) = ''
    OR btrim(COALESCE(k.urun_alt_grubu,'')) = ''
  )
`, kpiDate); err != nil {
		return 0, err
	}
	n, _ := res.RowsAffected()
	return int(n), nil
}

func ListProductPerformance(ctx context.Context, pg *sql.DB, f ProductPerformanceFilters) ([]models.ProductPerformanceRow, int, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, 0, err
	}
	limit := f.Limit
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	page := f.Page
	if page <= 0 {
		page = 1
	}
	if !productPerformanceHasServerFilters(f) {
		if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceRow](ctx, pg, productPerformanceSnapshotKey("products"), limit); err != nil {
			return nil, 0, err
		} else if ok {
			applyProductPerformanceProductRowScores(rows)
			return rows, len(rows), nil
		}
	}
	where, args := productPerformanceWhere(f)
	countQuery := `SELECT COUNT(*) FROM mk_product_performance_kpi_daily WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)` + where
	var total int
	if err := pg.QueryRowContext(ctx, countQuery, args...).Scan(&total); err != nil {
		return nil, 0, err
	}
	args = append(args, limit, (page-1)*limit)
	orderBy := productPerformanceOrderBy(f.SortBy, f.Descending)
	rows, err := pg.QueryContext(ctx, `
SELECT
  to_char(kpi_date,'YYYY-MM-DD'), product_code, color_code, yaka_kodu, item_description,
  kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key, stock_qty,
  sales_qty_30d, sales_qty_90d, sales_qty_180d, sales_qty_365d, sales_qty_730d, sales_qty_total,
  sales_usd_30d, sales_usd_90d, sales_usd_180d, sales_usd_365d, sales_usd_total,
  avg_daily_sales_90d, avg_daily_sales_180d, avg_daily_sales_365d, avg_daily_sales_total,
  avg_stock_90d, avg_stock_180d, avg_stock_365d, avg_stock_total,
  stock_days_90d, stock_days_180d, stock_days_365d, stock_days_total,
  stock_turnover_90d, stock_turnover_180d, stock_turnover_365d, stock_turnover_total,
  avg_price_usd_90d, avg_price_usd_180d, cost_price_usd, base_price_usd, base_price_try,
  gross_profit_usd_90d, gross_profit_usd_180d, gross_margin_90d, gross_margin_180d,
  unit_profit_cost_90d, unit_profit_cost_180d, unit_profit_base_90d, unit_profit_base_180d,
  market_count_90d, market_count_180d, market_count_365d, market_count_total,
  customer_count_90d, customer_count_180d, customer_count_365d, customer_count_total,
  sales_index_90d, sales_index_180d, sales_index_365d, sales_index_total,
  price_index_90d, margin_index_90d, performance_score, performance_bucket,
  recommendation, COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''), last_ref_number,
  to_char(updated_at,'YYYY-MM-DD HH24:MI:SS')
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)`+where+`
ORDER BY `+orderBy+`
LIMIT $`+fmt.Sprint(len(args)-1)+` OFFSET $`+fmt.Sprint(len(args)), args...)
	if err != nil {
		return nil, 0, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceRow, 0, limit)
	colorCodes := make([]string, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceRow
		if err := rows.Scan(
			&r.KpiDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
			&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.StockQty,
			&r.SalesQty30, &r.SalesQty90, &r.SalesQty180, &r.SalesQty365, &r.SalesQty730, &r.SalesQtyTotal,
			&r.SalesUSD30, &r.SalesUSD90, &r.SalesUSD180, &r.SalesUSD365, &r.SalesUSDTotal,
			&r.AvgDailySales90, &r.AvgDailySales180, &r.AvgDailySales365, &r.AvgDailySalesTotal,
			&r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal,
			&r.StockDays90, &r.StockDays180, &r.StockDays365, &r.StockDaysTotal,
			&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal,
			&r.AvgPriceUSD90, &r.AvgPriceUSD180, &r.CostPriceUSD, &r.BasePriceUSD, &r.BasePriceTRY,
			&r.GrossProfitUSD90, &r.GrossProfitUSD180, &r.GrossMargin90, &r.GrossMargin180,
			&r.UnitProfitCost90, &r.UnitProfitCost180, &r.UnitProfitBase90, &r.UnitProfitBase180,
			&r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal,
			&r.CustomerCount90, &r.CustomerCount180, &r.CustomerCount365, &r.CustomerCountTotal,
			&r.SalesIndex90, &r.SalesIndex180, &r.SalesIndex365, &r.SalesIndexTotal,
			&r.PriceIndex90, &r.MarginIndex90, &r.PerformanceScore, &r.PerformanceBucket,
			&r.Recommendation, &r.LastSaleDate, &r.LastRefNumber, &r.UpdatedAt,
		); err != nil {
			return nil, 0, err
		}
		r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		return nil, 0, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	for i := range out {
		out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
	}
	applyProductPerformanceProductRowScores(out)
	return out, total, nil
}

func applyProductPerformanceProductRowScores(rows []models.ProductPerformanceRow) {
	avg := productPerformanceProductRowAverages(rows)
	for i := range rows {
		rows[i].SalesIndex90 = productPerformanceRelativeIndex(rows[i].SalesUSD90, avg.salesUSD90)
		rows[i].SalesIndex180 = productPerformanceRelativeIndex(rows[i].SalesUSD180, avg.salesUSD180)
		rows[i].SalesIndex365 = productPerformanceRelativeIndex(rows[i].SalesUSD365, avg.salesUSD365)
		rows[i].SalesIndexTotal = productPerformanceRelativeIndex(rows[i].SalesUSDTotal, avg.salesUSDTotal)

		margin365 := productPerformanceMarginFromSales(rows[i].SalesUSD365, rows[i].SalesQty365, rows[i].CostPriceUSD)
		marginTotal := productPerformanceMarginFromSales(rows[i].SalesUSDTotal, rows[i].SalesQtyTotal, rows[i].CostPriceUSD)

		rows[i].PerformanceScore90 = productPerformanceProductScore(
			"90d",
			rows[i].SalesUSD90,
			rows[i].SalesIndex90,
			rows[i].GrossMargin90,
			rows[i].StockTurnover90,
			float64(rows[i].MarketCount90),
			float64(rows[i].CustomerCount90),
		)
		rows[i].PerformanceScore180 = productPerformanceProductScore(
			"180d",
			rows[i].SalesUSD180,
			rows[i].SalesIndex180,
			rows[i].GrossMargin180,
			rows[i].StockTurnover180,
			float64(rows[i].MarketCount180),
			float64(rows[i].CustomerCount180),
		)
		rows[i].PerformanceScore365 = productPerformanceProductScore(
			"365d",
			rows[i].SalesUSD365,
			rows[i].SalesIndex365,
			margin365,
			rows[i].StockTurnover365,
			float64(rows[i].MarketCount365),
			float64(rows[i].CustomerCount365),
		)
		rows[i].PerformanceScoreTotal = productPerformanceProductScore(
			"total",
			rows[i].SalesUSDTotal,
			rows[i].SalesIndexTotal,
			marginTotal,
			rows[i].StockTurnoverTotal,
			float64(rows[i].MarketCountTotal),
			float64(rows[i].CustomerCountTotal),
			productPerformanceProductRowTotalPeriodDays(rows[i]),
		)
		rows[i].PerformanceScore = rows[i].PerformanceScore90
	}
}

type productPerformanceProductRowAverage struct {
	salesUSD90    float64
	salesUSD180   float64
	salesUSD365   float64
	salesUSDTotal float64
}

func productPerformanceProductRowAverages(rows []models.ProductPerformanceRow) productPerformanceProductRowAverage {
	var sum90, count90, sum180, count180, sum365, count365, sumTotal, countTotal float64
	for _, row := range rows {
		if row.SalesUSD90 > 0 {
			sum90 += row.SalesUSD90
			count90++
		}
		if row.SalesUSD180 > 0 {
			sum180 += row.SalesUSD180
			count180++
		}
		if row.SalesUSD365 > 0 {
			sum365 += row.SalesUSD365
			count365++
		}
		if row.SalesUSDTotal > 0 {
			sumTotal += row.SalesUSDTotal
			countTotal++
		}
	}
	out := productPerformanceProductRowAverage{}
	if count90 > 0 {
		out.salesUSD90 = sum90 / count90
	}
	if count180 > 0 {
		out.salesUSD180 = sum180 / count180
	}
	if count365 > 0 {
		out.salesUSD365 = sum365 / count365
	}
	if countTotal > 0 {
		out.salesUSDTotal = sumTotal / countTotal
	}
	return out
}

func productPerformanceProductRowTotalPeriodDays(row models.ProductPerformanceRow) float64 {
	return productPerformancePeriodDays(map[string]any{"kpi_date": row.KpiDate}, "total")
}

func productPerformanceMarginFromSales(salesUSD, salesQty, unitCost float64) float64 {
	if salesUSD <= 0 {
		return 0
	}
	return (salesUSD - (salesQty * unitCost)) / salesUSD
}

func GetProductPerformanceSummary(ctx context.Context, pg *sql.DB) (models.ProductPerformanceSummary, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return models.ProductPerformanceSummary{}, err
	}
	if summary, ok, err := loadProductPerformanceSnapshotItem[models.ProductPerformanceSummary](ctx, pg, productPerformanceSnapshotKey("summary")); err != nil {
		return models.ProductPerformanceSummary{}, err
	} else if ok {
		return summary, nil
	}
	var s models.ProductPerformanceSummary
	err := pg.QueryRowContext(ctx, `
WITH Latest AS (
  SELECT MAX(kpi_date) AS kpi_date
  FROM mk_product_performance_kpi_daily
),
KPI AS (
  SELECT *
  FROM mk_product_performance_kpi_daily
  WHERE kpi_date = (SELECT kpi_date FROM Latest)
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
),
VariantStock AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    MAX(stock_qty) AS stock_qty,
    MAX(cost_price_usd) AS cost_price_usd,
    BOOL_OR(performance_bucket IN ('STOK_RISKI','TAKIP')) AS has_risk_bucket,
    SUM(COALESCE(sales_qty_90d,0)) AS sales_qty_90d
  FROM KPI
  GROUP BY product_code, color_code, yaka_kodu
),
StockTotals AS (
  SELECT
    COALESCE(SUM(stock_qty),0) AS total_stock,
    COALESCE(SUM(stock_qty * cost_price_usd),0) AS stock_cost_usd,
    COALESCE(SUM(CASE WHEN has_risk_bucket AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0) AS risk_cost_usd
  FROM VariantStock
),
Sales90 AS (
  SELECT
    COUNT(DISTINCT market_key) AS market_count_90d,
    COUNT(DISTINCT NULLIF(customer_code, '-')) AS customer_count_90d
  FROM mk_product_performance_sales_daily
  WHERE sales_date BETWEEN (SELECT kpi_date FROM Latest) - INTERVAL '89 days' AND (SELECT kpi_date FROM Latest)
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
)
SELECT
  COALESCE(to_char(MAX(kpi_date),'YYYY-MM-DD'),''),
  COUNT(*),
  COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN'),
  COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI'),
  COUNT(*) FILTER (WHERE performance_bucket='STOKSUZ_TALEP'),
  COALESCE(MAX(StockTotals.total_stock),0),
  COALESCE(MAX(StockTotals.stock_cost_usd),0),
  COALESCE(MAX(StockTotals.risk_cost_usd),0),
  COALESCE(SUM(sales_qty_90d),0),
  COALESCE(SUM(sales_usd_90d),0),
  COALESCE(SUM(gross_profit_usd_90d),0),
  COALESCE(MAX(Sales90.market_count_90d),0)::integer,
  COALESCE(MAX(Sales90.customer_count_90d),0)::integer,
  COALESCE(to_char(MAX(updated_at),'YYYY-MM-DD HH24:MI:SS'),'')
FROM KPI
CROSS JOIN Sales90
CROSS JOIN StockTotals
`).Scan(&s.KpiDate, &s.TotalRows, &s.StarCount, &s.StockRisk, &s.NoStockDemand, &s.TotalStock, &s.StockCostUSD, &s.RiskCostUSD, &s.SalesQty90, &s.SalesUSD90, &s.GrossProfit90, &s.MarketCount90, &s.CustomerCount90, &s.UpdatedAt)
	return s, err
}

func ListProductPerformanceGeneral(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceGeneralRow, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceGeneralRow](ctx, pg, productPerformanceSnapshotKey("general"), limit); err != nil {
		return nil, err
	} else if ok {
		applyProductPerformanceGeneralRowScores(rows)
		return rows, nil
	}
	rows, err := pg.QueryContext(ctx, `
WITH Bounds AS (
  SELECT
    DATE '2022-01-01' AS period_start,
    COALESCE(
      (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
      (SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
      current_date
    ) AS period_end
),
LatestStockDate AS (
  SELECT MAX(stock_date) AS stock_date
  FROM mk_product_performance_stock_daily
),
SalesAgg AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    market_key,
    MAX(item_description) AS item_description,
    MAX(kategori) AS kategori,
    MAX(seri) AS seri,
    MAX(yas_grubu) AS yas_grubu,
    COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan,
    MAX(urun_ilk_grubu) AS urun_ilk_grubu,
    MAX(urun_ana_grubu) AS urun_ana_grubu,
    MAX(urun_alt_grubu) AS urun_alt_grubu,
    MIN(sales_date) AS first_sale_date,
    MAX(sales_date) AS last_sale_date,
    MAX(last_ref_number) AS last_ref_number,
    COALESCE(SUM(sales_qty),0) AS sales_qty_total,
    COALESCE(SUM(sales_usd),0) AS sales_usd_total,
    COALESCE(COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0),0)::integer AS customer_count_total,
    COALESCE(SUM(invoice_count),0)::integer AS invoice_count_total
  FROM mk_product_performance_sales_daily, Bounds
  WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
  GROUP BY product_code, color_code, yaka_kodu, market_key
),
StockAgg AS (
  SELECT
    s.product_code,
    s.color_code,
    s.yaka_kodu,
    COALESCE(SUM(s.stock_qty),0) AS stock_qty
  FROM mk_product_performance_stock_daily s
  WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
  GROUP BY s.product_code, s.color_code, s.yaka_kodu
),
StockTotalStart AS (
  SELECT DISTINCT ON (s.product_code, s.color_code, s.yaka_kodu)
    s.product_code,
    s.color_code,
    s.yaka_kodu,
    s.stock_qty
  FROM mk_product_performance_stock_daily s
  WHERE s.stock_date <= DATE '2022-01-01'
  ORDER BY s.product_code, s.color_code, s.yaka_kodu, s.stock_date DESC
),
Dim AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    item_description,
    COALESCE(kategori,'') AS kategori,
    seri,
    yas_grubu,
    CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
    urun_ilk_grubu,
    urun_ana_grubu,
    urun_alt_grubu
  FROM (
    SELECT
      product_code,
      color_code,
      yaka_kodu,
      item_description,
      kategori,
      seri,
      yas_grubu,
      askili_yan,
      urun_ilk_grubu,
      urun_ana_grubu,
      urun_alt_grubu,
      1 AS src_rank
    FROM mk_product_performance_kpi_daily
    WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
      AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
    UNION ALL
    SELECT
      product_code,
      color_code,
      yaka_kodu,
      MAX(item_description) AS item_description,
      MAX(kategori) AS kategori,
      MAX(seri) AS seri,
      MAX(yas_grubu) AS yas_grubu,
      COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan,
      MAX(urun_ilk_grubu) AS urun_ilk_grubu,
      MAX(urun_ana_grubu) AS urun_ana_grubu,
      MAX(urun_alt_grubu) AS urun_alt_grubu,
      2 AS src_rank
    FROM mk_product_performance_sales_daily
    WHERE upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
    GROUP BY product_code, color_code, yaka_kodu
  ) x
  ORDER BY product_code, color_code, yaka_kodu, src_rank
),
StockOnly AS (
  SELECT
    s.product_code,
    s.color_code,
    s.yaka_kodu,
    'STOK'::text AS market_key,
    COALESCE(MAX(d.item_description),'') AS item_description,
    COALESCE(MAX(d.kategori),'') AS kategori,
    COALESCE(MAX(d.seri),'') AS seri,
    COALESCE(MAX(d.yas_grubu),'') AS yas_grubu,
    COALESCE(MAX(NULLIF(d.askili_yan, '-')),'') AS askili_yan,
    COALESCE(MAX(d.urun_ilk_grubu),'') AS urun_ilk_grubu,
    COALESCE(MAX(d.urun_ana_grubu),'') AS urun_ana_grubu,
    COALESCE(MAX(d.urun_alt_grubu),'') AS urun_alt_grubu,
    NULL::date AS first_sale_date,
    NULL::date AS last_sale_date,
    ''::text AS last_ref_number,
    0::numeric AS sales_qty_total,
    0::numeric AS sales_usd_total,
    0::integer AS customer_count_total,
    0::integer AS invoice_count_total
  FROM mk_product_performance_stock_daily s
  LEFT JOIN Dim d
    ON d.product_code = s.product_code
   AND d.color_code = s.color_code
   AND d.yaka_kodu = s.yaka_kodu
  WHERE s.stock_date = (SELECT stock_date FROM LatestStockDate)
    AND upper(translate(btrim(COALESCE(d.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
    AND NOT EXISTS (
      SELECT 1
      FROM SalesAgg a
      WHERE a.product_code = s.product_code
        AND a.color_code = s.color_code
        AND a.yaka_kodu = s.yaka_kodu
    )
  GROUP BY s.product_code, s.color_code, s.yaka_kodu
),
BaseRows AS (
  SELECT * FROM SalesAgg
  UNION ALL
  SELECT * FROM StockOnly
),
Spread AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    COUNT(DISTINCT NULLIF(market_key, 'STOK')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS market_count_total,
    COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE COALESCE(sales_usd,0) > 0)::integer AS customer_count_total_all
  FROM mk_product_performance_sales_daily, Bounds
  WHERE sales_date BETWEEN Bounds.period_start AND Bounds.period_end
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
  GROUP BY product_code, color_code, yaka_kodu
),
Scored AS (
  SELECT
    Bounds.period_start,
    Bounds.period_end,
    b.product_code,
    b.color_code,
    b.yaka_kodu,
    b.item_description,
    b.kategori,
    b.seri,
    b.yas_grubu,
    b.askili_yan,
    b.urun_ilk_grubu,
    b.urun_ana_grubu,
    b.urun_alt_grubu,
    b.market_key,
    COALESCE(st.stock_qty, 0) AS stock_qty,
    b.sales_qty_total,
    b.sales_usd_total,
    b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)) AS avg_daily_sales_total,
    CASE
      WHEN b.sales_qty_total <= 0 THEN 9999
      ELSE ((COALESCE(st_start.stock_qty, 0) + COALESCE(st.stock_qty, 0)) / 2.0) / NULLIF(b.sales_qty_total / GREATEST(1, (Bounds.period_end - Bounds.period_start + 1)), 0)
    END AS stock_days_total,
    CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE b.sales_usd_total / NULLIF(b.sales_qty_total,0) END AS avg_price_usd_total,
    COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
    COALESCE(pd.base_price_usd,0) AS base_price_usd,
    b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0)) AS gross_profit_usd_total,
    CASE WHEN b.sales_usd_total <= 0 THEN 0 ELSE (b.sales_usd_total - (b.sales_qty_total * COALESCE(pd.cost_price_usd,0))) / NULLIF(b.sales_usd_total,0) END AS gross_margin_total,
    CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.cost_price_usd,0) END AS unit_profit_cost_total,
    CASE WHEN b.sales_qty_total <= 0 THEN 0 ELSE (b.sales_usd_total / NULLIF(b.sales_qty_total,0)) - COALESCE(pd.base_price_usd,0) END AS unit_profit_base_total,
    CASE WHEN b.market_key <> 'STOK' AND b.sales_usd_total > 0 THEN 1 ELSE 0 END AS market_count_total,
    b.customer_count_total AS customer_count_total,
    b.invoice_count_total,
    CASE
      WHEN AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu) <= 0 THEN 0
      ELSE b.sales_qty_total / NULLIF(AVG(b.sales_qty_total) OVER (PARTITION BY b.market_key, b.kategori, b.urun_ana_grubu),0)
    END AS sales_index_total,
    b.first_sale_date,
    b.last_sale_date,
    b.last_ref_number
  FROM BaseRows b
  CROSS JOIN Bounds
  LEFT JOIN StockAgg st
    ON st.product_code = b.product_code
   AND st.color_code = b.color_code
   AND st.yaka_kodu = b.yaka_kodu
  LEFT JOIN StockTotalStart st_start
    ON st_start.product_code = b.product_code
   AND st_start.color_code = b.color_code
   AND st_start.yaka_kodu = b.yaka_kodu
  LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = b.product_code
  LEFT JOIN Spread sp
    ON sp.product_code = b.product_code
   AND sp.color_code = b.color_code
   AND sp.yaka_kodu = b.yaka_kodu
)
SELECT
  to_char(period_start,'YYYY-MM-DD'),
  to_char(period_end,'YYYY-MM-DD'),
  product_code, color_code, yaka_kodu, item_description,
  kategori, seri, yas_grubu, askili_yan, urun_ilk_grubu, urun_ana_grubu, urun_alt_grubu, market_key,
  stock_qty, sales_qty_total, sales_usd_total, avg_daily_sales_total, stock_days_total, avg_price_usd_total,
  cost_price_usd, base_price_usd, gross_profit_usd_total, gross_margin_total, unit_profit_cost_total, unit_profit_base_total,
  market_count_total, customer_count_total, invoice_count_total, sales_index_total,
  ROUND((
    LEAST(35, GREATEST(0, sales_index_total) * 18)
    + LEAST(25, GREATEST(0, gross_margin_total) * 50)
    + LEAST(20, customer_count_total * 1.5)
    + LEAST(10, market_count_total * 2.5)
    + CASE
        WHEN sales_qty_total > 0 AND stock_days_total BETWEEN 20 AND 180 THEN 10
        WHEN sales_qty_total > 0 AND stock_days_total > 365 THEN -10
        WHEN sales_qty_total = 0 AND stock_qty > 0 THEN -15
        ELSE 0
      END
  )::numeric, 4) AS performance_score,
  CASE
    WHEN sales_qty_total > 0 AND stock_qty <= 0 AND sales_index_total >= 1 THEN 'STOKSUZ_TALEP'
    WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'STOK_RISKI'
    WHEN gross_margin_total < 0 THEN 'FIYAT_BASKISI'
    WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'YILDIZ_URUN'
    WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'FIYAT_FIRSATI'
    ELSE 'TAKIP'
  END AS performance_bucket,
  CASE
    WHEN sales_qty_total > 0 AND stock_qty <= 0 THEN 'Talep var, stok yok. Uretim/satin alma onceligi ver.'
    WHEN sales_qty_total = 0 AND stock_qty > 0 THEN 'Satis yok, stok maliyeti tasiyor. Piyasa/fiyat aksiyonu gerekli.'
    WHEN gross_margin_total < 0 THEN 'Ciplak maliyet altinda satis var. Fiyat veya maliyet kontrol edilmeli.'
    WHEN sales_index_total >= 1.4 AND gross_margin_total >= 0.45 THEN 'Genel donemde guclu urun. Stok ve fiyat korunmali.'
    WHEN gross_margin_total >= 0.35 AND sales_index_total < 0.8 THEN 'Karli ama yavas. Dogru piyasada satis firsati var.'
    ELSE 'Izleme ve piyasa bazli aksiyon.'
  END AS recommendation,
  COALESCE(to_char(first_sale_date,'YYYY-MM-DD'),''),
  COALESCE(to_char(last_sale_date,'YYYY-MM-DD'),''),
  COALESCE(last_ref_number,'')
FROM Scored
ORDER BY performance_score DESC, sales_usd_total DESC, product_code ASC, color_code ASC, yaka_kodu ASC
LIMIT $1
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceGeneralRow, 0, limit)
	colorCodes := make([]string, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceGeneralRow
		if err := rows.Scan(
			&r.PeriodStart, &r.PeriodEnd, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
			&r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey,
			&r.StockQty, &r.SalesQtyTotal, &r.SalesUSDTotal, &r.AvgDailySalesTotal, &r.StockDaysTotal, &r.AvgPriceUSDTotal,
			&r.CostPriceUSD, &r.BasePriceUSD, &r.GrossProfitUSDTotal, &r.GrossMarginTotal, &r.UnitProfitCostTotal, &r.UnitProfitBaseTotal,
			&r.MarketCountTotal, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesIndexTotal, &r.PerformanceScore,
			&r.PerformanceBucket, &r.Recommendation, &r.FirstSaleDate, &r.LastSaleDate, &r.LastRefNumber,
		); err != nil {
			return nil, err
		}
		r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	for i := range out {
		out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
	}
	applyProductPerformanceGeneralRowScores(out)
	return out, nil
}

func applyProductPerformanceGeneralRowScores(rows []models.ProductPerformanceGeneralRow) {
	for i := range rows {
		turnover := 0.0
		if rows[i].StockQty > 0 {
			turnover = rows[i].SalesQtyTotal / rows[i].StockQty
		}
		rows[i].PerformanceScore = productPerformanceProductScore(
			"total",
			rows[i].SalesUSDTotal,
			rows[i].SalesIndexTotal,
			rows[i].GrossMarginTotal,
			turnover,
			float64(rows[i].MarketCountTotal),
			float64(rows[i].CustomerCountTotal),
		)
	}
}

func ListProductPerformanceOrderAnalysis(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderAnalysisRow, error) {
	if db.MssqlDB == nil {
		return nil, fmt.Errorf("mssql db nil")
	}
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderAnalysisRow](ctx, pg, productPerformanceSnapshotKey("orders"), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}

	rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS (
  SELECT
    OrderDate = CAST(h.OrderDate AS date),
    DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
    h.OrderHeaderID,
    h.OrderNumber,
    h.CurrAccCode,
    ProductCode = UPPER(LTRIM(RTRIM(l.ItemCode))),
    ColorCode = UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))),
    YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))),
    ItemDescription = dbo.HG_Temizlik(ISNULL((
      SELECT ItemDescription
      FROM cdItemDesc WITH(NOLOCK)
      WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemDesc.ItemCode = l.ItemCode
        AND cdItemDesc.LangCode = 'TR'
    ), SPACE(0))),
    Kategori = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdItemAttributeDesc WITH(NOLOCK)
      WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemAttributeDesc.AttributeTypeCode = 44
        AND cdItemAttributeDesc.AttributeCode = (
          SELECT TOP 1 AttributeCode
          FROM prItemAttribute WITH(NOLOCK)
          WHERE AttributeTypeCode = 44
            AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
            AND prItemAttribute.ItemCode = l.ItemCode
        )
        AND cdItemAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    Seri = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdItemAttributeDesc WITH(NOLOCK)
      WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemAttributeDesc.AttributeTypeCode = 2
        AND cdItemAttributeDesc.AttributeCode = (
          SELECT TOP 1 AttributeCode
          FROM prItemAttribute WITH(NOLOCK)
          WHERE AttributeTypeCode = 2
            AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
            AND prItemAttribute.ItemCode = l.ItemCode
        )
        AND cdItemAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    YasGrubu = '',
    UrunIlkGrubu = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdItemAttributeDesc WITH(NOLOCK)
      WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemAttributeDesc.AttributeTypeCode = 42
        AND cdItemAttributeDesc.AttributeCode = (
          SELECT TOP 1 AttributeCode
          FROM prItemAttribute WITH(NOLOCK)
          WHERE AttributeTypeCode = 42
            AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
            AND prItemAttribute.ItemCode = l.ItemCode
        )
        AND cdItemAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    AskiliYan = dbo.HG_Temizlik(ISNULL((
      SELECT TOP 1 ProductAtt45
      FROM ProductAttributesFilter WITH(NOLOCK)
      WHERE ProductAttributesFilter.ItemCode = l.ItemCode
    ), SPACE(0))),
    UrunAnaGrubu = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdItemAttributeDesc WITH(NOLOCK)
      WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemAttributeDesc.AttributeTypeCode = 1
        AND cdItemAttributeDesc.AttributeCode = (
          SELECT TOP 1 AttributeCode
          FROM prItemAttribute WITH(NOLOCK)
          WHERE AttributeTypeCode = 1
            AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
            AND prItemAttribute.ItemCode = l.ItemCode
        )
        AND cdItemAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    UrunAltGrubu = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdItemAttributeDesc WITH(NOLOCK)
      WHERE cdItemAttributeDesc.ItemTypeCode = l.ItemTypeCode
        AND cdItemAttributeDesc.AttributeTypeCode = 2
        AND cdItemAttributeDesc.AttributeCode = (
          SELECT TOP 1 AttributeCode
          FROM prItemAttribute WITH(NOLOCK)
          WHERE AttributeTypeCode = 2
            AND prItemAttribute.ItemTypeCode = l.ItemTypeCode
            AND prItemAttribute.ItemCode = l.ItemCode
        )
        AND cdItemAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    MarketKey = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdCurrAccAttributeDesc WITH(NOLOCK)
      WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
        AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
        AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
        AND cdCurrAccAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    Qty = ISNULL(l.Qty1, 0),
    AmountUSD = CASE
      WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
      WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
      WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
      ELSE 0
    END
  FROM dbo.trOrderHeader h WITH(NOLOCK)
  INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
    ON l.OrderHeaderID = h.OrderHeaderID
  LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
    ON c.OrderLineID = l.OrderLineID
   AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
  LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
    ON caf.CurrAccTypeCode = h.CurrAccTypeCode
   AND caf.CurrAccCode = h.CurrAccCode
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.AllExchangeRates WITH(NOLOCK)
    WHERE CurrencyCode = 'USD'
      AND RelationCurrencyCode = 'TRY'
      AND ExchangeTypeCode = 6
      AND Rate > 0
      AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
    ORDER BY Date DESC
  ) usd
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.AllExchangeRates WITH(NOLOCK)
    WHERE CurrencyCode = h.DocCurrencyCode
      AND RelationCurrencyCode = 'TRY'
      AND ExchangeTypeCode = 6
      AND Rate > 0
      AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
    ORDER BY Date DESC
  ) cur
  WHERE ISNULL(h.IsCancelOrder, 0) = 0
    AND ISNULL(h.IsClosed, 0) = 0
    AND h.OrderTypeCode = 1
    AND h.ProcessCode = 'WS'
    AND ISNULL(l.IsClosed, 0) = 0
    AND l.ItemTypeCode = 1
    AND ISNULL(l.Qty1, 0) > 0
    AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
),
Spread AS (
  SELECT
    ProductCode,
    ColorCode,
    YakaKodu,
    MarketCount = COUNT(DISTINCT NULLIF(MarketKey, '')),
    CustomerCount = COUNT(DISTINCT NULLIF(CurrAccCode, ''))
  FROM OpenOrderLines
  GROUP BY ProductCode, ColorCode, YakaKodu
)
SELECT TOP (@p1)
  o.ProductCode,
  o.ColorCode,
  o.YakaKodu,
  ItemDescription = MAX(o.ItemDescription),
  Kategori = MAX(o.Kategori),
  Seri = MAX(o.Seri),
  YasGrubu = MAX(o.YasGrubu),
  AskiliYan = ISNULL(NULLIF(MAX(o.AskiliYan), '-'), ''),
  UrunIlkGrubu = MAX(o.UrunIlkGrubu),
  UrunAnaGrubu = MAX(o.UrunAnaGrubu),
  UrunAltGrubu = MAX(o.UrunAltGrubu),
  o.MarketKey,
  OrderQty = SUM(o.Qty),
  OrderUSD = SUM(o.AmountUSD),
  AvgOrderPriceUSD = CASE WHEN SUM(o.Qty) = 0 THEN 0 ELSE SUM(o.AmountUSD) / NULLIF(SUM(o.Qty), 0) END,
  MarketCount = MAX(s.MarketCount),
  CustomerCount = COUNT(DISTINCT NULLIF(o.CurrAccCode, '')),
  OrderCount = COUNT(DISTINCT o.OrderHeaderID),
  LineCount = COUNT(*),
  FirstOrderDate = CONVERT(varchar, MIN(o.OrderDate), 23),
  LastOrderDate = CONVERT(varchar, MAX(o.OrderDate), 23),
  EarliestDueDate = CONVERT(varchar, MIN(o.DueDate), 23),
  OverdueQty = SUM(CASE WHEN o.DueDate < CAST(GETDATE() AS date) THEN o.Qty ELSE 0 END)
FROM OpenOrderLines o
LEFT JOIN Spread s
  ON s.ProductCode = o.ProductCode
 AND s.ColorCode = o.ColorCode
 AND s.YakaKodu = o.YakaKodu
GROUP BY o.ProductCode, o.ColorCode, o.YakaKodu, o.MarketKey
ORDER BY SUM(o.AmountUSD) DESC, SUM(o.Qty) DESC, o.ProductCode, o.ColorCode, o.YakaKodu
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]models.ProductPerformanceOrderAnalysisRow, 0, limit)
	productCodes := make([]string, 0, limit)
	stockKeys := make([]string, 0, limit)
	colorCodes := make([]string, 0, limit)
	seenProducts := map[string]bool{}
	seenStockKeys := map[string]bool{}
	for rows.Next() {
		var r models.ProductPerformanceOrderAnalysisRow
		if err := rows.Scan(
			&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan,
			&r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu, &r.MarketKey, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD,
			&r.MarketCount, &r.CustomerCount, &r.OrderCount, &r.LineCount, &r.FirstOrderDate, &r.LastOrderDate, &r.EarliestDueDate, &r.OverdueQty,
		); err != nil {
			return nil, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		r.AskiliYan = cleanProductPerformanceOptionalAttr(r.AskiliYan)
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
		if !seenProducts[r.ProductCode] {
			seenProducts[r.ProductCode] = true
			productCodes = append(productCodes, r.ProductCode)
		}
		key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
		if !seenStockKeys[key] {
			seenStockKeys[key] = true
			stockKeys = append(stockKeys, key)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
	if err != nil {
		return nil, err
	}
	stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	attrByKey, err := productPerformanceAttrLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	filtered := out[:0]
	for i := range out {
		row := &out[i]
		price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
		attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
			continue
		}
		if strings.TrimSpace(row.ItemDescription) == "" {
			row.ItemDescription = attr.itemDescription
		}
		if strings.TrimSpace(row.Kategori) == "" {
			row.Kategori = attr.kategori
		}
		row.AskiliYan = cleanProductPerformanceOptionalAttr(row.AskiliYan)
		if strings.TrimSpace(row.AskiliYan) == "" {
			row.AskiliYan = cleanProductPerformanceOptionalAttr(attr.askiliYan)
		}
		if strings.TrimSpace(row.UrunIlkGrubu) == "" {
			row.UrunIlkGrubu = attr.urunIlkGrubu
		}
		if strings.TrimSpace(row.UrunAnaGrubu) == "" {
			row.UrunAnaGrubu = attr.urunAnaGrubu
		}
		if strings.TrimSpace(row.UrunAltGrubu) == "" {
			row.UrunAltGrubu = attr.urunAltGrubu
		}
		row.CostPriceUSD = price.cost
		row.BasePriceUSD = price.base
		row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		row.NetStockAfterOrder = row.StockQty - row.OrderQty
		row.UnitProfitCostUSD = row.AvgOrderPriceUSD - row.CostPriceUSD
		row.UnitProfitBaseUSD = row.AvgOrderPriceUSD - row.BasePriceUSD
		row.ExpectedProfitCostUSD = row.UnitProfitCostUSD * row.OrderQty
		row.ExpectedProfitBaseUSD = row.UnitProfitBaseUSD * row.OrderQty
		if row.OrderUSD != 0 {
			row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
		}
		row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
		row.PerformanceBucket, row.Recommendation = productPerformanceOrderRecommendation(*row)
		filtered = append(filtered, *row)
	}
	return filtered, nil
}

func ListProductPerformanceOrderGroups(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceOrderGroupRow, error) {
	if db.MssqlDB == nil {
		return nil, fmt.Errorf("mssql db nil")
	}
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	mode := strings.ToLower(strings.TrimSpace(breakdown))
	if mode != "customer" {
		mode = "market"
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderGroupRow](ctx, pg, productPerformanceSnapshotKey("order-groups", mode), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}

	rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS (
  SELECT
    h.OrderHeaderID,
    h.CurrAccCode,
    CustomerName = ISNULL(cad.CurrAccDescription, ''),
    ProductCode = UPPER(LTRIM(RTRIM(l.ItemCode))),
    ColorCode = UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))),
    YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))),
    MarketKey = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription
      FROM cdCurrAccAttributeDesc WITH(NOLOCK)
      WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
        AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
        AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
        AND cdCurrAccAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    Qty = ISNULL(l.Qty1, 0),
    AmountUSD = CASE
      WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
      WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
      WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
      ELSE 0
    END,
    DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
  FROM dbo.trOrderHeader h WITH(NOLOCK)
  INNER JOIN dbo.trOrderLine l WITH(NOLOCK)
    ON l.OrderHeaderID = h.OrderHeaderID
  LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK)
    ON c.OrderLineID = l.OrderLineID
   AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
  LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK)
    ON caf.CurrAccTypeCode = h.CurrAccTypeCode
   AND caf.CurrAccCode = h.CurrAccCode
  LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK)
    ON cad.CurrAccTypeCode = h.CurrAccTypeCode
   AND cad.CurrAccCode = h.CurrAccCode
   AND cad.LangCode = 'TR'
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.AllExchangeRates WITH(NOLOCK)
    WHERE CurrencyCode = 'USD'
      AND RelationCurrencyCode = 'TRY'
      AND ExchangeTypeCode = 6
      AND Rate > 0
      AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
    ORDER BY Date DESC
  ) usd
  OUTER APPLY (
    SELECT TOP 1 Rate
    FROM dbo.AllExchangeRates WITH(NOLOCK)
    WHERE CurrencyCode = h.DocCurrencyCode
      AND RelationCurrencyCode = 'TRY'
      AND ExchangeTypeCode = 6
      AND Rate > 0
      AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date)
    ORDER BY Date DESC
  ) cur
  WHERE ISNULL(h.IsCancelOrder, 0) = 0
    AND ISNULL(h.IsClosed, 0) = 0
    AND h.OrderTypeCode = 1
    AND h.ProcessCode = 'WS'
    AND ISNULL(l.IsClosed, 0) = 0
    AND l.ItemTypeCode = 1
    AND ISNULL(l.Qty1, 0) > 0
    AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
)
SELECT TOP (@p1)
  ProductCode,
  ColorCode,
  YakaKodu,
  MarketKey,
  CurrAccCode,
  CustomerName,
  OrderQty = SUM(Qty),
  OrderUSD = SUM(AmountUSD),
  OrderCount = COUNT(DISTINCT OrderHeaderID),
  LineCount = COUNT(*),
  OverdueQty = SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
FROM OpenOrderLines
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
`, limit*4)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	type detail struct {
		productCode  string
		colorCode    string
		yakaKodu     string
		marketKey    string
		customerCode string
		customerName string
		orderQty     float64
		orderUSD     float64
		orderCount   int
		lineCount    int
		overdueQty   float64
	}
	details := make([]detail, 0, limit*2)
	productCodes := make([]string, 0, limit)
	stockKeys := make([]string, 0, limit)
	seenProducts := map[string]bool{}
	seenStockKeys := map[string]bool{}
	for rows.Next() {
		var d detail
		if err := rows.Scan(&d.productCode, &d.colorCode, &d.yakaKodu, &d.marketKey, &d.customerCode, &d.customerName, &d.orderQty, &d.orderUSD, &d.orderCount, &d.lineCount, &d.overdueQty); err != nil {
			return nil, err
		}
		normalizeProductPerformanceVariantCodes(&d.productCode, &d.colorCode, &d.yakaKodu)
		details = append(details, d)
		if !seenProducts[d.productCode] {
			seenProducts[d.productCode] = true
			productCodes = append(productCodes, d.productCode)
		}
		key := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
		if !seenStockKeys[key] {
			seenStockKeys[key] = true
			stockKeys = append(stockKeys, key)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
	if err != nil {
		return nil, err
	}
	stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}

	grouped := map[string]*models.ProductPerformanceOrderGroupRow{}
	productSets := map[string]map[string]bool{}
	marketSets := map[string]map[string]bool{}
	customerSets := map[string]map[string]bool{}
	stockVariantSets := map[string]map[string]bool{}
	for _, d := range details {
		groupKey := strings.TrimSpace(d.marketKey)
		customerCode := ""
		customerName := ""
		if mode == "customer" {
			groupKey = strings.TrimSpace(d.customerCode)
			customerCode = strings.TrimSpace(d.customerCode)
			customerName = strings.TrimSpace(d.customerName)
		}
		if groupKey == "" {
			groupKey = "-"
		}
		row := grouped[groupKey]
		if row == nil {
			row = &models.ProductPerformanceOrderGroupRow{
				Breakdown:    mode,
				GroupKey:     groupKey,
				MarketKey:    strings.TrimSpace(d.marketKey),
				CustomerCode: customerCode,
				CustomerName: customerName,
			}
			grouped[groupKey] = row
			productSets[groupKey] = map[string]bool{}
			marketSets[groupKey] = map[string]bool{}
			customerSets[groupKey] = map[string]bool{}
			stockVariantSets[groupKey] = map[string]bool{}
		}
		price := priceByProduct[normalizeProductPerformanceProductCode(d.productCode)]
		variantKey := productPerformanceVariantKey(d.productCode, d.colorCode, d.yakaKodu)
		row.OrderQty += d.orderQty
		row.OrderUSD += d.orderUSD
		row.BaseCostUSD += d.orderQty * price.base
		row.CostAmountUSD += d.orderQty * price.cost
		if !stockVariantSets[groupKey][variantKey] {
			row.StockQty += stockByKey[variantKey]
			stockVariantSets[groupKey][variantKey] = true
		}
		row.OrderCount += d.orderCount
		row.LineCount += d.lineCount
		row.OverdueQty += d.overdueQty
		productSets[groupKey][d.productCode] = true
		if strings.TrimSpace(d.marketKey) != "" {
			marketSets[groupKey][strings.TrimSpace(d.marketKey)] = true
		}
		if strings.TrimSpace(d.customerCode) != "" {
			customerSets[groupKey][strings.TrimSpace(d.customerCode)] = true
		}
	}

	out := make([]models.ProductPerformanceOrderGroupRow, 0, len(grouped))
	for key, row := range grouped {
		row.ProductCount = len(productSets[key])
		row.MarketCount = len(marketSets[key])
		row.CustomerCount = len(customerSets[key])
		row.NetStockAfterOrder = row.StockQty - row.OrderQty
		if row.OrderQty != 0 {
			row.AvgOrderPriceUSD = row.OrderUSD / row.OrderQty
		}
		row.ExpectedProfitBaseUSD = row.OrderUSD - row.BaseCostUSD
		row.ExpectedProfitCostUSD = row.OrderUSD - row.CostAmountUSD
		if row.OrderUSD != 0 {
			row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
			row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
		}
		row.PerformanceBucket, row.Recommendation = productPerformanceOrderGroupRecommendation(*row)
		out = append(out, *row)
	}
	sort.Slice(out, func(i, j int) bool {
		if out[i].ExpectedProfitCostUSD < 0 && out[j].ExpectedProfitCostUSD >= 0 {
			return true
		}
		if out[i].ExpectedProfitCostUSD >= 0 && out[j].ExpectedProfitCostUSD < 0 {
			return false
		}
		return out[i].OrderUSD > out[j].OrderUSD
	})
	if len(out) > limit {
		out = out[:limit]
	}
	return out, nil
}

func ListProductPerformanceOrderProductCustomers(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderProductCustomerRow, error) {
	if db.MssqlDB == nil {
		return nil, fmt.Errorf("mssql db nil")
	}
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderProductCustomerRow](ctx, pg, productPerformanceSnapshotKey("order-product-customers"), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}
	rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS (
  SELECT
    h.OrderHeaderID,
    h.CurrAccCode,
    CustomerName = ISNULL(cad.CurrAccDescription, ''),
    ProductCode = UPPER(LTRIM(RTRIM(l.ItemCode))),
    ColorCode = UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))),
    YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))),
    ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
    MarketKey = dbo.HG_Temizlik(ISNULL((
      SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK)
      WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3
        AND cdCurrAccAttributeDesc.AttributeTypeCode = 1
        AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01
        AND cdCurrAccAttributeDesc.LangCode = 'TR'
    ), SPACE(0))),
    Qty = ISNULL(l.Qty1, 0),
    AmountUSD = CASE
      WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
      WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
      WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
      ELSE 0
    END,
    DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date)
  FROM dbo.trOrderHeader h WITH(NOLOCK)
  INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
  LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
  LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
  LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
  OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
  OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
  WHERE ISNULL(h.IsCancelOrder, 0) = 0
    AND ISNULL(h.IsClosed, 0) = 0
    AND h.OrderTypeCode = 1
    AND h.ProcessCode = 'WS'
    AND ISNULL(l.IsClosed, 0) = 0
    AND l.ItemTypeCode = 1
    AND ISNULL(l.Qty1, 0) > 0
    AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
)
SELECT TOP (@p1)
  ProductCode, ColorCode, YakaKodu, MAX(ItemDescription), MarketKey, CurrAccCode, CustomerName,
  SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
  COUNT(DISTINCT OrderHeaderID), COUNT(*), SUM(CASE WHEN DueDate < CAST(GETDATE() AS date) THEN Qty ELSE 0 END)
FROM OpenOrderLines
GROUP BY ProductCode, ColorCode, YakaKodu, MarketKey, CurrAccCode, CustomerName
ORDER BY SUM(AmountUSD) DESC, SUM(Qty) DESC
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceOrderProductCustomerRow, 0, limit)
	productCodes := make([]string, 0, limit)
	stockKeys := make([]string, 0, limit)
	colorCodes := make([]string, 0, limit)
	seenProducts := map[string]bool{}
	seenStockKeys := map[string]bool{}
	for rows.Next() {
		var r models.ProductPerformanceOrderProductCustomerRow
		if err := rows.Scan(&r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.OrderCount, &r.LineCount, &r.OverdueQty); err != nil {
			return nil, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
		if !seenProducts[r.ProductCode] {
			seenProducts[r.ProductCode] = true
			productCodes = append(productCodes, r.ProductCode)
		}
		key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
		if !seenStockKeys[key] {
			seenStockKeys[key] = true
			stockKeys = append(stockKeys, key)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
	if err != nil {
		return nil, err
	}
	stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	attrByKey, err := productPerformanceAttrLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	filtered := out[:0]
	for i := range out {
		row := &out[i]
		price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
		attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
			continue
		}
		if strings.TrimSpace(row.ItemDescription) == "" {
			row.ItemDescription = attr.itemDescription
		}
		row.Kategori = attr.kategori
		row.AskiliYan = cleanProductPerformanceOptionalAttr(attr.askiliYan)
		row.UrunIlkGrubu = attr.urunIlkGrubu
		row.UrunAnaGrubu = attr.urunAnaGrubu
		row.UrunAltGrubu = attr.urunAltGrubu
		row.CostPriceUSD = price.cost
		row.BasePriceUSD = price.base
		row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		row.NetStockAfterOrder = row.StockQty - row.OrderQty
		row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
		row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
		if row.OrderUSD != 0 {
			row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
			row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
		}
		row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
		row.PerformanceBucket, row.Recommendation = productPerformanceOrderProductCustomerRecommendation(*row)
		filtered = append(filtered, *row)
	}
	return filtered, nil
}

func ListProductPerformanceOrderMarketDetails(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceOrderMarketDetailRow, error) {
	if db.MssqlDB == nil {
		return nil, fmt.Errorf("mssql db nil")
	}
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 800
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceOrderMarketDetailRow](ctx, pg, productPerformanceSnapshotKey("order-market-details"), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}
	rows, err := db.MssqlDB.QueryContext(ctx, `
WITH OpenOrderLines AS (
  SELECT
    MarketKey = dbo.HG_Temizlik(ISNULL((SELECT AttributeDescription FROM cdCurrAccAttributeDesc WITH(NOLOCK) WHERE cdCurrAccAttributeDesc.CurrAccTypeCode = 3 AND cdCurrAccAttributeDesc.AttributeTypeCode = 1 AND cdCurrAccAttributeDesc.AttributeCode = caf.CustomerAtt01 AND cdCurrAccAttributeDesc.LangCode = 'TR'), SPACE(0))),
    h.CurrAccCode,
    CustomerName = ISNULL(cad.CurrAccDescription, ''),
    h.OrderNumber,
    OrderDate = CAST(h.OrderDate AS date),
    DueDate = CAST(ISNULL(l.DeliveryDate, h.AverageDueDate) AS date),
    ProductCode = UPPER(LTRIM(RTRIM(l.ItemCode))),
    ColorCode = UPPER(LTRIM(RTRIM(ISNULL(l.ColorCode, '')))),
    YakaKodu = UPPER(LTRIM(RTRIM(ISNULL(l.ItemDim2Code, '')))),
    ItemDescription = dbo.HG_Temizlik(ISNULL((SELECT ItemDescription FROM cdItemDesc WITH(NOLOCK) WHERE cdItemDesc.ItemTypeCode = l.ItemTypeCode AND cdItemDesc.ItemCode = l.ItemCode AND cdItemDesc.LangCode = 'TR'), SPACE(0))),
    Qty = ISNULL(l.Qty1, 0),
    AmountUSD = CASE
      WHEN h.DocCurrencyCode = 'USD' THEN ISNULL(c.NetAmount, 0)
      WHEN h.DocCurrencyCode = 'TRY' AND usd.Rate > 0 THEN ISNULL(c.NetAmount, 0) / usd.Rate
      WHEN h.DocCurrencyCode IN ('EUR', 'GBP') AND cur.Rate > 0 AND usd.Rate > 0 THEN (ISNULL(c.NetAmount, 0) * cur.Rate) / usd.Rate
      ELSE 0
    END
  FROM dbo.trOrderHeader h WITH(NOLOCK)
  INNER JOIN dbo.trOrderLine l WITH(NOLOCK) ON l.OrderHeaderID = h.OrderHeaderID
  LEFT JOIN dbo.trOrderLineCurrency c WITH(NOLOCK) ON c.OrderLineID = l.OrderLineID AND c.CurrencyCode = ISNULL(h.DocCurrencyCode, 'TRY')
  LEFT JOIN dbo.CustomerAttributesFilter caf WITH(NOLOCK) ON caf.CurrAccTypeCode = h.CurrAccTypeCode AND caf.CurrAccCode = h.CurrAccCode
  LEFT JOIN dbo.cdCurrAccDesc cad WITH(NOLOCK) ON cad.CurrAccTypeCode = h.CurrAccTypeCode AND cad.CurrAccCode = h.CurrAccCode AND cad.LangCode = 'TR'
  OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = 'USD' AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) usd
  OUTER APPLY (SELECT TOP 1 Rate FROM dbo.AllExchangeRates WITH(NOLOCK) WHERE CurrencyCode = h.DocCurrencyCode AND RelationCurrencyCode = 'TRY' AND ExchangeTypeCode = 6 AND Rate > 0 AND Date <= CAST(ISNULL(h.OrderDate, GETDATE()) AS date) ORDER BY Date DESC) cur
  WHERE ISNULL(h.IsCancelOrder, 0) = 0
    AND ISNULL(h.IsClosed, 0) = 0
    AND h.OrderTypeCode = 1
    AND h.ProcessCode = 'WS'
    AND ISNULL(l.IsClosed, 0) = 0
    AND l.ItemTypeCode = 1
    AND ISNULL(l.Qty1, 0) > 0
    AND LEN(LTRIM(RTRIM(l.ItemCode))) = 13
)
SELECT TOP (@p1)
  MarketKey, CurrAccCode, CustomerName, OrderNumber, CONVERT(varchar, OrderDate, 23), CONVERT(varchar, DueDate, 23),
  ProductCode, ColorCode, YakaKodu, MAX(ItemDescription),
  SUM(Qty), SUM(AmountUSD), CASE WHEN SUM(Qty)=0 THEN 0 ELSE SUM(AmountUSD)/NULLIF(SUM(Qty),0) END,
  CASE WHEN MIN(DueDate) < CAST(GETDATE() AS date) THEN CAST(1 AS bit) ELSE CAST(0 AS bit) END
FROM OpenOrderLines
GROUP BY MarketKey, CurrAccCode, CustomerName, OrderNumber, OrderDate, DueDate, ProductCode, ColorCode, YakaKodu
ORDER BY MarketKey, CustomerName, OrderDate DESC, OrderNumber, SUM(AmountUSD) DESC
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceOrderMarketDetailRow, 0, limit)
	productCodes := make([]string, 0, limit)
	stockKeys := make([]string, 0, limit)
	colorCodes := make([]string, 0, limit)
	seenProducts := map[string]bool{}
	seenStockKeys := map[string]bool{}
	for rows.Next() {
		var r models.ProductPerformanceOrderMarketDetailRow
		if err := rows.Scan(&r.MarketKey, &r.CustomerCode, &r.CustomerName, &r.OrderNumber, &r.OrderDate, &r.DueDate, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription, &r.OrderQty, &r.OrderUSD, &r.AvgOrderPriceUSD, &r.IsOverdue); err != nil {
			return nil, err
		}
		normalizeProductPerformanceVariantCodes(&r.ProductCode, &r.ColorCode, &r.YakaKodu)
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
		if !seenProducts[r.ProductCode] {
			seenProducts[r.ProductCode] = true
			productCodes = append(productCodes, r.ProductCode)
		}
		key := productPerformanceVariantKey(r.ProductCode, r.ColorCode, r.YakaKodu)
		if !seenStockKeys[key] {
			seenStockKeys[key] = true
			stockKeys = append(stockKeys, key)
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	priceByProduct, err := productPerformancePriceLookup(ctx, pg, productCodes)
	if err != nil {
		return nil, err
	}
	stockByKey, err := productPerformanceStockLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	attrByKey, err := productPerformanceAttrLookup(ctx, pg, stockKeys)
	if err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	filtered := out[:0]
	for i := range out {
		row := &out[i]
		price := priceByProduct[normalizeProductPerformanceProductCode(row.ProductCode)]
		attr := attrByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		if isExcludedProductPerformanceFirstGroup(attr.urunIlkGrubu) {
			continue
		}
		row.CostPriceUSD = price.cost
		row.BasePriceUSD = price.base
		row.StockQty = stockByKey[productPerformanceVariantKey(row.ProductCode, row.ColorCode, row.YakaKodu)]
		row.NetStockAfterOrder = row.StockQty - row.OrderQty
		row.ExpectedProfitBaseUSD = row.OrderUSD - row.OrderQty*row.BasePriceUSD
		row.ExpectedProfitCostUSD = row.OrderUSD - row.OrderQty*row.CostPriceUSD
		if row.OrderUSD != 0 {
			row.ExpectedMarginBase = row.ExpectedProfitBaseUSD / row.OrderUSD
			row.ExpectedMarginCost = row.ExpectedProfitCostUSD / row.OrderUSD
		}
		row.ColorDescription = colorDescriptions[normalizeProductPerformanceCode(row.ColorCode)]
		row.PerformanceBucket, row.Recommendation = productPerformanceOrderMarketDetailRecommendation(*row)
		filtered = append(filtered, *row)
	}
	return filtered, nil
}

func ListProductPerformanceMarkets(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceMarketRow, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceMarketRow](ctx, pg, productPerformanceSnapshotKey("markets"), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}
	rows, err := pg.QueryContext(ctx, `
SELECT
  market_key,
  MAX(kategori) AS kategori,
  MAX(seri) AS seri,
  MAX(yas_grubu) AS yas_grubu,
  COALESCE(MAX(NULLIF(askili_yan, '-')), '') AS askili_yan,
  MAX(urun_ilk_grubu) AS urun_ilk_grubu,
  MAX(urun_ana_grubu) AS urun_ana_grubu,
  MAX(urun_alt_grubu) AS urun_alt_grubu,
  COUNT(DISTINCT product_code) AS product_count,
  COUNT(*) FILTER (WHERE performance_bucket='YILDIZ_URUN') AS star_count,
  COUNT(*) FILTER (WHERE performance_bucket='STOK_RISKI') AS stock_risk_count,
  COALESCE(SUM(stock_qty),0) AS stock_qty,
  COALESCE(SUM(stock_qty * cost_price_usd),0) AS stock_cost_value_usd,
  COALESCE(SUM(CASE WHEN performance_bucket IN ('STOK_RISKI','TAKIP') AND COALESCE(sales_qty_90d,0)=0 THEN stock_qty * cost_price_usd ELSE 0 END),0) AS risk_stock_cost_value_usd,
  COALESCE(SUM(sales_qty_90d),0) AS sales_qty_90d,
  COALESCE(SUM(sales_usd_90d),0) AS sales_usd_90d,
  COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
  COALESCE(AVG(NULLIF(gross_margin_90d,0)),0) AS avg_gross_margin_90d,
  COALESCE(AVG(NULLIF(stock_days_90d,0)),0) AS avg_stock_days_90d
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
  AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY market_key
ORDER BY risk_stock_cost_value_usd DESC, stock_cost_value_usd DESC, sales_usd_90d DESC
LIMIT $1
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceMarketRow, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceMarketRow
		if err := rows.Scan(
			&r.MarketKey, &r.Kategori, &r.Seri, &r.YasGrubu, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu,
			&r.ProductCount, &r.StarCount, &r.StockRiskCount, &r.StockQty,
			&r.StockCostValueUSD, &r.RiskStockCostValueUSD, &r.SalesQty90,
			&r.SalesUSD90, &r.GrossProfitUSD90, &r.AvgGrossMargin90, &r.AvgStockDays90,
		); err != nil {
			return nil, err
		}
		r.UrunIlkGrubu = cleanProductPerformanceFirstGroup(r.UrunIlkGrubu)
		out = append(out, r)
	}
	return out, rows.Err()
}

func ListProductPerformanceCountries(ctx context.Context, pg *sql.DB, limit int) ([]models.ProductPerformanceCountryRow, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceCountryRow](ctx, pg, productPerformanceSnapshotKey("countries"), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}
	rows, err := pg.QueryContext(ctx, `
SELECT
  customer_country,
  customer_segment,
  market_key,
  MAX(kategori) AS kategori,
  MAX(seri) AS seri,
  COUNT(DISTINCT product_code) AS product_count,
  COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_qty_90d,
  COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_usd_90d,
  CASE
    WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
    ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
       / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
  END AS avg_price_usd_90d,
  COUNT(DISTINCT NULLIF(customer_code, '-')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d,
  COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d,
  COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_qty_365d,
  COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d
FROM mk_product_performance_sales_daily
WHERE sales_date >= current_date - INTERVAL '364 days'
  AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY customer_country, customer_segment, market_key
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
LIMIT $1
`, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceCountryRow, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceCountryRow
		if err := rows.Scan(
			&r.Country, &r.CustomerSegment, &r.MarketKey, &r.Kategori, &r.Seri,
			&r.ProductCount, &r.SalesQty90, &r.SalesUSD90, &r.AvgPriceUSD90,
			&r.CustomerCount90, &r.InvoiceCount90, &r.SalesQty365, &r.SalesUSD365,
		); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func ListProductPerformanceCustomers(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceCustomerRow, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	mode := strings.ToLower(strings.TrimSpace(breakdown))
	if mode == "" {
		mode = "market_customer"
	}
	selectMarket := "''"
	selectCountry := "''"
	selectSegment := "''"
	groupCols := []string{"customer_code", "customer_name"}
	switch mode {
	case "country_customer":
		selectCountry = "customer_country"
		selectSegment = "customer_segment"
		groupCols = append(groupCols, "customer_country", "customer_segment")
	case "market_country_customer":
		selectMarket = "market_key"
		selectCountry = "customer_country"
		selectSegment = "customer_segment"
		groupCols = append(groupCols, "market_key", "customer_country", "customer_segment")
	default:
		mode = "market_customer"
		selectMarket = "market_key"
		groupCols = append(groupCols, "market_key")
	}
	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceCustomerRow](ctx, pg, productPerformanceSnapshotKey("customers", mode), limit); err != nil {
		return nil, err
	} else if ok {
		return rows, nil
	}

	query := fmt.Sprintf(`
SELECT
  $2::text AS breakdown,
  %s AS market_key,
  %s AS country,
  %s AS customer_segment,
  customer_code,
  customer_name,
  COUNT(DISTINCT product_code) AS product_count,
  COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_qty_90d,
  COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_usd_90d,
  CASE
    WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
    ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
       / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
  END AS avg_price_usd_90d,
  COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d,
  COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_qty_365d,
  COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '364 days'),0) AS sales_usd_365d,
  COALESCE(to_char(MAX(sales_date),'YYYY-MM-DD'),'') AS last_sale_date
FROM mk_product_performance_sales_daily
WHERE sales_date >= current_date - INTERVAL '364 days'
  AND COALESCE(customer_code,'') <> ''
  AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
GROUP BY %s
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
LIMIT $1
`, selectMarket, selectCountry, selectSegment, strings.Join(groupCols, ", "))

	rows, err := pg.QueryContext(ctx, query, limit, mode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceCustomerRow, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceCustomerRow
		if err := rows.Scan(
			&r.Breakdown, &r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName,
			&r.ProductCount, &r.SalesQty90, &r.SalesUSD90, &r.AvgPriceUSD90, &r.InvoiceCount90,
			&r.SalesQty365, &r.SalesUSD365, &r.LastSaleDate,
		); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func ListProductPerformanceSalesBreakdown(ctx context.Context, pg *sql.DB, breakdown string, limit int) ([]models.ProductPerformanceSalesBreakdownRow, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if limit <= 0 {
		limit = 50000
	} else if limit > 50000 {
		limit = 50000
	}
	mode := strings.ToLower(strings.TrimSpace(breakdown))
	fields := map[string]string{
		"product_code":     "s.product_code",
		"color_code":       "s.color_code",
		"yaka_kodu":        "s.yaka_kodu",
		"item_description": "MAX(s.item_description)",
		"kategori":         "COALESCE(MAX(s.kategori),'')",
		"askili_yan":       "COALESCE(MAX(CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END),'')",
		"urun_ilk_grubu":   "COALESCE(MAX(" + productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu") + "),'')",
		"urun_ana_grubu":   "COALESCE(MAX(s.urun_ana_grubu),'')",
		"urun_alt_grubu":   "MAX(s.urun_alt_grubu)",
		"market_key":       "s.market_key",
		"country":          "s.customer_country",
		"customer_segment": "s.customer_segment",
		"customer_code":    "s.customer_code",
		"customer_name":    "MAX(s.customer_name)",
	}
	selected := map[string]bool{}
	groupCols := []string{}
	switch mode {
	case "color_yaka_market_customer":
		selected["product_code"] = true
		selected["color_code"] = true
		selected["yaka_kodu"] = true
		selected["item_description"] = true
		selected["kategori"] = true
		selected["askili_yan"] = true
		selected["urun_ilk_grubu"] = true
		selected["urun_ana_grubu"] = true
		selected["urun_alt_grubu"] = true
		selected["country"] = true
		selected["customer_segment"] = true
		selected["market_key"] = true
		selected["customer_code"] = true
		selected["customer_name"] = true
		groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
	case "product_country_segment_market_customer":
		selected["product_code"] = true
		selected["item_description"] = true
		selected["kategori"] = true
		selected["askili_yan"] = true
		selected["urun_ilk_grubu"] = true
		selected["urun_ana_grubu"] = true
		selected["urun_alt_grubu"] = true
		selected["color_code"] = true
		selected["yaka_kodu"] = true
		selected["country"] = true
		selected["customer_segment"] = true
		selected["market_key"] = true
		selected["customer_code"] = true
		selected["customer_name"] = true
		groupCols = []string{"s.market_key", "s.customer_code", "s.product_code", "s.color_code", "s.yaka_kodu", "s.customer_country", "s.customer_segment"}
	case "market_customer_product":
		selected["market_key"] = true
		selected["customer_code"] = true
		selected["customer_name"] = true
		selected["product_code"] = true
		selected["color_code"] = true
		selected["yaka_kodu"] = true
		selected["item_description"] = true
		selected["kategori"] = true
		selected["askili_yan"] = true
		selected["urun_ilk_grubu"] = true
		selected["urun_ana_grubu"] = true
		selected["urun_alt_grubu"] = true
		groupCols = []string{"s.market_key", "s.customer_code", "s.product_code", "s.color_code", "s.yaka_kodu"}
	case "country_segment_market_customer_product":
		selected["country"] = true
		selected["customer_segment"] = true
		selected["market_key"] = true
		selected["customer_code"] = true
		selected["customer_name"] = true
		selected["product_code"] = true
		selected["color_code"] = true
		selected["yaka_kodu"] = true
		selected["item_description"] = true
		selected["kategori"] = true
		selected["askili_yan"] = true
		selected["urun_ilk_grubu"] = true
		selected["urun_ana_grubu"] = true
		selected["urun_alt_grubu"] = true
		groupCols = []string{"s.customer_country", "s.customer_segment", "s.market_key", "s.customer_code", "s.product_code", "s.color_code", "s.yaka_kodu"}
	default:
		mode = "color_yaka_market_customer"
		selected["product_code"] = true
		selected["color_code"] = true
		selected["yaka_kodu"] = true
		selected["item_description"] = true
		selected["kategori"] = true
		selected["askili_yan"] = true
		selected["urun_ilk_grubu"] = true
		selected["urun_ana_grubu"] = true
		selected["urun_alt_grubu"] = true
		selected["country"] = true
		selected["customer_segment"] = true
		selected["market_key"] = true
		selected["customer_code"] = true
		selected["customer_name"] = true
		groupCols = []string{productPerformanceCleanFirstGroupSQL("s.urun_ilk_grubu"), "s.color_code", "s.yaka_kodu", "CASE WHEN btrim(COALESCE(s.askili_yan,'')) = '-' THEN '' ELSE s.askili_yan END", "s.kategori", "s.urun_ana_grubu", "s.urun_alt_grubu", "s.product_code", "s.customer_country", "s.market_key", "s.customer_segment", "s.customer_code"}
	}

	if rows, ok, err := loadProductPerformanceSnapshotRows[models.ProductPerformanceSalesBreakdownRow](ctx, pg, productPerformanceSnapshotKey("sales-breakdown", mode), limit); err != nil {
		return nil, err
	} else if ok {
		applyProductPerformanceSalesBreakdownScores(rows)
		return rows, nil
	}

	selectExpr := func(name string) string {
		if selected[name] {
			return fields[name] + " AS " + name
		}
		return "''::text AS " + name
	}

	query := fmt.Sprintf(`
WITH LatestKPI AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    COALESCE(kategori,'') AS kategori,
    CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
    CASE
      WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
      WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
      ELSE COALESCE(urun_ilk_grubu,'')
    END AS urun_ilk_grubu,
    COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
    urun_alt_grubu
  FROM mk_product_performance_kpi_daily
  WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
  ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
),
StockAgg AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    COALESCE(SUM(stock_qty),0) AS stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date = (SELECT MAX(stock_date) FROM mk_product_performance_stock_daily)
  GROUP BY product_code, color_code, yaka_kodu
),
Stock90Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date <= current_date - INTERVAL '89 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock180Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date <= current_date - INTERVAL '179 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock365Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date <= current_date - INTERVAL '359 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
StockTotalStart AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code,
    color_code,
    yaka_kodu,
    stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date <= DATE '2022-01-01'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
FirstSaleByProduct AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days') AS first_sale_date_90d,
    MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days') AS first_sale_date_180d,
    MIN(sales_date) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days') AS first_sale_date_365d,
    MIN(sales_date) FILTER (WHERE sales_date >= DATE '2022-01-01') AS first_sale_date_total
  FROM mk_product_performance_sales_daily
  WHERE sales_date >= DATE '2022-01-01'
  GROUP BY product_code, color_code, yaka_kodu
),
Agg AS (
  SELECT
    $2::text AS breakdown,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    %s,
    COUNT(DISTINCT product_code) AS product_count,
    COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days')::integer AS product_group_count_90d,
    COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days')::integer AS product_group_count_180d,
    COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days')::integer AS product_group_count_365d,
    COUNT(DISTINCT COALESCE(NULLIF(urun_ana_grubu,''), NULLIF(urun_alt_grubu,''), NULLIF(kategori,''), product_code)) FILTER (WHERE sales_date >= DATE '2022-01-01')::integer AS product_group_count_total,
    COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_90d,
    COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_180d,
    COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_365d,
    COUNT(DISTINCT NULLIF(NULLIF(market_key,'STOK'),'')) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(sales_usd,0) > 0)::integer AS market_count_total,
    COALESCE(MAX(stock_qty),0) AS stock_qty,
    COALESCE(MAX(avg_stock_90d),0) AS avg_stock_90d,
    COALESCE(MAX(avg_stock_180d),0) AS avg_stock_180d,
    COALESCE(MAX(avg_stock_365d),0) AS avg_stock_365d,
    COALESCE(MAX(avg_stock_total),0) AS avg_stock_total,
    COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_90d,
    COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)::integer AS invoice_count_90d,
    COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_qty_90d,
    COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0) AS sales_usd_90d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
    END AS avg_price_usd_90d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * base_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
    END AS base_price_usd_90d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '89 days'),0)
    END AS cost_price_usd_90d,
    COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_180d,
    COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)::integer AS invoice_count_180d,
    COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_qty_180d,
    COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0) AS sales_usd_180d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
    END AS avg_price_usd_180d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * base_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
    END AS base_price_usd_180d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '179 days'),0)
    END AS cost_price_usd_180d,
    COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_qty_365d,
    COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0) AS sales_usd_365d,
    COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_365d,
    COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)::integer AS invoice_count_365d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
    END AS avg_price_usd_365d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * base_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
    END AS base_price_usd_365d,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= current_date - INTERVAL '359 days'),0)
    END AS cost_price_usd_365d,
    COUNT(DISTINCT customer_code) FILTER (WHERE sales_date >= DATE '2022-01-01' AND COALESCE(customer_code,'') <> '' AND COALESCE(sales_usd,0) > 0)::integer AS customer_count_total,
    COALESCE(SUM(invoice_count) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)::integer AS invoice_count_total,
    COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_qty_total,
    COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0) AS sales_usd_total,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
    END AS avg_price_usd_total,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * base_price_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
    END AS base_price_usd_total,
    CASE WHEN COALESCE(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)=0 THEN 0
      ELSE COALESCE(SUM(sales_qty * cost_price_usd) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
        / NULLIF(SUM(sales_qty) FILTER (WHERE sales_date >= DATE '2022-01-01'),0)
    END AS cost_price_usd_total,
    COALESCE(to_char(MAX(sales_date),'YYYY-MM-DD'),'') AS last_sale_date
  FROM (
    SELECT
      s.*,
      COALESCE(pd.base_price_usd,0) AS base_price_usd,
      COALESCE(pd.cost_price_usd,0) AS cost_price_usd,
      COALESCE(st.stock_qty,0) AS stock_qty,
      (COALESCE(s90.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_90d,
      (COALESCE(s180.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_180d,
      (COALESCE(s365.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_365d,
      (COALESCE(stotal.stock_qty, st.stock_qty, 0) + COALESCE(st.stock_qty,0)) / 2.0 AS avg_stock_total
    FROM mk_product_performance_sales_daily s
    LEFT JOIN mk_product_performance_price_dim pd ON pd.product_code = s.product_code
    LEFT JOIN LatestKPI k ON k.product_code = s.product_code AND k.color_code = s.color_code AND k.yaka_kodu = s.yaka_kodu
    LEFT JOIN FirstSaleByProduct fs ON fs.product_code = s.product_code AND fs.color_code = s.color_code AND fs.yaka_kodu = s.yaka_kodu
    LEFT JOIN StockAgg st ON st.product_code = s.product_code AND st.color_code = s.color_code AND st.yaka_kodu = s.yaka_kodu
    LEFT JOIN LATERAL (
      SELECT x.stock_qty
      FROM mk_product_performance_stock_daily x
      WHERE x.product_code = s.product_code
        AND x.color_code = s.color_code
        AND x.yaka_kodu = s.yaka_kodu
        AND x.stock_date <= COALESCE(fs.first_sale_date_90d, current_date - INTERVAL '89 days')
      ORDER BY x.stock_date DESC
      LIMIT 1
    ) s90 ON TRUE
    LEFT JOIN LATERAL (
      SELECT x.stock_qty
      FROM mk_product_performance_stock_daily x
      WHERE x.product_code = s.product_code
        AND x.color_code = s.color_code
        AND x.yaka_kodu = s.yaka_kodu
        AND x.stock_date <= COALESCE(fs.first_sale_date_180d, current_date - INTERVAL '179 days')
      ORDER BY x.stock_date DESC
      LIMIT 1
    ) s180 ON TRUE
    LEFT JOIN LATERAL (
      SELECT x.stock_qty
      FROM mk_product_performance_stock_daily x
      WHERE x.product_code = s.product_code
        AND x.color_code = s.color_code
        AND x.yaka_kodu = s.yaka_kodu
        AND x.stock_date <= COALESCE(fs.first_sale_date_365d, current_date - INTERVAL '359 days')
      ORDER BY x.stock_date DESC
      LIMIT 1
    ) s365 ON TRUE
    LEFT JOIN LATERAL (
      SELECT x.stock_qty
      FROM mk_product_performance_stock_daily x
      WHERE x.product_code = s.product_code
        AND x.color_code = s.color_code
        AND x.yaka_kodu = s.yaka_kodu
        AND x.stock_date <= COALESCE(fs.first_sale_date_total, DATE '2022-01-01')
      ORDER BY x.stock_date DESC
      LIMIT 1
    ) stotal ON TRUE
  ) s
  WHERE sales_date >= DATE '2022-01-01'
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
  GROUP BY %s
),
Enriched AS (
  SELECT
    Agg.*,
    sales_usd_90d - (sales_qty_90d * base_price_usd_90d) AS gross_profit_base_usd_90d,
    sales_usd_90d - (sales_qty_90d * cost_price_usd_90d) AS gross_profit_cost_usd_90d,
    CASE WHEN sales_usd_90d <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * base_price_usd_90d)) / NULLIF(sales_usd_90d,0) END AS gross_margin_base_90d,
    CASE WHEN sales_usd_90d <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * cost_price_usd_90d)) / NULLIF(sales_usd_90d,0) END AS gross_margin_cost_90d,
    sales_usd_180d - (sales_qty_180d * base_price_usd_180d) AS gross_profit_base_usd_180d,
    sales_usd_180d - (sales_qty_180d * cost_price_usd_180d) AS gross_profit_cost_usd_180d,
    CASE WHEN sales_usd_180d <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * base_price_usd_180d)) / NULLIF(sales_usd_180d,0) END AS gross_margin_base_180d,
    CASE WHEN sales_usd_180d <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * cost_price_usd_180d)) / NULLIF(sales_usd_180d,0) END AS gross_margin_cost_180d,
    sales_usd_365d - (sales_qty_365d * base_price_usd_365d) AS gross_profit_base_usd_365d,
    sales_usd_365d - (sales_qty_365d * cost_price_usd_365d) AS gross_profit_cost_usd_365d,
    CASE WHEN sales_usd_365d <= 0 THEN 0 ELSE (sales_usd_365d - (sales_qty_365d * base_price_usd_365d)) / NULLIF(sales_usd_365d,0) END AS gross_margin_base_365d,
    CASE WHEN sales_usd_365d <= 0 THEN 0 ELSE (sales_usd_365d - (sales_qty_365d * cost_price_usd_365d)) / NULLIF(sales_usd_365d,0) END AS gross_margin_cost_365d,
    sales_usd_total - (sales_qty_total * base_price_usd_total) AS gross_profit_base_usd_total,
    sales_usd_total - (sales_qty_total * cost_price_usd_total) AS gross_profit_cost_usd_total,
    CASE WHEN sales_usd_total <= 0 THEN 0 ELSE (sales_usd_total - (sales_qty_total * base_price_usd_total)) / NULLIF(sales_usd_total,0) END AS gross_margin_base_total,
    CASE WHEN sales_usd_total <= 0 THEN 0 ELSE (sales_usd_total - (sales_qty_total * cost_price_usd_total)) / NULLIF(sales_usd_total,0) END AS gross_margin_cost_total,
    (base_price_usd_total > 0 AND cost_price_usd_total > 0) AS has_cost
  FROM Agg
),
Scored AS (
  SELECT
    Enriched.*,
    CASE WHEN AVG(NULLIF(sales_usd_90d,0)) OVER () IS NULL THEN 0
      ELSE sales_usd_90d / NULLIF(AVG(NULLIF(sales_usd_90d,0)) OVER (),0)
    END AS sales_index_90d
  FROM Enriched
)
SELECT
  breakdown,
  product_code,
  color_code,
  yaka_kodu,
  item_description,
  kategori,
  askili_yan,
  urun_ilk_grubu,
  urun_ana_grubu,
  urun_alt_grubu,
  market_key,
  country,
  customer_segment,
  customer_code,
  customer_name,
  product_count,
  product_group_count_90d,
  product_group_count_180d,
  product_group_count_365d,
  product_group_count_total,
  market_count_90d,
  market_count_180d,
  market_count_365d,
  market_count_total,
  stock_qty,
  customer_count_90d,
  invoice_count_90d,
  sales_qty_90d,
  sales_usd_90d,
  avg_price_usd_90d,
  base_price_usd_90d,
  cost_price_usd_90d,
  gross_profit_base_usd_90d,
  gross_profit_cost_usd_90d,
  gross_margin_base_90d,
  gross_margin_cost_90d,
  customer_count_180d,
  invoice_count_180d,
  sales_qty_180d,
  sales_usd_180d,
  avg_price_usd_180d,
  base_price_usd_180d,
  cost_price_usd_180d,
  gross_profit_base_usd_180d,
  gross_profit_cost_usd_180d,
  gross_margin_base_180d,
  gross_margin_cost_180d,
  sales_qty_365d,
  sales_usd_365d,
  avg_price_usd_365d,
  base_price_usd_365d,
  cost_price_usd_365d,
  gross_profit_base_usd_365d,
  gross_profit_cost_usd_365d,
  gross_margin_base_365d,
  gross_margin_cost_365d,
  customer_count_365d,
  invoice_count_365d,
  customer_count_total,
  invoice_count_total,
  sales_qty_total,
  sales_usd_total,
  avg_price_usd_total,
  base_price_usd_total,
  cost_price_usd_total,
  gross_profit_base_usd_total,
  gross_profit_cost_usd_total,
  gross_margin_base_total,
  gross_margin_cost_total,
  avg_stock_90d,
  avg_stock_180d,
  avg_stock_365d,
  avg_stock_total,
  CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
  CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
  CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
  CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, (current_date - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
  has_cost,
  sales_index_90d,
  CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
    ROUND((
      LEAST(35, sales_usd_90d / 25000 * 35)
      + LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
      + LEAST(20, product_group_count_90d / 8.0 * 20)
      + LEAST(15, sales_qty_90d / 500 * 15)
    )::numeric, 4)
  END AS customer_score_90d,
  CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_180d <= 0 THEN 1 ELSE
    ROUND((
      LEAST(35, sales_usd_180d / 50000 * 35)
      + LEAST(30, GREATEST(gross_margin_cost_180d,0) / 0.45 * 30)
      + LEAST(20, product_group_count_180d / 8.0 * 20)
      + LEAST(15, sales_qty_180d / 1000 * 15)
    )::numeric, 4)
  END AS customer_score_180d,
  CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_365d <= 0 THEN 1 ELSE
    ROUND((
      LEAST(35, sales_usd_365d / 100000 * 35)
      + LEAST(30, GREATEST(gross_margin_cost_365d,0) / 0.45 * 30)
      + LEAST(20, product_group_count_365d / 8.0 * 20)
      + LEAST(15, sales_qty_365d / 2000 * 15)
    )::numeric, 4)
  END AS customer_score_365d,
  CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_total <= 0 THEN 1 ELSE
    ROUND((
      LEAST(35, sales_usd_total / 300000 * 35)
      + LEAST(30, GREATEST(gross_margin_cost_total,0) / 0.45 * 30)
      + LEAST(20, product_group_count_total / 8.0 * 20)
      + LEAST(15, sales_qty_total / 6000 * 15)
    )::numeric, 4)
  END AS customer_score_total,
  CASE WHEN NOT has_cost THEN 0 WHEN sales_usd_90d <= 0 THEN 1 ELSE
    ROUND(
      LEAST(30, GREATEST(gross_margin_cost_90d,0) / 0.45 * 30)
      + LEAST(20, (CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END) / 4.0 * 20)
      + LEAST(20, sales_usd_90d / 25000 * 20)
      + LEAST(5, market_count_90d / 3.0 * 5)
      + LEAST(25, customer_count_90d / 8.0 * 25),
      4
    )
  END AS performance_score,
  CASE
    WHEN NOT has_cost THEN 'MALIYET_YOK'
    WHEN sales_index_90d >= 1.25 AND customer_count_90d >= 2 THEN 'YILDIZ_URUN'
    WHEN sales_qty_90d = 0 AND sales_qty_365d > 0 THEN 'STOK_RISKI'
    WHEN gross_margin_cost_90d < 0 THEN 'FIYAT_BASKISI'
    WHEN sales_index_90d < 0.75 THEN 'TAKIP'
    ELSE 'TAKIP'
  END AS performance_bucket,
  CASE
    WHEN sales_index_90d >= 1.25 AND customer_count_90d >= 2 THEN 'Guclu satis kirilimi'
    WHEN sales_qty_90d = 0 AND sales_qty_365d > 0 THEN 'Son 90 gun zayif, kontrol et'
    ELSE 'Takip'
  END AS recommendation,
  last_sale_date
FROM Scored
ORDER BY sales_usd_90d DESC, sales_qty_90d DESC
LIMIT $1
`,
		selectExpr("product_code"),
		selectExpr("color_code"),
		selectExpr("yaka_kodu"),
		selectExpr("item_description"),
		selectExpr("kategori"),
		selectExpr("askili_yan"),
		selectExpr("urun_ilk_grubu"),
		selectExpr("urun_ana_grubu"),
		selectExpr("urun_alt_grubu"),
		selectExpr("market_key"),
		selectExpr("country"),
		selectExpr("customer_segment"),
		selectExpr("customer_code"),
		selectExpr("customer_name"),
		strings.Join(groupCols, ", "),
	)

	rows, err := pg.QueryContext(ctx, query, limit, mode)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceSalesBreakdownRow, 0, limit)
	colorCodes := make([]string, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceSalesBreakdownRow
		if err := rows.Scan(
			&r.Breakdown, &r.ProductCode, &r.ColorCode, &r.YakaKodu, &r.ItemDescription,
			&r.Kategori, &r.AskiliYan, &r.UrunIlkGrubu, &r.UrunAnaGrubu, &r.UrunAltGrubu,
			&r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName,
			&r.ProductCount, &r.ProductGroupCount90, &r.ProductGroupCount180, &r.ProductGroupCount365, &r.ProductGroupCountTotal,
			&r.MarketCount90, &r.MarketCount180, &r.MarketCount365, &r.MarketCountTotal,
			&r.StockQty, &r.CustomerCount90, &r.InvoiceCount90, &r.SalesQty90, &r.SalesUSD90,
			&r.AvgPriceUSD90, &r.BasePriceUSD90, &r.CostPriceUSD90, &r.GrossProfitBase90,
			&r.GrossProfitCost90, &r.GrossMarginBase90, &r.GrossMarginCost90, &r.CustomerCount180,
			&r.InvoiceCount180, &r.SalesQty180, &r.SalesUSD180, &r.AvgPriceUSD180, &r.BasePriceUSD180,
			&r.CostPriceUSD180, &r.GrossProfitBase180, &r.GrossProfitCost180, &r.GrossMarginBase180,
			&r.GrossMarginCost180, &r.SalesQty365, &r.SalesUSD365, &r.AvgPriceUSD365, &r.BasePriceUSD365,
			&r.CostPriceUSD365, &r.GrossProfitBase365, &r.GrossProfitCost365, &r.GrossMarginBase365,
			&r.GrossMarginCost365, &r.CustomerCount365, &r.InvoiceCount365, &r.CustomerCountTotal, &r.InvoiceCountTotal, &r.SalesQtyTotal, &r.SalesUSDTotal,
			&r.AvgPriceUSDTotal, &r.BasePriceUSDTotal, &r.CostPriceUSDTotal, &r.GrossProfitBaseTotal,
			&r.GrossProfitCostTotal, &r.GrossMarginBaseTotal, &r.GrossMarginCostTotal,
			&r.AvgStock90, &r.AvgStock180, &r.AvgStock365, &r.AvgStockTotal,
			&r.StockTurnover90, &r.StockTurnover180, &r.StockTurnover365, &r.StockTurnoverTotal, &r.HasCost,
			&r.SalesIndex90, &r.CustomerScore90, &r.CustomerScore180, &r.CustomerScore365, &r.CustomerScoreTotal, &r.PerformanceScore,
			&r.PerformanceBucket, &r.Recommendation, &r.LastSaleDate,
		); err != nil {
			return nil, err
		}
		colorCodes = append(colorCodes, r.ColorCode)
		out = append(out, r)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	for i := range out {
		out[i].ColorDescription = colorDescriptions[normalizeProductPerformanceCode(out[i].ColorCode)]
	}
	applyProductPerformanceSalesBreakdownScores(out)
	return out, nil
}

func applyProductPerformanceSalesBreakdownScores(rows []models.ProductPerformanceSalesBreakdownRow) {
	avg90 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD90 })
	avg180 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD180 })
	avg365 := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSD365 })
	avgTotal := productPerformanceSalesBreakdownAverage(rows, func(row models.ProductPerformanceSalesBreakdownRow) float64 { return row.SalesUSDTotal })

	for i := range rows {
		r := &rows[i]
		r.SalesIndex90 = productPerformanceRelativeIndex(r.SalesUSD90, avg90)
		r.SalesIndex180 = productPerformanceRelativeIndex(r.SalesUSD180, avg180)
		r.SalesIndex365 = productPerformanceRelativeIndex(r.SalesUSD365, avg365)
		r.SalesIndexTotal = productPerformanceRelativeIndex(r.SalesUSDTotal, avgTotal)

		r.CustomerScore90 = productPerformanceCustomerScore("90d", r.SalesUSD90, r.GrossMarginCost90, float64(r.ProductGroupCount90), r.SalesQty90)
		r.CustomerScore180 = productPerformanceCustomerScore("180d", r.SalesUSD180, r.GrossMarginCost180, float64(r.ProductGroupCount180), r.SalesQty180)
		r.CustomerScore365 = productPerformanceCustomerScore("365d", r.SalesUSD365, r.GrossMarginCost365, float64(r.ProductGroupCount365), r.SalesQty365)
		r.CustomerScoreTotal = productPerformanceCustomerScore("total", r.SalesUSDTotal, r.GrossMarginCostTotal, float64(r.ProductGroupCountTotal), r.SalesQtyTotal)

		r.PerformanceScore90 = productPerformanceProductScore("90d", r.SalesUSD90, r.SalesIndex90, r.GrossMarginCost90, r.StockTurnover90, float64(r.MarketCount90), float64(r.CustomerCount90))
		r.PerformanceScore180 = productPerformanceProductScore("180d", r.SalesUSD180, r.SalesIndex180, r.GrossMarginCost180, r.StockTurnover180, float64(r.MarketCount180), float64(r.CustomerCount180))
		r.PerformanceScore365 = productPerformanceProductScore("365d", r.SalesUSD365, r.SalesIndex365, r.GrossMarginCost365, r.StockTurnover365, float64(r.MarketCount365), float64(r.CustomerCount365))
		r.PerformanceScoreTotal = productPerformanceProductScore("total", r.SalesUSDTotal, r.SalesIndexTotal, r.GrossMarginCostTotal, r.StockTurnoverTotal, float64(r.MarketCountTotal), float64(r.CustomerCountTotal))
		r.PerformanceScore = r.PerformanceScore90
	}
}

func productPerformanceSalesBreakdownAverage(rows []models.ProductPerformanceSalesBreakdownRow, value func(models.ProductPerformanceSalesBreakdownRow) float64) float64 {
	var sum, count float64
	for _, row := range rows {
		v := value(row)
		if v <= 0 {
			continue
		}
		sum += v
		count++
	}
	if count == 0 {
		return 0
	}
	return sum / count
}

type ProductPerformanceGroupedRequest struct {
	Mode               string
	GroupLevels        []string
	ExpandedKeys       map[string]bool
	ExpandThroughLevel int
	Limit              int
	MainGroup          string
	Filters            map[string][]string
	SortBy             string
	Descending         bool
}

type ProductPerformanceGroupedFilterOptionsRequest struct {
	Mode        string
	GroupLevels []string
	MainGroup   string
	Fields      []string
	Limit       int
}

func ListProductPerformanceGrouped(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest) ([]map[string]any, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if req.Limit <= 0 || req.Limit > 50000 {
		req.Limit = 50000
	}
	levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels)
	if len(levels) == 0 {
		levels = defaultProductPerformanceGroupLevels(req.Mode)
	}
	if req.ExpandThroughLevel > len(levels)-2 {
		req.ExpandThroughLevel = len(levels) - 2
	}

	if out, ok, err := loadProductPerformancePreparedGroupedRows(ctx, pg, req, levels); ok || err != nil {
		return out, err
	}

	sourceRows, err := productPerformanceGroupedSourceRows(ctx, pg, req.Mode, req.Limit)
	if err != nil {
		return nil, err
	}
	sourceRows = filterProductPerformanceGroupedRows(sourceRows, productPerformanceGroupedEffectiveFilters(req))
	out := make([]map[string]any, 0, len(sourceRows))
	appendProductPerformanceGroupedRows(&out, sourceRows, levels, 0, 0, []string{"tab:" + req.Mode}, req.ExpandedKeys, req.ExpandThroughLevel)
	out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
	if len(out) > 0 || !productPerformanceLiveFallbackEnabled() {
		return out, nil
	}
	if out, ok, err := listProductPerformanceGroupedSQL(ctx, pg, req, levels); ok || err != nil {
		return out, err
	}
	return out, nil
}

func ListProductPerformanceGroupedFilterOptions(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedFilterOptionsRequest) (map[string][]string, error) {
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, err
	}
	if req.Limit <= 0 || req.Limit > 50000 {
		req.Limit = 50000
	}
	levels := sanitizeProductPerformanceGroupLevels(req.GroupLevels)
	if len(levels) == 0 {
		levels = defaultProductPerformanceGroupLevels(req.Mode)
	}
	reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
	if strings.TrimSpace(reportKey) == "" {
		return map[string][]string{}, nil
	}
	levelSet := map[string]bool{}
	for _, level := range levels {
		levelSet[level] = true
	}
	out := map[string][]string{}
	for _, field := range req.Fields {
		field = strings.TrimSpace(field)
		if !productPerformanceGroupedFilterOptionFieldAllowed(field) {
			continue
		}
		values, err := productPerformanceGroupedSnapshotFilterOptions(ctx, pg, reportKey, field, levelSet, strings.TrimSpace(req.MainGroup), req.Limit)
		if err != nil {
			return nil, err
		}
		out[field] = values
	}
	return out, nil
}

func productPerformanceGroupedFilterOptionFieldAllowed(field string) bool {
	switch field {
	case "kategori", "askili_yan", "urun_ilk_grubu", "urun_ana_grubu", "urun_alt_grubu",
		"product_code", "color_yaka", "market_key", "country", "customer_segment",
		"customer_code", "customer_name", "performance_bucket":
		return true
	default:
		return false
	}
}

func productPerformanceGroupedSnapshotFilterOptions(ctx context.Context, pg *sql.DB, reportKey, field string, levelSet map[string]bool, mainGroup string, limit int) ([]string, error) {
	if field == "urun_ana_grubu" && strings.Contains(reportKey, ":product_detail:") && strings.TrimSpace(mainGroup) != "" {
		return []string{strings.TrimSpace(mainGroup)}, nil
	}
	if field == "performance_bucket" {
		return productPerformanceGroupedSnapshotPayloadOptions(ctx, pg, reportKey, "performance_bucket", limit)
	}
	if !levelSet[field] {
		return []string{}, nil
	}
	rows, err := pg.QueryContext(ctx, `
SELECT DISTINCT btrim(group_value) AS value
FROM mk_product_performance_grouped_snapshot
WHERE report_key = $1
  AND group_field = $2
  AND btrim(group_value) <> ''
ORDER BY value
LIMIT $3
`, reportKey, field, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanProductPerformanceGroupedFilterOptionRows(rows)
}

func productPerformanceGroupedSnapshotPayloadOptions(ctx context.Context, pg *sql.DB, reportKey, field string, limit int) ([]string, error) {
	rows, err := pg.QueryContext(ctx, `
SELECT DISTINCT btrim(payload ->> $2) AS value
FROM mk_product_performance_grouped_snapshot
WHERE report_key = $1
  AND btrim(payload ->> $2) <> ''
ORDER BY value
LIMIT $3
`, reportKey, field, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	return scanProductPerformanceGroupedFilterOptionRows(rows)
}

func scanProductPerformanceGroupedFilterOptionRows(rows *sql.Rows) ([]string, error) {
	out := []string{}
	for rows.Next() {
		var value string
		if err := rows.Scan(&value); err != nil {
			return nil, err
		}
		value = strings.TrimSpace(value)
		if value != "" {
			out = append(out, value)
		}
	}
	return out, rows.Err()
}

func loadProductPerformancePreparedGroupedRows(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
	reportKey := productPerformanceGroupedSnapshotReportKey(req.Mode, levels, req.MainGroup)
	if strings.TrimSpace(reportKey) == "" {
		return nil, false, nil
	}
	effectiveFilters := productPerformancePreparedGroupedEffectiveFilters(req)
	hasFilters := len(effectiveFilters) > 0
	if hasFilters {
		return nil, false, nil
	}
	hasManualExpansion := len(req.ExpandedKeys) > 0
	query := `
SELECT payload
FROM mk_product_performance_grouped_snapshot
WHERE report_key = $1
`
	args := []any{reportKey}
	if !hasFilters && !hasManualExpansion {
		maxVisibleLevel := req.ExpandThroughLevel + 1
		if maxVisibleLevel < 0 {
			maxVisibleLevel = 0
		}
		args = append(args, maxVisibleLevel)
		query += fmt.Sprintf("  AND group_level <= $%d\n", len(args))
	}
	query += "ORDER BY row_order"
	if !hasFilters && !hasManualExpansion {
		args = append(args, req.Limit)
		query += fmt.Sprintf("\nLIMIT $%d", len(args))
	}

	rows, err := pg.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, false, err
	}
	defer rows.Close()

	out := make([]map[string]any, 0, minInt(req.Limit, 50000))
	for rows.Next() {
		var raw []byte
		if err := rows.Scan(&raw); err != nil {
			return nil, false, err
		}
		var item map[string]any
		if err := json.Unmarshal(raw, &item); err != nil {
			return nil, false, err
		}
		if item == nil {
			item = map[string]any{}
		}
		out = append(out, item)
	}
	if err := rows.Err(); err != nil {
		return nil, false, err
	}
	if len(out) == 0 {
		var exists bool
		if err := pg.QueryRowContext(ctx, `
SELECT EXISTS (
  SELECT 1
  FROM mk_product_performance_grouped_snapshot_meta
  WHERE report_key = $1
)
`, reportKey).Scan(&exists); err != nil {
			return nil, false, err
		}
		if !exists {
			return nil, false, nil
		}
	}
	out = filterProductPerformanceGroupedRowsForExpansion(out, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
	out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
	return out, true, nil
}

type productPerformanceGroupedSortNode struct {
	Row      map[string]any
	Children []*productPerformanceGroupedSortNode
}

func sortProductPerformancePreparedGroupedRows(rows []map[string]any, sortBy string, descending bool) []map[string]any {
	sortBy = strings.TrimSpace(sortBy)
	if len(rows) == 0 || sortBy == "" {
		return rows
	}
	roots := make([]*productPerformanceGroupedSortNode, 0)
	stack := make([]*productPerformanceGroupedSortNode, 0, 16)
	for _, row := range rows {
		node := &productPerformanceGroupedSortNode{Row: row}
		level := intFromMap(row, "level")
		if level < 0 {
			level = 0
		}
		if level < len(stack) {
			stack = stack[:level]
		}
		if level > 0 && level-1 < len(stack) {
			stack[level-1].Children = append(stack[level-1].Children, node)
		} else {
			roots = append(roots, node)
		}
		if level >= len(stack) {
			stack = append(stack, node)
		} else {
			stack[level] = node
		}
	}

	out := make([]map[string]any, 0, len(rows))
	var appendNodes func(nodes []*productPerformanceGroupedSortNode)
	appendNodes = func(nodes []*productPerformanceGroupedSortNode) {
		sort.SliceStable(nodes, func(i, j int) bool {
			cmp := compareProductPerformanceGroupedSortRows(nodes[i].Row, nodes[j].Row, sortBy)
			if cmp == 0 {
				cmp = strings.Compare(strings.ToLower(stringFromMap(nodes[i].Row, "label")), strings.ToLower(stringFromMap(nodes[j].Row, "label")))
			}
			if descending {
				return cmp > 0
			}
			return cmp < 0
		})
		for _, node := range nodes {
			out = append(out, node.Row)
			if len(node.Children) > 0 {
				appendNodes(node.Children)
			}
		}
	}
	appendNodes(roots)
	return out
}

func compareProductPerformanceGroupedSortRows(left, right map[string]any, sortBy string) int {
	leftValue := productPerformanceGroupedSortValue(left, sortBy)
	rightValue := productPerformanceGroupedSortValue(right, sortBy)
	leftNum, leftOK := numericProductPerformanceGroupedSortValue(leftValue)
	rightNum, rightOK := numericProductPerformanceGroupedSortValue(rightValue)
	if leftOK && rightOK {
		switch {
		case leftNum < rightNum:
			return -1
		case leftNum > rightNum:
			return 1
		default:
			return 0
		}
	}
	leftText := strings.ToLower(strings.TrimSpace(fmt.Sprint(leftValue)))
	rightText := strings.ToLower(strings.TrimSpace(fmt.Sprint(rightValue)))
	return strings.Compare(leftText, rightText)
}

func productPerformanceGroupedSortValue(row map[string]any, sortBy string) any {
	sortBy = strings.TrimSpace(sortBy)
	if sortBy == "" {
		return ""
	}
	if value, ok := row[sortBy]; ok {
		return value
	}
	switch sortBy {
	case "performance_score_total", "performance_score_90d":
		return row["performance_score"]
	case "gross_margin_base_total":
		return row["gross_margin_total"]
	case "gross_margin_cost_total":
		return row["gross_margin_total"]
	case "color_yaka":
		return mapGroupValue(row, "color_yaka")
	case "market_key":
		return mapGroupValue(row, "market_key")
	default:
		if sortBy == stringFromMap(row, "group_field") {
			return stringFromMap(row, "group_value")
		}
		return stringFromMap(row, "label")
	}
}

func numericProductPerformanceGroupedSortValue(value any) (float64, bool) {
	switch v := value.(type) {
	case nil:
		return 0, false
	case float64:
		return v, true
	case float32:
		return float64(v), true
	case int:
		return float64(v), true
	case int64:
		return float64(v), true
	case int32:
		return float64(v), true
	case json.Number:
		f, err := v.Float64()
		return f, err == nil
	case string:
		text := strings.TrimSpace(v)
		if text == "" {
			return 0, false
		}
		parsed, err := strconv.ParseFloat(text, 64)
		if err != nil {
			return 0, false
		}
		return parsed, true
	default:
		return 0, false
	}
}

type productPerformanceSQLGroupFilter struct {
	Field  string
	Value  string
	Values []string
}

func listProductPerformanceGroupedSQL(ctx context.Context, pg *sql.DB, req ProductPerformanceGroupedRequest, levels []string) ([]map[string]any, bool, error) {
	mode := strings.TrimSpace(req.Mode)
	if mode != "products" && mode != "product_detail" && mode != "idle" {
		return nil, false, nil
	}
	if err := EnsureProductPerformanceTables(pg); err != nil {
		return nil, true, err
	}
	out := make([]map[string]any, 0, 512)
	filters := productPerformanceGroupedBaseFilters(req)
	err := appendProductPerformanceGroupedSQLRows(ctx, pg, &out, mode, levels, 0, 0, []string{"tab:" + mode}, filters, req.ExpandedKeys, req.ExpandThroughLevel, req.Limit)
	if err != nil {
		return out, true, err
	}
	out = sortProductPerformancePreparedGroupedRows(out, req.SortBy, req.Descending)
	return out, true, nil
}

func productPerformanceGroupedBaseFilters(req ProductPerformanceGroupedRequest) []productPerformanceSQLGroupFilter {
	effectiveFilters := productPerformanceGroupedEffectiveFilters(req)
	filters := make([]productPerformanceSQLGroupFilter, 0, len(effectiveFilters))
	for field, values := range effectiveFilters {
		cleanValues := cleanProductPerformanceFilterValues(values)
		if len(cleanValues) == 0 {
			continue
		}
		filters = append(filters, productPerformanceSQLGroupFilter{Field: field, Values: cleanValues})
	}
	return filters
}

func productPerformanceGroupedEffectiveFilters(req ProductPerformanceGroupedRequest) map[string][]string {
	filters := make(map[string][]string, len(req.Filters)+1)
	for field, values := range req.Filters {
		cleanValues := cleanProductPerformanceFilterValues(values)
		if len(cleanValues) == 0 {
			continue
		}
		filters[field] = cleanValues
	}
	if strings.TrimSpace(req.Mode) == "product_detail" {
		mainGroup := cleanProductPerformanceFilterValues([]string{req.MainGroup})
		if len(mainGroup) > 0 {
			filters["urun_ana_grubu"] = mainGroup
		}
	}
	return filters
}

func productPerformancePreparedGroupedEffectiveFilters(req ProductPerformanceGroupedRequest) map[string][]string {
	filters := make(map[string][]string, len(req.Filters))
	for field, values := range req.Filters {
		if strings.TrimSpace(req.Mode) == "product_detail" && field == "urun_ana_grubu" {
			continue
		}
		cleanValues := cleanProductPerformanceFilterValues(values)
		if len(cleanValues) == 0 {
			continue
		}
		filters[field] = cleanValues
	}
	return filters
}

func appendProductPerformanceGroupedSQLRows(ctx context.Context, pg *sql.DB, out *[]map[string]any, mode string, levels []string, level int, visualLevel int, parentKeys []string, filters []productPerformanceSQLGroupFilter, expandedKeys map[string]bool, expandThroughLevel int, limit int) error {
	if level >= len(levels) {
		return nil
	}

	field := levels[level]
	rows, err := queryProductPerformanceSQLGroupRows(ctx, pg, mode, field, level, parentKeys, filters)
	if err != nil {
		return err
	}
	for _, row := range rows {
		value := stringFromMap(row, "group_value")
		if shouldSkipProductPerformanceGroupValue(field, value) {
			nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
			if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel, parentKeys, nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
				return err
			}
			continue
		}
		keyPart := field + ":" + value
		key := strings.Join(append(parentKeys, keyPart), "|")
		row["row_key"] = "group|" + key
		row["key"] = key
		row["level"] = visualLevel
		row["group_field"] = field
		row["group_value"] = value
		row["label"] = value
		row["__group"] = true
		row[field] = value
		deriveProductPerformanceGroupMetrics(row, field)
		*out = append(*out, row)
		if expandedKeys[key] || level <= expandThroughLevel {
			nextFilters := append(append([]productPerformanceSQLGroupFilter{}, filters...), productPerformanceSQLGroupFilter{Field: field, Value: value})
			if err := appendProductPerformanceGroupedSQLRows(ctx, pg, out, mode, levels, level+1, visualLevel+1, append(parentKeys, keyPart), nextFilters, expandedKeys, expandThroughLevel, limit); err != nil {
				return err
			}
		}
	}
	return nil
}

func queryProductPerformanceSQLGroupRows(ctx context.Context, pg *sql.DB, mode, field string, level int, parentKeys []string, filters []productPerformanceSQLGroupFilter) ([]map[string]any, error) {
	groupExpr, ok := productPerformanceSQLGroupExpr(field)
	if !ok {
		return nil, fmt.Errorf("unsupported product performance group field: %s", field)
	}
	whereSQL, args, err := productPerformanceSQLFilterWhere(filters)
	if err != nil {
		return nil, err
	}
	query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(`
SELECT (
  jsonb_build_object(
    'group_value', group_value,
    'label', group_value,
    'period_start', '2022-01-01',
    'period_end', COALESCE(to_char((SELECT kpi_date FROM LatestKPIDate),'YYYY-MM-DD'), ''),
    'count', row_count,
    'recommendation', CASE WHEN recommendation <> '' THEN recommendation ELSE row_count::text || ' satir' END,
    'image_product_code', image_product_code,
    'image_color_code', image_color_code,
    'image_yaka_kodu', image_yaka_kodu,
    'product_code', CASE WHEN $%d = 'product_code' THEN group_value ELSE '' END,
    'color_code', CASE WHEN $%d = 'color_code' THEN group_value ELSE '' END,
    'yaka_kodu', CASE WHEN $%d = 'yaka_kodu' THEN group_value ELSE '' END,
    'item_description', CASE WHEN $%d = 'item_description' THEN group_value ELSE '' END,
    'kategori', CASE WHEN $%d = 'kategori' THEN group_value ELSE '' END,
    'askili_yan', CASE WHEN $%d = 'askili_yan' THEN group_value ELSE '' END,
    'urun_ilk_grubu', CASE WHEN $%d = 'urun_ilk_grubu' THEN group_value ELSE '' END,
    'urun_ana_grubu', CASE WHEN $%d = 'urun_ana_grubu' THEN group_value ELSE '' END,
    'urun_alt_grubu', CASE WHEN $%d = 'urun_alt_grubu' THEN group_value ELSE '' END,
    'market_key', CASE WHEN $%d = 'market_key' THEN group_value ELSE '' END
  )
  || jsonb_build_object(
    'stock_qty', stock_qty,
    'avg_stock_90d', avg_stock_90d,
    'avg_stock_180d', avg_stock_180d,
    'avg_stock_365d', avg_stock_365d,
    'avg_stock_total', avg_stock_total,
    'sales_qty_90d', sales_qty_90d,
    'sales_qty_180d', sales_qty_180d,
    'sales_qty_365d', sales_qty_365d,
    'sales_qty_total', sales_qty_total,
    'sales_usd_90d', sales_usd_90d,
    'sales_usd_180d', sales_usd_180d,
    'sales_usd_365d', sales_usd_365d,
    'sales_usd_total', sales_usd_total,
    'avg_price_usd_90d', avg_price_usd_90d,
    'avg_price_usd_180d', avg_price_usd_180d,
    'cost_price_usd', cost_price_usd,
    'base_price_usd', base_price_usd,
    'base_price_try', base_price_try,
    'gross_profit_usd_90d', gross_profit_usd_90d,
    'gross_profit_usd_180d', gross_profit_usd_180d,
    'gross_margin_90d', gross_margin_90d,
    'gross_margin_180d', gross_margin_180d
  )
  || jsonb_build_object(
    'unit_profit_cost_90d', unit_profit_cost_90d,
    'unit_profit_cost_180d', unit_profit_cost_180d,
    'unit_profit_base_90d', unit_profit_base_90d,
    'unit_profit_base_180d', unit_profit_base_180d,
    'market_count_90d', market_count_90d,
    'market_count_180d', market_count_180d,
    'market_count_365d', market_count_365d,
    'market_count_total', market_count_total,
    'customer_count_90d', customer_count_90d,
    'customer_count_180d', customer_count_180d,
    'customer_count_365d', customer_count_365d,
    'customer_count_total', customer_count_total,
    'sales_index_90d', sales_index_90d,
    'sales_index_180d', sales_index_180d,
    'sales_index_365d', sales_index_365d,
    'sales_index_total', sales_index_total,
    'price_index_90d', price_index_90d,
    'margin_index_90d', margin_index_90d,
    'performance_score', performance_score,
    'performance_bucket', performance_bucket,
    'idle_cost_usd', idle_cost_usd,
    'stock_days_90d', stock_days_90d,
    'stock_days_180d', stock_days_180d,
    'stock_days_365d', stock_days_365d,
    'stock_days_total', stock_days_total,
    'stock_turnover_90d', stock_turnover_90d,
    'stock_turnover_180d', stock_turnover_180d,
    'stock_turnover_365d', stock_turnover_365d,
    'stock_turnover_total', stock_turnover_total
  )
) AS row_json
FROM (
  SELECT
    COALESCE(%s, '') AS group_value,
    COUNT(*)::integer AS row_count,
    (ARRAY_AGG(product_code ORDER BY performance_score DESC NULLS LAST))[1] AS image_product_code,
    (ARRAY_AGG(color_code ORDER BY performance_score DESC NULLS LAST))[1] AS image_color_code,
    (ARRAY_AGG(yaka_kodu ORDER BY performance_score DESC NULLS LAST))[1] AS image_yaka_kodu,
    COALESCE((ARRAY_AGG(NULLIF(product_code,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS product_code,
    COALESCE((ARRAY_AGG(NULLIF(color_code,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS color_code,
    COALESCE((ARRAY_AGG(NULLIF(yaka_kodu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS yaka_kodu,
    COALESCE((ARRAY_AGG(NULLIF(item_description,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS item_description,
    COALESCE((ARRAY_AGG(NULLIF(kategori,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS kategori,
    COALESCE((ARRAY_AGG(NULLIF(NULLIF(askili_yan,''), '-') ORDER BY performance_score DESC NULLS LAST))[1], '') AS askili_yan,
    COALESCE((ARRAY_AGG(NULLIF(urun_ilk_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_ilk_grubu,
    COALESCE((ARRAY_AGG(NULLIF(urun_ana_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_ana_grubu,
    COALESCE((ARRAY_AGG(NULLIF(urun_alt_grubu,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS urun_alt_grubu,
    COALESCE((ARRAY_AGG(NULLIF(market_key,'') ORDER BY performance_score DESC NULLS LAST))[1], '') AS market_key,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) AS stock_qty,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN avg_stock_90d ELSE 0 END),0) AS avg_stock_90d,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN avg_stock_180d ELSE 0 END),0) AS avg_stock_180d,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN avg_stock_365d ELSE 0 END),0) AS avg_stock_365d,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN avg_stock_total ELSE 0 END),0) AS avg_stock_total,
    COALESCE(SUM(sales_qty_90d),0) AS sales_qty_90d,
    COALESCE(SUM(sales_qty_180d),0) AS sales_qty_180d,
    COALESCE(SUM(sales_qty_365d),0) AS sales_qty_365d,
    COALESCE(SUM(sales_qty_total),0) AS sales_qty_total,
    COALESCE(SUM(sales_usd_90d),0) AS sales_usd_90d,
    COALESCE(SUM(sales_usd_180d),0) AS sales_usd_180d,
    COALESCE(SUM(sales_usd_365d),0) AS sales_usd_365d,
    COALESCE(SUM(sales_usd_total),0) AS sales_usd_total,
    CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_90d * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_90d,
    CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_180d * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_180d,
    CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_365d * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_365d,
    CASE WHEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END) > 0 THEN SUM(CASE WHEN stock_variant_rank = 1 THEN stock_days_total * stock_qty ELSE 0 END) / NULLIF(SUM(CASE WHEN stock_variant_rank = 1 THEN stock_qty ELSE 0 END),0) ELSE 0 END AS stock_days_total,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_usd_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS avg_price_usd_90d,
    CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_usd_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS avg_price_usd_180d,
    CASE
      WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0
      THEN SUM(CASE WHEN sales_qty_total > 0 THEN cost_price_usd * sales_qty_total ELSE 0 END)
        / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
      ELSE COALESCE(AVG(NULLIF(cost_price_usd,0)),0)
    END AS cost_price_usd,
    CASE
      WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0
      THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_usd * sales_qty_total ELSE 0 END)
        / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
      ELSE COALESCE(AVG(NULLIF(base_price_usd,0)),0)
    END AS base_price_usd,
    CASE
      WHEN SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END) > 0
      THEN SUM(CASE WHEN sales_qty_total > 0 THEN base_price_try * sales_qty_total ELSE 0 END)
        / NULLIF(SUM(CASE WHEN sales_qty_total > 0 THEN sales_qty_total ELSE 0 END),0)
      ELSE COALESCE(AVG(NULLIF(base_price_try,0)),0)
    END AS base_price_try,
    COALESCE(SUM(gross_profit_usd_90d),0) AS gross_profit_usd_90d,
    COALESCE(SUM(gross_profit_usd_180d),0) AS gross_profit_usd_180d,
    CASE WHEN SUM(sales_usd_90d) > 0 THEN SUM(gross_profit_usd_90d) / NULLIF(SUM(sales_usd_90d),0) ELSE 0 END AS gross_margin_90d,
    CASE WHEN SUM(sales_usd_180d) > 0 THEN SUM(gross_profit_usd_180d) / NULLIF(SUM(sales_usd_180d),0) ELSE 0 END AS gross_margin_180d,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_cost_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_cost_90d,
    CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_cost_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_cost_180d,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(unit_profit_base_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS unit_profit_base_90d,
    CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(unit_profit_base_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS unit_profit_base_180d,
    COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_90d,0) > 0)::integer AS market_count_90d,
    COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_180d,0) > 0)::integer AS market_count_180d,
    COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_365d,0) > 0)::integer AS market_count_365d,
    COUNT(DISTINCT NULLIF(NULLIF(display_market_key,'STOK'),'')) FILTER (WHERE COALESCE(sales_usd_total,0) > 0)::integer AS market_count_total,
    COALESCE(SUM(CASE WHEN COALESCE(sales_usd_90d,0) > 0 THEN customer_count_90d ELSE 0 END),0)::integer AS customer_count_90d,
    COALESCE(SUM(CASE WHEN COALESCE(sales_usd_180d,0) > 0 THEN customer_count_180d ELSE 0 END),0)::integer AS customer_count_180d,
    COALESCE(SUM(CASE WHEN COALESCE(sales_usd_365d,0) > 0 THEN customer_count_365d ELSE 0 END),0)::integer AS customer_count_365d,
    COALESCE(SUM(CASE WHEN COALESCE(sales_usd_total,0) > 0 THEN customer_count_total ELSE 0 END),0)::integer AS customer_count_total,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(sales_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS sales_index_90d,
    CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(sales_index_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS sales_index_180d,
    CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(sales_index_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS sales_index_365d,
    CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(sales_index_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS sales_index_total,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(price_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS price_index_90d,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(margin_index_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS margin_index_90d,
    CASE WHEN SUM(sales_qty_90d) > 0 THEN SUM(stock_turnover_90d * sales_qty_90d) / NULLIF(SUM(sales_qty_90d),0) ELSE 0 END AS stock_turnover_90d,
    CASE WHEN SUM(sales_qty_180d) > 0 THEN SUM(stock_turnover_180d * sales_qty_180d) / NULLIF(SUM(sales_qty_180d),0) ELSE 0 END AS stock_turnover_180d,
    CASE WHEN SUM(sales_qty_365d) > 0 THEN SUM(stock_turnover_365d * sales_qty_365d) / NULLIF(SUM(sales_qty_365d),0) ELSE 0 END AS stock_turnover_365d,
    CASE WHEN SUM(sales_qty_total) > 0 THEN SUM(stock_turnover_total * sales_qty_total) / NULLIF(SUM(sales_qty_total),0) ELSE 0 END AS stock_turnover_total,
    CASE
      WHEN SUM(CASE
        WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
        ELSE 0
      END) > 0
      THEN SUM(performance_score * CASE
        WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
        ELSE 0
      END) / NULLIF(SUM(CASE
        WHEN COALESCE(sales_qty_90d,0) > 0 THEN sales_qty_90d
        ELSE 0
      END),0)
      ELSE COALESCE(AVG(performance_score),0)
    END AS performance_score,
    MODE() WITHIN GROUP (ORDER BY performance_bucket) AS performance_bucket,
    COALESCE(MODE() WITHIN GROUP (ORDER BY NULLIF(recommendation,'')), '') AS recommendation,
    COALESCE(SUM(CASE WHEN stock_variant_rank = 1 THEN idle_cost_usd ELSE 0 END),0) AS idle_cost_usd
  FROM (
    SELECT
      Source.*,
      btrim(regexp_replace(COALESCE(Source.market_key,''), '^.*[|]', '')) AS display_market_key,
      CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END AS stock_turnover_90d,
      CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END AS stock_turnover_180d,
      CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END AS stock_turnover_365d,
      CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END AS stock_turnover_total,
      ROW_NUMBER() OVER (
        PARTITION BY COALESCE(%s, ''), product_code, color_code, yaka_kodu
        ORDER BY performance_score DESC NULLS LAST
      ) AS stock_variant_rank
    FROM Source
  %s
  ) s
  GROUP BY COALESCE(%s, '')
) g
ORDER BY group_value
`, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, len(args)+1, groupExpr, groupExpr, whereSQL, groupExpr)
	args = append(args, field)
	return queryProductPerformanceJSONRows(ctx, pg, query, args...)
}

func queryProductPerformanceSQLLeafRows(ctx context.Context, pg *sql.DB, mode string, filters []productPerformanceSQLGroupFilter, limit int) ([]map[string]any, error) {
	whereSQL, args, err := productPerformanceSQLFilterWhere(filters)
	if err != nil {
		return nil, err
	}
	args = append(args, limit)
	query := productPerformanceSQLSourceCTE(mode) + fmt.Sprintf(`
SELECT to_jsonb(t) || jsonb_build_object(
  'row_key', 'leaf|' || product_code || '|' || color_code || '|' || yaka_kodu || '|' || market_key,
  'stock_turnover_90d', CASE WHEN avg_stock_90d > 0 THEN sales_qty_90d / NULLIF(avg_stock_90d,0) * 4.0 ELSE 0 END,
  'stock_turnover_180d', CASE WHEN avg_stock_180d > 0 THEN sales_qty_180d / NULLIF(avg_stock_180d,0) * 2.0 ELSE 0 END,
  'stock_turnover_365d', CASE WHEN avg_stock_365d > 0 THEN sales_qty_365d / NULLIF(avg_stock_365d,0) ELSE 0 END,
  'stock_turnover_total', CASE WHEN avg_stock_total > 0 THEN sales_qty_total / NULLIF(avg_stock_total,0) * 360.0 / GREATEST(1, ((SELECT kpi_date FROM LatestKPIDate) - DATE '2022-01-01') + 1) ELSE 0 END
) AS row_json
FROM Source t
%s
ORDER BY performance_score DESC, sales_usd_90d DESC
LIMIT $%d
`, whereSQL, len(args))
	return queryProductPerformanceJSONRows(ctx, pg, query, args...)
}

func queryProductPerformanceJSONRows(ctx context.Context, pg *sql.DB, query string, args ...any) ([]map[string]any, error) {
	rows, err := pg.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []map[string]any{}
	colorCodes := []string{}
	for rows.Next() {
		var raw []byte
		if err := rows.Scan(&raw); err != nil {
			return nil, err
		}
		row := map[string]any{}
		if err := json.Unmarshal(raw, &row); err != nil {
			return nil, err
		}
		colorCodes = append(colorCodes, stringFromMap(row, "color_code"))
		out = append(out, row)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	colorDescriptions := productPerformanceColorDescriptions(ctx, colorCodes)
	for _, row := range out {
		row["color_description"] = colorDescriptions[normalizeProductPerformanceCode(stringFromMap(row, "color_code"))]
	}
	return out, nil
}

func productPerformanceSQLSourceCTE(mode string) string {
	idleWhere := ""
	if mode == "idle" {
		idleWhere = `
  AND stock_qty > 0
  AND (performance_bucket = 'STOK_RISKI' OR COALESCE(sales_qty_90d,0) = 0 OR COALESCE(stock_days_90d,0) > 180)`
	}
	return `
WITH LatestKPIDate AS (
  SELECT MAX(kpi_date) AS kpi_date
  FROM mk_product_performance_kpi_daily
),
Stock90Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code, color_code, yaka_kodu, stock_qty
  FROM mk_product_performance_stock_daily, LatestKPIDate
  WHERE stock_date <= LatestKPIDate.kpi_date - INTERVAL '89 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock180Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code, color_code, yaka_kodu, stock_qty
  FROM mk_product_performance_stock_daily, LatestKPIDate
  WHERE stock_date <= LatestKPIDate.kpi_date - INTERVAL '179 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Stock365Start AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code, color_code, yaka_kodu, stock_qty
  FROM mk_product_performance_stock_daily, LatestKPIDate
  WHERE stock_date <= LatestKPIDate.kpi_date - INTERVAL '359 days'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
StockTotalStart AS (
  SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
    product_code, color_code, yaka_kodu, stock_qty
  FROM mk_product_performance_stock_daily
  WHERE stock_date <= DATE '2022-01-01'
  ORDER BY product_code, color_code, yaka_kodu, stock_date DESC
),
Source AS (
  SELECT
    product_code,
    color_code,
    yaka_kodu,
    item_description,
    kategori,
    seri,
    yas_grubu,
    CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
    CASE
      WHEN btrim(COALESCE(urun_ilk_grubu,'')) = '-' THEN ''
      WHEN upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN ''
      ELSE COALESCE(urun_ilk_grubu,'')
    END AS urun_ilk_grubu,
    COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
    urun_alt_grubu,
    market_key,
    COALESCE(stock_qty,0) AS stock_qty,
    COALESCE(NULLIF(avg_stock_90d,0), (COALESCE((SELECT s.stock_qty FROM Stock90Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_90d,
    COALESCE(NULLIF(avg_stock_180d,0), (COALESCE((SELECT s.stock_qty FROM Stock180Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_180d,
    COALESCE(NULLIF(avg_stock_365d,0), (COALESCE((SELECT s.stock_qty FROM Stock365Start s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_365d,
    COALESCE(NULLIF(avg_stock_total,0), (COALESCE((SELECT s.stock_qty FROM StockTotalStart s WHERE s.product_code = mk_product_performance_kpi_daily.product_code AND s.color_code = mk_product_performance_kpi_daily.color_code AND s.yaka_kodu = mk_product_performance_kpi_daily.yaka_kodu),0) + COALESCE(stock_qty,0)) / 2.0) AS avg_stock_total,
    COALESCE(sales_qty_90d,0) AS sales_qty_90d,
    COALESCE(sales_qty_180d,0) AS sales_qty_180d,
    COALESCE(sales_qty_365d,0) AS sales_qty_365d,
    COALESCE(sales_qty_total,0) AS sales_qty_total,
    COALESCE(sales_usd_90d,0) AS sales_usd_90d,
    COALESCE(sales_usd_180d,0) AS sales_usd_180d,
    COALESCE(sales_usd_365d,0) AS sales_usd_365d,
    COALESCE(sales_usd_total,0) AS sales_usd_total,
    COALESCE(stock_days_90d,0) AS stock_days_90d,
    COALESCE(stock_days_180d,0) AS stock_days_180d,
    COALESCE(stock_days_365d,0) AS stock_days_365d,
    COALESCE(stock_days_total,0) AS stock_days_total,
    COALESCE(avg_price_usd_90d,0) AS avg_price_usd_90d,
    COALESCE(avg_price_usd_180d,0) AS avg_price_usd_180d,
    COALESCE(cost_price_usd,0) AS cost_price_usd,
    COALESCE(base_price_usd,0) AS base_price_usd,
    COALESCE(base_price_try,0) AS base_price_try,
    COALESCE(gross_profit_usd_90d,0) AS gross_profit_usd_90d,
    COALESCE(gross_profit_usd_180d,0) AS gross_profit_usd_180d,
    COALESCE(gross_margin_90d,0) AS gross_margin_90d,
    COALESCE(gross_margin_180d,0) AS gross_margin_180d,
    COALESCE(unit_profit_cost_90d,0) AS unit_profit_cost_90d,
    COALESCE(unit_profit_cost_180d,0) AS unit_profit_cost_180d,
    COALESCE(unit_profit_base_90d,0) AS unit_profit_base_90d,
    COALESCE(unit_profit_base_180d,0) AS unit_profit_base_180d,
    COALESCE(market_count_90d,0) AS market_count_90d,
    COALESCE(market_count_180d,0) AS market_count_180d,
    COALESCE(market_count_365d,0) AS market_count_365d,
    COALESCE(market_count_total,0) AS market_count_total,
    COALESCE(customer_count_90d,0) AS customer_count_90d,
    COALESCE(customer_count_180d,0) AS customer_count_180d,
    COALESCE(customer_count_365d,0) AS customer_count_365d,
    COALESCE(customer_count_total,0) AS customer_count_total,
    COALESCE(sales_index_90d,0) AS sales_index_90d,
    COALESCE(sales_index_180d,0) AS sales_index_180d,
    COALESCE(sales_index_365d,0) AS sales_index_365d,
    COALESCE(sales_index_total,0) AS sales_index_total,
    COALESCE(price_index_90d,0) AS price_index_90d,
    COALESCE(margin_index_90d,0) AS margin_index_90d,
    COALESCE(performance_score,0) AS performance_score,
    COALESCE(performance_bucket,'') AS performance_bucket,
    COALESCE(recommendation,'') AS recommendation,
    COALESCE(stock_qty,0) * COALESCE(cost_price_usd,0) AS idle_cost_usd
  FROM mk_product_performance_kpi_daily
  WHERE kpi_date = (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily)
    AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')` + idleWhere + `
)`
}

func productPerformanceSQLGroupExpr(field string) (string, bool) {
	switch field {
	case "urun_ilk_grubu":
		return productPerformanceCleanFirstGroupSQL("urun_ilk_grubu"), true
	case "askili_yan":
		return "CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END", true
	case "urun_ana_grubu":
		return "COALESCE(NULLIF(btrim(COALESCE(urun_ana_grubu,'')), ''), NULLIF(btrim(COALESCE(urun_alt_grubu,'')), ''), NULLIF(btrim(COALESCE(kategori,'')), ''), '')", true
	case "kategori", "urun_alt_grubu", "product_code", "item_description", "color_code", "yaka_kodu", "performance_bucket":
		return field, true
	case "color_yaka":
		return "concat_ws('/', NULLIF(btrim(COALESCE(color_code,'')), ''), NULLIF(btrim(COALESCE(yaka_kodu,'')), ''))", true
	case "market_key":
		return "btrim(regexp_replace(COALESCE(market_key,''), '^.*[|]', ''))", true
	default:
		return "", false
	}
}

func shouldSkipProductPerformanceGroupValue(field, value string) bool {
	switch field {
	case "urun_ilk_grubu", "askili_yan":
		return strings.TrimSpace(value) == ""
	default:
		return false
	}
}

func productPerformanceSQLFilterWhere(filters []productPerformanceSQLGroupFilter) (string, []any, error) {
	if len(filters) == 0 {
		return "", nil, nil
	}
	parts := make([]string, 0, len(filters))
	args := make([]any, 0, len(filters))
	for _, filter := range filters {
		expr, ok := productPerformanceSQLGroupExpr(filter.Field)
		if !ok {
			return "", nil, fmt.Errorf("unsupported product performance filter field: %s", filter.Field)
		}
		values := filter.Values
		if len(values) == 0 && strings.TrimSpace(filter.Value) != "" {
			values = []string{filter.Value}
		}
		values = cleanProductPerformanceFilterValues(values)
		if len(values) == 0 {
			continue
		}
		args = append(args, pq.Array(values))
		parts = append(parts, fmt.Sprintf("COALESCE(%s, '') = ANY($%d)", expr, len(args)))
	}
	if len(parts) == 0 {
		return "", nil, nil
	}
	return "WHERE " + strings.Join(parts, " AND "), args, nil
}

func cleanProductPerformanceFilterValues(values []string) []string {
	out := make([]string, 0, len(values))
	seen := map[string]bool{}
	for _, value := range values {
		clean := normalizeProductPerformanceGroupValue(value)
		if clean == "" || seen[clean] {
			continue
		}
		seen[clean] = true
		out = append(out, clean)
		if len(out) >= 300 {
			break
		}
	}
	return out
}

func filterProductPerformanceGroupedRows(rows []map[string]any, filters map[string][]string) []map[string]any {
	if len(filters) == 0 || len(rows) == 0 {
		return rows
	}
	cleanFilters := map[string]map[string]bool{}
	for field, values := range filters {
		cleanValues := cleanProductPerformanceFilterValues(values)
		if len(cleanValues) == 0 {
			continue
		}
		set := map[string]bool{}
		for _, value := range cleanValues {
			set[value] = true
		}
		cleanFilters[field] = set
	}
	if len(cleanFilters) == 0 {
		return rows
	}
	out := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		matches := true
		for field, allowed := range cleanFilters {
			if !allowed[productPerformanceGroupedFilterValue(row, field)] {
				matches = false
				break
			}
		}
		if matches {
			out = append(out, row)
		}
	}
	return out
}

func filterProductPerformancePreparedGroupedRows(rows []map[string]any, filters map[string][]string) []map[string]any {
	if len(filters) == 0 || len(rows) == 0 {
		return rows
	}
	cleanFilters := map[string]map[string]bool{}
	for field, values := range filters {
		cleanValues := cleanProductPerformanceFilterValues(values)
		if len(cleanValues) == 0 {
			continue
		}
		set := map[string]bool{}
		for _, value := range cleanValues {
			set[value] = true
		}
		cleanFilters[field] = set
	}
	if len(cleanFilters) == 0 {
		return rows
	}

	rowByKey := make(map[string]map[string]any, len(rows))
	for _, row := range rows {
		if key := stringFromMap(row, "key"); key != "" {
			rowByKey[key] = row
		}
	}

	matchedKeys := map[string]bool{}
	allowedKeys := map[string]bool{}
	for _, row := range rows {
		key := stringFromMap(row, "key")
		if key == "" {
			continue
		}
		if !productPerformancePreparedGroupedPathMatches(key, rowByKey, cleanFilters) {
			continue
		}
		matchedKeys[key] = true
		for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
			allowedKeys[ancestor] = true
		}
	}
	if len(allowedKeys) == 0 {
		return []map[string]any{}
	}
	out := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		key := stringFromMap(row, "key")
		if allowedKeys[key] || productPerformanceGroupRowHasMatchedAncestor(key, matchedKeys) {
			out = append(out, row)
		}
	}
	return out
}

func productPerformancePreparedGroupedPathMatches(key string, rowByKey map[string]map[string]any, filters map[string]map[string]bool) bool {
	pathKeys := productPerformanceGroupKeyAncestors(key, true)
	if len(pathKeys) == 0 {
		pathKeys = []string{key}
	}
	for field, allowed := range filters {
		matched := false
		for _, pathKey := range pathKeys {
			row := rowByKey[pathKey]
			if row == nil {
				continue
			}
			if allowed[productPerformancePreparedGroupedFilterValue(row, field)] {
				matched = true
				break
			}
		}
		if !matched {
			return false
		}
	}
	return true
}

func productPerformancePreparedGroupedFilterValue(row map[string]any, field string) string {
	field = strings.TrimSpace(field)
	groupField := strings.TrimSpace(stringFromMap(row, "group_field"))
	if groupField == field {
		return normalizeProductPerformanceGroupValue(stringFromMap(row, "group_value"))
	}
	switch field {
	case "market_key", "color_yaka", "urun_ilk_grubu", "askili_yan":
		value := normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
		if value != "" {
			return value
		}
	}
	if value := normalizeProductPerformanceGroupValue(stringFromMap(row, field)); value != "" {
		return value
	}
	return ""
}

func productPerformanceGroupRowHasMatchedAncestor(key string, matchedKeys map[string]bool) bool {
	for _, ancestor := range productPerformanceGroupKeyAncestors(key, true) {
		if matchedKeys[ancestor] {
			return true
		}
	}
	return false
}

func filterProductPerformanceGroupedRowsForExpansion(rows []map[string]any, expandedKeys map[string]bool, expandThroughLevel int, limit int) []map[string]any {
	if len(rows) == 0 {
		return rows
	}
	if limit <= 0 || limit > 50000 {
		limit = 50000
	}
	out := make([]map[string]any, 0, minInt(limit, len(rows)))
	for _, row := range rows {
		if !productPerformanceGroupedRowVisible(row, expandedKeys, expandThroughLevel) {
			continue
		}
		out = append(out, row)
		if len(out) >= limit {
			break
		}
	}
	return out
}

func productPerformanceGroupedRowVisible(row map[string]any, expandedKeys map[string]bool, expandThroughLevel int) bool {
	level := intFromMap(row, "level")
	if level <= 0 {
		return true
	}
	key := stringFromMap(row, "key")
	if key == "" {
		return true
	}
	ancestors := productPerformanceGroupKeyAncestors(key, false)
	for ancestorLevel, ancestor := range ancestors {
		if ancestorLevel <= expandThroughLevel {
			continue
		}
		if expandedKeys != nil && expandedKeys[ancestor] {
			continue
		}
		return false
	}
	return true
}

func productPerformanceGroupKeyAncestors(key string, includeSelf bool) []string {
	parts := strings.Split(strings.TrimSpace(key), "|")
	if len(parts) <= 1 {
		return nil
	}
	last := len(parts) - 1
	if !includeSelf {
		last--
	}
	if last < 1 {
		return nil
	}
	out := make([]string, 0, last)
	for i := 1; i <= last; i++ {
		out = append(out, strings.Join(parts[:i+1], "|"))
	}
	return out
}

func productPerformanceParentGroupKey(key string) string {
	parts := strings.Split(strings.TrimSpace(key), "|")
	if len(parts) <= 2 {
		return ""
	}
	return strings.Join(parts[:len(parts)-1], "|")
}

func productPerformanceGroupedFilterValue(row map[string]any, field string) string {
	switch field {
	case "urun_ana_grubu":
		value := strings.TrimSpace(stringFromMap(row, "urun_ana_grubu"))
		if value != "" {
			return normalizeProductPerformanceGroupValue(value)
		}
		if value = strings.TrimSpace(stringFromMap(row, "urun_alt_grubu")); value != "" {
			return normalizeProductPerformanceGroupValue(value)
		}
		return normalizeProductPerformanceGroupValue(stringFromMap(row, "kategori"))
	case "market_key", "color_yaka", "urun_ilk_grubu", "askili_yan":
		return normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
	default:
		return normalizeProductPerformanceGroupValue(stringFromMap(row, field))
	}
}

func productPerformanceGroupedSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
	if rows, err := productPerformanceGroupedRawSnapshotSourceRows(ctx, pg, mode, limit); err != nil {
		return nil, err
	} else if rows != nil {
		return rows, nil
	}

	switch mode {
	case "products":
		rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: limit, Page: 1, SortBy: "performance_score", Descending: true})
		return structsToMaps(rows), err
	case "idle":
		rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: limit, Page: 1, SortBy: "performance_score", Descending: true})
		if err != nil {
			return nil, err
		}
		return productPerformanceIdleSourceRows(structsToMaps(rows)), nil
	case "sales_color_yaka_market_customer", "sales_product_country_segment_market_customer", "sales_market_customer_product", "sales_country_segment_market_customer_product":
		rows, err := ListProductPerformanceSalesBreakdown(ctx, pg, productPerformanceSalesBreakdownMode(mode), limit)
		return structsToMaps(rows), err
	case "order_product_customers":
		rows, err := ListProductPerformanceOrderProductCustomers(ctx, pg, limit)
		return structsToMaps(rows), err
	case "order_market_details":
		rows, err := ListProductPerformanceOrderMarketDetails(ctx, pg, limit)
		return structsToMaps(rows), err
	default:
		rows, _, err := ListProductPerformance(ctx, pg, ProductPerformanceFilters{Limit: limit, Page: 1, SortBy: "performance_score", Descending: true})
		return structsToMaps(rows), err
	}
}

func productPerformanceGroupedRawSnapshotSourceRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, error) {
	reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
	if !ok {
		return nil, nil
	}
	rows, exists, err := loadProductPerformanceSnapshotMapRows(ctx, pg, reportKey, limit)
	if err != nil {
		return nil, err
	}
	if !exists {
		return nil, nil
	}
	if strings.TrimSpace(mode) == "idle" {
		normalizeProductPerformanceGroupedSourceScores(rows, mode)
		return productPerformanceIdleSourceRows(rows), nil
	}
	if mode == "products" || mode == "product_detail" {
		rows = mergeProductPerformanceGeneralSnapshotMetrics(ctx, pg, rows)
		rows = mergeProductPerformanceSalesSpreadKeys(ctx, pg, rows)
	}
	normalizeProductPerformanceGroupedSourceScores(rows, mode)
	return rows, nil
}

func normalizeProductPerformanceGroupedSourceScores(rows []map[string]any, mode string) {
	if !shouldCascadeProductPerformanceGroupedScores(mode) {
		return
	}
	for _, row := range rows {
		deriveProductPerformanceGroupMetrics(row, "")
		for _, suffix := range productPerformancePeriodSuffixes() {
			row["performance_score_"+suffix] = productPerformanceSalesPeriodScore(row, suffix)
		}
		row["performance_score"] = row["performance_score_90d"]
	}
}

func mergeProductPerformanceGeneralSnapshotMetrics(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
	if len(rows) == 0 {
		return rows
	}
	generalRows, ok, err := loadProductPerformanceSnapshotMapRows(ctx, pg, productPerformanceSnapshotKey("general"), 50000)
	if err != nil || !ok || len(generalRows) == 0 {
		return rows
	}
	byKey := make(map[string]map[string]any, len(generalRows))
	for _, row := range generalRows {
		key := productPerformanceMapMarketVariantKey(row)
		if key != "" {
			byKey[key] = row
		}
	}
	for _, row := range rows {
		general := byKey[productPerformanceMapMarketVariantKey(row)]
		if general == nil {
			continue
		}
		for _, field := range []string{
			"market_count_total", "customer_count_total", "invoice_count_total", "sales_index_total",
			"avg_price_usd_total", "gross_profit_usd_total", "gross_margin_total",
			"unit_profit_cost_total", "unit_profit_base_total", "first_sale_date",
		} {
			if value, ok := general[field]; ok {
				row[field] = value
			}
		}
		if value, ok := general["performance_score"]; ok {
			if _, exists := row["performance_score_total"]; !exists {
				row["performance_score_total"] = value
			}
		}
	}
	return rows
}

func mergeProductPerformanceSalesSpreadKeys(ctx context.Context, pg *sql.DB, rows []map[string]any) []map[string]any {
	if len(rows) == 0 {
		return rows
	}
	const query = `
WITH Latest AS (
  SELECT COALESCE(
    (SELECT MAX(kpi_date) FROM mk_product_performance_kpi_daily),
    (SELECT MAX(sales_date) FROM mk_product_performance_sales_daily),
    current_date
  )::date AS kpi_date
),
Spread AS (
  SELECT
    s.product_code,
    s.color_code,
    s.yaka_kodu,
    s.market_key,
    COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND display_market_key NOT IN ('', 'STOK', '-')
    ), '[]'::jsonb) AS market_keys_90d,
    COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND display_market_key NOT IN ('', 'STOK', '-')
    ), '[]'::jsonb) AS market_keys_180d,
    COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND display_market_key NOT IN ('', 'STOK', '-')
    ), '[]'::jsonb) AS market_keys_365d,
    COALESCE(jsonb_agg(DISTINCT display_market_key) FILTER (
      WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND display_market_key NOT IN ('', 'STOK', '-')
    ), '[]'::jsonb) AS market_keys_total,
    COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '89 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
    ), '[]'::jsonb) AS customer_keys_90d,
    COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '179 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
    ), '[]'::jsonb) AS customer_keys_180d,
    COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
      WHERE s.sales_date BETWEEN Latest.kpi_date - INTERVAL '359 days' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
    ), '[]'::jsonb) AS customer_keys_365d,
    COALESCE(jsonb_agg(DISTINCT btrim(s.customer_code)) FILTER (
      WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
        AND COALESCE(s.sales_usd,0) > 0
        AND btrim(COALESCE(s.customer_code,'')) NOT IN ('', '-')
    ), '[]'::jsonb) AS customer_keys_total
  FROM (
    SELECT
      s.*,
      btrim(regexp_replace(COALESCE(s.market_key,''), '^.*[|]', '')) AS display_market_key
    FROM mk_product_performance_sales_daily s
  ) s
  CROSS JOIN Latest
  WHERE s.sales_date BETWEEN DATE '2022-01-01' AND Latest.kpi_date
    AND upper(translate(btrim(COALESCE(s.urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
  GROUP BY s.product_code, s.color_code, s.yaka_kodu, s.market_key
)
SELECT jsonb_build_object(
  'product_code', product_code,
  'color_code', color_code,
  'yaka_kodu', yaka_kodu,
  'market_key', market_key,
  '__market_keys_90d', market_keys_90d,
  '__market_keys_180d', market_keys_180d,
  '__market_keys_365d', market_keys_365d,
  '__market_keys_total', market_keys_total,
  '__customer_keys_90d', customer_keys_90d,
  '__customer_keys_180d', customer_keys_180d,
  '__customer_keys_365d', customer_keys_365d,
  '__customer_keys_total', customer_keys_total
)
FROM Spread`
	spreadRows, err := queryProductPerformanceJSONRows(ctx, pg, query)
	if err != nil {
		log.Printf("[ProductPerformanceRefresh] product sales spread keys skipped err=%v", err)
		return rows
	}
	byKey := make(map[string]map[string]any, len(spreadRows))
	for _, row := range spreadRows {
		key := productPerformanceMapMarketVariantKey(row)
		if key != "" {
			byKey[key] = row
		}
	}
	for _, row := range rows {
		spread := byKey[productPerformanceMapMarketVariantKey(row)]
		if spread == nil {
			continue
		}
		for _, suffix := range productPerformancePeriodSuffixes() {
			for _, prefix := range []string{"__market_keys_", "__customer_keys_"} {
				field := prefix + suffix
				if value, ok := spread[field]; ok {
					row[field] = value
				}
			}
		}
	}
	return rows
}

func productPerformanceMapMarketVariantKey(row map[string]any) string {
	productCode := normalizeProductPerformanceProductCode(stringFromMap(row, "product_code"))
	if productCode == "" {
		return ""
	}
	return strings.Join([]string{
		productCode,
		strings.TrimSpace(stringFromMap(row, "color_code")),
		strings.TrimSpace(stringFromMap(row, "yaka_kodu")),
		displayProductPerformanceMarketName(stringFromMap(row, "market_key")),
	}, "|")
}

func productPerformanceGroupedSnapshotRows(ctx context.Context, pg *sql.DB, mode string, limit int) ([]map[string]any, bool, error) {
	reportKey, ok := productPerformanceGroupedSnapshotKey(mode)
	if !ok {
		return nil, false, nil
	}
	rows, exists, err := loadProductPerformanceSnapshotMapRows(ctx, pg, reportKey, limit)
	if err != nil || !exists {
		return rows, exists, err
	}
	if strings.TrimSpace(mode) == "idle" {
		return productPerformanceIdleSourceRows(rows), true, nil
	}
	return rows, true, nil
}

func productPerformanceGroupedSnapshotReportKey(mode string, levels []string, mainGroup string) string {
	levels = sanitizeProductPerformanceGroupLevels(levels)
	if len(levels) == 0 {
		levels = defaultProductPerformanceGroupLevels(mode)
	}
	parts := []string{"grouped", strings.TrimSpace(mode), productPerformanceGroupedLevelsKey(levels)}
	if strings.TrimSpace(mainGroup) != "" {
		parts = append(parts, strings.TrimSpace(mainGroup))
	}
	return productPerformanceSnapshotKey(parts...)
}

func productPerformanceGroupedLevelsKey(levels []string) string {
	levels = sanitizeProductPerformanceGroupLevels(levels)
	if len(levels) == 0 {
		return ""
	}
	return strings.Join(levels, ">")
}

func productPerformanceGroupedSnapshotKey(mode string) (string, bool) {
	switch strings.TrimSpace(mode) {
	case "products", "product_detail", "idle", "":
		return productPerformanceSnapshotKey("products"), true
	case "order_product_customers":
		return productPerformanceSnapshotKey("order-product-customers"), true
	case "order_market_details":
		return productPerformanceSnapshotKey("order-market-details"), true
	case "sales_color_yaka_market_customer", "sales_product_country_segment_market_customer", "sales_market_customer_product", "sales_country_segment_market_customer_product":
		return productPerformanceSnapshotKey("sales-breakdown", productPerformanceSalesBreakdownMode(mode)), true
	default:
		return "", false
	}
}

func productPerformanceSalesBreakdownMode(mode string) string {
	switch strings.TrimSpace(mode) {
	case "sales_color_yaka_market_customer":
		return "color_yaka_market_customer"
	case "sales_product_country_segment_market_customer":
		return "product_country_segment_market_customer"
	case "sales_market_customer_product":
		return "market_customer_product"
	case "sales_country_segment_market_customer_product":
		return "country_segment_market_customer_product"
	default:
		return mode
	}
}

func productPerformanceIdleSourceRows(rows []map[string]any) []map[string]any {
	out := make([]map[string]any, 0, len(rows))
	for _, row := range rows {
		stockQty := floatFromMap(row, "stock_qty")
		salesQty90 := floatFromMap(row, "sales_qty_90d")
		stockDays90 := floatFromMap(row, "stock_days_90d")
		if stockQty <= 0 {
			continue
		}
		if stringFromMap(row, "performance_bucket") != "STOK_RISKI" && salesQty90 != 0 && stockDays90 <= 180 {
			continue
		}
		next := cloneMap(row)
		next["idle_cost_usd"] = stockQty * floatFromMap(row, "cost_price_usd")
		out = append(out, next)
	}
	return out
}

type productPerformanceGroupedSnapshotAvgState struct {
	Weighted float64
	Weight   float64
	Sum      float64
	Count    float64
}

type productPerformanceGroupedSnapshotNode struct {
	Key             string
	Level           int
	Field           string
	Value           string
	Row             map[string]any
	Count           int
	Children        map[string]*productPerformanceGroupedSnapshotNode
	ChildOrder      []string
	StockMetricSeen map[string]map[string]bool
	IdleSeen        map[string]bool
	Avg             map[string]*productPerformanceGroupedSnapshotAvgState
	BucketCounts    map[string]int
	Image           map[string]any
	MarketSeen      map[string]map[string]bool
	CustomerSeen    map[string]map[string]bool
}

func buildProductPerformanceGroupedSnapshotRows(sourceRows []map[string]any, levels []string, mode string) []map[string]any {
	roots := map[string]*productPerformanceGroupedSnapshotNode{}
	rootOrder := make([]string, 0)
	for _, row := range sourceRows {
		parentKeys := []string{"tab:" + mode}
		parentChildren := roots
		parentOrder := &rootOrder
		visualLevel := 0
		for _, field := range levels {
			value := normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
			if shouldSkipProductPerformanceGroupValue(field, value) {
				continue
			}
			keyPart := field + ":" + value
			key := strings.Join(append(parentKeys, keyPart), "|")
			node := parentChildren[key]
			if node == nil {
				node = &productPerformanceGroupedSnapshotNode{
					Key:      key,
					Level:    visualLevel,
					Field:    field,
					Value:    value,
					Row:      map[string]any{},
					Children: map[string]*productPerformanceGroupedSnapshotNode{},
				}
				parentChildren[key] = node
				*parentOrder = append(*parentOrder, key)
			}
			node.add(row)
			parentKeys = append(parentKeys, keyPart)
			parentChildren = node.Children
			parentOrder = &node.ChildOrder
			visualLevel++
		}
	}

	out := make([]map[string]any, 0, len(sourceRows))
	appendProductPerformanceGroupedSnapshotNodes(&out, roots, rootOrder)
	applyProductPerformanceGroupedChildScoreAverages(out, mode)
	return out
}

func applyProductPerformanceGroupedChildScoreAverages(rows []map[string]any, mode string) {
	if !shouldCascadeProductPerformanceGroupedScores(mode) || len(rows) == 0 {
		return
	}
	byKey := make(map[string]map[string]any, len(rows))
	children := map[string][]map[string]any{}
	maxLevel := -1
	for _, row := range rows {
		key := stringFromMap(row, "key")
		if key == "" {
			continue
		}
		byKey[key] = row
		if level := intFromMap(row, "level"); level > maxLevel {
			maxLevel = level
		}
	}
	for _, row := range rows {
		key := stringFromMap(row, "key")
		parentKey := productPerformanceParentGroupKey(key)
		if parentKey == "" || byKey[parentKey] == nil {
			continue
		}
		children[parentKey] = append(children[parentKey], row)
	}
	for level := maxLevel - 1; level >= 0; level-- {
		for _, row := range rows {
			if intFromMap(row, "level") != level {
				continue
			}
			childRows := children[stringFromMap(row, "key")]
			if len(childRows) == 0 {
				continue
			}
			for _, suffix := range productPerformancePeriodSuffixes() {
				field := "performance_score_" + suffix
				if !productPerformanceRowsHaveField(childRows, field) {
					continue
				}
				row[field] = weightedAverageProductPerformanceScoreRows(childRows, field, suffix)
			}
			for _, suffix := range productPerformancePeriodSuffixes() {
				field := "stock_turnover_" + suffix
				if !productPerformanceRowsHaveField(childRows, field) {
					continue
				}
				row[field] = weightedAverageProductPerformanceRows(childRows, field, "sales_qty_"+suffix)
			}
			if score, ok := productPerformanceOptionalFloat(row, "performance_score_90d"); ok {
				row["performance_score"] = score
			}
		}
	}
}

func shouldCascadeProductPerformanceGroupedScores(mode string) bool {
	switch strings.TrimSpace(mode) {
	case "products",
		"product_detail",
		"idle",
		"sales_color_yaka_market_customer",
		"sales_product_country_segment_market_customer",
		"sales_market_customer_product",
		"sales_country_segment_market_customer_product":
		return true
	default:
		return false
	}
}

func productPerformanceRowsHaveField(rows []map[string]any, field string) bool {
	for _, row := range rows {
		if _, ok := productPerformanceOptionalFloat(row, field); ok {
			return true
		}
	}
	return false
}

func (n *productPerformanceGroupedSnapshotNode) add(row map[string]any) {
	n.Count++
	if n.Image == nil && stringFromMap(row, "product_code") != "" {
		n.Image = row
	}
	if bucket := stringFromMap(row, "performance_bucket"); bucket != "" {
		if n.BucketCounts == nil {
			n.BucketCounts = map[string]int{}
		}
		n.BucketCounts[bucket]++
	}
	for key, value := range row {
		if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) || isProductPerformanceMarginField(key) {
			continue
		}
		switch {
		case isProductPerformanceDistinctVariantStockMetric(key):
			n.addDistinctVariantMetric(row, key, value)
		case key == "idle_cost_usd":
			variantKey := productPerformanceMapVariantKey(row)
			if variantKey == "" {
				n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
				continue
			}
			if n.IdleSeen == nil {
				n.IdleSeen = map[string]bool{}
			}
			if !n.IdleSeen[variantKey] {
				n.IdleSeen[variantKey] = true
				n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
			}
		case shouldAverageProductPerformanceField(key):
			if n.Avg == nil {
				n.Avg = map[string]*productPerformanceGroupedSnapshotAvgState{}
			}
			state := n.Avg[key]
			if state == nil {
				state = &productPerformanceGroupedSnapshotAvgState{}
				n.Avg[key] = state
			}
			number := floatFromAny(value)
			weight := productPerformanceMetricWeight(row, key)
			if weight > 0 {
				state.Weighted += number * weight
				state.Weight += weight
			}
			state.Sum += number
			state.Count++
		case shouldSumProductPerformanceField(key):
			n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
		default:
			if _, ok := n.Row[key]; !ok {
				n.Row[key] = value
			}
		}
	}
	n.addDistinctSpread(row)
}

func (n *productPerformanceGroupedSnapshotNode) addDistinctVariantMetric(row map[string]any, key string, value any) {
	variantKey := productPerformanceMapVariantKey(row)
	if variantKey == "" {
		n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
		return
	}
	if n.StockMetricSeen == nil {
		n.StockMetricSeen = map[string]map[string]bool{}
	}
	seen := n.StockMetricSeen[key]
	if seen == nil {
		seen = map[string]bool{}
		n.StockMetricSeen[key] = seen
	}
	if seen[variantKey] {
		return
	}
	seen[variantKey] = true
	n.Row[key] = floatFromAny(n.Row[key]) + floatFromAny(value)
}

func (n *productPerformanceGroupedSnapshotNode) addDistinctSpread(row map[string]any) {
	market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
	if market != "" && market != "STOK" {
		for _, suffix := range productPerformancePeriodSuffixes() {
			if floatFromMap(row, "sales_usd_"+suffix) > 0 {
				if n.MarketSeen == nil {
					n.MarketSeen = map[string]map[string]bool{}
				}
				if n.MarketSeen[suffix] == nil {
					n.MarketSeen[suffix] = map[string]bool{}
				}
				n.MarketSeen[suffix][market] = true
			}
		}
	}
	customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
	if customer != "" && customer != "-" {
		for _, suffix := range productPerformancePeriodSuffixes() {
			if floatFromMap(row, "sales_usd_"+suffix) > 0 {
				if n.CustomerSeen == nil {
					n.CustomerSeen = map[string]map[string]bool{}
				}
				if n.CustomerSeen[suffix] == nil {
					n.CustomerSeen[suffix] = map[string]bool{}
				}
				n.CustomerSeen[suffix][customer] = true
			}
		}
	}
	for _, suffix := range productPerformancePeriodSuffixes() {
		n.MarketSeen = productPerformanceAddSeenStrings(n.MarketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
		n.CustomerSeen = productPerformanceAddSeenStrings(n.CustomerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
	}
}

func appendProductPerformanceGroupedSnapshotNodes(out *[]map[string]any, nodes map[string]*productPerformanceGroupedSnapshotNode, order []string) {
	sort.SliceStable(order, func(i, j int) bool {
		left := nodes[order[i]]
		right := nodes[order[j]]
		if left == nil || right == nil {
			return order[i] < order[j]
		}
		return strings.Compare(left.Value, right.Value) < 0
	})
	for _, key := range order {
		node := nodes[key]
		if node == nil {
			continue
		}
		*out = append(*out, node.snapshotRow())
		if len(node.Children) > 0 {
			appendProductPerformanceGroupedSnapshotNodes(out, node.Children, node.ChildOrder)
		}
	}
}

func (n *productPerformanceGroupedSnapshotNode) snapshotRow() map[string]any {
	row := cloneMap(n.Row)
	for key, state := range n.Avg {
		if state == nil {
			continue
		}
		if state.Weight > 0 {
			row[key] = state.Weighted / state.Weight
		} else if state.Count > 0 {
			row[key] = state.Sum / state.Count
		}
	}
	applyProductPerformanceDistinctSpread(row, n.MarketSeen, n.CustomerSeen)
	deriveProductPerformanceGroupMetrics(row, n.Field)
	if bucket := n.dominantBucket(); bucket != "" {
		row["performance_bucket"] = bucket
	}
	clearProductPerformanceGroupDimensions(row, n.Field, n.Value)
	row["__group"] = true
	row["row_key"] = "group|" + n.Key
	row["key"] = n.Key
	row["level"] = n.Level
	row["group_field"] = n.Field
	row["group_value"] = n.Value
	row["label"] = n.Value
	row["count"] = n.Count
	row["recommendation"] = productPerformanceGroupRecommendation(row, n.Field, n.Count)
	image := n.Image
	if image == nil {
		image = row
	}
	row["image_product_code"] = stringFromMap(image, "product_code")
	row["image_color_code"] = stringFromMap(image, "color_code")
	row["image_yaka_kodu"] = stringFromMap(image, "yaka_kodu")
	return row
}

func (n *productPerformanceGroupedSnapshotNode) dominantBucket() string {
	best := ""
	bestCount := 0
	for bucket, count := range n.BucketCounts {
		if count > bestCount || (count == bestCount && bucket < best) {
			best = bucket
			bestCount = count
		}
	}
	return best
}

func appendProductPerformanceGroupedRows(out *[]map[string]any, sourceRows []map[string]any, levels []string, level int, visualLevel int, parentKeys []string, expandedKeys map[string]bool, expandThroughLevel int) {
	if level >= len(levels) {
		return
	}

	field := levels[level]
	grouped := make(map[string][]map[string]any)
	for _, row := range sourceRows {
		value := normalizeProductPerformanceGroupValue(mapGroupValue(row, field))
		grouped[value] = append(grouped[value], row)
	}

	values := make([]string, 0, len(grouped))
	for value := range grouped {
		values = append(values, value)
	}
	sort.Slice(values, func(i, j int) bool {
		return strings.Compare(values[i], values[j]) < 0
	})

	for _, value := range values {
		groupRows := grouped[value]
		if shouldSkipProductPerformanceGroupValue(field, value) {
			appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel, parentKeys, expandedKeys, expandThroughLevel)
			continue
		}
		keyPart := field + ":" + value
		key := strings.Join(append(parentKeys, keyPart), "|")
		*out = append(*out, makeProductPerformanceGroupedRow(key, visualLevel, field, value, groupRows))
		if expandedKeys[key] || level <= expandThroughLevel {
			appendProductPerformanceGroupedRows(out, groupRows, levels, level+1, visualLevel+1, append(parentKeys, keyPart), expandedKeys, expandThroughLevel)
		}
	}
}

func makeProductPerformanceGroupedRow(key string, level int, field, value string, rows []map[string]any) map[string]any {
	row := aggregateProductPerformanceRows(rows, field)
	clearProductPerformanceGroupDimensions(row, field, value)
	row["__group"] = true
	row["row_key"] = "group|" + key
	row["key"] = key
	row["level"] = level
	row["group_field"] = field
	row["group_value"] = value
	row["label"] = value
	row["count"] = len(rows)
	row["recommendation"] = productPerformanceGroupRecommendation(row, field, len(rows))

	image := firstProductPerformanceImageSource(rows)
	row["image_product_code"] = stringFromMap(image, "product_code")
	row["image_color_code"] = stringFromMap(image, "color_code")
	row["image_yaka_kodu"] = stringFromMap(image, "yaka_kodu")
	return row
}

var productPerformanceGroupDimensionFields = map[string]bool{
	"product_code":     true,
	"color_yaka":       true,
	"color_code":       true,
	"yaka_kodu":        true,
	"item_description": true,
	"kategori":         true,
	"askili_yan":       true,
	"urun_ilk_grubu":   true,
	"urun_ana_grubu":   true,
	"urun_alt_grubu":   true,
	"market_key":       true,
	"country":          true,
	"customer_segment": true,
	"customer_code":    true,
	"customer_name":    true,
}

func clearProductPerformanceGroupDimensions(row map[string]any, groupField, groupValue string) {
	for field := range productPerformanceGroupDimensionFields {
		row[field] = ""
	}
	if productPerformanceGroupDimensionFields[groupField] {
		row[groupField] = groupValue
	}
}

func aggregateProductPerformanceRows(rows []map[string]any, groupField string) map[string]any {
	out := map[string]any{}
	marketSeen := map[string]map[string]bool{}
	customerSeen := map[string]map[string]bool{}
	for _, row := range rows {
		addProductPerformanceDistinctSpread(row, marketSeen, customerSeen)
		for key, value := range row {
			if key == "row_key" || key == "key" || isProductPerformanceInternalGroupField(key) {
				continue
			}
			if isProductPerformanceMarginField(key) {
				continue
			}
			if isProductPerformanceDistinctVariantStockMetric(key) {
				out[key] = distinctProductPerformanceVariantNumber(rows, key)
				continue
			}
			if key == "idle_cost_usd" {
				out[key] = distinctProductPerformanceVariantStockCost(rows)
				continue
			}
			if shouldAverageProductPerformanceField(key) {
				continue
			}
			if shouldSumProductPerformanceField(key) {
				out[key] = floatFromAny(out[key]) + floatFromAny(value)
			} else if _, ok := out[key]; !ok {
				out[key] = value
			}
		}
	}
	for _, row := range rows {
		for key := range row {
			if isProductPerformanceInternalGroupField(key) {
				continue
			}
			if isProductPerformanceMarginField(key) {
				continue
			}
			if shouldAverageProductPerformanceField(key) {
				out[key] = weightedAverageProductPerformanceRows(rows, key, productPerformanceMetricWeightField(key))
			}
		}
	}
	applyProductPerformanceDistinctSpread(out, marketSeen, customerSeen)
	deriveProductPerformanceGroupMetrics(out, groupField)
	out["performance_bucket"] = dominantProductPerformanceValue(rows, "performance_bucket")
	return out
}

func addProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
	market := displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
	if market != "" && market != "STOK" {
		for _, suffix := range productPerformancePeriodSuffixes() {
			if floatFromMap(row, "sales_usd_"+suffix) > 0 {
				if marketSeen[suffix] == nil {
					marketSeen[suffix] = map[string]bool{}
				}
				marketSeen[suffix][market] = true
			}
		}
	}
	customer := strings.TrimSpace(stringFromMap(row, "customer_code"))
	if customer != "" && customer != "-" {
		for _, suffix := range productPerformancePeriodSuffixes() {
			if floatFromMap(row, "sales_usd_"+suffix) > 0 {
				if customerSeen[suffix] == nil {
					customerSeen[suffix] = map[string]bool{}
				}
				customerSeen[suffix][customer] = true
			}
		}
	}
	for _, suffix := range productPerformancePeriodSuffixes() {
		marketSeen = productPerformanceAddSeenStrings(marketSeen, suffix, productPerformanceStringSliceFromMap(row, "__market_keys_"+suffix))
		customerSeen = productPerformanceAddSeenStrings(customerSeen, suffix, productPerformanceStringSliceFromMap(row, "__customer_keys_"+suffix))
	}
}

func applyProductPerformanceDistinctSpread(row map[string]any, marketSeen, customerSeen map[string]map[string]bool) {
	for _, suffix := range productPerformancePeriodSuffixes() {
		marketField := "market_count_" + suffix
		customerField := "customer_count_" + suffix
		if seen := marketSeen[suffix]; len(seen) > 0 {
			row[marketField] = len(seen)
		}
		if seen := customerSeen[suffix]; len(seen) > 0 {
			row[customerField] = len(seen)
		}
		if floatFromMap(row, "sales_usd_"+suffix) > 0 && (suffix == "90d" || suffix == "total") {
			if intFromMap(row, marketField) == 0 {
				row[marketField] = 1
			}
			if intFromMap(row, customerField) == 0 {
				if suffix == "total" {
					row[customerField] = maxInt(1, intFromMap(row, "customer_count_90d"))
				} else {
					row[customerField] = 1
				}
			}
		}
	}
}

func productPerformanceAddSeenStrings(seen map[string]map[string]bool, suffix string, values []string) map[string]map[string]bool {
	if len(values) == 0 {
		return seen
	}
	if seen == nil {
		seen = map[string]map[string]bool{}
	}
	if seen[suffix] == nil {
		seen[suffix] = map[string]bool{}
	}
	for _, value := range values {
		value = strings.TrimSpace(value)
		if value == "" || value == "-" {
			continue
		}
		seen[suffix][value] = true
	}
	return seen
}

func productPerformanceStringSliceFromMap(row map[string]any, field string) []string {
	value, ok := row[field]
	if !ok || value == nil {
		return nil
	}
	switch v := value.(type) {
	case []string:
		return v
	case []any:
		out := make([]string, 0, len(v))
		for _, item := range v {
			text := strings.TrimSpace(fmt.Sprint(item))
			if text != "" {
				out = append(out, text)
			}
		}
		return out
	case string:
		text := strings.TrimSpace(v)
		if text == "" {
			return nil
		}
		var parsed []string
		if strings.HasPrefix(text, "[") && json.Unmarshal([]byte(text), &parsed) == nil {
			return parsed
		}
		return []string{text}
	default:
		return nil
	}
}

func isProductPerformanceInternalGroupField(field string) bool {
	return strings.HasPrefix(field, "__")
}

func productPerformanceGroupRecommendation(row map[string]any, groupField string, count int) string {
	groupField = strings.TrimSpace(groupField)
	salesQty90 := floatFromMap(row, "sales_qty_90d")
	salesUSD90 := floatFromMap(row, "sales_usd_90d")
	stockQty := floatFromMap(row, "stock_qty")
	marginCost := floatFromMap(row, "gross_margin_cost_90d")
	if marginCost == 0 {
		marginCost = floatFromMap(row, "gross_margin_90d")
	}
	switch groupField {
	case "market_key":
		if salesQty90 <= 0 && stockQty > 0 {
			return "Bu piyasada son 90 gunde satis yok"
		}
		if marginCost < 0 {
			return "Bu piyasada ciplak marj negatif"
		}
		return fmt.Sprintf("Piyasa satisi %.0f adet / %.0f USD", salesQty90, salesUSD90)
	case "color_yaka":
		if salesQty90 <= 0 && stockQty > 0 {
			return "Renk/yaka stokta, son 90 gun satisi yok"
		}
		if marginCost < 0 {
			return "Renk/yaka ciplak marji negatif"
		}
		return fmt.Sprintf("Renk/yaka satisi %.0f adet", salesQty90)
	default:
		if recommendation := strings.TrimSpace(stringFromMap(row, "recommendation")); recommendation != "" {
			return recommendation
		}
		return fmt.Sprintf("%d satir", count)
	}
}

func isProductPerformanceMarginField(field string) bool {
	return strings.HasPrefix(field, "gross_margin")
}

func isProductPerformanceDistinctVariantStockMetric(field string) bool {
	return field == "stock_qty" || strings.HasPrefix(field, "avg_stock_")
}

func distinctProductPerformanceVariantNumber(rows []map[string]any, field string) float64 {
	seen := map[string]bool{}
	total := 0.0
	hasKey := false
	for _, row := range rows {
		key := productPerformanceMapVariantKey(row)
		if key == "" {
			continue
		}
		hasKey = true
		if seen[key] {
			continue
		}
		seen[key] = true
		total += floatFromMap(row, field)
	}
	if hasKey {
		return total
	}
	for _, row := range rows {
		total += floatFromMap(row, field)
	}
	return total
}

func distinctProductPerformanceVariantStockCost(rows []map[string]any) float64 {
	seen := map[string]bool{}
	total := 0.0
	hasKey := false
	for _, row := range rows {
		key := productPerformanceMapVariantKey(row)
		if key == "" {
			continue
		}
		hasKey = true
		if seen[key] {
			continue
		}
		seen[key] = true
		total += floatFromMap(row, "stock_qty") * floatFromMap(row, "cost_price_usd")
	}
	if hasKey {
		return total
	}
	for _, row := range rows {
		total += floatFromMap(row, "idle_cost_usd")
	}
	return total
}

func productPerformanceMapVariantKey(row map[string]any) string {
	productCode := stringFromMap(row, "product_code")
	colorCode := stringFromMap(row, "color_code")
	yakaKodu := stringFromMap(row, "yaka_kodu")
	if productCode == "" && colorCode == "" && yakaKodu == "" {
		return ""
	}
	return productPerformanceVariantKey(productCode, colorCode, yakaKodu)
}

func deriveProductPerformanceGroupMetrics(out map[string]any, groupField string) {
	normalizeProductPerformanceCostFields(out)
	isGroup := strings.TrimSpace(groupField) != ""
	if isGroup {
		out["group_field"] = groupField
	}
	preservedCustomerScores := map[string]float64{}
	preservedProductScores := map[string]float64{}
	preservedStockTurnovers := map[string]float64{}
	for _, suffix := range productPerformancePeriodSuffixes() {
		if score, ok := productPerformanceOptionalFloat(out, "customer_score_"+suffix); ok {
			preservedCustomerScores[suffix] = score
		}
		if score, ok := productPerformanceOptionalFloat(out, "performance_score_"+suffix); ok {
			preservedProductScores[suffix] = score
		}
		if isGroup {
			if turnover, ok := productPerformanceOptionalFloat(out, "stock_turnover_"+suffix); ok {
				preservedStockTurnovers[suffix] = turnover
			}
		}
	}
	if score, ok := productPerformanceOptionalFloat(out, "performance_score"); ok {
		if _, exists := preservedProductScores["90d"]; !exists {
			preservedProductScores["90d"] = score
		}
	}
	for _, suffix := range productPerformancePeriodSuffixes() {
		sales := floatFromMap(out, "sales_usd_"+suffix)
		qty := floatFromMap(out, "sales_qty_"+suffix)
		stockQty := floatFromMap(out, "stock_qty")
		turnoverBase := floatFromMap(out, "avg_stock_"+suffix)
		if turnoverBase <= 0 && !isGroup {
			turnoverBase = stockQty
		}
		days := productPerformancePeriodDays(out, suffix)
		avgDaily := floatFromMap(out, "avg_daily_sales_"+suffix)
		if days > 0 {
			avgDaily = qty / days
			out["avg_daily_sales_"+suffix] = avgDaily
		}
		if avgDaily > 0 && turnoverBase > 0 {
			out["stock_days_"+suffix] = turnoverBase / avgDaily
		} else if qty <= 0 && stockQty > 0 {
			out["stock_days_"+suffix] = 9999
		} else if _, ok := out["stock_days_"+suffix]; !ok {
			out["stock_days_"+suffix] = 0
		}
		out["stock_turnover_"+suffix] = productPerformanceAnnualizedStockTurnover(qty, turnoverBase, days)
		if turnover, ok := preservedStockTurnovers[suffix]; ok {
			out["stock_turnover_"+suffix] = turnover
		}
		if qty > 0 {
			out["avg_price_usd_"+suffix] = sales / qty
		}
		basePrice, hasBasePrice := productPerformanceUnitCostForSuffix(out, "base_price_usd", suffix)
		costPrice, hasCostPrice := productPerformanceUnitCostForSuffix(out, "cost_price_usd", suffix)
		if hasBasePrice {
			out["base_price_usd_"+suffix] = basePrice
		}
		if hasCostPrice {
			out["cost_price_usd_"+suffix] = costPrice
		}
		if qty > 0 && hasBasePrice {
			avgPrice := sales / qty
			out["unit_profit_base_"+suffix] = avgPrice - basePrice
			out["gross_profit_base_usd_"+suffix] = sales - (qty * basePrice)
		} else {
			out["unit_profit_base_"+suffix] = 0
			out["gross_profit_base_usd_"+suffix] = 0
		}
		if qty > 0 && hasCostPrice {
			avgPrice := sales / qty
			out["unit_profit_cost_"+suffix] = avgPrice - costPrice
			out["gross_profit_cost_usd_"+suffix] = sales - (qty * costPrice)
			out["gross_profit_usd_"+suffix] = out["gross_profit_cost_usd_"+suffix]
		} else {
			out["unit_profit_cost_"+suffix] = 0
			out["gross_profit_cost_usd_"+suffix] = 0
			out["gross_profit_usd_"+suffix] = 0
		}
		if sales > 0 {
			out["gross_margin_base_"+suffix] = floatFromMap(out, "gross_profit_base_usd_"+suffix) / sales
			out["gross_margin_cost_"+suffix] = floatFromMap(out, "gross_profit_cost_usd_"+suffix) / sales
			out["gross_margin_"+suffix] = floatFromMap(out, "gross_profit_usd_"+suffix) / sales
		} else {
			out["gross_margin_base_"+suffix] = 0
			out["gross_margin_cost_"+suffix] = 0
			out["gross_margin_"+suffix] = 0
		}
	}
	orderUSD := floatFromMap(out, "order_usd")
	if orderUSD > 0 {
		out["expected_margin_base"] = floatFromMap(out, "expected_profit_base_usd") / orderUSD
		out["expected_margin_cost"] = floatFromMap(out, "expected_profit_cost_usd") / orderUSD
	}
	orderQty := floatFromMap(out, "order_qty")
	if orderQty > 0 {
		out["avg_order_price_usd"] = orderUSD / orderQty
	}
	if _, ok := out["net_stock_after_order"]; ok || orderQty > 0 {
		out["net_stock_after_order"] = floatFromMap(out, "stock_qty") - orderQty
	}
	for _, suffix := range productPerformancePeriodSuffixes() {
		if score, ok := preservedCustomerScores[suffix]; ok {
			out["customer_score_"+suffix] = score
		} else {
			out["customer_score_"+suffix] = productPerformanceCustomerSalesPeriodScore(rowPeriodMetricMap(out, suffix))
		}
		if score, ok := preservedProductScores[suffix]; ok {
			out["performance_score_"+suffix] = score
		} else {
			out["performance_score_"+suffix] = productPerformanceSalesPeriodScore(out, suffix)
		}
	}
	out["performance_score"] = out["performance_score_90d"]
	if floatFromMap(out, "order_qty") > 0 || floatFromMap(out, "order_usd") > 0 {
		score := productPerformanceOrderGroupScore(out)
		out["performance_score_90d"] = score
		out["performance_score_180d"] = score
		out["performance_score_365d"] = score
		out["performance_score_total"] = score
		out["performance_score"] = score
	}
}

func normalizeProductPerformanceCostFields(row map[string]any) {
	normalize := func(costField, baseField string) {
		cost, hasCost := productPerformanceOptionalFloat(row, costField)
		base, hasBase := productPerformanceOptionalFloat(row, baseField)
		if !hasCost && !hasBase {
			return
		}
		cost, base = normalizeProductPerformanceCostPair(cost, base)
		if hasCost || cost > 0 {
			row[costField] = cost
		}
		if hasBase || base > 0 {
			row[baseField] = base
		}
	}
	normalize("cost_price_usd", "base_price_usd")
	for _, suffix := range productPerformancePeriodSuffixes() {
		normalize("cost_price_usd_"+suffix, "base_price_usd_"+suffix)
	}
}

func productPerformancePeriodSuffixes() []string {
	return []string{"90d", "180d", "365d", "total"}
}

func productPerformancePeriodDays(row map[string]any, suffix string) float64 {
	switch suffix {
	case "90d":
		return 90
	case "180d":
		return 180
	case "365d":
		return 360
	case "total":
		start := parseProductPerformanceDate(stringFromMap(row, "period_start"))
		if start.IsZero() {
			start = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
		}
		end := parseProductPerformanceDate(stringFromMap(row, "period_end"))
		if end.IsZero() {
			end = parseProductPerformanceDate(stringFromMap(row, "kpi_date"))
		}
		if end.IsZero() || end.Before(start) {
			return 0
		}
		return end.Sub(start).Hours()/24 + 1
	default:
		return 0
	}
}

const productPerformanceStockTurnoverYearDays = 360.0

func productPerformanceAnnualizedStockTurnover(salesQty, avgStock, periodDays float64) float64 {
	if salesQty <= 0 || avgStock <= 0 {
		return 0
	}
	raw := salesQty / avgStock
	if periodDays <= 0 {
		return raw
	}
	return raw * productPerformanceStockTurnoverYearDays / periodDays
}

func parseProductPerformanceDate(value string) time.Time {
	value = strings.TrimSpace(value)
	if value == "" {
		return time.Time{}
	}
	if len(value) >= len("2006-01-02") {
		value = value[:len("2006-01-02")]
	}
	t, err := time.Parse("2006-01-02", value)
	if err != nil {
		return time.Time{}
	}
	return t
}

func productPerformanceOptionalFloat(row map[string]any, field string) (float64, bool) {
	value, ok := row[field]
	if !ok {
		return 0, false
	}
	return floatFromAny(value), true
}

func productPerformanceUnitCostForSuffix(row map[string]any, baseField, suffix string) (float64, bool) {
	hasSuffix := false
	if value, ok := row[baseField+"_"+suffix]; ok {
		hasSuffix = true
		if n := floatFromAny(value); n > 0 {
			return n, true
		}
	}
	if value, ok := row[baseField]; ok {
		return floatFromAny(value), true
	}
	if hasSuffix {
		return 0, true
	}
	return 0, false
}

func productPerformanceGroupScore(row map[string]any, groupField string) float64 {
	if floatFromMap(row, "order_qty") > 0 || floatFromMap(row, "order_usd") > 0 {
		return productPerformanceOrderGroupScore(row)
	}
	if isProductPerformanceCustomerGroup(groupField) {
		return productPerformanceCustomerSalesGroupScore(row)
	}
	return productPerformanceSalesGroupScore(row)
}

func isProductPerformanceCustomerGroup(groupField string) bool {
	switch strings.TrimSpace(groupField) {
	case "customer_code", "customer_name":
		return true
	default:
		return false
	}
}

func productPerformanceSalesGroupScore(row map[string]any) float64 {
	return productPerformanceSalesPeriodScore(row, "90d")
}

func productPerformanceSalesPeriodScore(row map[string]any, suffix string) float64 {
	salesUSD := floatFromMap(row, "sales_usd_"+suffix)
	salesIndex := floatFromMap(row, "sales_index_"+suffix)
	if salesIndex <= 0 && suffix == "total" {
		salesIndex = floatFromMap(row, "sales_index_total")
	}
	margin := floatFromMap(row, "gross_margin_cost_"+suffix)
	if margin == 0 {
		margin = floatFromMap(row, "gross_margin_"+suffix)
	}
	salesQty := floatFromMap(row, "sales_qty_"+suffix)
	stockTurnover := floatFromMap(row, "stock_turnover_"+suffix)
	if stockTurnover == 0 {
		avgStock := floatFromMap(row, "avg_stock_"+suffix)
		if avgStock <= 0 && strings.TrimSpace(stringFromMap(row, "group_field")) == "" {
			avgStock = floatFromMap(row, "stock_qty")
		}
		if avgStock > 0 {
			stockTurnover = productPerformanceAnnualizedStockTurnover(salesQty, avgStock, productPerformancePeriodDays(row, suffix))
		}
	}
	return productPerformanceProductScore(
		suffix,
		salesUSD,
		salesIndex,
		margin,
		stockTurnover,
		productPerformancePeriodCount(row, "market_count", suffix),
		productPerformancePeriodCount(row, "customer_count", suffix),
		productPerformancePeriodDays(row, suffix),
	)
}

func productPerformanceCustomerSalesGroupScore(row map[string]any) float64 {
	scores := []float64{
		floatFromMap(row, "customer_score_90d"),
		floatFromMap(row, "customer_score_180d"),
		floatFromMap(row, "customer_score_365d"),
		floatFromMap(row, "customer_score_total"),
	}
	total := 0.0
	count := 0.0
	for _, score := range scores {
		if score <= 0 {
			continue
		}
		total += score
		count++
	}
	if count == 0 {
		return 0
	}
	return total / count
}

func productPerformanceCustomerSalesPeriodScore(row map[string]any) float64 {
	salesUSD := floatFromMap(row, "sales_usd")
	margin := floatFromMap(row, "gross_margin_cost")
	if margin == 0 {
		margin = floatFromMap(row, "gross_margin")
	}
	salesQty := floatFromMap(row, "sales_qty")
	productCount := floatFromMap(row, "product_count")
	if groupCount := floatFromMap(row, "product_group_count"); groupCount > 0 {
		productCount = groupCount
	}
	suffix := strings.TrimSpace(stringFromMap(row, "suffix"))
	if suffix == "" {
		suffix = "90d"
	}
	return productPerformanceCustomerScore(suffix, salesUSD, margin, productCount, salesQty)
}

func productPerformanceProductScore(suffix string, salesUSD, salesIndex, margin, stockTurnover, marketCount, customerCount float64, periodDays ...float64) float64 {
	if salesUSD <= 0 {
		return 1
	}
	days := productPerformanceScorePeriodDays(suffix, periodDays...)
	revenueScore := productPerformanceRevenueScore(suffix, salesUSD, salesIndex, productPerformanceProductRevenueTargetForDays(suffix, days))
	score := 0.30*productPerformanceMarginComponentScore(margin) +
		0.20*productPerformanceRatioScore(stockTurnover, productPerformanceStockTurnoverTarget(suffix)) +
		0.20*revenueScore +
		0.05*productPerformanceRatioScore(marketCount, productPerformanceMarketSpreadTarget(suffix)) +
		0.25*productPerformanceRatioScore(customerCount, productPerformanceCustomerSpreadTargetForDays(suffix, days))
	return productPerformanceRoundScore(score)
}

func productPerformanceStockTurnoverTarget(suffix string) float64 {
	return 4
}

func productPerformanceMarketSpreadTarget(suffix string) float64 {
	switch suffix {
	case "90d":
		return 3
	case "180d":
		return 3
	case "365d":
		return 6
	default:
		return 6
	}
}

func productPerformanceCustomerSpreadTarget(suffix string) float64 {
	return productPerformanceCustomerSpreadTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
}

func productPerformanceCustomerSpreadTargetForDays(suffix string, periodDays float64) float64 {
	switch suffix {
	case "90d":
		return 8
	case "180d":
		return 20
	case "365d":
		return 50
	default:
		return 20 * productPerformanceTotalPeriodMultiplier(periodDays)
	}
}

func productPerformanceCustomerScore(suffix string, salesUSD, margin, productGroupCount, salesQty float64) float64 {
	if salesUSD <= 0 {
		return 1
	}
	score := 0.35*productPerformanceRatioScore(salesUSD, productPerformanceCustomerRevenueTarget(suffix)) +
		0.30*productPerformanceMarginComponentScore(margin) +
		0.20*productPerformanceRatioScore(productGroupCount, 8) +
		0.15*productPerformanceRatioScore(salesQty, productPerformanceCustomerQtyTarget(suffix))
	return productPerformanceRoundScore(score)
}

func productPerformanceRevenueScore(suffix string, salesUSD, salesIndex, absoluteTarget float64) float64 {
	return productPerformanceRatioScore(salesUSD, absoluteTarget)
}

func productPerformanceRelativeIndex(value, average float64) float64 {
	if value <= 0 || average <= 0 {
		return 0
	}
	return value / average
}

func productPerformanceMarginComponentScore(margin float64) float64 {
	return productPerformanceRatioScore(maxFloat(0, margin), 0.45)
}

func productPerformanceRatioScore(value, target float64) float64 {
	if target <= 0 || value <= 0 {
		return 0
	}
	return minFloat(100, maxFloat(0, value)*100/target)
}

func productPerformanceRoundScore(score float64) float64 {
	if score < 0 {
		score = 0
	}
	if score > 100 {
		score = 100
	}
	return math.Round(score*10000) / 10000
}

func productPerformanceProductRevenueTarget(suffix string) float64 {
	return productPerformanceProductRevenueTargetForDays(suffix, productPerformanceScorePeriodDays(suffix))
}

func productPerformanceProductRevenueTargetForDays(suffix string, periodDays float64) float64 {
	switch suffix {
	case "180d":
		return 50000
	case "365d":
		return 100000
	case "total":
		return 50000 * productPerformanceTotalPeriodMultiplier(periodDays)
	default:
		return 25000
	}
}

func productPerformanceScorePeriodDays(suffix string, periodDays ...float64) float64 {
	if len(periodDays) > 0 && periodDays[0] > 0 {
		return periodDays[0]
	}
	switch suffix {
	case "90d":
		return 90
	case "180d":
		return 180
	case "365d":
		return 360
	case "total":
		return productPerformancePeriodDays(map[string]any{
			"kpi_date": time.Now().Format("2006-01-02"),
		}, "total")
	default:
		return 0
	}
}

func productPerformanceTotalPeriodMultiplier(periodDays float64) float64 {
	if periodDays <= 0 {
		periodDays = 180
	}
	return maxFloat(1, periodDays/180.0)
}

func productPerformanceCustomerRevenueTarget(suffix string) float64 {
	switch suffix {
	case "180d":
		return 50000
	case "365d":
		return 100000
	case "total":
		return 300000
	default:
		return 25000
	}
}

func productPerformanceCustomerQtyTarget(suffix string) float64 {
	switch suffix {
	case "180d":
		return 1000
	case "365d":
		return 2000
	case "total":
		return 6000
	default:
		return 500
	}
}

func productPerformancePeriodCount(row map[string]any, prefix, suffix string) float64 {
	if value, ok := row[prefix+"_"+suffix]; ok {
		if n := floatFromAny(value); n > 0 {
			return n
		}
	}
	if suffix == "total" {
		if value, ok := row[prefix+"_total"]; ok {
			return floatFromAny(value)
		}
	}
	if value, ok := row[prefix+"_90d"]; ok {
		return floatFromAny(value)
	}
	return floatFromAny(row[prefix])
}

func rowPeriodMetricMap(row map[string]any, suffix string) map[string]any {
	return map[string]any{
		"suffix":              suffix,
		"sales_usd":           floatFromMap(row, "sales_usd_"+suffix),
		"gross_margin_cost":   floatFromMap(row, "gross_margin_cost_"+suffix),
		"gross_margin":        floatFromMap(row, "gross_margin_"+suffix),
		"sales_qty":           floatFromMap(row, "sales_qty_"+suffix),
		"product_group_count": productPerformancePeriodCount(row, "product_group_count", suffix),
		"product_count":       floatFromMap(row, "product_count"),
	}
}

func productPerformanceOrderGroupScore(row map[string]any) float64 {
	orderUSD := floatFromMap(row, "order_usd")
	margin := floatFromMap(row, "expected_margin_cost")
	orderCount := floatFromMap(row, "order_count")
	orderQty := floatFromMap(row, "order_qty")
	netStockAfterOrder := floatFromMap(row, "net_stock_after_order")
	score := minFloat(35, orderUSD/1000) +
		minFloat(25, maxFloat(0, margin)*60) +
		minFloat(15, orderCount*2) +
		minFloat(15, orderQty/10)
	if netStockAfterOrder >= 0 {
		score += 10
	}
	return score
}

func minFloat(a, b float64) float64 {
	if a < b {
		return a
	}
	return b
}

func maxFloat(a, b float64) float64 {
	if a > b {
		return a
	}
	return b
}

func sanitizeProductPerformanceGroupLevels(levels []string) []string {
	allowed := map[string]bool{
		"kategori": true, "askili_yan": true, "urun_ilk_grubu": true, "urun_ana_grubu": true, "urun_alt_grubu": true,
		"product_code": true, "item_description": true, "color_code": true, "yaka_kodu": true, "color_yaka": true, "market_key": true, "customer_code": true,
		"customer_name": true, "country": true, "customer_segment": true,
	}
	out := make([]string, 0, len(levels))
	seen := map[string]bool{}
	for _, level := range levels {
		level = strings.TrimSpace(level)
		if level == "color_code" || level == "yaka_kodu" {
			level = "color_yaka"
		}
		if allowed[level] && !seen[level] {
			out = append(out, level)
			seen[level] = true
		}
	}
	return out
}

func defaultProductPerformanceGroupLevels(mode string) []string {
	switch mode {
	case "sales_color_yaka_market_customer":
		return []string{"urun_ana_grubu", "color_yaka", "market_key", "customer_code", "customer_name", "urun_alt_grubu", "product_code"}
	case "product_detail":
		return []string{"urun_alt_grubu", "product_code", "color_yaka", "market_key"}
	case "idle":
		return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
	case "sales_product_country_segment_market_customer":
		return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "country", "customer_segment", "market_key", "customer_code", "customer_name"}
	case "sales_market_customer_product":
		return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
	case "sales_country_segment_market_customer_product":
		return []string{"country", "customer_segment", "market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
	case "order_product_customers":
		return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key", "customer_code", "customer_name"}
	case "order_market_details":
		return []string{"market_key", "customer_code", "customer_name", "urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka"}
	default:
		return []string{"urun_ilk_grubu", "askili_yan", "kategori", "urun_ana_grubu", "urun_alt_grubu", "product_code", "color_yaka", "market_key"}
	}
}

func mapGroupValue(row map[string]any, field string) string {
	switch field {
	case "urun_ilk_grubu":
		return cleanProductPerformanceFirstGroup(stringFromMap(row, "urun_ilk_grubu"))
	case "market_key":
		return displayProductPerformanceMarketName(stringFromMap(row, "market_key"))
	case "color_yaka":
		parts := make([]string, 0, 2)
		if color := strings.TrimSpace(stringFromMap(row, "color_code")); color != "" {
			parts = append(parts, color)
		}
		if yaka := strings.TrimSpace(stringFromMap(row, "yaka_kodu")); yaka != "" {
			parts = append(parts, yaka)
		}
		return strings.Join(parts, "/")
	default:
		return stringFromMap(row, field)
	}
}

func shouldSumProductPerformanceField(field string) bool {
	return strings.HasPrefix(field, "sales_qty_") ||
		strings.HasPrefix(field, "sales_usd_") ||
		strings.HasPrefix(field, "avg_daily_sales_") ||
		strings.HasPrefix(field, "gross_profit_") ||
		strings.HasPrefix(field, "invoice_count_") ||
		strings.HasPrefix(field, "market_count_") ||
		strings.HasPrefix(field, "customer_count_") ||
		strings.HasPrefix(field, "product_group_count_") ||
		strings.HasPrefix(field, "product_count_") ||
		strings.HasSuffix(field, "_qty") ||
		strings.HasSuffix(field, "_usd") ||
		strings.HasSuffix(field, "_count") ||
		strings.HasSuffix(field, "_value_usd") ||
		field == "stock_qty" ||
		field == "line_count" ||
		field == "invoice_count" ||
		field == "order_count" ||
		field == "product_count" ||
		field == "market_count" ||
		field == "customer_count" ||
		field == "overdue_qty" ||
		field == "net_stock_after_order" ||
		field == "idle_cost_usd"
}

func shouldAverageProductPerformanceField(field string) bool {
	if strings.HasPrefix(field, "avg_daily_sales_") {
		return false
	}
	return strings.HasPrefix(field, "avg_") ||
		strings.HasPrefix(field, "unit_") ||
		strings.HasPrefix(field, "base_price") ||
		strings.HasPrefix(field, "cost_price") ||
		strings.HasPrefix(field, "gross_margin") ||
		strings.HasPrefix(field, "expected_margin") ||
		strings.HasPrefix(field, "sales_index") ||
		strings.HasPrefix(field, "performance_score") ||
		strings.HasPrefix(field, "customer_score") ||
		strings.HasPrefix(field, "stock_days") ||
		strings.HasPrefix(field, "stock_turnover")
}

func productPerformanceMetricWeightField(field string) string {
	if productPerformanceUsesCostWeight(field) {
		return "__product_cost_weight"
	}
	if strings.HasPrefix(field, "stock_turnover") {
		return "sales_qty_" + productPerformanceScoreSuffix(field)
	}
	if strings.Contains(field, "_180d") {
		return "sales_qty_180d"
	}
	if strings.Contains(field, "_365d") {
		return "sales_qty_365d"
	}
	if strings.Contains(field, "_total") {
		return "sales_qty_total"
	}
	if strings.Contains(field, "order") || strings.Contains(field, "expected_") {
		return "order_qty"
	}
	if strings.Contains(field, "stock") {
		return "stock_qty"
	}
	return "sales_qty_90d"
}

func productPerformanceUsesCostWeight(field string) bool {
	if !(strings.HasPrefix(field, "base_price") || strings.HasPrefix(field, "cost_price")) {
		return false
	}
	return !strings.Contains(field, "_90d") &&
		!strings.Contains(field, "_180d") &&
		!strings.Contains(field, "_365d") &&
		!strings.Contains(field, "_total")
}

func productPerformanceMetricWeight(row map[string]any, field string) float64 {
	if productPerformanceUsesScoreWeight(field) {
		return productPerformanceScoreWeight(row, productPerformanceScoreSuffix(field))
	}
	weightField := productPerformanceMetricWeightField(field)
	if weightField == "__product_cost_weight" {
		return productPerformanceCostWeight(row)
	}
	return floatFromMap(row, weightField)
}

func productPerformanceUsesScoreWeight(field string) bool {
	return strings.HasPrefix(field, "performance_score") ||
		strings.HasPrefix(field, "customer_score")
}

func productPerformanceScoreSuffix(field string) string {
	switch {
	case strings.Contains(field, "_180d"):
		return "180d"
	case strings.Contains(field, "_365d"):
		return "365d"
	case strings.Contains(field, "_total"):
		return "total"
	default:
		return "90d"
	}
}

func productPerformanceScoreWeight(row map[string]any, suffix string) float64 {
	if weight := floatFromMap(row, "sales_qty_"+suffix); weight > 0 {
		return weight
	}
	return 0
}

func productPerformanceCostWeight(row map[string]any) float64 {
	if weight := floatFromMap(row, "sales_qty_total"); weight > 0 {
		return weight
	}
	for _, field := range []string{"sales_qty_365d", "sales_qty_180d", "sales_qty_90d"} {
		if weight := floatFromMap(row, field); weight > 0 {
			return weight
		}
	}
	return 0
}

func weightedAverageProductPerformanceRows(rows []map[string]any, valueField, qtyField string) float64 {
	if productPerformanceUsesScoreWeight(valueField) {
		return weightedAverageProductPerformanceScoreRows(rows, valueField, productPerformanceScoreSuffix(valueField))
	}
	var weighted, qty float64
	for _, row := range rows {
		value := floatFromMap(row, valueField)
		weight := floatFromMap(row, qtyField)
		if qtyField == "__product_cost_weight" {
			weight = productPerformanceCostWeight(row)
		}
		if weight > 0 {
			weighted += value * weight
			qty += weight
		}
	}
	if qty > 0 {
		return weighted / qty
	}
	if qtyField == "__product_cost_weight" {
		return averageProductPerformanceDistinctVariantField(rows, valueField)
	}
	return 0
}

func weightedAverageProductPerformanceScoreRows(rows []map[string]any, valueField, suffix string) float64 {
	var weighted, weight, sum, count float64
	for _, row := range rows {
		value, ok := productPerformanceOptionalFloat(row, valueField)
		if !ok {
			continue
		}
		rowWeight := productPerformanceScoreWeight(row, suffix)
		if rowWeight > 0 {
			weighted += value * rowWeight
			weight += rowWeight
		}
		sum += value
		count++
	}
	if weight > 0 {
		return weighted / weight
	}
	if count > 0 {
		return sum / count
	}
	return 0
}

func averageProductPerformanceDistinctVariantField(rows []map[string]any, valueField string) float64 {
	seen := map[string]bool{}
	sum := 0.0
	count := 0.0
	hasKey := false
	for _, row := range rows {
		key := productPerformanceMapVariantKey(row)
		if key == "" {
			continue
		}
		hasKey = true
		if seen[key] {
			continue
		}
		seen[key] = true
		value := floatFromMap(row, valueField)
		if value <= 0 {
			continue
		}
		sum += value
		count++
	}
	if !hasKey {
		for _, row := range rows {
			value := floatFromMap(row, valueField)
			if value <= 0 {
				continue
			}
			sum += value
			count++
		}
	}
	if count <= 0 {
		return 0
	}
	return sum / count
}

func dominantProductPerformanceValue(rows []map[string]any, field string) string {
	counts := map[string]int{}
	for _, row := range rows {
		value := stringFromMap(row, field)
		if field == "urun_ilk_grubu" {
			value = cleanProductPerformanceFirstGroup(value)
		}
		if value != "" {
			counts[value]++
		}
	}
	var best string
	var bestCount int
	for value, count := range counts {
		if count > bestCount {
			best = value
			bestCount = count
		}
	}
	return best
}

func firstProductPerformanceImageSource(rows []map[string]any) map[string]any {
	for _, row := range rows {
		if stringFromMap(row, "product_code") != "" {
			return row
		}
	}
	if len(rows) > 0 {
		return rows[0]
	}
	return map[string]any{}
}

func structsToMaps[T any](rows []T) []map[string]any {
	raw, err := json.Marshal(rows)
	if err != nil {
		return nil
	}
	var out []map[string]any
	if err := json.Unmarshal(raw, &out); err != nil {
		return nil
	}
	return out
}

func cloneMap(row map[string]any) map[string]any {
	out := make(map[string]any, len(row))
	for key, value := range row {
		out[key] = value
	}
	return out
}

func normalizeProductPerformanceGroupValue(value string) string {
	return strings.TrimSpace(value)
}

func displayProductPerformanceMarketName(value string) string {
	parts := strings.Split(value, "|")
	for i := len(parts) - 1; i >= 0; i-- {
		part := strings.TrimSpace(parts[i])
		if part != "" {
			return part
		}
	}
	return ""
}

func stringFromMap(row map[string]any, field string) string {
	value, ok := row[field]
	if !ok || value == nil {
		return ""
	}
	return strings.TrimSpace(fmt.Sprint(value))
}

func cleanProductPerformanceFirstGroup(value string) string {
	value = strings.TrimSpace(value)
	if value == "-" {
		return ""
	}
	switch normalizeProductPerformanceTurkishText(value) {
	case "YETISKIN", "YETISKIN/GARSON", "GARSON":
		return ""
	default:
		return value
	}
}

func normalizeProductPerformanceTurkishText(value string) string {
	value = strings.ToUpper(strings.TrimSpace(value))
	return strings.NewReplacer(
		"\u0130", "I",
		"\u015E", "S",
		"\u011E", "G",
		"\u00DC", "U",
		"\u00D6", "O",
		"\u00C7", "C",
		"\u0131", "I",
		"\u015F", "S",
		"\u011F", "G",
		"\u00FC", "U",
		"\u00F6", "O",
		"\u00E7", "C",
	).Replace(value)
}

func cleanProductPerformanceOptionalAttr(value string) string {
	value = strings.TrimSpace(value)
	if value == "-" {
		return ""
	}
	return value
}

func floatFromMap(row map[string]any, field string) float64 {
	return floatFromAny(row[field])
}

func intFromMap(row map[string]any, field string) int {
	return int(floatFromAny(row[field]))
}

func floatFromAny(value any) float64 {
	switch v := value.(type) {
	case float64:
		return v
	case float32:
		return float64(v)
	case int:
		return float64(v)
	case int64:
		return float64(v)
	case json.Number:
		f, _ := v.Float64()
		return f
	default:
		return 0
	}
}

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

func ListProductPerformanceSalesDetails(ctx context.Context, pg *sql.DB, productCode, colorCode, yakaKodu string, limit int) ([]models.ProductPerformanceSalesDetailRow, error) {
	if limit <= 0 || limit > 500 {
		limit = 100
	}
	rows, err := pg.QueryContext(ctx, `
SELECT
  to_char(sales_date,'YYYY-MM-DD') AS sales_date,
  last_ref_number,
  market_key,
  customer_country,
  customer_segment,
  customer_code,
  customer_name,
  sales_qty,
  sales_usd,
  avg_price_usd
FROM mk_product_performance_sales_daily
WHERE product_code=$1
  AND color_code=$2
  AND yaka_kodu=$3
  AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
ORDER BY sales_usd DESC, sales_date DESC
LIMIT $4
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu), limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceSalesDetailRow, 0, limit)
	for rows.Next() {
		var r models.ProductPerformanceSalesDetailRow
		if err := rows.Scan(&r.SalesDate, &r.RefNumber, &r.MarketKey, &r.Country, &r.CustomerSegment, &r.CustomerCode, &r.CustomerName, &r.SalesQty, &r.SalesUSD, &r.AvgPriceUSD); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func ListProductPerformanceStockSizes(ctx context.Context, productCode, colorCode, yakaKodu string) ([]models.ProductPerformanceStockSizeRow, error) {
	if db.MssqlDB == nil {
		return nil, fmt.Errorf("mssql db nil")
	}
	productCode = normalizeProductPerformanceProductCode(productCode)
	colorCode = normalizeProductPerformanceCode(colorCode)
	yakaKodu = normalizeProductPerformanceCode(yakaKodu)
	rows, err := db.MssqlDB.QueryContext(ctx, `
;WITH ActiveWarehouses AS (
    SELECT WarehouseCode
    FROM (VALUES
        ('1-0-14'),('1-0-10'),('1-0-8'),('1-2-5'),('1-2-4'),('1-0-12'),('100'),('1-0-28'),
        ('1-0-24'),('1-2-6'),('1-1-14'),('1-0-2'),('1-0-52'),('1-1-2'),('1-0-21'),('1-1-3'),
        ('1-0-33'),('101'),('1-014'),('1-0-49'),('1-0-36'),('1-0-4'),('1-0-29')
    ) W(WarehouseCode)
),
Stock AS (
  SELECT
    SizeCode = UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))),
    WarehouseCode = LTRIM(RTRIM(S.WarehouseCode)),
    InventoryQty1 = SUM(S.In_Qty1 - S.Out_Qty1)
  FROM trStock S WITH(NOLOCK)
  INNER JOIN ActiveWarehouses W
    ON W.WarehouseCode = LTRIM(RTRIM(S.WarehouseCode))
  WHERE S.ItemTypeCode = 1
    AND UPPER(LTRIM(RTRIM(S.ItemCode))) = @p1
    AND UPPER(LTRIM(RTRIM(ISNULL(S.ColorCode, '')))) = @p2
    AND UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim2Code, '')))) = @p3
  GROUP BY UPPER(LTRIM(RTRIM(ISNULL(S.ItemDim1Code, '')))), LTRIM(RTRIM(S.WarehouseCode))
),
Pick AS (
  SELECT
    SizeCode = UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim1Code, '')))),
    WarehouseCode = LTRIM(RTRIM(P.WarehouseCode)),
    PickingQty1 = SUM(P.Qty1)
  FROM PickingStates P WITH(NOLOCK)
  INNER JOIN ActiveWarehouses W
    ON W.WarehouseCode = LTRIM(RTRIM(P.WarehouseCode))
  WHERE P.ItemTypeCode = 1
    AND UPPER(LTRIM(RTRIM(P.ItemCode))) = @p1
    AND UPPER(LTRIM(RTRIM(ISNULL(P.ColorCode, '')))) = @p2
    AND UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim2Code, '')))) = @p3
  GROUP BY UPPER(LTRIM(RTRIM(ISNULL(P.ItemDim1Code, '')))), LTRIM(RTRIM(P.WarehouseCode))
),
Reserve AS (
  SELECT
    SizeCode = UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim1Code, '')))),
    WarehouseCode = LTRIM(RTRIM(R.WarehouseCode)),
    ReserveQty1 = SUM(R.Qty1)
  FROM ReserveStates R WITH(NOLOCK)
  INNER JOIN ActiveWarehouses W
    ON W.WarehouseCode = LTRIM(RTRIM(R.WarehouseCode))
  WHERE R.ItemTypeCode = 1
    AND UPPER(LTRIM(RTRIM(R.ItemCode))) = @p1
    AND UPPER(LTRIM(RTRIM(ISNULL(R.ColorCode, '')))) = @p2
    AND UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim2Code, '')))) = @p3
  GROUP BY UPPER(LTRIM(RTRIM(ISNULL(R.ItemDim1Code, '')))), LTRIM(RTRIM(R.WarehouseCode))
),
Disp AS (
  SELECT
    SizeCode = UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim1Code, '')))),
    WarehouseCode = LTRIM(RTRIM(D.WarehouseCode)),
    DispOrderQty1 = SUM(D.Qty1)
  FROM DispOrderStates D WITH(NOLOCK)
  INNER JOIN ActiveWarehouses W
    ON W.WarehouseCode = LTRIM(RTRIM(D.WarehouseCode))
  WHERE D.ItemTypeCode = 1
    AND UPPER(LTRIM(RTRIM(D.ItemCode))) = @p1
    AND UPPER(LTRIM(RTRIM(ISNULL(D.ColorCode, '')))) = @p2
    AND UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim2Code, '')))) = @p3
  GROUP BY UPPER(LTRIM(RTRIM(ISNULL(D.ItemDim1Code, '')))), LTRIM(RTRIM(D.WarehouseCode))
),
AvailableBySizeWarehouse AS (
  SELECT
    S.SizeCode,
    StockQty =
      ISNULL(S.InventoryQty1, 0)
      - ISNULL(P.PickingQty1, 0)
      - ISNULL(R.ReserveQty1, 0)
      - ISNULL(D.DispOrderQty1, 0)
  FROM Stock S
  LEFT JOIN Pick P ON P.SizeCode = S.SizeCode AND P.WarehouseCode = S.WarehouseCode
  LEFT JOIN Reserve R ON R.SizeCode = S.SizeCode AND R.WarehouseCode = S.WarehouseCode
  LEFT JOIN Disp D ON D.SizeCode = S.SizeCode AND D.WarehouseCode = S.WarehouseCode
  WHERE S.InventoryQty1 >= 0
)
SELECT
  SizeCode,
  StockQty = CAST(ROUND(SUM(StockQty), 2) AS FLOAT)
FROM AvailableBySizeWarehouse
GROUP BY SizeCode
HAVING SUM(StockQty) > 0
ORDER BY SizeCode
`, strings.TrimSpace(productCode), strings.TrimSpace(colorCode), strings.TrimSpace(yakaKodu))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := make([]models.ProductPerformanceStockSizeRow, 0, 32)
	for rows.Next() {
		var r models.ProductPerformanceStockSizeRow
		if err := rows.Scan(&r.SizeCode, &r.StockQty); err != nil {
			return nil, err
		}
		out = append(out, r)
	}
	return out, rows.Err()
}

func productPerformanceWhere(f ProductPerformanceFilters) (string, []any) {
	parts := []string{" AND " + productPerformanceAllowedFirstGroupSQL("urun_ilk_grubu")}
	args := make([]any, 0, 6)
	add := func(cond string, value any) {
		args = append(args, value)
		parts = append(parts, fmt.Sprintf(cond, len(args)))
	}
	if q := strings.TrimSpace(f.Search); q != "" {
		args = append(args, q)
		idx := len(args)
		parts = append(parts, fmt.Sprintf(" AND (product_code ILIKE '%%' || $%d || '%%' OR item_description ILIKE '%%' || $%d || '%%')", idx, idx))
	}
	if v := normalizeProductPerformanceProductCode(f.ProductCode); v != "" {
		add(" AND product_code = $%d", v)
	}
	if v := strings.TrimSpace(f.MarketKey); v != "" {
		add(" AND market_key = $%d", v)
	}
	if v := strings.TrimSpace(f.Kategori); v != "" {
		add(" AND kategori = $%d", v)
	}
	if v := strings.TrimSpace(f.Seri); v != "" {
		args = append(args, v)
		idx := len(args)
		parts = append(parts, fmt.Sprintf(" AND (seri = $%d OR urun_ana_grubu = $%d)", idx, idx))
	}
	if v := strings.TrimSpace(f.Bucket); v != "" {
		add(" AND performance_bucket = $%d", v)
	}
	return strings.Join(parts, ""), args
}

func productPerformanceHasServerFilters(f ProductPerformanceFilters) bool {
	return strings.TrimSpace(f.Search) != "" ||
		strings.TrimSpace(f.ProductCode) != "" ||
		strings.TrimSpace(f.MarketKey) != "" ||
		strings.TrimSpace(f.Kategori) != "" ||
		strings.TrimSpace(f.Seri) != "" ||
		strings.TrimSpace(f.Bucket) != "" ||
		f.Page > 1
}

func productPerformanceOrderBy(sortBy string, desc bool) string {
	allowed := map[string]string{
		"product_code":           "product_code",
		"color_code":             "color_code",
		"yaka_kodu":              "yaka_kodu",
		"item_description":       "item_description",
		"kategori":               "kategori",
		"urun_ilk_grubu":         "urun_ilk_grubu",
		"askili_yan":             "askili_yan",
		"urun_ana_grubu":         "urun_ana_grubu",
		"urun_alt_grubu":         "urun_alt_grubu",
		"market_key":             "market_key",
		"stock_qty":              "stock_qty",
		"sales_qty_90d":          "sales_qty_90d",
		"sales_qty_180d":         "sales_qty_180d",
		"sales_qty_365d":         "sales_qty_365d",
		"sales_qty_total":        "sales_qty_total",
		"sales_usd_90d":          "sales_usd_90d",
		"sales_usd_180d":         "sales_usd_180d",
		"sales_usd_365d":         "sales_usd_365d",
		"sales_usd_total":        "sales_usd_total",
		"stock_days_90d":         "stock_days_90d",
		"stock_days_180d":        "stock_days_180d",
		"stock_days_365d":        "stock_days_365d",
		"stock_days_total":       "stock_days_total",
		"stock_turnover_90d":     "stock_turnover_90d",
		"stock_turnover_180d":    "stock_turnover_180d",
		"stock_turnover_365d":    "stock_turnover_365d",
		"stock_turnover_total":   "stock_turnover_total",
		"avg_price_usd_90d":      "avg_price_usd_90d",
		"avg_price_usd_180d":     "avg_price_usd_180d",
		"base_price_usd":         "base_price_usd",
		"cost_price_usd":         "cost_price_usd",
		"unit_profit_base_90d":   "unit_profit_base_90d",
		"unit_profit_cost_90d":   "unit_profit_cost_90d",
		"unit_profit_base_180d":  "unit_profit_base_180d",
		"unit_profit_cost_180d":  "unit_profit_cost_180d",
		"gross_margin_base_90d":  "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_90d,0) END)",
		"gross_margin_cost_90d":  "(CASE WHEN COALESCE(sales_usd_90d,0) <= 0 THEN 0 ELSE (sales_usd_90d - (sales_qty_90d * COALESCE(cost_price_usd,0))) / NULLIF(sales_usd_90d,0) END)",
		"gross_margin_base_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(base_price_usd,0))) / NULLIF(sales_usd_180d,0) END)",
		"gross_margin_cost_180d": "(CASE WHEN COALESCE(sales_usd_180d,0) <= 0 THEN 0 ELSE (sales_usd_180d - (sales_qty_180d * COALESCE(cost_price_usd,0))) / NULLIF(sales_usd_180d,0) END)",
		"gross_margin_90d":       "gross_margin_90d",
		"gross_margin_180d":      "gross_margin_180d",
		"market_count_90d":       "market_count_90d",
		"customer_count_90d":     "customer_count_90d",
		"sales_index_90d":        "sales_index_90d",
		"performance_score":      "performance_score",
		"performance_bucket":     "performance_bucket",
		"last_sale_date":         "last_sale_date",
	}
	col := allowed[strings.TrimSpace(sortBy)]
	if col == "" {
		col = "performance_score"
		desc = true
	}
	dir := "ASC"
	if desc {
		dir = "DESC"
	}
	return col + " " + dir + ", product_code ASC, color_code ASC, yaka_kodu ASC"
}

type productPerformancePriceInfo struct {
	cost float64
	base float64
}

type productPerformanceAttrInfo struct {
	itemDescription string
	kategori        string
	askiliYan       string
	urunIlkGrubu    string
	urunAnaGrubu    string
	urunAltGrubu    string
}

func productPerformanceVariantKey(productCode, colorCode, yakaKodu string) string {
	return normalizeProductPerformanceProductCode(productCode) + "|" + strings.TrimSpace(colorCode) + "|" + strings.TrimSpace(yakaKodu)
}

func normalizeProductPerformanceCostPair(costPriceUSD, basePriceUSD float64) (float64, float64) {
	if costPriceUSD > 0 && basePriceUSD > 0 && costPriceUSD > basePriceUSD {
		return basePriceUSD, costPriceUSD
	}
	return costPriceUSD, basePriceUSD
}

const productPerformanceTurkishTranslateSQL = "U&'\\0130\\015E\\011E\\00DC\\00D6\\00C7\\0131\\015F\\011F\\00FC\\00F6\\00E7', 'ISGUOCisguoc'"
const productPerformanceExcludedFirstGroupsSQL = "('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')"

func productPerformanceNormalizedTextSQL(expr string) string {
	return "upper(translate(btrim(COALESCE(" + expr + ",'')), " + productPerformanceTurkishTranslateSQL + "))"
}

func productPerformanceCleanFirstGroupSQL(expr string) string {
	return "CASE WHEN btrim(COALESCE(" + expr + ",'')) = '-' THEN '' WHEN " + productPerformanceNormalizedTextSQL(expr) + " IN ('YETISKIN', 'YETISKIN/GARSON', 'GARSON') THEN '' ELSE COALESCE(" + expr + ",'') END"
}

func productPerformanceAllowedFirstGroupSQL(expr string) string {
	return productPerformanceNormalizedTextSQL(expr) + " NOT IN " + productPerformanceExcludedFirstGroupsSQL
}

func isExcludedProductPerformanceFirstGroup(value string) bool {
	switch normalizeProductPerformanceTurkishText(value) {
	case "MALZEMELI FASON", "MALZEMESIZ FASON", "MAZLEMELI FASON", "MAZEMESIZ FASON", "DIGER":
		return true
	default:
		return false
	}
}

func productPerformancePriceLookup(ctx context.Context, pg *sql.DB, productCodes []string) (map[string]productPerformancePriceInfo, error) {
	out := make(map[string]productPerformancePriceInfo, len(productCodes))
	if len(productCodes) == 0 {
		return out, nil
	}
	normalizedCodes := make([]string, 0, len(productCodes))
	seen := map[string]bool{}
	for _, productCode := range productCodes {
		code := normalizeProductPerformanceProductCode(productCode)
		if code == "" || seen[code] {
			continue
		}
		seen[code] = true
		normalizedCodes = append(normalizedCodes, code)
	}
	if len(normalizedCodes) == 0 {
		return out, nil
	}
	rows, err := pg.QueryContext(ctx, `
SELECT product_code, COALESCE(cost_price_usd,0), COALESCE(base_price_usd,0)
FROM mk_product_performance_price_dim
WHERE product_code = ANY($1)
`, pq.Array(normalizedCodes))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var productCode string
		var p productPerformancePriceInfo
		if err := rows.Scan(&productCode, &p.cost, &p.base); err != nil {
			return nil, err
		}
		p.cost, p.base = normalizeProductPerformanceCostPair(p.cost, p.base)
		out[normalizeProductPerformanceProductCode(productCode)] = p
	}
	return out, rows.Err()
}

func productPerformanceAttrLookup(ctx context.Context, pg *sql.DB, variantKeys []string) (map[string]productPerformanceAttrInfo, error) {
	out := make(map[string]productPerformanceAttrInfo, len(variantKeys))
	if len(variantKeys) == 0 {
		return out, nil
	}
	rows, err := pg.QueryContext(ctx, `
WITH Latest AS (
  SELECT MAX(kpi_date) AS kpi_date
  FROM mk_product_performance_kpi_daily
)
SELECT DISTINCT ON (product_code, color_code, yaka_kodu)
  product_code,
  color_code,
  yaka_kodu,
  COALESCE(item_description,'') AS item_description,
  COALESCE(kategori,'') AS kategori,
  CASE WHEN btrim(COALESCE(askili_yan,'')) = '-' THEN '' ELSE COALESCE(askili_yan,'') END AS askili_yan,
  COALESCE(urun_ilk_grubu,'') AS urun_ilk_grubu,
  COALESCE(urun_ana_grubu,'') AS urun_ana_grubu,
  COALESCE(urun_alt_grubu,'') AS urun_alt_grubu
FROM mk_product_performance_kpi_daily
WHERE kpi_date = (SELECT kpi_date FROM Latest)
  AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
  AND upper(translate(btrim(COALESCE(urun_ilk_grubu,'')), U&'\0130\015E\011E\00DC\00D6\00C7\0131\015F\011F\00FC\00F6\00E7', 'ISGUOCisguoc')) NOT IN ('MALZEMELI FASON', 'MALZEMESIZ FASON', 'MAZLEMELI FASON', 'MAZEMESIZ FASON', 'DIGER')
ORDER BY product_code, color_code, yaka_kodu, performance_score DESC
`, pq.Array(variantKeys))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var productCode, colorCode, yakaKodu string
		var p productPerformanceAttrInfo
		if err := rows.Scan(&productCode, &colorCode, &yakaKodu, &p.itemDescription, &p.kategori, &p.askiliYan, &p.urunIlkGrubu, &p.urunAnaGrubu, &p.urunAltGrubu); err != nil {
			return nil, err
		}
		p.askiliYan = cleanProductPerformanceOptionalAttr(p.askiliYan)
		p.urunIlkGrubu = cleanProductPerformanceFirstGroup(p.urunIlkGrubu)
		out[productPerformanceVariantKey(productCode, colorCode, yakaKodu)] = p
	}
	return out, rows.Err()
}

func productPerformanceStockLookup(ctx context.Context, pg *sql.DB, variantKeys []string) (map[string]float64, error) {
	out := make(map[string]float64, len(variantKeys))
	if len(variantKeys) == 0 {
		return out, nil
	}
	rows, err := pg.QueryContext(ctx, `
WITH Latest AS (
  SELECT MAX(stock_date) AS stock_date
  FROM mk_product_performance_stock_daily
)
SELECT
  product_code || '|' || color_code || '|' || yaka_kodu AS variant_key,
  COALESCE(SUM(stock_qty),0) AS stock_qty
FROM mk_product_performance_stock_daily
WHERE stock_date = (SELECT stock_date FROM Latest)
  AND product_code || '|' || color_code || '|' || yaka_kodu = ANY($1)
GROUP BY product_code, color_code, yaka_kodu
`, pq.Array(variantKeys))
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	for rows.Next() {
		var key string
		var stockQty float64
		if err := rows.Scan(&key, &stockQty); err != nil {
			return nil, err
		}
		out[key] = stockQty
	}
	return out, rows.Err()
}

func productPerformanceOrderRecommendation(row models.ProductPerformanceOrderAnalysisRow) (string, string) {
	switch {
	case row.OrderQty > 0 && row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
		return "STOKSUZ_TALEP", "Acik siparis stoktan buyuk. Karli talep var; uretim/satin alma onceligi ver."
	case row.ExpectedProfitCostUSD < 0:
		return "FIYAT_BASKISI", "Acik siparis ciplak maliyete gore zarar yaziyor. Fiyat/maliyet kontrol edilmeli."
	case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder >= 0:
		return "YILDIZ_URUN", "Acik siparis karli ve stok karsiliyor. Teslimat korunmali."
	case row.ExpectedMarginCost >= 0.25 && row.NetStockAfterOrder < 0:
		return "FIYAT_FIRSATI", "Karli talep var ama stok yetersiz. Uretim planina alinmali."
	case row.OverdueQty > 0:
		return "TAKIP", "Termin gecikmesi olan acik siparis var. Operasyon takibi gerekli."
	default:
		return "TAKIP", "Siparis, stok ve fiyat duzenli izlenmeli."
	}
}

func productPerformanceOrderGroupRecommendation(row models.ProductPerformanceOrderGroupRow) (string, string) {
	switch {
	case row.ExpectedProfitCostUSD < 0:
		return "FIYAT_BASKISI", "Ciplak maliyete gore zarar yazan acik siparis var. Fiyat/maliyet acil kontrol edilmeli."
	case row.ExpectedProfitBaseUSD < 0:
		return "FIYAT_BASKISI", "Taban maliyete gore brut zarar var. Satis fiyati veya iskonto kontrol edilmeli."
	case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
		return "STOKSUZ_TALEP", "Karli acik talep var ama stok yetersiz. Uretim/satin alma onceligi ver."
	case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
		return "YILDIZ_URUN", "Piyasa/musteri acik siparisleri karli. Teslimat ve stok korunmali."
	case row.OverdueQty > 0:
		return "TAKIP", "Geciken acik siparis var. Operasyon takibi gerekli."
	default:
		return "TAKIP", "Siparis karliligi ve stok yeterliligi izlenmeli."
	}
}

func productPerformanceOrderProductCustomerRecommendation(row models.ProductPerformanceOrderProductCustomerRow) (string, string) {
	switch {
	case row.ExpectedProfitCostUSD < 0:
		return "FIYAT_BASKISI", "Bu musteri/urun acik siparisi ciplak maliyete gore zarar yaziyor."
	case row.ExpectedProfitBaseUSD < 0:
		return "FIYAT_BASKISI", "Bu musteri/urun acik siparisi taban maliyete gore brut zarar yaziyor."
	case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
		return "STOKSUZ_TALEP", "Musteride karli talep var ama stok yetersiz."
	case row.ExpectedMarginCost >= 0.25 && row.ExpectedMarginBase >= 0.15:
		return "YILDIZ_URUN", "Musteri bazinda karli acik talep var."
	case row.OverdueQty > 0:
		return "TAKIP", "Bu musteri/urun kiriliminda geciken acik siparis var."
	default:
		return "TAKIP", "Musteri talebi, fiyat ve stok birlikte izlenmeli."
	}
}

func productPerformanceOrderMarketDetailRecommendation(row models.ProductPerformanceOrderMarketDetailRow) (string, string) {
	switch {
	case row.ExpectedProfitCostUSD < 0:
		return "FIYAT_BASKISI", "Siparis satiri ciplak maliyete gore zarar yaziyor."
	case row.ExpectedProfitBaseUSD < 0:
		return "FIYAT_BASKISI", "Siparis satiri taban maliyete gore brut zarar yaziyor."
	case row.NetStockAfterOrder < 0 && row.ExpectedProfitCostUSD >= 0:
		return "STOKSUZ_TALEP", "Karli siparis var ama mevcut stok siparisi karsilamiyor."
	case row.IsOverdue:
		return "TAKIP", "Termin gecmis; teslimat aksiyonu gerekli."
	case row.ExpectedMarginCost >= 0.25:
		return "YILDIZ_URUN", "Siparis karli; teslimat ve stok korunmali."
	default:
		return "TAKIP", "Siparis fiyati, stok ve termin izlenmeli."
	}
}

func dateOnly(t time.Time) time.Time {
	y, m, d := t.Date()
	return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
}

type productPerformanceSalesDaily struct {
	SalesDate        time.Time
	ProductCode      string
	ColorCode        string
	YakaKodu         string
	ItemDescription  string
	Kategori         string
	Seri             string
	YasGrubu         string
	AskiliYan        string
	UrunIlkGrubu     string
	UrunAnaGrubu     string
	UrunAltGrubu     string
	MarketKey        string
	ChannelCode      string
	CustomerCountry  string
	CustomerSegment  string
	CustomerCode     string
	CustomerName     string
	SalesQty         float64
	SalesTL          float64
	SalesUSD         float64
	AvgPriceUSD      float64
	InvoiceLineCount int
	InvoiceCount     int
	CustomerCount    int
	LastRefNumber    string
}

type productPerformanceStockDaily struct {
	StockDate         time.Time
	ProductCode       string
	ColorCode         string
	YakaKodu          string
	StockQty          float64
	InQty             float64
	OutQty            float64
	KpiInQty          float64
	KpiOutQty         float64
	SalesMovementQty  float64
	ProductionInQty   float64
	PurchaseInQty     float64
	ConsumptionOutQty float64
	CountDiffQty      float64
}
