package queries

import (
	"bssapp-backend/models"
	"bssapp-backend/utils"
	"context"
	"database/sql"
	"fmt"
	"strconv"
	"strings"
	"time"
	"unicode"
)

func GetNextOnMLNoFrom100k(ctx context.Context, uretimDB *sql.DB) (int, error) {
	// nOnMLNo is NOT identity. Generate next number safely.
	// Requirement: always continue from MAX, but never below 100001.
	sqlText := `
SELECT
	ISNULL(MAX(M.nOnMLNo), 100000) + 1 AS NextNo
FROM dbo.spUrtOnMLMas M WITH (UPDLOCK, HOLDLOCK)
`
	var next int
	if err := uretimDB.QueryRowContext(ctx, sqlText).Scan(&next); err != nil {
		return 0, err
	}
	if next < 100001 {
		next = 100001
	}
	return next, nil
}

func GetOnMLMamulTuruNoByAciklama(ctx context.Context, tx *sql.Tx, sAciklama string) (int, error) {
	sAciklama = strings.TrimSpace(sAciklama)
	if sAciklama == "" {
		return 1, nil
	}

	var existing int
	err := tx.QueryRowContext(ctx, `
SELECT TOP 1 ISNULL(nMamulTuruNo, 0)
FROM dbo.spUrtOnMLMamulTuru WITH (UPDLOCK, HOLDLOCK)
WHERE LTRIM(RTRIM(ISNULL(sAciklama,''))) = @p1
ORDER BY nMamulTuruNo
`, sAciklama).Scan(&existing)
	if err == sql.ErrNoRows {
		return 0, nil
	}
	if err != nil {
		return 0, err
	}
	return existing, nil
}

func LookupFirmaIDByKodu(ctx context.Context, uretimDB *sql.DB, firmaKodu string) (int, error) {
	firmaKodu = strings.TrimSpace(firmaKodu)
	if firmaKodu == "" {
		return 0, nil
	}
	sqlText := `
SELECT TOP 1 ISNULL(F.nFirmaID, 0) AS nFirmaID
FROM dbo.tbFirma F
WHERE LTRIM(RTRIM(ISNULL(F.sKodu, ''))) = @p1
ORDER BY F.nFirmaID
`
	var id int
	if err := uretimDB.QueryRowContext(ctx, sqlText, firmaKodu).Scan(&id); err != nil {
		if err == sql.ErrNoRows {
			return 0, nil
		}
		return 0, err
	}
	return id, nil
}

type OnMLHeaderUpsertArgs struct {
	NOnMLNo       int
	UrunKodu      string
	UrunAdi       string
	Tarihi        time.Time
	NMamulTuruNo  int
	NUrtReceteID  sql.NullInt64
	UretimSekliID sql.NullInt64
	SAciklama     sql.NullString
	NFirmaID      int
	SUser         string
	LTutarTL      float64
	LTutarUSD     float64
	LTutarEURO    float64
	SDovizCinsi   string
	LTutarDoviz   float64
}

func UpsertOnMLHeader(tx *sql.Tx, ctx context.Context, args OnMLHeaderUpsertArgs) error {
	// Note: Many NOT NULL columns exist. We use dummy/system values as per business rules.
	sqlText := `
IF EXISTS (SELECT 1 FROM dbo.spUrtOnMLMas WITH (UPDLOCK, HOLDLOCK) WHERE nOnMLNo = @p1)
BEGIN
	UPDATE dbo.spUrtOnMLMas
	SET
		UrunKodu = @p2,
		UrunAdi = @p3,
		Tarihi = @p4,
		nDonemNo = 1,
		nMamulTuruNo = @p16,
		nBolgeNo = 2,
		nZorlukNo = 1,
		bDurum = 1,
		lTutarTL = @p5,
		lTutarUSD = @p6,
		lTutarEURO = @p7,
		sDovizCinsi = @p8,
		lTutarDoviz = @p9,
		bSablon = 0,
		dteGuncellemeTarihi = GETDATE(),
		sGuncellemeKullaniciAdi = @p10,
		nUrtReceteID = @p11,
		sAciklama = @p12,
		lMasMiktar = 0,
		sRenk = NULLIF(LTRIM(RTRIM(@p13)), ''),
		nFirmaID = @p14,
		uretim_sekli_id = @p15
	WHERE nOnMLNo = @p1
END
ELSE
BEGIN
	INSERT INTO dbo.spUrtOnMLMas (
		nOnMLNo,
		UrunKodu,
		UrunAdi,
		Tarihi,
		nDonemNo,
		nMamulTuruNo,
		nBolgeNo,
		nZorlukNo,
		dteKayitTarihi,
		sKullaniciAdi,
		bDurum,
		lTutarTL,
		lTutarUSD,
		lTutarEURO,
		sDovizCinsi,
		lTutarDoviz,
		bSablon,
		dteGuncellemeTarihi,
		sGuncellemeKullaniciAdi,
		nUrtReceteID,
		sAciklama,
		lMasMiktar,
		nFirmaID,
		bVarsayilan,
		bOnay,
		bIptal,
		bRParcaTakip,
		uretim_sekli_id
	)
	VALUES (
		@p1,
		@p2,
		@p3,
		@p4,
		1,
		@p16,
		2,
		1,
		GETDATE(),
		@p10,
		1,
		@p5,
		@p6,
		@p7,
		@p8,
		@p9,
		0,
		GETDATE(),
		@p10,
		@p11,
		@p12,
		0,
		@p14,
		0,
		0,
		0,
		0,
		@p15
	)
END
`
	_, err := tx.ExecContext(
		ctx,
		sqlText,
		args.NOnMLNo,
		strings.TrimSpace(args.UrunKodu),
		strings.TrimSpace(args.UrunAdi),
		args.Tarihi,
		args.LTutarTL,
		args.LTutarUSD,
		args.LTutarEURO,
		strings.TrimSpace(args.SDovizCinsi),
		args.LTutarDoviz,
		strings.TrimSpace(args.SUser),
		args.NUrtReceteID,
		args.SAciklama,
		"", // sRenk (header) currently not driven from UI
		args.NFirmaID,
		args.UretimSekliID,
		args.NMamulTuruNo,
	)
	return err
}

// ============================================================
// V3 (Nebim) base price update
// ============================================================

func UpsertV3ItemBasePriceUSD(
	ctx context.Context,
	mssqlDB *sql.DB,
	itemCode string,
	priceDate string, // YYYY-MM-DD
	priceUSD float64,
	user string,
) error {
	itemCode = strings.TrimSpace(itemCode)
	priceDate = strings.TrimSpace(priceDate)
	user = strings.TrimSpace(user)
	if mssqlDB == nil || itemCode == "" || priceDate == "" {
		return fmt.Errorf("missing params for base price upsert")
	}

	// 1. Find a CountryCode that is NOT yet used for this specific item in prItemBasePrice.
	// We query cdCountry for a code that has no matching entry in prItemBasePrice for this item.
	// We exclude 'TR' to keep the original record safe.
	var targetCountry string
	err := mssqlDB.QueryRowContext(ctx, `
SELECT TOP 1 C.CountryCode
FROM dbo.cdCountry C
WHERE C.CountryCode <> 'TR'
  AND NOT EXISTS (
    SELECT 1 FROM dbo.prItemBasePrice P
    WHERE P.ItemTypeCode = 1
      AND LTRIM(RTRIM(P.ItemCode)) = LTRIM(RTRIM(@p1))
      AND P.BasePriceCode = 1
      AND P.CountryCode = C.CountryCode
  )
ORDER BY NEWID() -- Randomly pick one of the available country codes
`, itemCode).Scan(&targetCountry)

	if err != nil {
		if err == sql.ErrNoRows {
			// Fallback: If ALL countries are exhausted (unlikely), default to 'AD' as a last resort.
			targetCountry = "AD"
		} else {
			return fmt.Errorf("failed to find available country code: %w", err)
		}
	}

	appUser := user
	if strings.TrimSpace(appUser) == "" {
		appUser = "BSSAPP"
	} else {
		appUser = "BSSAPP:" + strings.TrimSpace(appUser)
	}

	sqlText := `
MERGE dbo.prItemBasePrice AS T
USING (
	SELECT
		@p1 AS ItemTypeCode,
		@p2 AS ItemCode,
		@p6 AS CountryCode,
		'' AS SeasonCode,
		1 AS BasePriceCode,
		CONVERT(date, @p3, 23) AS PriceDate,
		'USD' AS CurrencyCode
) AS S
ON T.ItemTypeCode = S.ItemTypeCode
 AND LTRIM(RTRIM(T.ItemCode)) = LTRIM(RTRIM(S.ItemCode))
 AND ISNULL(T.CountryCode,'') = S.CountryCode
 AND ISNULL(T.SeasonCode,'') = S.SeasonCode
 AND ISNULL(T.BasePriceCode,0) = S.BasePriceCode
WHEN MATCHED THEN
	UPDATE SET
		PriceDate = S.PriceDate,
		CurrencyCode = S.CurrencyCode,
		Price = @p4,
		LastUpdatedUserName = @p5,
		LastUpdatedDate = GETDATE()
WHEN NOT MATCHED THEN
	INSERT (
		ItemTypeCode, ItemCode, CountryCode, SeasonCode, BasePriceCode,
		PriceDate, CurrencyCode, Price, CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate
	)
	VALUES (
		S.ItemTypeCode, S.ItemCode, S.CountryCode, S.SeasonCode, S.BasePriceCode,
		S.PriceDate, S.CurrencyCode, @p4, @p5, GETDATE(), @p5, GETDATE()
	);
`

	_, err = mssqlDB.ExecContext(ctx, sqlText, 1, itemCode, priceDate, priceUSD, appUser, targetCountry)
	return err
}

func GetProductAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunAnaGrubu string, urunAltGrubu string, err error) {
	urunKodu = strings.TrimSpace(urunKodu)
	if mssqlDB == nil || urunKodu == "" {
		return "", "", nil
	}

	// Nebim V3: ProductFilterWithDescription exposes ProductAtt01Desc (ana grup) and ProductAtt02Desc (alt grup).
	sqlText := `
SELECT TOP 1
	ISNULL(ProductAtt01Desc, '') AS UrunAnaGrubu,
	ISNULL(ProductAtt02Desc, '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR')
WHERE IsBlocked = 0
  AND LTRIM(RTRIM(ProductCode)) = @p1
ORDER BY ProductCode;
`

	row := mssqlDB.QueryRowContext(ctx, sqlText, urunKodu)
	if err := row.Scan(&urunAnaGrubu, &urunAltGrubu); err != nil {
		if err == sql.ErrNoRows {
			return "", "", nil
		}
		return "", "", err
	}
	return urunAnaGrubu, urunAltGrubu, nil
}

func GetProductIlkAnaAltGrupByUrunKodu(ctx context.Context, mssqlDB *sql.DB, urunKodu string) (urunIlkGrubu string, urunAnaGrubu string, urunAltGrubu string, err error) {
	urunKodu = strings.TrimSpace(urunKodu)
	if mssqlDB == nil || urunKodu == "" {
		return "", "", "", nil
	}

	// Nebim V3: ProductFilterWithDescription exposes ProductAtt42Desc (ilk grup), ProductAtt01Desc (ana), ProductAtt02Desc (alt).
	sqlText := `
SELECT TOP 1
	ISNULL(ProductAtt42Desc, '') AS UrunIlkGrubu,
	ISNULL(ProductAtt01Desc, '') AS UrunAnaGrubu,
	ISNULL(ProductAtt02Desc, '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR')
WHERE IsBlocked = 0
  AND LTRIM(RTRIM(ProductCode)) = @p1
ORDER BY ProductCode;
`

	row := mssqlDB.QueryRowContext(ctx, sqlText, urunKodu)
	if err := row.Scan(&urunIlkGrubu, &urunAnaGrubu, &urunAltGrubu); err != nil {
		if err == sql.ErrNoRows {
			return "", "", "", nil
		}
		return "", "", "", err
	}
	return urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nil
}

func GetProductionProductCostingAnaGrupOptions(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 50
	}
	searchLike := "%" + search + "%"
	sqlText := `
SELECT TOP (@p2)
	ISNULL(NULLIF(LTRIM(RTRIM(P.ProductAtt01Desc)), ''), '') AS UrunAnaGrubu
FROM ProductFilterWithDescription('TR') AS P
WHERE P.IsBlocked = 0
  AND NULLIF(LTRIM(RTRIM(P.ProductAtt01Desc)), '') IS NOT NULL
  AND (@p1 = '' OR P.ProductAtt01Desc LIKE @p3)
GROUP BY P.ProductAtt01Desc
ORDER BY P.ProductAtt01Desc
`
	return mssqlDB.QueryContext(ctx, sqlText, search, limit, searchLike)
}

func GetProductionProductCostingAltGrupOptions(ctx context.Context, mssqlDB *sql.DB, urunAnaGrubu string, search string, limit int) (*sql.Rows, error) {
	urunAnaGrubu = strings.TrimSpace(urunAnaGrubu)
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 50
	}
	searchLike := "%" + search + "%"
	sqlText := `
SELECT TOP (@p3)
	ISNULL(NULLIF(LTRIM(RTRIM(P.ProductAtt02Desc)), ''), '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR') AS P
WHERE P.IsBlocked = 0
  AND NULLIF(LTRIM(RTRIM(P.ProductAtt02Desc)), '') IS NOT NULL
  AND (@p1 = '' OR LTRIM(RTRIM(P.ProductAtt01Desc)) = @p1)
  AND (@p2 = '' OR P.ProductAtt02Desc LIKE @p4)
GROUP BY P.ProductAtt02Desc
ORDER BY P.ProductAtt02Desc
`
	return mssqlDB.QueryContext(ctx, sqlText, urunAnaGrubu, search, limit, searchLike)
}

func GetProductionProductCostingAnaAltComboRows(ctx context.Context, mssqlDB *sql.DB, search string, limit int) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 500
	}
	if limit > 5000 {
		limit = 5000
	}
	// For very short searches, avoid leading-wildcard LIKE which can be extremely expensive on MSSQL side.
	// Example: "ce" should match "CEKET%" cheaply vs "%ce%" scanning everything.
	searchLike := "%" + search + "%"
	if len([]rune(search)) > 0 && len([]rune(search)) < 3 {
		searchLike = search + "%"
	}
	sqlText := `
SELECT TOP (@p2)
	ISNULL(NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt42Desc))), ''), '') AS UrunIlkGrubu,
	ISNULL(NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt01Desc))), ''), '') AS UrunAnaGrubu,
	ISNULL(NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt02Desc))), ''), '') AS UrunAltGrubu
FROM ProductFilterWithDescription('TR') AS P
WHERE P.IsBlocked = 0
  AND NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt42Desc))), '') IS NOT NULL
  AND NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt01Desc))), '') IS NOT NULL
  AND NULLIF(LTRIM(RTRIM(CONVERT(NVARCHAR(200), P.ProductAtt02Desc))), '') IS NOT NULL
  AND (
	@p1 = ''
	OR CONVERT(NVARCHAR(200), P.ProductAtt42Desc) LIKE @p3
	OR CONVERT(NVARCHAR(200), P.ProductAtt01Desc) LIKE @p3
	OR CONVERT(NVARCHAR(200), P.ProductAtt02Desc) LIKE @p3
  )
GROUP BY
	CONVERT(NVARCHAR(200), P.ProductAtt42Desc),
	CONVERT(NVARCHAR(200), P.ProductAtt01Desc),
	CONVERT(NVARCHAR(200), P.ProductAtt02Desc)
ORDER BY
	CONVERT(NVARCHAR(200), P.ProductAtt42Desc),
	CONVERT(NVARCHAR(200), P.ProductAtt02Desc),
	CONVERT(NVARCHAR(200), P.ProductAtt01Desc)
`
	return mssqlDB.QueryContext(ctx, sqlText, search, limit, searchLike)
}

func GetProductionProductCostingMTBolumOptions(ctx context.Context, uretimDB *sql.DB, search string, limit int) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 50
	}
	searchLike := "%" + search + "%"
	sqlText := `
SELECT TOP (@p2)
	ISNULL(B.nUrtMTBolumID, 0) AS nUrtMTBolumID,
	ISNULL(B.sAdi, '') AS sAdi
FROM dbo.spUrtMTBolum B
WHERE ISNULL(B.bAktif, 0) = 1
  AND ISNULL(B.nUrtTipiID, 0) = 1
  AND (@p1 = '' OR ISNULL(B.sAdi, '') LIKE @p3 OR CONVERT(VARCHAR(32), ISNULL(B.nUrtMTBolumID, 0)) LIKE @p3)
ORDER BY B.nUrtMTBolumID
`
	return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike)
}

// ============================================================
// Maliyet Parca Eslestirme (URETIM DB)
// ============================================================

func ListProductionProductCostingParcaMappings(ctx context.Context, uretimDB *sql.DB, urunIlkGrubu string, urunAnaGrubu string, urunAltGrubu string, nUrtMTBolumID int, onlyActive *bool) (*sql.Rows, error) {
	urunIlkGrubu = strings.TrimSpace(urunIlkGrubu)
	urunAnaGrubu = strings.TrimSpace(urunAnaGrubu)
	urunAltGrubu = strings.TrimSpace(urunAltGrubu)
	sqlText := `
SELECT
	M.id,
	ISNULL(M.UrunIlkGrubu, '') AS UrunIlkGrubu,
	ISNULL(M.UrunAnaGrubu, '') AS UrunAnaGrubu,
	ISNULL(M.UrunAltGrubu, '') AS UrunAltGrubu,
	ISNULL(M.nUrtMTBolumID, 0) AS nUrtMTBolumID,
	ISNULL(B.sAdi, '') AS MTBolumAdi,
	ISNULL(H.HammaddeTurleri, '') AS HammaddeTurleri,
	CAST(CASE WHEN ISNULL(M.bAktif, 0) = 1 THEN 1 ELSE 0 END AS bit) AS bAktif,
	CONVERT(VARCHAR(16), M.dteIslemTarihi, 120) AS dteIslemTarihi,
	ISNULL(M.sKullaniciAdi, '') AS sKullaniciAdi
FROM dbo.mk_MaliyetParcaEslestirme M
LEFT JOIN dbo.spUrtMTBolum B
	ON B.nUrtMTBolumID = M.nUrtMTBolumID
	AND ISNULL(B.nUrtTipiID, 0) = 1
OUTER APPLY (
	SELECT
		STUFF((
			SELECT ',' + LTRIM(RTRIM(CONVERT(VARCHAR(32), X.nHammaddeTuruNo)))
			FROM dbo.mk_MaliyetParcaEslestirme_HammaddeTuru X
			WHERE X.mapping_id = M.id
			ORDER BY X.nHammaddeTuruNo
			FOR XML PATH(''), TYPE
		).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS HammaddeTurleri
) H
WHERE (@p1 = '' OR LTRIM(RTRIM(M.UrunIlkGrubu)) = @p1)
  AND (@p2 = '' OR LTRIM(RTRIM(M.UrunAnaGrubu)) = @p2)
  AND (@p3 = '' OR LTRIM(RTRIM(M.UrunAltGrubu)) = @p3)
  AND (@p4 <= 0 OR M.nUrtMTBolumID = @p4)
  AND (@p5 IS NULL OR ISNULL(M.bAktif, 0) = @p5)
ORDER BY M.UrunAltGrubu, M.UrunAnaGrubu, M.nUrtMTBolumID
`
	var activeParam any = nil
	if onlyActive != nil {
		if *onlyActive {
			activeParam = 1
		} else {
			activeParam = 0
		}
	}
	return uretimDB.QueryContext(ctx, sqlText, urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nUrtMTBolumID, activeParam)
}

func UpsertProductionProductCostingParcaMapping(ctx context.Context, uretimDB *sql.DB, urunIlkGrubu string, urunAnaGrubu string, urunAltGrubu string, nUrtMTBolumID int, nHammaddeTurleri []int, bAktif bool, user string) (mappingID int, err error) {
	urunIlkGrubu = strings.TrimSpace(urunIlkGrubu)
	urunAnaGrubu = strings.TrimSpace(urunAnaGrubu)
	urunAltGrubu = strings.TrimSpace(urunAltGrubu)
	user = strings.TrimSpace(user)
	activeVal := 0
	if bAktif {
		activeVal = 1
	}

	tx, err := uretimDB.BeginTx(ctx, nil)
	if err != nil {
		return 0, err
	}
	defer func() {
		if err != nil {
			_ = tx.Rollback()
		}
	}()

	// 1) Upsert header row and get mapping id
	sqlText := `
DECLARE @id INT;

	SELECT TOP 1 @id = id
	FROM dbo.mk_MaliyetParcaEslestirme WITH (UPDLOCK, HOLDLOCK)
	WHERE LTRIM(RTRIM(UrunIlkGrubu)) = @p1
	  AND LTRIM(RTRIM(UrunAnaGrubu)) = @p2
	  AND LTRIM(RTRIM(UrunAltGrubu)) = @p3
	  AND nUrtMTBolumID = @p4;

	IF @id IS NULL
	BEGIN
		INSERT INTO dbo.mk_MaliyetParcaEslestirme
			(UrunIlkGrubu, UrunAnaGrubu, UrunAltGrubu, nUrtMTBolumID, bAktif, sKullaniciAdi, dteIslemTarihi)
		VALUES
			(@p1, @p2, @p3, @p4, @p5, NULLIF(@p6, ''), GETDATE());
		SET @id = SCOPE_IDENTITY();
	END
	ELSE
	BEGIN
		UPDATE dbo.mk_MaliyetParcaEslestirme
		SET bAktif = @p5,
			sKullaniciAdiDeg = NULLIF(@p6, ''),
			dteIslemTarihiDeg = GETDATE()
		WHERE id = @id;
	END

	SELECT @id;
	`
	if err = tx.QueryRowContext(ctx, sqlText, urunIlkGrubu, urunAnaGrubu, urunAltGrubu, nUrtMTBolumID, activeVal, user).Scan(&mappingID); err != nil {
		return 0, err
	}

	// 2) Replace child rows
	if _, err = tx.ExecContext(ctx, `DELETE FROM dbo.mk_MaliyetParcaEslestirme_HammaddeTuru WHERE mapping_id = @p1;`, mappingID); err != nil {
		return 0, err
	}

	insertChild := `INSERT INTO dbo.mk_MaliyetParcaEslestirme_HammaddeTuru (mapping_id, nHammaddeTuruNo) VALUES (@p1, @p2);`
	seen := make(map[int]bool, len(nHammaddeTurleri))
	for _, n := range nHammaddeTurleri {
		if n <= 0 {
			continue
		}
		if seen[n] {
			continue
		}
		seen[n] = true
		if _, err = tx.ExecContext(ctx, insertChild, mappingID, n); err != nil {
			return 0, err
		}
	}

	if err = tx.Commit(); err != nil {
		return 0, err
	}
	return mappingID, nil
}

type ProductionProductCostingInvalidHammaddeForPart struct {
	NHammaddeTuruNo int
	Aciklama        string
	MTBolumID       int
}

// FilterHammaddeTurleriForPart enforces spUrtOnMLHammaddeTuru.MTnUrtMTBolumID rules:
// - If MTnUrtMTBolumID > 0, the hammadde type is allowed only for that part (nUrtMTBolumID).
// - If MTnUrtMTBolumID is 0/NULL, it is considered global/unknown and allowed.
func FilterHammaddeTurleriForPart(
	ctx context.Context,
	uretimDB *sql.DB,
	nUrtMTBolumID int,
	nHammaddeTurleri []int,
) (allowed []int, invalid []ProductionProductCostingInvalidHammaddeForPart, err error) {
	seen := make(map[int]bool, len(nHammaddeTurleri))
	unique := make([]int, 0, len(nHammaddeTurleri))
	for _, n := range nHammaddeTurleri {
		if n <= 0 || seen[n] {
			continue
		}
		seen[n] = true
		unique = append(unique, n)
	}
	if len(unique) == 0 {
		return []int{}, []ProductionProductCostingInvalidHammaddeForPart{}, nil
	}

	// Build IN list safely.
	ph := make([]string, 0, len(unique))
	args := make([]any, 0, len(unique))
	for i, n := range unique {
		ph = append(ph, fmt.Sprintf("@p%d", i+1))
		args = append(args, n)
	}

	sqlText := fmt.Sprintf(`
SELECT
	ISNULL(H.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
	ISNULL(H.sAciklama, '') AS sAciklama,
	ISNULL(H.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID
FROM dbo.spUrtOnMLHammaddeTuru H
WHERE H.nHammaddeTuruNo IN (%s)
`, strings.Join(ph, ","))

	rows, qerr := uretimDB.QueryContext(ctx, sqlText, args...)
	if qerr != nil {
		return nil, nil, qerr
	}
	defer rows.Close()

	type meta struct {
		aciklama  string
		mtBolumID int
	}
	metaByNo := map[int]meta{}
	for rows.Next() {
		var no int
		var aciklama string
		var mtBolum int
		if scanErr := rows.Scan(&no, &aciklama, &mtBolum); scanErr != nil {
			return nil, nil, scanErr
		}
		metaByNo[no] = meta{aciklama: strings.TrimSpace(aciklama), mtBolumID: mtBolum}
	}
	if rowsErr := rows.Err(); rowsErr != nil {
		return nil, nil, rowsErr
	}

	allowed = make([]int, 0, len(unique))
	invalid = make([]ProductionProductCostingInvalidHammaddeForPart, 0)
	for _, n := range unique {
		m, ok := metaByNo[n]
		if !ok {
			// Unknown in lookup; allow (we can't validate).
			allowed = append(allowed, n)
			continue
		}
		if m.mtBolumID > 0 && nUrtMTBolumID > 0 && m.mtBolumID != nUrtMTBolumID {
			invalid = append(invalid, ProductionProductCostingInvalidHammaddeForPart{
				NHammaddeTuruNo: n,
				Aciklama:        m.aciklama,
				MTBolumID:       m.mtBolumID,
			})
			continue
		}
		allowed = append(allowed, n)
	}
	return allowed, invalid, nil
}

func SetProductionProductCostingParcaMappingActive(ctx context.Context, uretimDB *sql.DB, id int, bAktif bool, user string) error {
	user = strings.TrimSpace(user)
	activeVal := 0
	if bAktif {
		activeVal = 1
	}
	sqlText := `
UPDATE dbo.mk_MaliyetParcaEslestirme
SET bAktif = @p2,
	sKullaniciAdiDeg = NULLIF(@p3, ''),
	dteIslemTarihiDeg = GETDATE()
WHERE id = @p1;
`
	_, err := uretimDB.ExecContext(ctx, sqlText, id, activeVal, user)
	return err
}

func DeleteProductionProductCostingParcaMapping(ctx context.Context, uretimDB *sql.DB, id int) error {
	tx, err := uretimDB.BeginTx(ctx, nil)
	if err != nil {
		return err
	}
	defer func() {
		if err != nil {
			_ = tx.Rollback()
		}
	}()

	if _, err = tx.ExecContext(ctx, `DELETE FROM dbo.mk_MaliyetParcaEslestirme_HammaddeTuru WHERE mapping_id = @p1;`, id); err != nil {
		return err
	}
	if _, err = tx.ExecContext(ctx, `DELETE FROM dbo.mk_MaliyetParcaEslestirme WHERE id = @p1;`, id); err != nil {
		return err
	}
	return tx.Commit()
}

func GetProductionNoCostProducts(
	ctx context.Context,
	uretimDB *sql.DB,
	fromDate string,
	search string,
) (*sql.Rows, error) {
	fromDate = strings.TrimSpace(fromDate)
	if fromDate == "" {
		fromDate = "2025-06-01"
	}
	search = strings.TrimSpace(search)

	sqlText := `
SELECT
	CASE
		WHEN LEFT(LTRIM(RTRIM(R.sMModelKodu)), 1) IN ('N','X','S','O','I','K')
			THEN N'BAGGI URUN TARAFINDAN URETIME VERILEN'
		WHEN LEFT(LTRIM(RTRIM(R.sMModelKodu)), 1) IN ('P','F','M')
			THEN N'FASON ICIN URETILEN'
		ELSE N'Tanimsiz'
	END AS UretimSekli,

	RTRIM(CONVERT(VARCHAR(32), ISNULL(SonIsEmri.nUrtSiparisNo, 0))) AS nUrtSiparisNo,
	ISNULL(CONVERT(VARCHAR(10), SonIsEmri.dteIslemTarihi, 23), '') AS dteIslemTarihi,
	ISNULL(SonIsEmri.FirmaKodu, '') AS FirmaKodu,
	ISNULL(SonIsEmri.FirmaAdi, '') AS FirmaAdi,
	ISNULL(SonIsEmri.sVeren, '') AS SonIsEmriVeren,
	ISNULL(SonIsEmri.lMMiktar_G, 0) AS lMMiktar_G,
	LTRIM(RTRIM(R.sMModelKodu)) AS sMModelKodu,
	ISNULL(R.sKodu, '') AS sKodu,
	ISNULL(R.sAdi, '') AS sAdi,
	ISNULL(R.sKullaniciAdi, '') AS sKullaniciAdi,
	ISNULL(R.sKullaniciAdiGunc, '') AS sKullaniciAdiGunc

FROM dbo.spUrtRecete R

OUTER APPLY (
	SELECT TOP 1
		SD.nUrtSiparisID,
		SM.nUrtSiparisNo,
		SD.dteIslemTarihi,
		SD.lMMiktar_G,
		SM.sVeren,
		F.nFirmaID,
		F.sKodu AS FirmaKodu,
		F.sAciklama AS FirmaAdi
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE SD.nUrtReceteID = R.nUrtReceteID
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) SonIsEmri

WHERE LTRIM(RTRIM(R.sMModelKodu)) <> ''
  AND LEN(LTRIM(RTRIM(R.sMModelKodu))) = 13
  AND R.dteIslemTarihi > @p1
  AND (
		@p2 = ''
		OR LTRIM(RTRIM(R.sMModelKodu)) LIKE '%' + @p2 + '%'
		OR ISNULL(SonIsEmri.FirmaKodu, '') LIKE '%' + @p2 + '%'
		OR ISNULL(SonIsEmri.FirmaAdi, '') LIKE '%' + @p2 + '%'
		OR ISNULL(SonIsEmri.sVeren, '') LIKE '%' + @p2 + '%'
  )
  AND NOT EXISTS (
	SELECT 1
	FROM dbo.spUrtOnMLMas M
	WHERE LTRIM(RTRIM(M.UrunKodu)) = LTRIM(RTRIM(R.sMModelKodu))
  )
ORDER BY SonIsEmri.dteIslemTarihi DESC, LTRIM(RTRIM(R.sMModelKodu)) ASC
`

	return uretimDB.QueryContext(ctx, sqlText, fromDate, search)
}

func GetProductionHasCostProducts(
	ctx context.Context,
	uretimDB *sql.DB,
	search string,
	offset int,
	limit int,
) (*sql.Rows, error) {
	search = strings.TrimSpace(search)

	sqlText := `
WITH SonOnMaliyet AS (
	SELECT
		M.*,
		ROW_NUMBER() OVER (
			PARTITION BY LTRIM(RTRIM(M.UrunKodu))
			ORDER BY
				M.Tarihi DESC,
				M.dteGuncellemeTarihi DESC,
				M.nOnMLNo DESC
		) AS rn
	FROM dbo.spUrtOnMLMas M
	WHERE LTRIM(RTRIM(M.UrunKodu)) <> ''
),
SonSiparis AS (
	SELECT
		LTRIM(RTRIM(sMModelKodu)) AS UrunKodu,
		MAX(dteIslemTarihi) AS SonSiparisTarihi
	FROM dbo.spUrtSiparisDet
	GROUP BY LTRIM(RTRIM(sMModelKodu))
)
SELECT
	CASE
		WHEN LEFT(LTRIM(RTRIM(OM.UrunKodu)), 1) IN ('N','X','S','O','I','K')
			THEN N'BAGGI URUN TARAFINDAN URETIME VERILEN'
		WHEN LEFT(LTRIM(RTRIM(OM.UrunKodu)), 1) IN ('P','F','M')
			THEN N'FASON ICIN URETILEN'
		ELSE N'Tanimsiz'
	END AS UretimSekli,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) AS nOnMLNo,
	LTRIM(RTRIM(ISNULL(OM.UrunKodu, ''))) AS UrunKodu,
	ISNULL(OM.UrunAdi, '') AS UrunAdi,
	CONVERT(VARCHAR(10), OM.Tarihi, 23) AS Tarihi,
	CONVERT(VARCHAR(10), OM.dteKayitTarihi, 23) AS dteKayitTarihi,
	ISNULL(OM.sKullaniciAdi, '') AS sKullaniciAdi,
	ISNULL(OM.lTutarTL, 0) AS lTutarTL,
	ISNULL(OM.lTutarUSD, 0) AS lTutarUSD,
	ISNULL(OM.lTutarEURO, 0) AS lTutarEURO,
	CONVERT(VARCHAR(10), OM.dteGuncellemeTarihi, 23) AS dteGuncellemeTarihi,
	ISNULL(OM.sGuncellemeKullaniciAdi, '') AS sGuncellemeKullaniciAdi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nUrtReceteID, 0))) AS nUrtReceteID,
	ISNULL(OM.sAciklama, '') AS sAciklama,
	ISNULL(CONVERT(VARCHAR(10), SS.SonSiparisTarihi, 23), '') AS SonSiparisTarihi,
	CASE
		WHEN ISNULL(OM.lTutarUSD, 0) = 0 THEN N'GUNCELLEME GEREKIYOR'
		WHEN SS.SonSiparisTarihi IS NULL THEN N'SIPARIS YOK'
		WHEN SS.SonSiparisTarihi > OM.Tarihi THEN N'GUNCELLEME GEREKIYOR'
		ELSE N'GUNCEL'
	END AS MaliyetDurumu
FROM SonOnMaliyet OM
LEFT JOIN SonSiparis SS
	ON SS.UrunKodu = LTRIM(RTRIM(OM.UrunKodu))
WHERE OM.rn = 1
  AND (
	@p1 = ''
	OR RTRIM(CONVERT(VARCHAR(32), ISNULL(OM.nOnMLNo, 0))) LIKE '%' + @p1 + '%'
	OR LTRIM(RTRIM(OM.UrunKodu)) LIKE '%' + @p1 + '%'
	OR ISNULL(OM.UrunAdi, '') LIKE '%' + @p1 + '%'
	OR ISNULL(OM.sAciklama, '') LIKE '%' + @p1 + '%'
  )
ORDER BY
	SS.SonSiparisTarihi DESC,
	OM.nOnMLNo DESC
OFFSET @p2 ROWS
FETCH NEXT @p3 ROWS ONLY
`

	return uretimDB.QueryContext(ctx, sqlText, search, offset, limit)
}

func GetProductionHasCostHistoryByProductCode(
	ctx context.Context,
	uretimDB *sql.DB,
	productCode string,
) (*sql.Rows, error) {
	productCode = strings.TrimSpace(productCode)

	sqlText := `
SELECT
	RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nOnMLNo, 0))) AS nOnMLNo,
	LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu,
	ISNULL(M.UrunAdi, '') AS UrunAdi,
	CONVERT(VARCHAR(16), M.Tarihi, 120) AS Tarihi,
	ISNULL(M.sKullaniciAdi, '') AS sKullaniciAdi,
	ISNULL(M.lTutarUSD, 0) AS lTutarUSD,
	ISNULL(M.lTutarTL, 0) AS lTutarTL,
	ISNULL(M.lTutarEURO, 0) AS lTutarEURO,
	ISNULL(M.sDovizCinsi, '') AS sDovizCinsi,
	ISNULL(M.lTutarDoviz, 0) AS lTutarDoviz,
	CONVERT(VARCHAR(16), M.dteGuncellemeTarihi, 120) AS dteGuncellemeTarihi,
	ISNULL(M.sGuncellemeKullaniciAdi, '') AS sGuncellemeKullaniciAdi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nUrtReceteID, 0))) AS nUrtReceteID,
	ISNULL(M.sAciklama, '') AS sAciklama
FROM dbo.spUrtOnMLMas M
WHERE LTRIM(RTRIM(M.UrunKodu)) = @p1
ORDER BY
	M.Tarihi DESC,
	M.dteGuncellemeTarihi DESC,
	M.nOnMLNo DESC
`

	return uretimDB.QueryContext(ctx, sqlText, productCode)
}

func GetProductionHasCostDetailRowsByOnMLNo(
	ctx context.Context,
	uretimDB *sql.DB,
	nOnMLNo int,
) (*sql.Rows, error) {
	sqlText := `
  SELECT
  	-- Prefer the group label stored on the OnML detail row (D.sAciklama3),
  	-- because some hammadde type master rows may have empty/legacy group labels.
  	ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ')) AS sAciklama3,
 	SUM(ISNULL(D.lTutar, 0)) OVER (
 		PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ'))
 	) AS GroupTotalTutar,
 	SUM(ISNULL(D.lMiktar, 0) * ISNULL(D.lDovizFiyati, 0)) OVER (
 		PARTITION BY ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'TANIMSIZ'))
 	) AS GroupTotalUSDTutar,
 	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
 	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLDetNo, 0))) AS nOnMLDetNo,
  	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
  	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nUrtMTBolumID, 0))) AS nUrtMTBolumID,
  	-- Normalize code to variantless (tbStok.sModel) when D.sKodu is a variant-coded stock record.
  	ISNULL(NULLIF(SX.sModel, ''), ISNULL(D.sKodu, '')) AS sKodu,
  	ISNULL(NULLIF(SX.sAciklama, ''), ISNULL(D.sAciklama, '')) AS sAciklama,
  	ISNULL(D.sRenk, '') AS sRenk,
  	ISNULL(D.sBeden, '') AS sBeden,
  	ISNULL(D.sAciklama2, '') AS sAciklama2,
	ISNULL(D.lMiktar, 0) AS lMiktar,
	ISNULL(D.lFiyat, 0) AS lFiyat,
	ISNULL(D.lTutar, 0) AS lTutar,
	ISNULL(D.sFiyatTipi, '') AS sFiyatTipi,
	ISNULL(D.sDovizCinsi, '') AS sDovizCinsi,
	ISNULL(D.lDovizKuru, 0) AS lDovizKuru,
	ISNULL(D.lDovizFiyati, 0) AS lDovizFiyati,
	D.fiyat_girilen AS fiyat_girilen,
	D.fiyat_doviz AS fiyat_doviz,
	CAST(CASE WHEN ISNULL(D.Maliyete_dahil, 0) = 1 THEN 1 ELSE 0 END AS bit) AS maliyete_dahil,
	D.cm_price_type_id AS cm_price_type_id,
	ISNULL(D.lMiktar, 0) * ISNULL(D.lDovizFiyati, 0) AS usdTutar,
	0.0 AS eurTutar,
	0.0 AS gbpTutar,
	ISNULL(D.sBirim, '') AS sBirim,
	ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
	ISNULL(B.sAdi, '') AS sParcaAdi
   FROM dbo.spUrtOnMLMasDet D
   LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
   	ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
   LEFT JOIN dbo.spUrtMTBolum B
   	ON B.nUrtMTBolumID = D.nUrtMTBolumID
   LEFT JOIN dbo.tbStok SX
   	ON (SX.IsBlocked = 0 OR SX.IsBlocked IS NULL)
   	AND ISNULL(SX.sKodu,'') = ISNULL(D.sKodu,'')
   WHERE D.nOnMLNo = @p1
  ORDER BY
  	GroupTotalTutar DESC,
  	sAciklama3 ASC,
 	ISNULL(D.lTutar, 0) DESC,
 	D.nOnMLDetNo ASC
 `

	return uretimDB.QueryContext(ctx, sqlText, nOnMLNo)
}

func GetProductionHasCostDetailHeaderByOnMLNo(
	ctx context.Context,
	uretimDB *sql.DB,
	nOnMLNo int,
) (*sql.Row, error) {
	sqlText := `
SELECT TOP 1
	ISNULL(UF.UretimiYapanFirma, '') AS UretimiYapanFirma,
	ISNULL(UF.SonIsEmriVeren, '') AS SonIsEmriVeren,
	ISNULL(UF.FirmaKodu, '') AS FirmaKodu,
	ISNULL(UF.nFirmaID, 0) AS nFirmaID,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nOnMLNo, 0))) AS nOnMLNo,
	LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))) AS UrunKodu,
	ISNULL(M.UrunAdi, '') AS UrunAdi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(M.uretim_sekli_id, 0))) AS uretim_sekli_id,
	ISNULL(US.aciklama, '') AS uretim_sekli,
	CONVERT(VARCHAR(10), M.Tarihi, 120) AS maliyetTarihi,
	CONVERT(VARCHAR(16), M.dteKayitTarihi, 120) AS dteKayitTarihi,
	ISNULL(M.sKullaniciAdi, '') AS sKullaniciAdi,
	ISNULL(M.lTutarTL, 0) AS lTutarTL,
	ISNULL(M.lTutarUSD, 0) AS lTutarUSD,
	ISNULL(M.lTutarEURO, 0) AS lTutarEURO,
	0.0 AS lTutarGBP,
	ISNULL(M.sDovizCinsi, '') AS sDovizCinsi,
	ISNULL(M.lTutarDoviz, 0) AS lTutarDoviz,
	CONVERT(VARCHAR(16), M.dteGuncellemeTarihi, 120) AS dteGuncellemeTarihi,
	ISNULL(M.sGuncellemeKullaniciAdi, '') AS sGuncellemeKullaniciAdi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(M.nUrtReceteID, 0))) AS nUrtReceteID
FROM dbo.spUrtOnMLMas M
LEFT JOIN dbo.mk_uretim_sekli US
	ON US.id = M.uretim_sekli_id
OUTER APPLY (
	SELECT TOP 1
		ISNULL(F.sAciklama, '') AS UretimiYapanFirma,
		ISNULL(F.sKodu, '') AS FirmaKodu,
		ISNULL(F.nFirmaID, 0) AS nFirmaID,
		ISNULL(SM.sVeren, '') AS SonIsEmriVeren
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE LTRIM(RTRIM(SD.sMModelKodu)) = LTRIM(RTRIM(M.UrunKodu))
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) UF
WHERE M.nOnMLNo = @p1
ORDER BY M.nOnMLNo DESC
`

	return uretimDB.QueryRowContext(ctx, sqlText, nOnMLNo), nil
}

func GetProductionNoCostDetailHeaderByRecipeCode(
	ctx context.Context,
	uretimDB *sql.DB,
	recipeCode string,
	productCode string,
) (*sql.Row, error) {
	recipeCode = strings.TrimSpace(recipeCode)
	productCode = strings.TrimSpace(productCode)
	utils.SlogFromContext(ctx).With(
		"query", "production-product-costing.no-cost-detail-header",
		"recete_kodu", recipeCode,
		"urun_kodu", productCode,
	).Info("query dispatch")

	sqlText := `
WITH RecipeMatch AS (
	SELECT TOP 1
		R.nUrtReceteID,
		LTRIM(RTRIM(ISNULL(R.sMModelKodu, ''))) AS UrunKodu,
		ISNULL(R.sAdi, '') AS UrunAdi,
		CONVERT(VARCHAR(16), R.dteIslemTarihi, 120) AS dteKayitTarihi,
		ISNULL(R.sKullaniciAdi, '') AS sKullaniciAdi,
		ISNULL(R.sKullaniciAdiGunc, '') AS sGuncellemeKullaniciAdi,
		CASE
			WHEN LEFT(LTRIM(RTRIM(R.sMModelKodu)), 1) IN ('N','X','S','O','I','K')
				THEN N'BAGGI URUN TARAFINDAN URETIME VERILEN'
			WHEN LEFT(LTRIM(RTRIM(R.sMModelKodu)), 1) IN ('P','F','M')
				THEN N'FASON ICIN URETILEN'
			ELSE N'Tanimsiz'
		END AS UretimSekli
	FROM dbo.spUrtRecete R
	WHERE LTRIM(RTRIM(ISNULL(R.sKodu, ''))) = @p1
	  AND (@p2 = '' OR LTRIM(RTRIM(ISNULL(R.sMModelKodu, ''))) = @p2)
	ORDER BY
		R.dteIslemTarihi DESC,
		R.nUrtReceteID DESC
)
SELECT TOP 1
	ISNULL(SonIsEmri.FirmaAdi, '') AS UretimiYapanFirma,
	ISNULL(SonIsEmri.SonIsEmriVeren, '') AS SonIsEmriVeren,
	ISNULL(SonIsEmri.FirmaKodu, '') AS FirmaKodu,
	ISNULL(SonIsEmri.nFirmaID, 0) AS nFirmaID,
	'' AS nOnMLNo,
	RM.UrunKodu,
	RM.UrunAdi,
	'' AS UretimSekliID,
	RM.UretimSekli,
	ISNULL(SonIsEmri.dteIslemTarihi, RM.dteKayitTarihi) AS dteKayitTarihi,
	RM.sKullaniciAdi,
	0.0 AS lTutarTL,
	0.0 AS lTutarUSD,
	0.0 AS lTutarEURO,
	0.0 AS lTutarGBP,
	'USD' AS sDovizCinsi,
	0.0 AS lTutarDoviz,
	'' AS dteGuncellemeTarihi,
	RM.sGuncellemeKullaniciAdi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(RM.nUrtReceteID, 0))) AS nUrtReceteID
FROM RecipeMatch RM
OUTER APPLY (
	SELECT TOP 1
		ISNULL(F.sAciklama, '') AS FirmaAdi,
		ISNULL(F.sKodu, '') AS FirmaKodu,
		ISNULL(F.nFirmaID, 0) AS nFirmaID,
		ISNULL(SM.sVeren, '') AS SonIsEmriVeren,
		CONVERT(VARCHAR(16), SD.dteIslemTarihi, 120) AS dteIslemTarihi
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE SD.nUrtReceteID = RM.nUrtReceteID
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) SonIsEmri
`

	return uretimDB.QueryRowContext(ctx, sqlText, recipeCode, productCode), nil
}

func GetProductionNoCostDetailRowsByRecipeCode(
	ctx context.Context,
	uretimDB *sql.DB,
	recipeCode string,
	productCode string,
) (*sql.Rows, error) {
	recipeCode = strings.TrimSpace(recipeCode)
	productCode = strings.TrimSpace(productCode)
	logger := utils.SlogFromContext(ctx).With(
		"query", "production-product-costing.no-cost-detail-rows",
		"recete_kodu", recipeCode,
		"urun_kodu", productCode,
	)
	logger.Info("query dispatch")

	sqlText := `
WITH RecipeMatch AS (
	SELECT TOP 1
		R.nUrtReceteID
	FROM dbo.spUrtRecete R
	WHERE LTRIM(RTRIM(ISNULL(R.sKodu, ''))) = @p1
	  AND (@p2 = '' OR LTRIM(RTRIM(ISNULL(R.sMModelKodu, ''))) = @p2)
	ORDER BY
		R.dteIslemTarihi DESC,
		R.nUrtReceteID DESC
),
HammaddeTekil AS (
	SELECT
		-- Group label: DT/TP/CM2/FABRIC... Prefer sAciklama3, then sAciklama2. Never fall back to sAciklama (name).
		COALESCE(NULLIF(LTRIM(RTRIM(HT.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(HT.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
		ISNULL(HT.nHammaddeTuruNo, 0) AS nHammaddeTuruNoSort,
		RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
		-- Match URETIM's sp_pUrtOnMaliyetRecetedenKop behavior: use model code + color code instead of variant stock code.
		ISNULL(S.sModel, ISNULL(S.sKodu, '')) AS sKodu,
		ISNULL(S.sAciklama, '') AS sAciklama,
		ISNULL(S.sRenk, '') AS sRenk,
		ISNULL(HT.sAciklama, '') AS sHammaddeTuruAdi,
		ISNULL(S.sBirimCinsi1, '') AS sBirim,
		ISNULL(RMik.lHMiktar, 0) AS lMiktar,
		ISNULL(HT.MTnUrtMTBolumID, 0) AS MTnUrtMTBolumID,
		RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.MTnUrtMTBolumID, 0))) AS nUrtMTBolumID,
		ISNULL(B.sAdi, '') AS sParcaAdi,
		ROW_NUMBER() OVER (
			PARTITION BY HT.nHammaddeTuruNo
			ORDER BY ISNULL(S.sModel, ISNULL(S.sKodu, ''))
		) AS rn,
		ROW_NUMBER() OVER (
			ORDER BY HT.nHammaddeTuruNo, ISNULL(S.sModel, ISNULL(S.sKodu, ''))
		) AS rowNo
	FROM RecipeMatch R
	INNER JOIN dbo.spUrtRecMBolumMik RMik
		ON RMik.nUrtReceteID = R.nUrtReceteID
	LEFT JOIN dbo.tbStok S
		ON S.nStokID = RMik.nHStokID
	OUTER APPLY (
		SELECT TOP 1
			H.nHammaddeTuruNo,
			H.sAciklama,
			H.sAciklama2,
			H.sAciklama3,
			H.MTnUrtMTBolumID
		FROM dbo.spUrtOnMLHammaddeTuru H
		-- In our data, RMik.nUrtMBolumID carries the required hammadde type number (e.g. 900/901/3300),
		-- not a "bolum id". So match by hammadde type number.
		WHERE H.nHammaddeTuruNo = RMik.nUrtMBolumID
		ORDER BY
			CASE WHEN H.MTnUrtMTBolumID = RMik.nUrtMTBolumID THEN 0 ELSE 1 END,
			H.nHammaddeTuruNo
	) HT
	LEFT JOIN dbo.spUrtMTBolum B
		ON B.nUrtMTBolumID = HT.MTnUrtMTBolumID
		AND ISNULL(B.nUrtTipiID, 0) = 1
	WHERE HT.nHammaddeTuruNo IS NOT NULL
)
 SELECT
 	HT.sAciklama3,
 	0.0 AS GroupTotalTutar,
 	0.0 AS GroupTotalUSDTutar,
 	'' AS nOnMLNo,
 	RTRIM(CONVERT(VARCHAR(32), ISNULL(HT.rowNo, 0))) AS nOnMLDetNo,
 	HT.nHammaddeTuruNo,
 	HT.nUrtMTBolumID,
 	HT.sKodu,
 	HT.sAciklama,
 	HT.sRenk AS sRenk,
 	'' AS sBeden,
 	'' AS sAciklama2,
	HT.lMiktar,
	0.0 AS lFiyat,
	0.0 AS lTutar,
	'' AS sFiyatTipi,
	'USD' AS sDovizCinsi,
	0.0 AS lDovizKuru,
	0.0 AS lDovizFiyati,
	NULL AS fiyat_girilen,
	'' AS fiyat_doviz,
	CAST(1 AS bit) AS maliyete_dahil,
	NULL AS cm_price_type_id,
	0.0 AS usdTutar,
	0.0 AS eurTutar,
	0.0 AS gbpTutar,
	HT.sBirim,
	HT.sHammaddeTuruAdi,
	HT.sParcaAdi AS sParcaAdi
FROM HammaddeTekil HT
WHERE HT.rn = 1
ORDER BY
	HT.nHammaddeTuruNoSort,
	HT.sKodu
`

	rows, err := uretimDB.QueryContext(ctx, sqlText, recipeCode, productCode)
	if err != nil {
		logger.Error("query error", "err", err)
		return nil, err
	}

	return rows, nil
}

func GetProductionTypes(ctx context.Context, uretimDB *sql.DB) (*sql.Rows, error) {
	sqlText := `SELECT id, aciklama FROM dbo.mk_uretim_sekli ORDER BY id`
	return uretimDB.QueryContext(ctx, sqlText)
}

func GetProductionHasCostDetailHammaddeTypeOptions(
	ctx context.Context,
	uretimDB *sql.DB,
	search string,
	limit int,
	group string,
	onlyActive *bool,
) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	group = strings.TrimSpace(group)
	if limit <= 0 {
		limit = 50
	}
	searchLike := "%" + search + "%"

	sqlText := `
 SELECT TOP (@p2)
 	RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) AS nHammaddeTuruNo,
 	ISNULL(T.sAciklama, '') AS sHammaddeTuruAdi,
 	COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') AS sAciklama3,
 	ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
 	ISNULL(B.sAdi, '') AS sParcaAdi
 FROM dbo.spUrtOnMLHammaddeTuru T
 LEFT JOIN dbo.spUrtMTBolum B
 	ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
 	AND ISNULL(B.nUrtTipiID, 0) = 1
 WHERE
 	-- Active filtering:
 	-- - By default, list active rows.
 	-- - If searching by exact number, allow inactive too (legacy CM2 etc.).
 	-- - If onlyActive flag is provided, honor it explicitly.
 	(
 		(@p5 IS NULL AND (ISNULL(T.bAktif, 0) = 1 OR (@p1 <> '' AND RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) = @p1)))
 		OR (@p5 IS NOT NULL AND ISNULL(T.bAktif, 0) = @p5)
 	)
 	AND (
 		@p4 = ''
 		OR COALESCE(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), NULLIF(LTRIM(RTRIM(T.sAciklama2)), ''), N'TANIMSIZ') = @p4
 	)
 	AND (
 		@p1 = ''
 		OR RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) LIKE @p3
 		OR ISNULL(T.sAciklama, '') LIKE @p3
 		OR ISNULL(T.sAciklama2, '') LIKE @p3
		OR ISNULL(T.sAciklama3, '') LIKE @p3
		OR ISNULL(B.sAdi, '') LIKE @p3
	)
ORDER BY
	CASE
		WHEN @p1 <> '' AND RTRIM(CONVERT(VARCHAR(32), ISNULL(T.nHammaddeTuruNo, 0))) = @p1 THEN 0
		WHEN @p1 <> '' AND ISNULL(T.sAciklama, '') = @p1 THEN 1
		ELSE 2
	END,
	T.nHammaddeTuruNo
`

	return uretimDB.QueryContext(ctx, sqlText, search, limit, searchLike, group, onlyActive)
}

func GetProductionHammaddeByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]models.ProductionProductCostingHammaddeByNosItem, error) {
	clean := make([]int, 0, len(nos))
	seen := make(map[int]struct{}, len(nos))
	for _, n := range nos {
		if n <= 0 {
			continue
		}
		if _, ok := seen[n]; ok {
			continue
		}
		seen[n] = struct{}{}
		clean = append(clean, n)
	}
	if len(clean) == 0 {
		return []models.ProductionProductCostingHammaddeByNosItem{}, nil
	}
	if len(clean) > 5000 {
		return nil, fmt.Errorf("too many nos")
	}

	valueRows := make([]string, 0, len(clean))
	args := make([]any, 0, len(clean))
	for i, n := range clean {
		valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
		args = append(args, n)
	}

	sqlText := `
WITH req(nHammaddeTuruNo) AS (
	SELECT DISTINCT v.nHammaddeTuruNo
	FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
)
SELECT
	ISNULL(T.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
	ISNULL(T.sAciklama, '') AS sAciklama,
	ISNULL(T.MTnUrtMTBolumID, 0) AS mtUrtMTBolumID,
	ISNULL(B.sAdi, '') AS sParcaAdi
FROM dbo.spUrtOnMLHammaddeTuru T
INNER JOIN req R
	ON R.nHammaddeTuruNo = T.nHammaddeTuruNo
LEFT JOIN dbo.spUrtMTBolum B
	ON B.nUrtMTBolumID = T.MTnUrtMTBolumID
	AND ISNULL(B.nUrtTipiID, 0) = 1
WHERE ISNULL(T.bAktif, 0) = 1
ORDER BY T.nHammaddeTuruNo;
`

	rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]models.ProductionProductCostingHammaddeByNosItem, 0, len(clean))
	for rows.Next() {
		var it models.ProductionProductCostingHammaddeByNosItem
		if err := rows.Scan(&it.NHammaddeTuruNo, &it.SAciklama, &it.MTUrtMTBolumID, &it.SParcaAdi); err != nil {
			return nil, err
		}
		out = append(out, it)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

// ============================================================
// Default quantities (URETIM): mk_MaliyetParcaEslestirme_vmiktarlar
// ============================================================

func ListProductionProductCostingDefaultQtyRows(ctx context.Context, uretimDB *sql.DB, search string, limit int) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 500
	}
	if limit > 5000 {
		limit = 5000
	}
	searchLike := "%" + search + "%"

	sqlText := `
SELECT TOP (@p3)
	ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
	ISNULL(H.sAciklama, '') AS sAciklama,
	ISNULL(V.lDefaultMiktar, 0) AS lDefaultMiktar,
	CONVERT(VARCHAR(16), V.dteCalcTarihi, 120) AS dteCalcTarihi,
	-- We intentionally don't depend on mk_MaliyetParcaEslestirme_vmiktarlar.bAktif
	-- to keep this feature robust across schema changes; only active hammadde types are listed.
	CAST(1 AS bit) AS bAktif
FROM dbo.mk_MaliyetParcaEslestirme_vmiktarlar V
LEFT JOIN dbo.spUrtOnMLHammaddeTuru H
	ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE
	(@p1 = '' OR CONVERT(VARCHAR(32), ISNULL(V.nHammaddeTuruNo, 0)) LIKE @p2)
	AND ISNULL(H.bAktif, 0) = 1
ORDER BY
	V.nHammaddeTuruNo ASC
`

	return uretimDB.QueryContext(ctx, sqlText, search, searchLike, limit)
}

func UpsertProductionProductCostingDefaultQtyRow(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, lDefaultMiktar float64, bAktif *bool) error {
	// NOTE: Legacy helper kept for backward-compat but now behaves as UPDATE-only.
	sqlText := `
UPDATE dbo.mk_MaliyetParcaEslestirme_vmiktarlar
SET
	lDefaultMiktar = @p2,
	dteCalcTarihi = GETDATE()
WHERE nHammaddeTuruNo = @p1;
SELECT @@ROWCOUNT;
`
	var affected int
	if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, lDefaultMiktar).Scan(&affected); err != nil {
		return err
	}
	if affected == 0 {
		return fmt.Errorf("default qty row not found: nHammaddeTuruNo=%d", nHammaddeTuruNo)
	}
	return nil
}

func RefreshProductionProductCostingDefaultQty(ctx context.Context, uretimDB *sql.DB, topN int) error {
	if topN <= 0 {
		topN = 10
	}
	if topN > 50 {
		topN = 50
	}
	sqlText := `
;WITH ranked AS (
	SELECT
		D.nHammaddeTuruNo,
		D.lMiktar,
		ROW_NUMBER() OVER (
			PARTITION BY D.nHammaddeTuruNo
			ORDER BY
				ISNULL(M.Tarihi, '19000101') DESC,
				ISNULL(M.dteGuncellemeTarihi, '19000101') DESC,
				D.nOnMLNo DESC,
				D.dteIslemTarihi DESC,
				D.nOnMLDetNo DESC
		) AS rn
	FROM dbo.spUrtOnMLMasDet D
	INNER JOIN dbo.spUrtOnMLMas M
		ON M.nOnMLNo = D.nOnMLNo
	WHERE ISNULL(D.lMiktar, 0) > 0
	  AND ISNULL(M.bIptal, 0) = 0
),
agg AS (
	SELECT
		nHammaddeTuruNo,
		CAST(AVG(CAST(lMiktar AS float)) AS numeric(14,4)) AS lDefaultMiktar
	FROM ranked
	WHERE rn <= @p1
	GROUP BY nHammaddeTuruNo
)
MERGE dbo.mk_MaliyetParcaEslestirme_vmiktarlar AS T
USING agg AS S
	ON T.nHammaddeTuruNo = S.nHammaddeTuruNo
WHEN MATCHED THEN
	UPDATE SET
		T.lDefaultMiktar = S.lDefaultMiktar,
		T.dteCalcTarihi  = GETDATE()
WHEN NOT MATCHED THEN
	INSERT (nHammaddeTuruNo, lDefaultMiktar, dteCalcTarihi)
	VALUES (S.nHammaddeTuruNo, S.lDefaultMiktar, GETDATE());
`
	_, err := uretimDB.ExecContext(ctx, sqlText, topN)
	return err
}

func CalcProductionProductCostingDefaultQtyFromLastOnML(ctx context.Context, uretimDB *sql.DB, nHammaddeTuruNo int, topN int) (float64, int, error) {
	if nHammaddeTuruNo <= 0 {
		return 0, 0, fmt.Errorf("nHammaddeTuruNo required")
	}
	if topN <= 0 {
		topN = 10
	}
	if topN > 50 {
		topN = 50
	}
	sqlText := `
;WITH ranked AS (
	SELECT TOP (@p2)
		ISNULL(D.lMiktar, 0) AS lMiktar
	FROM dbo.spUrtOnMLMasDet D
	INNER JOIN dbo.spUrtOnMLMas M
		ON M.nOnMLNo = D.nOnMLNo
	WHERE D.nHammaddeTuruNo = @p1
	  AND ISNULL(D.lMiktar, 0) > 0
	ORDER BY ISNULL(M.Tarihi, M.dteKayitTarihi) DESC, D.nOnMLNo DESC, D.nOnMLDetNo DESC
)
SELECT
	CAST(ISNULL(AVG(CAST(lMiktar AS DECIMAL(18,4))), 0) AS FLOAT) AS avgQty,
	COUNT(1) AS sampleCount
FROM ranked;
`
	var avg float64
	var cnt int
	if err := uretimDB.QueryRowContext(ctx, sqlText, nHammaddeTuruNo, topN).Scan(&avg, &cnt); err != nil {
		return 0, 0, err
	}
	return avg, cnt, nil
}

func LookupProductionProductCostingDefaultQtyByNos(ctx context.Context, uretimDB *sql.DB, nos []int) ([]struct {
	No       int
	Aciklama string
	Qty      float64
}, error) {
	clean := make([]int, 0, len(nos))
	seen := make(map[int]struct{}, len(nos))
	for _, n := range nos {
		if n <= 0 {
			continue
		}
		if _, ok := seen[n]; ok {
			continue
		}
		seen[n] = struct{}{}
		clean = append(clean, n)
	}
	if len(clean) == 0 {
		return []struct {
			No       int
			Aciklama string
			Qty      float64
		}{}, nil
	}
	if len(clean) > 1000 {
		return nil, fmt.Errorf("too many hammadde nos")
	}

	valueRows := make([]string, 0, len(clean))
	args := make([]any, 0, len(clean))
	for i, n := range clean {
		valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
		args = append(args, n)
	}

	sqlText := `
WITH req(nHammaddeTuruNo) AS (
	SELECT DISTINCT v.nHammaddeTuruNo
	FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
)
	SELECT
		ISNULL(V.nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
		ISNULL(H.sAciklama, '') AS sAciklama,
		CAST(ISNULL(V.lDefaultMiktar, 0) AS FLOAT) AS lDefaultMiktar
FROM req R
INNER JOIN dbo.mk_MaliyetParcaEslestirme_vmiktarlar V
	ON V.nHammaddeTuruNo = R.nHammaddeTuruNo
INNER JOIN dbo.spUrtOnMLHammaddeTuru H
	ON H.nHammaddeTuruNo = V.nHammaddeTuruNo
WHERE ISNULL(H.bAktif, 0) = 1;
`

	rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]struct {
		No       int
		Aciklama string
		Qty      float64
	}, 0, len(clean))
	for rows.Next() {
		var no int
		var aciklama sql.NullString
		var qty float64
		if err := rows.Scan(&no, &aciklama, &qty); err != nil {
			return nil, err
		}
		if no > 0 && qty > 0 {
			out = append(out, struct {
				No       int
				Aciklama string
				Qty      float64
			}{No: no, Aciklama: strings.TrimSpace(aciklama.String), Qty: qty})
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func LookupLastOnMLMasDetByHammaddeNos(
	ctx context.Context,
	uretimDB *sql.DB,
	nos []int,
	beforeDate string, // YYYY-MM-DD optional
	excludeOnMLNo int,
	nFirmaID int,
	onlyICode bool,
	limitPerType int,
) ([]models.ProductionProductCostingLastOnMLDetLookupItem, error) {
	if limitPerType <= 0 {
		limitPerType = 1
	}
	if limitPerType > 1 {
		limitPerType = 1
	}

	clean := make([]int, 0, len(nos))
	seen := make(map[int]struct{}, len(nos))
	for _, n := range nos {
		if n <= 0 {
			continue
		}
		if _, ok := seen[n]; ok {
			continue
		}
		seen[n] = struct{}{}
		clean = append(clean, n)
	}
	if len(clean) == 0 {
		return []models.ProductionProductCostingLastOnMLDetLookupItem{}, nil
	}
	if len(clean) > 500 {
		return nil, fmt.Errorf("too many hammadde nos")
	}

	valueRows := make([]string, 0, len(clean))
	args := make([]any, 0, len(clean)+2)
	for i, n := range clean {
		valueRows = append(valueRows, "(@p"+strconv.Itoa(i+1)+")")
		args = append(args, n)
	}
	// extra params at the end
	args = append(args, strings.TrimSpace(beforeDate))
	args = append(args, excludeOnMLNo)
	args = append(args, nFirmaID)
	if onlyICode {
		args = append(args, 1)
	} else {
		args = append(args, 0)
	}

	beforeParam := "@p" + strconv.Itoa(len(clean)+1)
	excludeParam := "@p" + strconv.Itoa(len(clean)+2)
	firmaParam := "@p" + strconv.Itoa(len(clean)+3)
	onlyIParam := "@p" + strconv.Itoa(len(clean)+4)

	sqlText := `
WITH req(nHammaddeTuruNo) AS (
	SELECT DISTINCT v.nHammaddeTuruNo
	FROM (VALUES ` + strings.Join(valueRows, ",") + `) v(nHammaddeTuruNo)
),
ranked AS (
	SELECT
		D.nHammaddeTuruNo,
		LTRIM(RTRIM(ISNULL(D.sKodu, ''))) AS sKodu,
		LTRIM(RTRIM(ISNULL(D.sAciklama, ''))) AS sAciklama,
		LTRIM(RTRIM(ISNULL(D.sBirim, ''))) AS sBirim,
		LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) AS fiyat_doviz,
		CAST(ISNULL(D.fiyat_girilen, 0) AS FLOAT) AS fiyat_girilen,
		CAST(CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 1 ELSE 0 END AS bit) AS is_same_firma,
		ROW_NUMBER() OVER (
			PARTITION BY D.nHammaddeTuruNo
			ORDER BY
				CASE WHEN ISNULL(` + firmaParam + `, 0) > 0 AND ISNULL(M.nFirmaID, 0) = ISNULL(` + firmaParam + `, 0) THEN 0 ELSE 1 END,
				COALESCE(M.Tarihi, M.dteKayitTarihi) DESC,
				M.nOnMLNo DESC,
				D.nOnMLDetNo DESC
		) AS rn
	FROM dbo.spUrtOnMLMasDet D
	INNER JOIN dbo.spUrtOnMLMas M
		ON M.nOnMLNo = D.nOnMLNo
	INNER JOIN req R
		ON R.nHammaddeTuruNo = D.nHammaddeTuruNo
	WHERE ISNULL(D.fiyat_girilen, 0) > 0
	  AND LTRIM(RTRIM(ISNULL(D.sKodu, ''))) <> ''
	  AND (ISNULL(` + onlyIParam + `, 0) = 0 OR LTRIM(RTRIM(ISNULL(D.sKodu, ''))) LIKE 'I.%')
	  AND (` + beforeParam + ` = '' OR CONVERT(date, COALESCE(M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, ` + beforeParam + `, 23))
	  AND (ISNULL(` + excludeParam + `, 0) <= 0 OR M.nOnMLNo <> ` + excludeParam + `)
)
SELECT
	ISNULL(nHammaddeTuruNo, 0) AS nHammaddeTuruNo,
	ISNULL(sKodu, '') AS sKodu,
	ISNULL(sAciklama, '') AS sAciklama,
	ISNULL(sBirim, '') AS sBirim,
	ISNULL(fiyat_doviz, '') AS fiyat_doviz,
	ISNULL(fiyat_girilen, 0) AS fiyat_girilen,
	CAST(ISNULL(is_same_firma, 0) AS bit) AS is_same_firma
FROM ranked
WHERE rn = 1;
`

	rows, err := uretimDB.QueryContext(ctx, sqlText, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	out := make([]models.ProductionProductCostingLastOnMLDetLookupItem, 0, len(clean))
	for rows.Next() {
		var item models.ProductionProductCostingLastOnMLDetLookupItem
		if err := rows.Scan(&item.NHammaddeTuruNo, &item.SKodu, &item.SAciklama, &item.SBirim, &item.FiyatDoviz, &item.FiyatGirilen, &item.IsSameFirma); err != nil {
			return nil, err
		}
		out = append(out, item)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func buildSQLServerFullTextPrefixQuery(search string) string {
	terms := strings.Fields(strings.TrimSpace(search))
	parts := make([]string, 0, len(terms))

	for _, term := range terms {
		cleaned := strings.Map(func(r rune) rune {
			if unicode.IsLetter(r) || unicode.IsDigit(r) {
				return r
			}
			return -1
		}, term)
		if len([]rune(cleaned)) < 2 {
			continue
		}
		parts = append(parts, fmt.Sprintf("\"%s*\"", cleaned))
	}

	return strings.Join(parts, " AND ")
}

func hasProductionHasCostDetailItemFullTextIndex(
	ctx context.Context,
	uretimDB *sql.DB,
) bool {
	var exists int
	err := uretimDB.QueryRowContext(ctx, `
SELECT CASE
	WHEN EXISTS (
		SELECT 1
		FROM sys.fulltext_indexes fi
		INNER JOIN sys.fulltext_index_columns fic
			ON fic.object_id = fi.object_id
		INNER JOIN sys.columns c
			ON c.object_id = fic.object_id
		   AND c.column_id = fic.column_id
		WHERE fi.object_id = OBJECT_ID('dbo.tbStok')
		  AND c.name = 'sAciklama'
	) THEN 1 ELSE 0
END`).Scan(&exists)
	return err == nil && exists == 1
}

func GetProductionHasCostDetailItemOptions(
	ctx context.Context,
	uretimDB *sql.DB,
	search string,
	limit int,
) (*sql.Rows, error) {
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 50
	}
	if search == "" {
		return uretimDB.QueryContext(ctx, `
SELECT TOP (0)
	RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) AS nStokID,
	LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) AS sKodu,
	LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama,
	LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
	LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) AS sBirim
FROM dbo.tbStok S`)
	}

	searchExact := search
	searchPrefix := search + "%"
	searchLike := "%" + search + "%"
	searchLen := len([]rune(search))
	numericStokID, numericErr := strconv.Atoi(search)
	hasNumericStokID := numericErr == nil
	fullTextSearch := buildSQLServerFullTextPrefixQuery(search)
	useFullText := searchLen >= 3 && fullTextSearch != "" && hasProductionHasCostDetailItemFullTextIndex(ctx, uretimDB)

	baseSelect := `
WITH c AS (
	SELECT
		RTRIM(CONVERT(VARCHAR(32), ISNULL(S.nStokID, 0))) AS nStokID,
		LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) AS sKodu,
		LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sAciklama, '')))) AS sAciklama,
		LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) AS sModel,
		LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(S.sBirimCinsi1, '')))) AS sBirim,
		CASE
			WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) = @p1 THEN 0
			WHEN (@p5 = 1 AND S.nStokID = @p6) THEN 1
			WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = @p1 THEN 2
			WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE @p3 THEN 3
			WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) LIKE @p3 THEN 4
			ELSE 5
		END AS match_rank,
		ROW_NUMBER() OVER (
			PARTITION BY LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, ''))))
			ORDER BY
				CASE
					WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) = @p1 THEN 0
					WHEN (@p5 = 1 AND S.nStokID = @p6) THEN 1
					WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) = @p1 THEN 2
					WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sModel, '')))) LIKE @p3 THEN 3
					WHEN LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, '')))) LIKE @p3 THEN 4
					ELSE 5
				END,
				LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(S.sKodu, ''))))
		) AS rn
	FROM dbo.tbStok S
	WHERE
		(ISNULL(S.IsBlocked, 0) = 0)
		AND S.sModel LIKE '_.%%'
		AND (
			(@p5 = 1 AND S.nStokID = @p6)
			OR S.sModel = @p1
			OR S.sModel LIKE @p3
			OR S.sKodu = @p1
			OR S.sKodu LIKE @p3
			OR %s
		)
)
SELECT TOP (@p2)
	nStokID,
	sKodu,
	sAciklama,
	sModel,
	sBirim
FROM c
WHERE rn = 1
ORDER BY match_rank, sModel, sKodu
OPTION (RECOMPILE)
`

	if useFullText {
		sqlText := fmt.Sprintf(baseSelect, `CONTAINS(S.sAciklama, @p4)`)
		return uretimDB.QueryContext(ctx, sqlText, searchExact, limit, searchPrefix, fullTextSearch, hasNumericStokID, numericStokID)
	}

	sqlText := fmt.Sprintf(baseSelect, `(@p4 >= 3 AND S.sAciklama LIKE @p7)`)
	return uretimDB.QueryContext(ctx, sqlText, searchExact, limit, searchPrefix, searchLen, hasNumericStokID, numericStokID, searchLike)
}

func GetProductionHasCostDetailColorOptions(
	ctx context.Context,
	mssqlDB *sql.DB,
	modelCode string,
	search string,
	limit int,
) (*sql.Rows, error) {
	modelCode = strings.TrimSpace(modelCode)
	search = strings.TrimSpace(search)
	if limit <= 0 {
		limit = 50
	}
	searchLike := "%" + search + "%"

	sqlText := `
WITH ColorSource AS (
	SELECT DISTINCT
		LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(T.sRenk, '')))) AS ColorCode
	FROM dbo.tbStok T
	WHERE ISNULL(T.IsBlocked, 0) = 0
	  AND LTRIM(RTRIM(CONVERT(NVARCHAR(255), ISNULL(T.sModel, '')))) = @p1
	  AND LTRIM(RTRIM(CONVERT(NVARCHAR(64), ISNULL(T.sRenk, '')))) <> ''
)
SELECT TOP (@p2)
	LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) AS colorCode,
	ISNULL(C.ColorDescription, '') AS colorDescription
FROM (
	SELECT ColorCode
	FROM ColorSource
) S
OUTER APPLY (
	SELECT TOP 1 ColorDescription
	FROM dbo.cdColorDesc CD
	WHERE CD.LangCode = 'TR'
	  AND LTRIM(RTRIM(ISNULL(CD.ColorCode, ''))) = S.ColorCode
) C
WHERE @p1 <> ''
  AND (
	@p3 = ''
	OR LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) LIKE @p4
	OR ISNULL(C.ColorDescription, '') LIKE @p4
  )
ORDER BY
	CASE
		WHEN @p3 <> '' AND LTRIM(RTRIM(ISNULL(S.ColorCode, ''))) = @p3 THEN 0
		WHEN @p3 <> '' AND ISNULL(C.ColorDescription, '') = @p3 THEN 1
		ELSE 2
	END,
	LTRIM(RTRIM(ISNULL(S.ColorCode, '')))
`

	return mssqlDB.QueryContext(ctx, sqlText, modelCode, limit, search, searchLike)
}

func GetProductionHasCostDetailExchangeRatesByDate(
	ctx context.Context,
	mssqlDB *sql.DB,
	costDate string,
) (*sql.Row, error) {
	costDate = strings.TrimSpace(costDate)

	sqlText := `
DECLARE @targetDate date = ISNULL(CONVERT(date, NULLIF(@p1, ''), 23), CONVERT(date, GETDATE()))

SELECT
	CONVERT(VARCHAR(10), @targetDate, 23) AS rateDate,
	ISNULL((
		SELECT TOP 1 Rate
		FROM dbo.AllExchangeRates
		WHERE CurrencyCode = 'USD'
		  AND RelationCurrencyCode = 'TRY'
		  AND ExchangeTypeCode = 6
		  AND Rate > 0
		  AND CONVERT(date, [Date]) <= @targetDate
		ORDER BY
			CASE WHEN CONVERT(date, [Date]) = @targetDate THEN 0 ELSE 1 END,
			[Date] DESC
	), 0) AS usdRate,
	ISNULL((
		SELECT TOP 1 Rate
		FROM dbo.AllExchangeRates
		WHERE CurrencyCode = 'EUR'
		  AND RelationCurrencyCode = 'TRY'
		  AND ExchangeTypeCode = 6
		  AND Rate > 0
		  AND CONVERT(date, [Date]) <= @targetDate
		ORDER BY
			CASE WHEN CONVERT(date, [Date]) = @targetDate THEN 0 ELSE 1 END,
			[Date] DESC
	), 0) AS eurRate,
	ISNULL((
		SELECT TOP 1 Rate
		FROM dbo.AllExchangeRates
		WHERE CurrencyCode = 'GBP'
		  AND RelationCurrencyCode = 'TRY'
		  AND ExchangeTypeCode = 6
		  AND Rate > 0
		  AND CONVERT(date, [Date]) <= @targetDate
		ORDER BY
			CASE WHEN CONVERT(date, [Date]) = @targetDate THEN 0 ELSE 1 END,
			[Date] DESC
	), 0) AS gbpRate
`

	return mssqlDB.QueryRowContext(ctx, sqlText, costDate), nil
}

func GetProductionHasCostLatestPurchasePriceForItem(
	ctx context.Context,
	mssqlDB *sql.DB,
	sKodu string,
	colorCode string,
	itemDim1Code string,
	costDate string,
) (*sql.Row, error) {
	sKodu = strings.TrimSpace(sKodu)
	colorCode = strings.TrimSpace(colorCode)
	itemDim1Code = strings.TrimSpace(itemDim1Code)
	costDate = strings.TrimSpace(costDate)

	sqlText := `
WITH BASE AS (
	SELECT
		A.InvoiceDate,
		A.InvoiceNumber,
		A.ProcessCode,
		A.CurrAccTypeCode,
		A.CurrAccCode,
		A.ItemTypeCode,
		A.ItemCode,
		A.ColorCode,
		A.ItemDim1Code,
		A.Qty1,
		A.Doc_Price,
		A.Doc_Amount,
		A.Doc_CurrencyCode
	FROM AllInvoicesWithAttributes A
	WHERE A.ProcessCode IN ('BP')
	  AND A.ATAtt01 IN (1, 2)
	  AND A.CompanyCode IN (1, 2, 5)
	  AND A.IsCompleted = 1
	  AND YEAR(A.InvoiceDate) >= 2022
	  AND LTRIM(RTRIM(A.ItemCode)) = @p1
	  AND (NULLIF(@p2, '') IS NULL OR CONVERT(date, A.InvoiceDate) < CONVERT(date, NULLIF(@p2, ''), 23))
)
SELECT TOP 1
	'MAN' AS priceType,
	CONVERT(VARCHAR(16), B.InvoiceDate, 120) AS Tarih,
	ISNULL(B.InvoiceNumber, '') AS FaturaKodu,
	LTRIM(RTRIM(ISNULL(B.ItemCode, ''))) AS MasrafKodu,
	ISNULL(ID.ItemDescription, '') AS MasrafDetay,
	ISNULL(B.ColorCode, '') AS ColorCode,
	ISNULL(COL.ColorDescription, '') AS ColorDescription,
	ISNULL(B.ItemDim1Code, '') AS ItemDim1Code,
	ISNULL(DIM1.ItemDim1Description, '') AS ItemDim1Description,
	ISNULL(B.Doc_Price, 0) AS EvrakFiyat,
	ISNULL(B.Doc_CurrencyCode, '') AS EvrakDoviz
FROM BASE B
LEFT JOIN cdItem CI
	ON CI.ItemTypeCode = B.ItemTypeCode
	AND CI.ItemCode = B.ItemCode
OUTER APPLY (
	SELECT TOP 1 ItemDescription
	FROM cdItemDesc
	WHERE ItemTypeCode = B.ItemTypeCode
	  AND ItemCode = B.ItemCode
	  AND LangCode = 'TR'
) ID
OUTER APPLY (
	SELECT TOP 1 ItemDim1Description
	FROM cdItemDim1Desc
	WHERE ItemDim1Code = B.ItemDim1Code
	  AND LangCode = 'TR'
) DIM1
OUTER APPLY (
	SELECT TOP 1 ColorDescription
	FROM cdColorDesc
	WHERE ColorCode = B.ColorCode
	  AND LangCode = 'TR'
) COL
ORDER BY
	CASE
		WHEN @p3 <> '' AND LTRIM(RTRIM(ISNULL(B.ColorCode, ''))) = @p3 THEN 0
		WHEN @p3 = '' THEN 0
		ELSE 1
	END,
	CASE
		WHEN @p4 <> '' AND LTRIM(RTRIM(ISNULL(B.ItemDim1Code, ''))) = @p4 THEN 0
		WHEN @p4 = '' THEN 0
		ELSE 1
	END,
	B.InvoiceDate DESC,
	B.InvoiceNumber DESC
`

	return mssqlDB.QueryRowContext(ctx, sqlText, sKodu, costDate, colorCode, itemDim1Code), nil
}

// Best-effort: resolve TR item description for an item code. Used for warnings export readability.
func GetItemDescriptionTRByItemCode(ctx context.Context, mssqlDB *sql.DB, itemCode string) (string, error) {
	itemCode = strings.TrimSpace(itemCode)
	if itemCode == "" {
		return "", nil
	}
	sqlText := ` 
SELECT TOP 1 ISNULL(D.ItemDescription, '') AS ItemDescription 
FROM cdItem CI 
INNER JOIN cdItemDesc D 
  ON D.ItemTypeCode = CI.ItemTypeCode 
 AND D.ItemCode = CI.ItemCode 
 AND D.LangCode = 'TR' 
WHERE LTRIM(RTRIM(CI.ItemCode)) = @p1 
`
	var desc string
	if err := mssqlDB.QueryRowContext(ctx, sqlText, itemCode).Scan(&desc); err != nil {
		if err == sql.ErrNoRows {
			return "", nil
		}
		return "", err
	}
	return strings.TrimSpace(desc), nil
}

// Bulk version of GetProductionHasCostLatestPurchasePriceForItem.
// Uses OPENJSON to avoid 1-query-per-item fan-out.
// For each requested rowKey, picks the latest purchase invoice before costDate,
// preferring exact ColorCode/ItemDim1Code when provided.
func GetProductionHasCostLatestPurchasePricesForItems(
	ctx context.Context,
	mssqlDB *sql.DB,
	itemsJSON string,
	costDate string,
) (*sql.Rows, error) {
	itemsJSON = strings.TrimSpace(itemsJSON)
	costDate = strings.TrimSpace(costDate)

	sqlText := ` 
DECLARE @targetDate date = TRY_CONVERT(date, NULLIF(@p2, ''), 23); 
 
WITH REQ AS ( 
	SELECT 
		RowKey, 
		LTRIM(RTRIM(ISNULL(ItemCode, ''))) AS ItemCode, 
		LTRIM(RTRIM(ISNULL(ColorCode, ''))) AS ColorCode, 
		LTRIM(RTRIM(ISNULL(ItemDim1Code, ''))) AS ItemDim1Code 
	FROM OPENJSON(@p1) WITH ( 
		RowKey NVARCHAR(128) '$.rowKey', 
		ItemCode NVARCHAR(128) '$.sKodu', 
		ColorCode NVARCHAR(64) '$.colorCode', 
		ItemDim1Code NVARCHAR(64) '$.itemDim1Code' 
	) 
	WHERE LTRIM(RTRIM(ISNULL(ItemCode, ''))) <> '' 
), BASE AS ( 
	SELECT 
		R.RowKey, 
		A.InvoiceDate, 
		A.InvoiceNumber, 
		A.ItemTypeCode, 
		A.ItemCode, 
		A.ColorCode, 
		A.ItemDim1Code, 
		A.Qty1, 
		A.Doc_Price, 
		A.Doc_CurrencyCode, 
		CASE 
			WHEN R.ColorCode <> '' AND LTRIM(RTRIM(ISNULL(A.ColorCode, ''))) = R.ColorCode THEN 0 
			WHEN R.ColorCode = '' THEN 0 
			ELSE 1 
		END AS colorRank, 
		CASE 
			WHEN R.ItemDim1Code <> '' AND LTRIM(RTRIM(ISNULL(A.ItemDim1Code, ''))) = R.ItemDim1Code THEN 0 
			WHEN R.ItemDim1Code = '' THEN 0 
			ELSE 1 
		END AS dimRank 
	FROM REQ R 
	INNER JOIN AllInvoicesWithAttributes A 
		ON LTRIM(RTRIM(A.ItemCode)) = R.ItemCode 
	WHERE A.ProcessCode IN ('BP') 
	  AND A.ATAtt01 IN (1, 2) 
	  AND A.CompanyCode IN (1, 2, 5) 
	  AND A.IsCompleted = 1 
	  AND YEAR(A.InvoiceDate) >= 2022 
	  AND (@targetDate IS NULL OR CONVERT(date, A.InvoiceDate) < @targetDate) 
), RANKED AS ( 
	SELECT 
		B.*, 
		ROW_NUMBER() OVER (PARTITION BY B.RowKey ORDER BY B.colorRank, B.dimRank, B.InvoiceDate DESC, B.InvoiceNumber DESC) AS rn 
	FROM BASE B 
) 
SELECT 
	R.RowKey, 
	'MAN' AS priceType, 
	CONVERT(VARCHAR(16), R.InvoiceDate, 120) AS Tarih, 
	ISNULL(R.InvoiceNumber, '') AS FaturaKodu, 
	LTRIM(RTRIM(ISNULL(R.ItemCode, ''))) AS MasrafKodu, 
	ISNULL(ID.ItemDescription, '') AS MasrafDetay, 
	ISNULL(R.ColorCode, '') AS ColorCode, 
	ISNULL(COL.ColorDescription, '') AS ColorDescription, 
	ISNULL(R.ItemDim1Code, '') AS ItemDim1Code, 
	ISNULL(DIM1.ItemDim1Description, '') AS ItemDim1Description, 
	ISNULL(R.Doc_Price, 0) AS EvrakFiyat, 
	ISNULL(R.Doc_CurrencyCode, '') AS EvrakDoviz 
FROM RANKED R 
OUTER APPLY ( 
	SELECT TOP 1 ItemDescription 
	FROM cdItemDesc 
	WHERE ItemTypeCode = R.ItemTypeCode 
	  AND ItemCode = R.ItemCode 
	  AND LangCode = 'TR' 
) ID 
OUTER APPLY ( 
	SELECT TOP 1 ItemDim1Description 
	FROM cdItemDim1Desc 
	WHERE ItemDim1Code = R.ItemDim1Code 
	  AND LangCode = 'TR' 
) DIM1 
OUTER APPLY ( 
	SELECT TOP 1 ColorDescription 
	FROM cdColorDesc 
	WHERE ColorCode = R.ColorCode 
	  AND LangCode = 'TR' 
) COL 
WHERE R.rn = 1; 
`

	return mssqlDB.QueryContext(ctx, sqlText, itemsJSON, costDate)
}

func GetProductionHasCostPurchaseHistoryByExpenseCode(
	ctx context.Context,
	mssqlDB *sql.DB,
	sKodu string,
	costDate string,
	limit int,
) (*sql.Rows, error) {
	sKodu = strings.TrimSpace(sKodu)
	costDate = strings.TrimSpace(costDate)
	if limit <= 0 {
		limit = 250
	}

	sqlText := `
WITH BASE AS (
	SELECT
		A.InvoiceDate,
		A.InvoiceNumber,
		A.ProcessCode,
		A.CurrAccTypeCode,
		A.CurrAccCode,
		A.ItemTypeCode,
		A.ItemCode,
		A.ColorCode,
		A.ItemDim1Code,
		A.Qty1,
		A.Doc_Price,
		A.Doc_Amount,
		A.Doc_CurrencyCode
	FROM AllInvoicesWithAttributes A
	WHERE A.ProcessCode IN ('BP')
	  AND A.ATAtt01 IN (1, 2)
	  AND A.CompanyCode IN (1, 2, 5)
	  AND A.IsCompleted = 1
	  AND YEAR(A.InvoiceDate) >= 2022
	  AND LTRIM(RTRIM(A.ItemCode)) = @p1
	  AND (ISNULL(A.Doc_Price, 0) > 0 OR ISNULL(A.Doc_Amount, 0) > 0)
	  AND (NULLIF(@p2, '') IS NULL OR CONVERT(date, A.InvoiceDate) < CONVERT(date, NULLIF(@p2, ''), 23))
)
SELECT TOP (@p3)
	'purchase' AS sourceType,
	'MAN' AS priceType,
	CONVERT(VARCHAR(16), B.InvoiceDate, 120) AS Tarih,
	ISNULL(B.InvoiceNumber, '') AS FaturaKodu,
	ISNULL(B.CurrAccCode, '') AS FirmaKodu,
	ISNULL(CAD.CurrAccDescription, '') AS FirmaAciklama,
	LTRIM(RTRIM(ISNULL(B.ItemCode, ''))) AS MasrafKodu,
	ISNULL(ID.ItemDescription, '') AS MasrafDetay,
	ISNULL(B.ColorCode, '') AS ColorCode,
	ISNULL(COL.ColorDescription, '') AS ColorDescription,
	ISNULL(B.ItemDim1Code, '') AS ItemDim1Code,
	ISNULL(DIM1.ItemDim1Description, '') AS ItemDim1Description,
	ISNULL(B.Qty1, 0) AS Miktar,
	CASE
		WHEN B.ProcessCode = 'EP' THEN ''
		ELSE ISNULL(CI.UnitOfMeasureCode1, '')
	END AS BIRIM,
	ISNULL(B.Doc_Price, 0) AS EvrakFiyat,
	ISNULL(B.Doc_Amount, 0) AS EvrakTutar,
	ISNULL(B.Doc_CurrencyCode, '') AS EvrakDoviz
FROM BASE B
LEFT JOIN cdItem CI
	ON CI.ItemTypeCode = B.ItemTypeCode
	AND CI.ItemCode = B.ItemCode
OUTER APPLY (
	SELECT TOP 1 ItemDescription
	FROM cdItemDesc
	WHERE ItemTypeCode = B.ItemTypeCode
	  AND ItemCode = B.ItemCode
	  AND LangCode = 'TR'
) ID
OUTER APPLY (
	SELECT TOP 1 ItemDim1Description
	FROM cdItemDim1Desc
	WHERE ItemDim1Code = B.ItemDim1Code
	  AND LangCode = 'TR'
) DIM1
OUTER APPLY (
	SELECT TOP 1 ColorDescription
	FROM cdColorDesc
	WHERE ColorCode = B.ColorCode
	  AND LangCode = 'TR'
) COL
OUTER APPLY (
	SELECT TOP 1 CurrAccDescription
	FROM cdCurrAccDesc
	WHERE CurrAccTypeCode = B.CurrAccTypeCode
	  AND CurrAccCode = B.CurrAccCode
	  AND LangCode = 'TR'
) CAD
ORDER BY
	B.InvoiceDate DESC,
	B.InvoiceNumber DESC
`

	return mssqlDB.QueryContext(ctx, sqlText, sKodu, costDate, limit)
}

func GetProductionHasCostRecipeHistoryByExpenseCode(
	ctx context.Context,
	uretimDB *sql.DB,
	currentOnMLNo int,
	sKodu string,
	colorCode string,
	costDate string,
	limit int,
) (*sql.Rows, error) {
	sKodu = strings.TrimSpace(sKodu)
	colorCode = strings.TrimSpace(colorCode)
	costDate = strings.TrimSpace(costDate)
	if limit <= 0 {
		limit = 250
	}

	sqlText := `
SELECT TOP (@p5)
	'recipe' AS sourceType,
	ISNULL(NULLIF(LTRIM(RTRIM(D.sFiyatTipi)), ''), 'SAF') AS priceType,
	CONVERT(VARCHAR(16), COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi), 120) AS dteIslemTarihi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
	ISNULL(UF.FirmaKodu, '') AS FirmaKodu,
	ISNULL(UF.FirmaAciklama, '') AS FirmaAciklama,
	ISNULL(D.sKodu, '') AS sKodu,
	ISNULL(D.sAciklama, '') AS sAciklama,
	ISNULL(D.sRenk, '') AS sRenk,
	ISNULL(D.lMiktar, 0) AS lMiktar,
	ISNULL(D.sBirim, '') AS sBirim,
	CASE
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0)
		ELSE ISNULL(D.lDovizFiyati, 0)
	END AS lDovizFiyati,
	CASE
		WHEN ISNULL(D.lDovizTutari, 0) > 0 THEN ISNULL(D.lDovizTutari, 0)
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0) * ISNULL(D.lMiktar, 0)
		ELSE ISNULL(D.lDovizFiyati, 0) * ISNULL(D.lMiktar, 0)
	END AS lDovizTutari,
	CASE
		WHEN LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) <> '' THEN LTRIM(RTRIM(D.fiyat_doviz))
		WHEN LTRIM(RTRIM(ISNULL(D.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(D.sDovizCinsi))
		WHEN LTRIM(RTRIM(ISNULL(M.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(M.sDovizCinsi))
		ELSE 'USD'
	END AS USD,
	ISNULL(NULLIF(LTRIM(RTRIM(T.sAciklama3)), ''), N'DUMMY') AS DUMMY
FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M
	ON M.nOnMLNo = D.nOnMLNo
LEFT JOIN dbo.spUrtOnMLHammaddeTuru T
	ON T.nHammaddeTuruNo = D.nHammaddeTuruNo
OUTER APPLY (
	SELECT TOP 1
		ISNULL(F.sKodu, '') AS FirmaKodu,
		ISNULL(F.sAciklama, '') AS FirmaAciklama
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE (
			(ISNULL(M.nUrtReceteID, 0) > 0 AND SD.nUrtReceteID = M.nUrtReceteID)
			OR (ISNULL(M.nUrtReceteID, 0) <= 0 AND LTRIM(RTRIM(ISNULL(SD.sMModelKodu, ''))) = LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))))
		  )
	  AND SD.dteIslemTarihi <= COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) UF
WHERE LTRIM(RTRIM(D.sKodu)) = @p1
  AND (@p2 <= 0 OR D.nOnMLNo <> @p2)
  AND (
		ISNULL(D.fiyat_girilen, 0) > 0
		OR ISNULL(D.lDovizFiyati, 0) > 0
		OR ISNULL(D.lDovizTutari, 0) > 0
  )
  AND (
		NULLIF(@p3, '') IS NULL
		OR CONVERT(date, COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, NULLIF(@p3, ''), 23)
  )
ORDER BY
	CASE
		WHEN @p4 <> '' AND LTRIM(RTRIM(ISNULL(D.sRenk, ''))) = @p4 THEN 0
		WHEN @p4 = '' THEN 0
		ELSE 1
	END,
	COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi) DESC,
	D.nOnMLNo DESC,
	D.nOnMLDetNo DESC
`

	return uretimDB.QueryContext(ctx, sqlText, sKodu, currentOnMLNo, costDate, colorCode, limit)
}

func BuildProductionHasCostSimilarCodePrefix(sKodu string) string {
	normalizedCode := strings.ToUpper(strings.TrimSpace(sKodu))
	if normalizedCode == "" {
		return ""
	}

	runes := []rune(normalizedCode)
	if len(runes) <= 4 {
		return normalizedCode
	}
	if len(runes) >= 5 && len(runes) > 1 && runes[1] == '.' {
		return string(runes[:5])
	}

	return string(runes[:4])
}

func GetProductionHasCostPurchaseHistoryByCodePrefix(
	ctx context.Context,
	mssqlDB *sql.DB,
	sKoduPrefix string,
	costDate string,
	limit int,
) (*sql.Rows, error) {
	sKoduPrefix = strings.TrimSpace(sKoduPrefix)
	costDate = strings.TrimSpace(costDate)
	if limit <= 0 {
		limit = 250
	}

	sqlText := `
WITH BASE AS (
	SELECT
		A.InvoiceDate,
		A.InvoiceNumber,
		A.ProcessCode,
		A.CurrAccTypeCode,
		A.CurrAccCode,
		A.ItemTypeCode,
		A.ItemCode,
		A.ColorCode,
		A.ItemDim1Code,
		A.Qty1,
		A.Doc_Price,
		A.Doc_Amount,
		A.Doc_CurrencyCode
	FROM AllInvoicesWithAttributes A
	WHERE A.ProcessCode IN ('BP')
	  AND A.ATAtt01 IN (1, 2)
	  AND A.CompanyCode IN (1, 2, 5)
	  AND A.IsCompleted = 1
	  AND YEAR(A.InvoiceDate) >= 2022
	  AND NULLIF(@p1, '') IS NOT NULL
	  AND LTRIM(RTRIM(A.ItemCode)) LIKE @p1 + '%'
	  AND (ISNULL(A.Doc_Price, 0) > 0 OR ISNULL(A.Doc_Amount, 0) > 0)
	  AND (NULLIF(@p2, '') IS NULL OR CONVERT(date, A.InvoiceDate) < CONVERT(date, NULLIF(@p2, ''), 23))
)
SELECT TOP (@p3)
	'purchase' AS sourceType,
	'BNZ' AS priceType,
	CONVERT(VARCHAR(16), B.InvoiceDate, 120) AS Tarih,
	ISNULL(B.InvoiceNumber, '') AS FaturaKodu,
	ISNULL(B.CurrAccCode, '') AS FirmaKodu,
	ISNULL(CAD.CurrAccDescription, '') AS FirmaAciklama,
	LTRIM(RTRIM(ISNULL(B.ItemCode, ''))) AS MasrafKodu,
	ISNULL(ID.ItemDescription, '') AS MasrafDetay,
	ISNULL(B.ColorCode, '') AS ColorCode,
	ISNULL(COL.ColorDescription, '') AS ColorDescription,
	ISNULL(B.ItemDim1Code, '') AS ItemDim1Code,
	ISNULL(DIM1.ItemDim1Description, '') AS ItemDim1Description,
	ISNULL(B.Qty1, 0) AS Miktar,
	CASE
		WHEN B.ProcessCode = 'EP' THEN ''
		ELSE ISNULL(CI.UnitOfMeasureCode1, '')
	END AS BIRIM,
	ISNULL(B.Doc_Price, 0) AS EvrakFiyat,
	ISNULL(B.Doc_Amount, 0) AS EvrakTutar,
	ISNULL(B.Doc_CurrencyCode, '') AS EvrakDoviz
FROM BASE B
LEFT JOIN cdItem CI
	ON CI.ItemTypeCode = B.ItemTypeCode
	AND CI.ItemCode = B.ItemCode
OUTER APPLY (
	SELECT TOP 1 ItemDescription
	FROM cdItemDesc
	WHERE ItemTypeCode = B.ItemTypeCode
	  AND ItemCode = B.ItemCode
	  AND LangCode = 'TR'
) ID
OUTER APPLY (
	SELECT TOP 1 ItemDim1Description
	FROM cdItemDim1Desc
	WHERE ItemDim1Code = B.ItemDim1Code
	  AND LangCode = 'TR'
) DIM1
OUTER APPLY (
	SELECT TOP 1 ColorDescription
	FROM cdColorDesc
	WHERE ColorCode = B.ColorCode
	  AND LangCode = 'TR'
) COL
OUTER APPLY (
	SELECT TOP 1 CurrAccDescription
	FROM cdCurrAccDesc
	WHERE CurrAccTypeCode = B.CurrAccTypeCode
	  AND CurrAccCode = B.CurrAccCode
	  AND LangCode = 'TR'
) CAD
ORDER BY
	B.InvoiceDate DESC,
	B.InvoiceNumber DESC
`

	return mssqlDB.QueryContext(ctx, sqlText, sKoduPrefix, costDate, limit)
}

func GetProductionHasCostOnMLHistoryByCodePrefix(
	ctx context.Context,
	uretimDB *sql.DB,
	sKoduPrefix string,
	costDate string,
	limit int,
) (*sql.Rows, error) {
	sKoduPrefix = strings.TrimSpace(sKoduPrefix)
	costDate = strings.TrimSpace(costDate)
	if limit <= 0 {
		limit = 250
	}

	sqlText := `
SELECT TOP (@p3)
	'recipe' AS sourceType,
	'BNZ' AS priceType,
	CONVERT(VARCHAR(16), COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi), 120) AS dteIslemTarihi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
	ISNULL(UF.FirmaKodu, '') AS FirmaKodu,
	ISNULL(UF.FirmaAciklama, '') AS FirmaAciklama,
	ISNULL(D.sKodu, '') AS sKodu,
	ISNULL(D.sAciklama, '') AS sAciklama,
	ISNULL(D.sRenk, '') AS sRenk,
	ISNULL(D.lMiktar, 0) AS lMiktar,
	ISNULL(D.sBirim, '') AS sBirim,
	CASE
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0)
		ELSE ISNULL(D.lDovizFiyati, 0)
	END AS lDovizFiyati,
	CASE
		WHEN ISNULL(D.lDovizTutari, 0) > 0 THEN ISNULL(D.lDovizTutari, 0)
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0) * ISNULL(D.lMiktar, 0)
		ELSE ISNULL(D.lDovizFiyati, 0) * ISNULL(D.lMiktar, 0)
	END AS lDovizTutari,
	CASE
		WHEN LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) <> '' THEN LTRIM(RTRIM(D.fiyat_doviz))
		WHEN LTRIM(RTRIM(ISNULL(D.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(D.sDovizCinsi))
		WHEN LTRIM(RTRIM(ISNULL(M.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(M.sDovizCinsi))
		ELSE 'USD'
	END AS USD,
	ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), N'DUMMY') AS DUMMY
FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M
	ON M.nOnMLNo = D.nOnMLNo
OUTER APPLY (
	SELECT TOP 1
		ISNULL(F.sKodu, '') AS FirmaKodu,
		ISNULL(F.sAciklama, '') AS FirmaAciklama
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE (
			(ISNULL(M.nUrtReceteID, 0) > 0 AND SD.nUrtReceteID = M.nUrtReceteID)
			OR (ISNULL(M.nUrtReceteID, 0) <= 0 AND LTRIM(RTRIM(ISNULL(SD.sMModelKodu, ''))) = LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))))
		  )
	  AND SD.dteIslemTarihi <= COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) UF
WHERE NULLIF(@p1, '') IS NOT NULL
  AND LTRIM(RTRIM(ISNULL(D.sKodu, ''))) LIKE @p1 + '%'
  AND (
		ISNULL(D.fiyat_girilen, 0) > 0
		OR ISNULL(D.lDovizFiyati, 0) > 0
		OR ISNULL(D.lDovizTutari, 0) > 0
  )
  AND (
		NULLIF(@p2, '') IS NULL
		OR CONVERT(date, COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, NULLIF(@p2, ''), 23)
  )
ORDER BY
	COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi) DESC,
	D.nOnMLNo DESC,
	D.nOnMLDetNo DESC
`

	return uretimDB.QueryContext(ctx, sqlText, sKoduPrefix, costDate, limit)
}

func GetProductionHasCostOnMLHistoryByHammaddeTuruNo(
	ctx context.Context,
	uretimDB *sql.DB,
	nHammaddeTuruNo string,
	costDate string,
	limit int,
) (*sql.Rows, error) {
	nHammaddeTuruNo = strings.TrimSpace(nHammaddeTuruNo)
	costDate = strings.TrimSpace(costDate)
	if limit <= 0 {
		limit = 250
	}

	sqlText := `
SELECT TOP (@p3)
	'recipe' AS sourceType,
	'BNZ' AS priceType,
	CONVERT(VARCHAR(16), COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi), 120) AS dteIslemTarihi,
	RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
	ISNULL(UF.FirmaKodu, '') AS FirmaKodu,
	ISNULL(UF.FirmaAciklama, '') AS FirmaAciklama,
	ISNULL(D.sKodu, '') AS sKodu,
	ISNULL(D.sAciklama, '') AS sAciklama,
	ISNULL(D.sRenk, '') AS sRenk,
	ISNULL(D.lMiktar, 0) AS lMiktar,
	ISNULL(D.sBirim, '') AS sBirim,
	CASE
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0)
		ELSE ISNULL(D.lDovizFiyati, 0)
	END AS lDovizFiyati,
	CASE
		WHEN ISNULL(D.lDovizTutari, 0) > 0 THEN ISNULL(D.lDovizTutari, 0)
		WHEN ISNULL(D.fiyat_girilen, 0) > 0 THEN ISNULL(D.fiyat_girilen, 0) * ISNULL(D.lMiktar, 0)
		ELSE ISNULL(D.lDovizFiyati, 0) * ISNULL(D.lMiktar, 0)
	END AS lDovizTutari,
	CASE
		WHEN LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) <> '' THEN LTRIM(RTRIM(D.fiyat_doviz))
		WHEN LTRIM(RTRIM(ISNULL(D.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(D.sDovizCinsi))
		WHEN LTRIM(RTRIM(ISNULL(M.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(M.sDovizCinsi))
		ELSE 'USD'
	END AS USD,
	ISNULL(NULLIF(LTRIM(RTRIM(D.sAciklama3)), ''), N'DUMMY') AS DUMMY
FROM dbo.spUrtOnMLMasDet D
INNER JOIN dbo.spUrtOnMLMas M
	ON M.nOnMLNo = D.nOnMLNo
OUTER APPLY (
	SELECT TOP 1
		ISNULL(F.sKodu, '') AS FirmaKodu,
		ISNULL(F.sAciklama, '') AS FirmaAciklama
	FROM dbo.spUrtSiparisDet SD
	INNER JOIN dbo.spUrtSiparis SM
		ON SM.nUrtSiparisID = SD.nUrtSiparisID
	LEFT JOIN dbo.tbFirma F
		ON F.nFirmaID = SM.nFirmaID
	WHERE (
			(ISNULL(M.nUrtReceteID, 0) > 0 AND SD.nUrtReceteID = M.nUrtReceteID)
			OR (ISNULL(M.nUrtReceteID, 0) <= 0 AND LTRIM(RTRIM(ISNULL(SD.sMModelKodu, ''))) = LTRIM(RTRIM(ISNULL(M.UrunKodu, ''))))
		  )
	  AND SD.dteIslemTarihi <= COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)
	ORDER BY SD.dteIslemTarihi DESC, SD.nUrtSiparisID DESC
) UF
WHERE D.nHammaddeTuruNo = @p1
  AND ISNULL(D.fiyat_girilen, 0) > 0
  AND (
		NULLIF(@p2, '') IS NULL
		OR CONVERT(date, COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, NULLIF(@p2, ''), 23)
  )
ORDER BY
	COALESCE(D.dteIslemTarihiDeg, D.dteIslemTarihi, M.Tarihi, M.dteKayitTarihi) DESC,
	D.nOnMLNo DESC,
	D.nOnMLDetNo DESC
`

	return uretimDB.QueryContext(ctx, sqlText, nHammaddeTuruNo, costDate, limit)
}

func GetProductionHasCostSimilarItemHistory(
	ctx context.Context,
	mssqlDB *sql.DB,
	uretimDB *sql.DB,
	nHammaddeTuruNo string,
	costDate string,
	limit int,
) ([]any, error) {
	if limit <= 0 {
		limit = 50
	}

	// 1. Satinalma (V3) tarafini sorgula - Simdilik V3 tarafi icin hammadde turu eslestirmesi belirsiz oldugundan pasif
	/*
		v3Sql := `
		SELECT TOP (@p2)
			'purchase' AS sourceType,
			'BNZ' AS priceType,
			CONVERT(VARCHAR(16), A.InvoiceDate, 120) AS Tarih,
			ISNULL(A.InvoiceNumber, '') AS FaturaKodu,
			LTRIM(RTRIM(ISNULL(A.ItemCode, ''))) AS MasrafKodu,
			ISNULL(ID.ItemDescription, '') AS MasrafDetay,
			ISNULL(A.ColorCode, '') AS ColorCode,
			ISNULL(A.Qty1, 0) AS Miktar,
			ISNULL(CI.UnitOfMeasureCode1, '') AS BIRIM,
			ISNULL(A.Doc_Price, 0) AS EvrakFiyat,
			ISNULL(A.Doc_CurrencyCode, '') AS EvrakDoviz
		FROM AllInvoicesWithAttributes A
		LEFT JOIN cdItem CI ON CI.ItemTypeCode = A.ItemTypeCode AND CI.ItemCode = A.ItemCode
		OUTER APPLY (
			SELECT TOP 1 ItemDescription FROM cdItemDesc
			WHERE ItemTypeCode = A.ItemTypeCode AND ItemCode = A.ItemCode AND LangCode = 'TR'
		) ID
		WHERE A.ProcessCode IN ('BP')
		  AND A.ATAtt01 IN (1, 2)
		  AND A.CompanyCode IN (1, 2, 5)
		  AND A.IsCompleted = 1
		  AND YEAR(A.InvoiceDate) >= 2023
		  AND (NULLIF(@p1, '') IS NULL OR CONVERT(date, A.InvoiceDate) < CONVERT(date, NULLIF(@p1, ''), 23))
		  AND EXISTS (
			  -- Hammadde turu eslestirmesi (V3 tarafindaki karsiligi varsa)
			  SELECT 1 FROM cdItem WHERE ItemCode = A.ItemCode
		  )
		ORDER BY A.InvoiceDate DESC
		`
	*/

	// 2. Uretim (Recete) tarafini sorgula
	uretimSql := `
	SELECT TOP (@p3)
		'recipe' AS sourceType,
		'BNZ' AS priceType,
		CONVERT(VARCHAR(16), ISNULL(M.Tarihi, M.dteKayitTarihi), 120) AS dteIslemTarihi,
		RTRIM(CONVERT(VARCHAR(32), ISNULL(D.nOnMLNo, 0))) AS nOnMLNo,
		ISNULL(D.sKodu, '') AS sKodu,
		ISNULL(D.sAciklama, '') AS sAciklama,
		ISNULL(D.sRenk, '') AS sRenk,
		ISNULL(D.lMiktar, 0) AS lMiktar,
		ISNULL(D.sBirim, '') AS sBirim,
		ISNULL(D.lDovizFiyati, 0) AS lDovizFiyati,
		CASE
			WHEN LTRIM(RTRIM(ISNULL(D.fiyat_doviz, ''))) <> '' THEN LTRIM(RTRIM(D.fiyat_doviz))
			WHEN LTRIM(RTRIM(ISNULL(M.sDovizCinsi, ''))) <> '' THEN LTRIM(RTRIM(M.sDovizCinsi))
			ELSE 'USD'
		END AS USD
	FROM dbo.spUrtOnMLMasDet D
	INNER JOIN dbo.spUrtOnMLMas M ON M.nOnMLNo = D.nOnMLNo
	WHERE D.nHammaddeTuruNo = @p1
	  AND (NULLIF(@p2, '') IS NULL OR CONVERT(date, ISNULL(M.Tarihi, M.dteKayitTarihi)) < CONVERT(date, NULLIF(@p2, ''), 23))
	ORDER BY ISNULL(M.Tarihi, M.dteKayitTarihi) DESC
	`

	// Not: nHammaddeTuruNo parametresine gore sadece Reçete tarafı şu an doğrudan filtrelenebiliyor.
	// V3 tarafı için ItemTypeCode veya özel bir grup kodu gerekebilir.
	// Kullanıcının talebi üzerine Reçete (URETIM) odaklı sorguyu önceliklendiriyoruz.

	rows, err := uretimDB.QueryContext(ctx, uretimSql, nHammaddeTuruNo, costDate, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var results []any
	for rows.Next() {
		// Basit bir map veya struct ile dondurebiliriz
		var row struct {
			SourceType string  `json:"sourceType"`
			PriceType  string  `json:"priceType"`
			Tarih      string  `json:"Tarih"`
			EvrakKodu  string  `json:"EvrakKodu"`
			Kod        string  `json:"Kod"`
			Aciklama   string  `json:"Aciklama"`
			Renk       string  `json:"Renk"`
			Miktar     float64 `json:"Miktar"`
			Birim      string  `json:"Birim"`
			Fiyat      float64 `json:"Fiyat"`
			Doviz      string  `json:"Doviz"`
		}
		if err := rows.Scan(
			&row.SourceType, &row.PriceType, &row.Tarih, &row.EvrakKodu,
			&row.Kod, &row.Aciklama, &row.Renk, &row.Miktar, &row.Birim,
			&row.Fiyat, &row.Doviz,
		); err == nil {
			results = append(results, row)
		}
	}

	return results, nil
}
