// ===================== PART 1 (Satır 1-300) =====================
// =======================================================
// order_write.go — v4.3 FINAL (Insert + Update + Delete)
//   - PCTCode her zaman "%0" olarak yazılır (cdPCT FK uyumlu)
//   - INSERT/UPDATE öncesi ItemVariant Guard + Duplicate Guard (payload içi)
//   - UpdateOrder: DELETE öncesi child tablolar (trOrderLineCurrency) temizlenir
// =======================================================

package queries

import (
	"bssapp-backend/db"
	"bssapp-backend/models"
	"database/sql"
	"fmt"
	"math"
	"strings"
	"time"

	"github.com/google/uuid"
)

func nf0(v models.NullFloat64) float64 {
	if !v.Valid {
		return 0
	}
	return v.Float64
}

// =======================================================
// ✅ trOrderLineCurrency UPSERT (Doc Price / Amount yaz)
// =======================================================
func upsertLineCurrency(
	tx *sql.Tx,
	lineID string,

	docCurrency string,
	exRate float64,

	docPrice float64,
	qty float64,

	lDis float64, // Line Discount %
	tDis float64, // Total Discount %

	vatRate float64,
	ln models.OrderDetail,

	user string,
) error {

	// ================= NORMALIZE =================

	if docCurrency == "" {
		docCurrency = "TRY"
	}

	if exRate <= 0 {
		exRate = 1
	}

	relationCurrency := safeNS(ln.RelationCurrencyCode)
	if relationCurrency == "" {
		relationCurrency = docCurrency
	}

	if ln.LineDiscount.Valid {
		lDis = ln.LineDiscount.Float64
	}
	if ln.TotalDiscount.Valid {
		tDis = ln.TotalDiscount.Float64
	}

	basePrice := docPrice // Price (KDV hariç)
	if docCurrency == "TRY" && ln.LocalPrice.Valid && ln.LocalPrice.Float64 > 0 {
		basePrice = ln.LocalPrice.Float64
	}

	amountBase := basePrice * qty // Amount (KDV hariç)
	if docCurrency == "TRY" && ln.LocalAmount.Valid && ln.LocalAmount.Float64 > 0 {
		amountBase = ln.LocalAmount.Float64
	}

	priceVI := basePrice // PriceVI (KDV dahil)
	if ln.DocPrice.Valid && ln.DocPrice.Float64 > 0 {
		priceVI = ln.DocPrice.Float64
	}

	amountVI := priceVI * qty // AmountVI (KDV dahil)
	if ln.DocAmount.Valid && ln.DocAmount.Float64 > 0 {
		amountVI = ln.DocAmount.Float64
		if qty > 0 {
			priceVI = amountVI / qty
		}
	}

	// ================= DISCOUNT =================

	afterLineDis := amountBase * (1 - lDis/100)
	afterTotalDis := afterLineDis * (1 - tDis/100)

	// ================= TAX =================

	taxBase := afterTotalDis
	if ln.TaxBase.Valid {
		taxBase = ln.TaxBase.Float64
	}

	vat := taxBase * vatRate / 100
	if ln.VatAmount.Valid {
		vat = ln.VatAmount.Float64
	}

	vatDeducation := 0.0
	if ln.VatDeducation.Valid {
		vatDeducation = ln.VatDeducation.Float64
	}

	net := taxBase + vat - vatDeducation
	if ln.NetAmount.Valid {
		net = ln.NetAmount.Float64
	}

	pct := 0.0
	if ln.Pct.Valid {
		pct = ln.Pct.Float64
	}

	// payload yoksa doc tutarı net ile hizala
	if !ln.DocAmount.Valid {
		amountVI = net
		if qty > 0 {
			priceVI = amountVI / qty
		}
	}

	// ================= LOCAL =================

	localPrice := basePrice * exRate
	localAmount := amountBase * exRate

	localPriceVI := priceVI * exRate
	localAmountVI := amountVI * exRate

	localTaxBase := taxBase * exRate
	localVat := vat * exRate
	localVatDeducation := vatDeducation * exRate
	localNet := net * exRate

	// ================= DOC =================

	_, err := tx.Exec(`
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
USING (SELECT @p1 AS OrderLineID, @p2 AS CurrencyCode) AS S
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode

WHEN MATCHED THEN UPDATE SET
	RelationCurrencyCode=@p3,

	ExchangeRate=@p4,

	PriceVI=@p5,
	AmountVI=@p6,

	Price=@p7,
	Amount=@p8,

	LDiscount1=@p9,
	TDiscount1=@p10,

	TaxBase=@p11,
	Pct=@p12,
	Vat=@p13,
	VatDeducation=@p14,
	NetAmount=@p15,

	LastUpdatedUserName=@p16,
	LastUpdatedDate=GETDATE()

WHEN NOT MATCHED THEN INSERT (
	OrderLineID,
	CurrencyCode,
	RelationCurrencyCode,

	ExchangeRate,

	PriceVI,
	AmountVI,

	Price,
	Amount,

	LDiscount1,
	TDiscount1,

	TaxBase,
	Pct,
	Vat,
	VatDeducation,
	NetAmount,

	CreatedUserName,
	CreatedDate
)
VALUES (
	@p1,@p2,@p3,
	@p4,
	@p5,@p6,
	@p7,@p8,
	@p9,@p10,
	@p11,@p12,@p13,@p14,@p15,
	@p16,GETDATE()
);`,
		lineID,
		docCurrency,
		relationCurrency,

		exRate,

		priceVI,
		amountVI,

		basePrice,
		amountBase,

		lDis,
		tDis,

		taxBase,
		pct,
		vat,
		vatDeducation,
		net,

		user,
	)

	if err != nil {
		return err
	}

	if docCurrency != "TRY" {
		_, err = tx.Exec(`
MERGE BAGGI_V3.dbo.trOrderLineCurrency AS T
USING (SELECT @p1 AS OrderLineID,'TRY' AS CurrencyCode) AS S
ON T.OrderLineID=S.OrderLineID AND T.CurrencyCode=S.CurrencyCode

WHEN MATCHED THEN UPDATE SET
	RelationCurrencyCode=@p2,

	ExchangeRate=@p3,

	PriceVI=@p4,
	AmountVI=@p5,

	Price=@p6,
	Amount=@p7,

	LDiscount1=@p8,
	TDiscount1=@p9,

	TaxBase=@p10,
	Pct=@p11,
	Vat=@p12,
	VatDeducation=@p13,
	NetAmount=@p14,

	LastUpdatedUserName=@p15,
	LastUpdatedDate=GETDATE()

WHEN NOT MATCHED THEN INSERT (
	OrderLineID,
	CurrencyCode,
	RelationCurrencyCode,

	ExchangeRate,

	PriceVI,
	AmountVI,

	Price,
	Amount,

	LDiscount1,
	TDiscount1,

	TaxBase,
	Pct,
	Vat,
	VatDeducation,
	NetAmount,

	CreatedUserName,
	CreatedDate
)
VALUES (
	@p1,'TRY',@p2,
	@p3,
	@p4,@p5,
	@p6,@p7,
	@p8,@p9,
	@p10,@p11,@p12,@p13,@p14,
	@p15,GETDATE()
);`,
			lineID,
			docCurrency,
			exRate,

			localPriceVI,
			localAmountVI,

			localPrice,
			localAmount,

			lDis,
			tDis,

			localTaxBase,
			pct,
			localVat,
			localVatDeducation,
			localNet,

			user,
		)

		if err != nil {
			return err
		}
	}

	return nil
}

// =======================================================
// COMBO KEY & STRING HELPERS
// =======================================================
func normalizeComboKey(s string) string {
	return strings.ToUpper(strings.TrimSpace(s))
}

// makeComboKey: frontend tarafında NullString olmayan struct’lar için
func makeComboKey(ln models.OrderDetail) string {
	model := safeNS(ln.ItemCode)
	renk := safeNS(ln.ColorCode)
	renk2 := safeNS(ln.ItemDim2Code)
	beden := safeNS(ln.ItemDim1Code)

	return normalizeComboKey(
		fmt.Sprintf("%s||%s||%s||%s", model, renk, beden, renk2),
	)
}

// qtyValue → NullFloat64 güvenli float64
func qtyValue(q models.NullFloat64) float64 {
	if !q.Valid {
		return 0
	}
	return q.Float64
}

func buildV3AuditUser(user *models.User) string {
	if user == nil {
		return "V3-0"
	}

	v3Name := strings.ToUpper(strings.TrimSpace(user.V3Username))
	v3Group := user.V3UserGroup
	if v3Group >= 100 && v3Group%100 == 0 {
		v3Group = v3Group / 100
	}

	if v3Name != "" && v3Group > 0 {
		return fmt.Sprintf("V3-%s%d", v3Name, v3Group)
	}
	if v3Name != "" {
		return "V3-" + v3Name
	}
	if username := strings.TrimSpace(user.Username); username != "" {
		return username
	}
	if v3Group > 0 {
		return fmt.Sprintf("V3-%d", v3Group)
	}
	return "V3-0"
}

// VatCode: NullString → string
// - NULL  → ""
// - "0"   → "" (FK patlamasın, sadece anlamlı kodlar gönderiyoruz)
// - "%0", "%10" vb → trimlenmiş hali
func normalizeVatCode(ns models.NullString) string {
	if !ns.Valid {
		return ""
	}
	s := strings.TrimSpace(ns.String)
	if s == "0" {
		return ""
	}
	return s
}

// PCTCode: NullString → string
// - Artık her durumda "%0" döndürür (tek PCT tipi kullanımı)
// NOT: SQL tarafında da "%0" cdPCT içinde tanımlı olmalı
func normalizePCTCode(ns models.NullString) string {
	return "%0"
}

// models.NullString → trimlenmiş string (NULL ise "")
func safeNS(ns models.NullString) string {
	if !ns.Valid {
		return ""
	}
	return strings.TrimSpace(ns.String)
}

// =======================================================
// COMBO KEY HELPERS (DB alanlarından)
// =======================================================

// makeComboKeyParts: düz string alanlardan comboKey üret
// comboKey = model || renk || beden || renk2
func makeComboKeyParts(item, color, dim1, dim2 string) string {
	item = strings.TrimSpace(item)
	color = strings.TrimSpace(color)
	dim1 = strings.TrimSpace(dim1)
	dim2 = strings.TrimSpace(dim2)

	if item == "" && color == "" && dim1 == "" && dim2 == "" {
		return ""
	}

	return normalizeComboKey(item + "||" + color + "||" + dim1 + "||" + dim2)
}

// comboFromNulls: NullString alanlardan comboKey üret
func comboFromNulls(item, color, dim1, dim2 models.NullString) string {
	return makeComboKeyParts(
		safeNS(item),
		safeNS(color),
		safeNS(dim1),
		safeNS(dim2),
	)
}

// =======================================================
// ✅ ItemVariant Guard — INSERT / UPDATE öncesi
// =======================================================

// normalizeKeyPart: NullString → trim + UPPER
func normalizeKeyPart(ns models.NullString) string {
	s := strings.TrimSpace(safeNS(ns))
	return strings.ToUpper(s)
}

// normalizeNumericToken: sadece rakamlardan oluşan değeri baştaki sıfırlardan arındırır.
// Rakam dışı içerik varsa boş döner.
func normalizeNumericToken(s string) string {
	if s == "" {
		return ""
	}
	for i := 0; i < len(s); i++ {
		if s[i] < '0' || s[i] > '9' {
			return ""
		}
	}
	i := 0
	for i < len(s) && s[i] == '0' {
		i++
	}
	if i == len(s) {
		return "0"
	}
	return s[i:]
}

// normalizeDim1Token: variant karşılaştırması için Dim1'i eski kuralla normalize eder.
// (boşluk, YAS ve Y kaldırılır; UPPER)
func normalizeDim1Token(s string) string {
	s = strings.ToUpper(strings.TrimSpace(s))
	s = strings.ReplaceAll(s, " ", "")
	s = strings.ReplaceAll(s, "YAS", "")
	s = strings.ReplaceAll(s, "Y", "")
	return s
}

func variantCacheKey(item, color, dim2 string) string {
	return item + "||" + color + "||" + dim2
}

func loadVariantDim1SetTx(tx *sql.Tx, item, color, dim2 string) (map[string]struct{}, error) {
	rows, err := tx.Query(`
        SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code
        FROM BAGGI_V3.dbo.prItemVariant V
        WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
          AND (
                (
                  ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
                  AND (
                       ISNULL(LTRIM(RTRIM(@p3)),'') = ''
                       OR ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p3
                  )
                )
                OR (
                  ISNULL(LTRIM(RTRIM(@p3)),'') = ''
                  AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p2
                )
              )
    `, item, color, dim2)
	if err != nil {
		return nil, fmt.Errorf("variant set query hatası: %w", err)
	}
	defer rows.Close()

	set := make(map[string]struct{})
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			return nil, fmt.Errorf("variant set scan hatası: %w", err)
		}
		norm := normalizeDim1Token(raw)
		if norm == "" {
			set["#EMPTY_DIM1"] = struct{}{}
			continue
		}
		set[norm] = struct{}{}
		if num := normalizeNumericToken(norm); num != "" {
			set["#NUM:"+num] = struct{}{}
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("variant set rows hatası: %w", err)
	}
	return set, nil
}

func loadVariantDim1SetDB(conn *sql.DB, item, color, dim2 string) (map[string]struct{}, error) {
	rows, err := conn.Query(`
        SELECT ISNULL(LTRIM(RTRIM(V.ItemDim1Code)),'') AS ItemDim1Code
        FROM BAGGI_V3.dbo.prItemVariant V
        WHERE ISNULL(LTRIM(RTRIM(V.ItemCode)),'') = @p1
          AND (
                (
                  ISNULL(LTRIM(RTRIM(V.ColorCode)),'') = @p2
                  AND (
                       ISNULL(LTRIM(RTRIM(@p3)),'') = ''
                       OR ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p3
                  )
                )
                OR (
                  ISNULL(LTRIM(RTRIM(@p3)),'') = ''
                  AND ISNULL(LTRIM(RTRIM(V.ItemDim2Code)),'') = @p2
                )
              )
    `, item, color, dim2)
	if err != nil {
		return nil, fmt.Errorf("variant set query hatası: %w", err)
	}
	defer rows.Close()

	set := make(map[string]struct{})
	for rows.Next() {
		var raw string
		if err := rows.Scan(&raw); err != nil {
			return nil, fmt.Errorf("variant set scan hatası: %w", err)
		}
		norm := normalizeDim1Token(raw)
		if norm == "" {
			set["#EMPTY_DIM1"] = struct{}{}
			continue
		}
		set[norm] = struct{}{}
		if num := normalizeNumericToken(norm); num != "" {
			set["#NUM:"+num] = struct{}{}
		}
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("variant set rows hatası: %w", err)
	}
	return set, nil
}

// =======================================================
// AKSBIR DETECTION
// =======================================================

// =======================================================
// COMBO KEY BUILDER (AKSBIR AWARE)
// =======================================================

// Variant check: ItemCode + ColorCode + Dim1 + Dim2
func ValidateItemVariant(tx *sql.Tx, ln models.OrderDetail) error {
	return ValidateItemVariantCached(tx, ln, nil)
}

func ValidateItemVariantCached(tx *sql.Tx, ln models.OrderDetail, cache map[string]map[string]struct{}) error {
	fmt.Printf(
		"🧪 VARIANT GUARD INPUT | ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
		safeNS(ln.ClientKey),
		safeNS(ln.ItemCode),
		safeNS(ln.ColorCode),
		safeNS(ln.ItemDim1Code),
		safeNS(ln.ItemDim2Code),
		safeNS(ln.ItemDim3Code),
		nf0(ln.Qty1),
	)

	item := normalizeKeyPart(ln.ItemCode)
	color := normalizeKeyPart(ln.ColorCode)
	dim1 := normalizeKeyPart(ln.ItemDim1Code)
	dim2 := normalizeKeyPart(ln.ItemDim2Code)

	// ✅ Placeholder/boş standardizasyon (SENDE "_" geliyor)
	normalizeEmpty := func(s string) string {
		s = strings.TrimSpace(strings.ToUpper(s))
		if s == "_" || s == "-" {
			return ""
		}
		return s
	}

	item = normalizeEmpty(item)
	color = normalizeEmpty(color)
	dim1 = normalizeEmpty(dim1)
	dim2 = normalizeEmpty(dim2)
	dim1Norm := normalizeDim1Token(dim1)
	dim1Numeric := normalizeNumericToken(dim1Norm)

	fmt.Printf(
		"🧪 VARIANT NORMALIZED | Item=%q Color=%q Dim1=%q Dim2=%q\n",
		item, color, dim1Norm, dim2,
	)

	if item == "" {
		return &models.ValidationError{
			Code:      "INVALID_ITEM_VARIANT",
			Message:   "Tanımsız ürün kombinasyonu",
			ClientKey: safeNS(ln.ClientKey),
			ItemCode:  item,
			ColorCode: color,
			Dim1:      dim1,
			Dim2:      dim2,
		}
	}

	key := variantCacheKey(item, color, dim2)
	set := map[string]struct{}(nil)
	if cache != nil {
		set = cache[key]
	}
	if set == nil {
		var err error
		set, err = loadVariantDim1SetTx(tx, item, color, dim2)
		if err != nil {
			return fmt.Errorf("ItemVariant kontrol query hatası: %w", err)
		}
		if cache != nil {
			cache[key] = set
		}
	}

	_, okNorm := set[dim1Norm]
	_, okNum := set["#NUM:"+dim1Numeric]
	_, okEmpty := set["#EMPTY_DIM1"]
	if !(okNorm || (dim1Numeric != "" && okNum) || (dim1Norm == "" && okEmpty)) {
		return &models.ValidationError{
			Code:      "INVALID_ITEM_VARIANT",
			Message:   "Tanımsız ürün kombinasyonu",
			ClientKey: safeNS(ln.ClientKey),
			ItemCode:  item,
			ColorCode: color,
			Dim1:      dim1,
			Dim2:      dim2,
		}
	}

	return nil
}

// ValidateOrderVariants: save/update öncesi payload satırlarını prItemVariant'a göre doğrular.
// invalid döner; error sadece DB/prepare/query hatalarında döner.
func ValidateOrderVariants(db *sql.DB, lines []models.OrderDetail) ([]models.InvalidVariant, error) {

	normalizeEmpty := func(s string) string {
		s = strings.TrimSpace(strings.ToUpper(s))
		if s == "_" || s == "-" {
			return ""
		}
		return s
	}

	invalid := make([]models.InvalidVariant, 0)
	cache := make(map[string]map[string]struct{})

	for i, ln := range lines {
		qty := qtyValue(ln.Qty1)
		if qty <= 0 {
			continue
		}

		item := normalizeEmpty(normalizeKeyPart(ln.ItemCode))
		color := normalizeEmpty(normalizeKeyPart(ln.ColorCode))
		dim1 := normalizeEmpty(normalizeKeyPart(ln.ItemDim1Code))
		dim2 := normalizeEmpty(normalizeKeyPart(ln.ItemDim2Code))
		dim1Norm := normalizeDim1Token(dim1)
		dim1Numeric := normalizeNumericToken(dim1Norm)

		// ItemCode boş ise invalid
		if strings.TrimSpace(item) == "" {
			invalid = append(invalid, models.InvalidVariant{
				Index:     i,
				ClientKey: safeNS(ln.ClientKey),
				ItemCode:  item,
				ColorCode: color,
				Dim1:      dim1,
				Dim2:      dim2,
				Qty1:      qty,
				ComboKey:  safeNS(ln.ComboKey),
				Reason:    "ItemCode boş",
			})
			continue
		}

		key := variantCacheKey(item, color, dim2)
		set := cache[key]
		if set == nil {
			var err error
			set, err = loadVariantDim1SetDB(db, item, color, dim2)
			if err != nil {
				return nil, fmt.Errorf("validate query hatası (i=%d): %w", i, err)
			}
			cache[key] = set
		}

		_, okNorm := set[dim1Norm]
		_, okNum := set["#NUM:"+dim1Numeric]
		_, okEmpty := set["#EMPTY_DIM1"]
		if !(okNorm || (dim1Numeric != "" && okNum) || (dim1Norm == "" && okEmpty)) {
			invalid = append(invalid, models.InvalidVariant{
				Index:     i,
				ClientKey: safeNS(ln.ClientKey),
				ItemCode:  item,
				ColorCode: color,
				Dim1:      dim1,
				Dim2:      dim2,
				Qty1:      qty,
				ComboKey:  safeNS(ln.ComboKey),
				Reason:    "prItemVariant’ta yok",
			})
		}
	}

	return invalid, nil
}

// =======================================================
// LineResult → frontend senkronu için
// =======================================================

type OrderLineResult struct {
	ClientKey   string `json:"clientKey"`
	OrderLineID string `json:"orderLineID"`
}

// =======================================================
// PART 1 — InsertOrder (header + lines insert) — FINAL v5.1
// =======================================================

func InsertOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) (string, []OrderLineResult, error) {
	conn := db.GetDB()

	fmt.Println("🟦 InsertOrder() BAŞLADI -----------------------------")

	tx, err := conn.Begin()
	if err != nil {
		return "", nil, fmt.Errorf("tx baslatilamadi: %w", err)
	}
	defer tx.Rollback()

	now := time.Now()
	v3User := buildV3AuditUser(user)

	// =======================================================
	// 1) BACKEND — OrderHeaderID üretimi (HER ZAMAN)
	// =======================================================
	realHeaderID := uuid.New().String()

	fmt.Println("🟩 Backend yeni OrderHeaderID üretti:", realHeaderID)

	header.OrderHeaderID = realHeaderID

	// =======================================================
	// 2) OrderNumber üretimi (LOCAL-* ise gerçek WS numarası)
	// =======================================================
	if !header.OrderNumber.Valid ||
		strings.HasPrefix(header.OrderNumber.String, "LOCAL-") ||
		len(strings.TrimSpace(header.OrderNumber.String)) == 0 {

		fmt.Println("🟨 LOCAL numara geldi → gerçek WS numarası üretilecek...")

		var realNumber string
		err := tx.QueryRow(`
            SELECT CONCAT(
                '1-WS-3-',
                RIGHT('00000' + CAST(NEXT VALUE FOR BAGGI_V3.dbo.Seq_OrderNumber_WS AS VARCHAR(10)), 5)
            )
        `).Scan(&realNumber)

		if err != nil {
			return "", nil, fmt.Errorf("Gerçek sipariş numarası üretilemedi: %w", err)
		}

		fmt.Println("🟩 Üretilen gerçek WS numarası:", realNumber)

		header.OrderNumber.String = realNumber
		header.OrderNumber.Valid = true
	}

	newID := realHeaderID // artık DB’ye bu yazılacak

	// =======================================================
	// 3) Döviz kuru çözümü
	// =======================================================
	exRate := 1.0
	if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
		if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil {
			if c.Rate > 0 {
				exRate = c.Rate
			}
		}
	}
	// =======================================================
	// 4) HEADER INSERT
	// =======================================================

	queryHeader := `
INSERT INTO BAGGI_V3.dbo.trOrderHeader (
  OrderHeaderID, OrderTypeCode, ProcessCode, OrderNumber, IsCancelOrder,
  OrderDate, OrderTime, DocumentNumber, PaymentTerm,
  AverageDueDate, Description, InternalDescription,
  CurrAccTypeCode, CurrAccCode, SubCurrAccID, ContactID,
  ShipmentMethodCode, ShippingPostalAddressID, BillingPostalAddressID,
  GuarantorContactID, GuarantorContactID2, RoundsmanCode,
  DeliveryCompanyCode, TaxTypeCode, WithHoldingTaxTypeCode, DOVCode,
  TaxExemptionCode, CompanyCode, OfficeCode, StoreTypeCode, StoreCode,
  POSTerminalID, WarehouseCode, ToWarehouseCode,
  OrdererCompanyCode, OrdererOfficeCode, OrdererStoreCode,
  GLTypeCode, DocCurrencyCode, LocalCurrencyCode, ExchangeRate,
  TDisRate1, TDisRate2, TDisRate3, TDisRate4, TDisRate5,
  DiscountReasonCode, SurplusOrderQtyToleranceRate,
  ImportFileNumber, ExportFileNumber,
  IncotermCode1, IncotermCode2, LettersOfCreditNumber,
  PaymentMethodCode, IsInclutedVat, IsCreditSale, IsCreditableConfirmed,
  CreditableConfirmedUser, CreditableConfirmedDate,
  IsSalesViaInternet, IsSuspended, IsCompleted, IsPrinted,
  IsLocked, UserLocked, IsClosed,
  ApplicationCode, ApplicationID,
  CreatedUserName, CreatedDate, LastUpdatedUserName, LastUpdatedDate,
  IsProposalBased
)
VALUES (
  @p1,@p2,@p3,@p4,
  @p5,@p6,@p7,@p8,
  @p9,@p10,@p11,
  @p12,@p13,@p14,@p15,
  @p16,@p17,@p18,
  @p19,@p20,@p21,
  @p22,@p23,@p24,@p25,
  @p26,@p27,@p28,@p29,@p30,
  @p31,@p32,@p33,
  @p34,@p35,@p36,
  @p37,@p38,@p39,@p40,
  @p41,@p42,@p43,@p44,@p45,
  @p46,@p47,
  @p48,@p49,
  @p50,@p51,@p52,
  @p53,@p54,@p55,@p56,@p57,
  @p58,@p59,@p60,@p61,@p62,
  @p63,@p64,@p65,
  @p66,@p67,@p68,@p69,@p70,@p71,@p72,@p73
);
`

	fmt.Println("🟪 HEADER INSERT ÇALIŞIYOR...")
	fmt.Printf(
		"🧾 HEADER DEBUG | CurrAccType=%v CurrAcc=%q Office=%q StoreType=%v Store=%q OrdererOffice=%q OrdererStore=%q\n",
		nullableInt16(header.CurrAccTypeCode, 0).Int16,
		safeNS(header.CurrAccCode),
		safeNS(header.OfficeCode),
		nullableInt16(header.StoreTypeCode, 0).Int16,
		safeNS(header.StoreCode),
		safeNS(header.OrdererOfficeCode),
		safeNS(header.OrdererStoreCode),
	)

	// ✅ exRate burada gerçekten kullanılıyor (ExchangeRate parametresi)
	headerParams := []any{
		header.OrderHeaderID,
		nullableInt16(header.OrderTypeCode, 1),
		nullableString(header.ProcessCode, "WS"),
		nullableString(header.OrderNumber, ""),
		nullableBool(header.IsCancelOrder, false),

		nullableDateString(header.OrderDate),
		nullableTimeString(header.OrderTime),
		nullableString(header.DocumentNumber, ""),
		nullableInt16(header.PaymentTerm, 0),

		nullableDateString(header.AverageDueDate),
		nullableString(header.Description, ""),
		nullableString(header.InternalDescription, ""),

		nullableInt16(header.CurrAccTypeCode, 0),
		nullableString(header.CurrAccCode, ""),
		nullableUUID(header.SubCurrAccID),
		nullableUUID(header.ContactID),

		nullableString(header.ShipmentMethodCode, ""),
		nullableUUID(header.ShippingPostalAddressID),
		nullableUUID(header.BillingPostalAddressID),

		nullableUUID(header.GuarantorContactID),
		nullableUUID(header.GuarantorContactID2),
		nullableString(header.RoundsmanCode, ""),

		nullableString(header.DeliveryCompanyCode, ""),
		nullableInt16(header.TaxTypeCode, 0),
		nullableString(header.WithHoldingTaxTypeCode, ""),
		nullableString(header.DOVCode, ""),

		nullableInt16(header.TaxExemptionCode, 0),
		nullableInt32ToInt16(header.CompanyCode, 1),
		nullableString(header.OfficeCode, "101"),
		nullableInt16(header.StoreTypeCode, 5),
		nullableString(models.NullString{}, ""),

		nullableInt16(header.POSTerminalID, 0),
		nullableString(header.WarehouseCode, "1-0-12"),
		nullableString(header.ToWarehouseCode, ""),

		nullableInt32ToInt16(header.OrdererCompanyCode, 1),
		nullableString(header.OrdererOfficeCode, "101"),
		nullableString(models.NullString{}, ""),

		nullableString(header.GLTypeCode, ""),
		nullableString(header.DocCurrencyCode, "TRY"),
		nullableString(header.LocalCurrencyCode, "TRY"),
		nullableFloat64(header.ExchangeRate, exRate), // ✅ exRate kullanıldı

		nullableFloat64(header.TDisRate1, 0),
		nullableFloat64(header.TDisRate2, 0),
		nullableFloat64(header.TDisRate3, 0),
		nullableFloat64(header.TDisRate4, 0),
		nullableFloat64(header.TDisRate5, 0),

		nullableInt16(header.DiscountReasonCode, 0),
		nullableFloat64(header.SurplusOrderQtyToleranceRate, 0),

		nullableString(header.ImportFileNumber, ""),
		nullableString(header.ExportFileNumber, ""),

		nullableString(header.IncotermCode1, ""),
		nullableString(header.IncotermCode2, ""),
		nullableString(header.LettersOfCreditNumber, ""),

		nullableString(header.PaymentMethodCode, ""),
		nullableBool(header.IsInclutedVat, false),
		nullableBool(header.IsCreditSale, true),
		nullableBool(header.IsCreditableConfirmed, true),

		nullableString(header.CreditableConfirmedUser, v3User),
		nullableDateTime(header.CreditableConfirmedDate, now),

		nullableBool(header.IsSalesViaInternet, false),
		nullableBool(header.IsSuspended, false),
		nullableBool(header.IsCompleted, true),
		nullableBool(header.IsPrinted, false),

		nullableBool(header.IsLocked, false),
		nullableBool(header.UserLocked, false),
		nullableBool(header.IsClosed, false),

		nullableString(header.ApplicationCode, "Order"),
		nullableUUID(header.ApplicationID),

		nullableString(header.CreatedUserName, v3User),
		nullableDateTimeString(header.CreatedDate, now),
		nullableString(header.LastUpdatedUserName, v3User),
		nullableDateTimeString(header.LastUpdatedDate, now),

		nullableBool(header.IsProposalBased, false),
	}

	// ✅ queryHeader artık gerçekten kullanılıyor → "Unused variable 'queryHeader'" biter
	if _, err := tx.Exec(queryHeader, headerParams...); err != nil {
		fmt.Println("❌ HEADER INSERT ERROR:", err)
		return "", nil, fmt.Errorf("header insert hatasi: %w", err)
	}

	fmt.Println("🟩 HEADER INSERT OK — ID:", newID)

	// headerParams ... (senin mevcut hali aynen)
	// =======================================================
	// 5) LINE INSERT
	// =======================================================

	insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
    OrderLineID,
    SortOrder, ItemTypeCode, ItemCode, ColorCode,
    ItemDim1Code, ItemDim2Code, ItemDim3Code,
    Qty1, Qty2,
    CancelQty1, CancelQty2,
    OrderCancelReasonCode,
    DeliveryDate, PlannedDateOfLading,
    LineDescription,
    UsedBarcode, CostCenterCode,
    VatCode, VatRate, PCTCode, PCTRate,
    LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
    DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
    Price,
    BaseProcessCode, BaseOrderNumber,
    BaseCustomerTypeCode, BaseCustomerCode,
    BaseSubCurrAccID, BaseStoreCode,
    OrderHeaderID,
    CreatedUserName, CreatedDate,
    LastUpdatedUserName, LastUpdatedDate,
    SurplusOrderQtyToleranceRate,
    WithHoldingTaxTypeCode,
    DOVCode
)
VALUES (
  @p1,@p2,@p3,@p4,
  @p5,@p6,@p7,@p8,
  @p9,@p10,
  @p11,@p12,
  @p13,
  @p14,@p15,
  @p16,
  @p17,@p18,
  @p19,@p20,@p21,@p22,
  @p23,@p24,@p25,@p26,@p27,
  @p28,@p29,@p30,
  @p31,
  @p32,@p33,
  @p34,@p35,
  @p36,@p37,
  @p38,
  @p39,@p40,
  @p41,@p42,
  @p43,
  @p44,
  @p45
)`)
	if err != nil {
		return "", nil, fmt.Errorf("line insert stmt hazirlanamadi: %w", err)
	}
	defer insStmt.Close()

	lineResults := make([]OrderLineResult, 0, len(lines))
	variantCache := make(map[string]map[string]struct{})

	// ✅ Duplicate Guard (payload içi)
	seenCombo := make(map[string]bool)

	for i, ln := range lines {
		fmt.Println("────────────────────────────────────")
		fmt.Printf("🟨 [INSERT] LINE %d — gelen OrderLineID=%s\n", i+1, ln.OrderLineID)

		// Her satır için yeni GUID
		if ln.OrderLineID == "" {
			newLineID := uuid.New().String()
			fmt.Println("🆕 Yeni LineID üretildi:", newLineID)
			ln.OrderLineID = newLineID
		}

		// ✅ Duplicate Guard (comboKey)
		comboKey := normalizeComboKey(safeNS(ln.ComboKey))
		if comboKey == "" {
			comboKey = makeComboKey(ln)
		}

		if qtyValue(ln.Qty1) > 0 && comboKey != "" {
			if seenCombo[comboKey] {
				return "", nil, fmt.Errorf(
					"Duplicate satır (comboKey=%s, ClientKey=%s)",
					comboKey,
					safeNS(ln.ClientKey),
				)
			}
			seenCombo[comboKey] = true
		}

		// V2 Logic → %0 PCT
		vatCode := normalizeVatCode(ln.VatCode)
		pctCode := normalizePCTCode(ln.PCTCode)

		var pctParam any
		if pctCode == "" {
			pctParam = nil
		} else {
			pctParam = pctCode
		}

		planned := nullableDateString(ln.PlannedDateOfLading)

		// ✅ INSERT ÖNCESİ ItemVariant GUARD
		if qtyValue(ln.Qty1) > 0 {
			if err := ValidateItemVariantCached(tx, ln, variantCache); err != nil {
				fmt.Println("❌ VARIANT GUARD (INSERT):", err)
				return "", nil, err
			}
		}

		fmt.Printf(
			"🚨 INSERT LINE[%d] | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q Qty1=%v\n",
			i+1,
			ln.OrderLineID,
			safeNS(ln.ClientKey),
			safeNS(ln.ItemCode),
			safeNS(ln.ColorCode),
			safeNS(ln.ItemDim1Code),
			safeNS(ln.ItemDim2Code),
			safeNS(ln.ItemDim3Code),
			nf0(ln.Qty1),
		)

		_, err := insStmt.Exec(
			ln.OrderLineID,
			ln.SortOrder,
			ln.ItemTypeCode,
			nullableString(ln.ItemCode, ""),
			safeNS(ln.ColorCode),
			safeNS(ln.ItemDim1Code),
			safeNS(ln.ItemDim2Code),
			nullableString(ln.ItemDim3Code, ""),
			ln.Qty1, ln.Qty2,
			ln.CancelQty1, ln.CancelQty2,
			nullableString(ln.OrderCancelReasonCode, ""),
			nullableTime(ln.DeliveryDate, now),
			planned,
			nullableString(ln.LineDescription, ""),
			nullableString(ln.UsedBarcode, ""),
			nullableString(ln.CostCenterCode, ""),
			vatCode, nf0(ln.VatRate),
			pctParam, nf0(ln.PCTRate),
			nf0(ln.LDisRate1),
			nf0(ln.LDisRate2),
			nf0(ln.LDisRate3),
			nf0(ln.LDisRate4),
			nf0(ln.LDisRate5),
			nullableString(ln.DocCurrencyCode, "TRY"),
			nullableString(ln.PriceCurrencyCode, "TRY"),
			nf0(ln.PriceExchangeRate),
			nf0(ln.Price),
			nullableString(ln.BaseProcessCode, ""),
			nullableString(ln.BaseOrderNumber, ""),
			ln.BaseCustomerTypeCode,
			nullableString(ln.BaseCustomerCode, ""),
			nullableUUID(ln.BaseSubCurrAccID),
			nullableString(ln.BaseStoreCode, ""),
			header.OrderHeaderID,
			v3User, now,
			v3User, now,
			nf0(ln.SurplusOrderQtyToleranceRate),
			nullableString(ln.WithHoldingTaxTypeCode, ""),
			nullableString(ln.DOVCode, ""),
		)

		if err != nil {
			fmt.Println("❌ INSERT LINE ERROR")
			fmt.Printf(
				"💥 FAILED LINE | LineID=%s ClientKey=%s Item=%q Color=%q Dim1=%q Dim2=%q Dim3=%q\n",
				ln.OrderLineID,
				safeNS(ln.ClientKey),
				safeNS(ln.ItemCode),
				safeNS(ln.ColorCode),
				safeNS(ln.ItemDim1Code),
				safeNS(ln.ItemDim2Code),
				safeNS(ln.ItemDim3Code),
			)
			fmt.Println("SQL ERROR:", err)
			return "", nil, fmt.Errorf("line insert hatasi: %w", err)
		}

		// ✅ NEW: trOrderLineCurrency yaz
		if err := upsertLineCurrency(
			tx,
			ln.OrderLineID,

			safeNS(ln.DocCurrencyCode),
			nf0(ln.PriceExchangeRate),

			nf0(ln.Price),
			nf0(ln.Qty1),

			nf0(ln.LDisRate1), // ✅ Line discount
			0,                 // ✅ Total discount (istersen header’dan alırsın)

			nf0(ln.VatRate), // ✅ Vat rate
			ln,

			v3User,
		); err != nil {
			return "", nil, fmt.Errorf("currency insert hatası: %w", err)
		}

		if ln.ClientKey.Valid && ln.ClientKey.String != "" {
			lineResults = append(lineResults, OrderLineResult{
				ClientKey:   ln.ClientKey.String,
				OrderLineID: ln.OrderLineID,
			})
		}
	}

	// =======================================================
	// 6) COMMIT
	// =======================================================
	fmt.Println("🟨 COMMIT EDİLİYOR...")

	if err := tx.Commit(); err != nil {
		fmt.Println("❌ COMMIT ERROR:", err)
		return "", nil, err
	}

	fmt.Println("✅ COMMIT OK — INSERT ORDER BİTTİ")
	fmt.Println("────────────────────────────────────")

	return newID, lineResults, nil
}

// =======================================================
// PART 2 — UpdateOrder FULL DEBUG (v4.3)
// =======================================================

func UpdateOrder(header models.OrderHeader, lines []models.OrderDetail, user *models.User) ([]OrderLineResult, error) {
	conn := db.GetDB()

	fmt.Println("══════════════════════════════════════")
	fmt.Println("🔍 [DEBUG] UpdateOrder çağrıldı")
	fmt.Printf("🔍 HeaderID: %v\n", header.OrderHeaderID)
	fmt.Printf("🔍 Line sayısı: %v\n", len(lines))
	fmt.Printf("🔍 User: %v (V3: %s/%d)\n", user.Username, user.V3Username, user.V3UserGroup)
	fmt.Println("══════════════════════════════════════")

	tx, err := conn.Begin()
	if err != nil {
		return nil, fmt.Errorf("tx baslatilamadi: %w", err)
	}
	defer tx.Rollback()

	now := time.Now()
	v3User := buildV3AuditUser(user)

	// Hard delete bazen FK nedeniyle patlayabiliyor.
	// Bu durumda satırı soft-close ederek akışı güvenli şekilde tamamlarız.
	deleteOrSoftCloseLine := func(lineID string) error {
		if _, err := tx.Exec(`DELETE FROM BAGGI_V3.dbo.trOrderLineCurrency WHERE OrderLineID=@p1`, lineID); err != nil {
			return fmt.Errorf("line currency delete failed line_id=%s: %w", lineID, err)
		}

		if _, err := tx.Exec(`
DELETE FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1 AND OrderLineID=@p2 AND ISNULL(IsClosed,0)=0
`, header.OrderHeaderID, lineID); err != nil {
			fmt.Printf("[ORDER_UPDATE] hard delete failed, trying qty-zero soft-close line_id=%s err=%v\n", lineID, err)

			// IsClosed computed olabilir; sadece miktarları sıfırla.
			if _, err2 := tx.Exec(`
UPDATE BAGGI_V3.dbo.trOrderLine
SET
	Qty1 = 0,
	Qty2 = 0,
	CancelQty1 = 0,
	CancelQty2 = 0,
	LastUpdatedUserName = @p1,
	LastUpdatedDate = @p2
WHERE OrderHeaderID=@p3 AND OrderLineID=@p4 AND ISNULL(IsClosed,0)=0
`, v3User, now, header.OrderHeaderID, lineID); err2 != nil {
				return fmt.Errorf("line delete failed line_id=%s: %v; qty-zero soft-close failed: %w", lineID, err, err2)
			}
		}

		return nil
	}

	// Döviz kuru (Header ExchangeRate fallback)
	exRate := 1.0
	if header.DocCurrencyCode.Valid && header.DocCurrencyCode.String != "TRY" {
		if c, err := GetTodayCurrencyV3(conn, header.DocCurrencyCode.String); err == nil && c.Rate > 0 {
			exRate = c.Rate
		}
	}

	// =======================================================
	// 0) Mevcut satırları oku (GUID STRING olarak!)
	// =======================================================
	existingOpen := make(map[string]bool)
	existingClosed := make(map[string]bool)
	existingOpenCombo := make(map[string]string)
	existingClosedCombo := make(map[string]string)
	existingOpenMeta := make(map[string]struct {
		item         string
		color        string
		dim1         string
		dim2         string
		qty1         float64
		price        float64
		docCurrency  string
		vatRate      float64
		lineDesc     string
		deliveryDate string
		plannedDate  string
	})

	rows, err := tx.Query(`
SELECT 
    CONVERT(varchar(36), OrderLineID),
    ISNULL(IsClosed, 0),
    ISNULL(ItemCode,''),
    ISNULL(ColorCode,''),
    ISNULL(ItemDim1Code,''),
    ISNULL(ItemDim2Code,''),
    ISNULL(Qty1, 0),
    ISNULL(Price, 0),
    ISNULL(DocCurrencyCode, ''),
    ISNULL(VatRate, 0),
    ISNULL(LineDescription, ''),
    CONVERT(varchar(10), DeliveryDate, 23),
    CONVERT(varchar(10), PlannedDateOfLading, 23)
FROM BAGGI_V3.dbo.trOrderLine
WHERE OrderHeaderID=@p1
`, header.OrderHeaderID)
	if err != nil {
		return nil, fmt.Errorf("mevcut satirlar okunamadi: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id, item, color, dim1, dim2 string
		var docCurrency, lineDesc, deliveryDate, plannedDate string
		var qty1, price, vatRate float64
		var closed bool

		if err := rows.Scan(
			&id, &closed, &item, &color, &dim1, &dim2,
			&qty1, &price, &docCurrency, &vatRate, &lineDesc,
			&deliveryDate, &plannedDate,
		); err != nil {
			return nil, err
		}

		combo := makeComboKeyParts(item, color, dim1, dim2)

		if closed {
			existingClosed[id] = true
			if combo != "" {
				existingClosedCombo[combo] = id
			}
		} else {
			existingOpen[id] = true
			existingOpenMeta[id] = struct {
				item         string
				color        string
				dim1         string
				dim2         string
				qty1         float64
				price        float64
				docCurrency  string
				vatRate      float64
				lineDesc     string
				deliveryDate string
				plannedDate  string
			}{
				item:         strings.TrimSpace(item),
				color:        strings.TrimSpace(color),
				dim1:         strings.TrimSpace(dim1),
				dim2:         strings.TrimSpace(dim2),
				qty1:         qty1,
				price:        price,
				docCurrency:  strings.TrimSpace(docCurrency),
				vatRate:      vatRate,
				lineDesc:     strings.TrimSpace(lineDesc),
				deliveryDate: strings.TrimSpace(deliveryDate),
				plannedDate:  strings.TrimSpace(plannedDate),
			}
			if combo != "" {
				existingOpenCombo[combo] = id
			}
		}
	}

	isLineInvoiced := func(lineID string) (bool, error) {
		var exists int
		err := tx.QueryRow(`
SELECT CASE WHEN EXISTS (
    SELECT 1
    FROM BAGGI_V3.dbo.trInvoiceLine
    WHERE OrderLineID=@p1
) THEN 1 ELSE 0 END
`, lineID).Scan(&exists)
		if err != nil {
			return false, fmt.Errorf("invoice line kontrol query hatasi: %w", err)
		}
		return exists == 1, nil
	}

	// ======================================================
	// HEADER UPDATE
	// ======================================================
	_, err = tx.Exec(`
UPDATE BAGGI_V3.dbo.trOrderHeader SET
	OrderDate=@p1,
	OrderTime=@p2,
	AverageDueDate=@p3,
	Description=@p4,
	InternalDescription=@p5,
	DocCurrencyCode=@p6,
	LocalCurrencyCode=@p7,
	ExchangeRate=@p8,
	IsCreditableConfirmed=@p9,
	CreditableConfirmedUser=@p10,
	CreditableConfirmedDate=@p11,
	LastUpdatedUserName=@p12,
	LastUpdatedDate=@p13
WHERE OrderHeaderID=@p14
`,
		nullableDateString(header.OrderDate),
		nullableTimeString(header.OrderTime),
		nullableDateString(header.AverageDueDate),
		nullableString(header.Description, ""),
		nullableString(header.InternalDescription, ""),
		nullableString(header.DocCurrencyCode, "TRY"),
		nullableString(header.LocalCurrencyCode, "TRY"),
		nullableFloat64(header.ExchangeRate, exRate),
		true,
		nullableString(header.CreditableConfirmedUser, v3User),
		nullableDateTime(header.CreditableConfirmedDate, now),
		v3User,
		now,
		header.OrderHeaderID,
	)
	if err != nil {
		return nil, err
	}

	// ======================================================
	// PREPARE STATEMENTS
	// ======================================================
	insStmt, err := tx.Prepare(`
INSERT INTO BAGGI_V3.dbo.trOrderLine (
OrderLineID, SortOrder, ItemTypeCode, ItemCode, ColorCode,
ItemDim1Code, ItemDim2Code, ItemDim3Code,
Qty1, Qty2, CancelQty1, CancelQty2, OrderCancelReasonCode,
DeliveryDate, PlannedDateOfLading, LineDescription,
UsedBarcode, CostCenterCode,
VatCode, VatRate, PCTCode, PCTRate,
LDisRate1, LDisRate2, LDisRate3, LDisRate4, LDisRate5,
DocCurrencyCode, PriceCurrencyCode, PriceExchangeRate,
Price, BaseProcessCode, BaseOrderNumber,
BaseCustomerTypeCode, BaseCustomerCode,
BaseSubCurrAccID, BaseStoreCode,
OrderHeaderID, CreatedUserName, CreatedDate,
LastUpdatedUserName, LastUpdatedDate,
SurplusOrderQtyToleranceRate,
WithHoldingTaxTypeCode, DOVCode
)
VALUES (
@p1,@p2,@p3,@p4,@p5,@p6,@p7,@p8,@p9,@p10,
@p11,@p12,@p13,@p14,@p15,@p16,@p17,@p18,
@p19,@p20,@p21,@p22,@p23,@p24,@p25,@p26,@p27,
@p28,@p29,@p30,@p31,@p32,@p33,@p34,@p35,
@p36,@p37,@p38,@p39,@p40,@p41,@p42,@p43,
@p44,@p45
)`)
	if err != nil {
		return nil, err
	}
	defer insStmt.Close()

	updStmt, err := tx.Prepare(`
UPDATE BAGGI_V3.dbo.trOrderLine SET
SortOrder=@p1, ItemTypeCode=@p2, ItemCode=@p3, ColorCode=@p4,
ItemDim1Code=@p5, ItemDim2Code=@p6, ItemDim3Code=@p7,
Qty1=@p8, Qty2=@p9, CancelQty1=@p10, CancelQty2=@p11,
OrderCancelReasonCode=@p12,
DeliveryDate=@p13, PlannedDateOfLading=@p14,
LineDescription=@p15, UsedBarcode=@p16, CostCenterCode=@p17,
VatCode=@p18, VatRate=@p19, PCTCode=@p20, PCTRate=@p21,
LDisRate1=@p22, LDisRate2=@p23, LDisRate3=@p24,
LDisRate4=@p25, LDisRate5=@p26,
DocCurrencyCode=@p27, PriceCurrencyCode=@p28,
PriceExchangeRate=@p29, Price=@p30,
BaseProcessCode=@p31, BaseOrderNumber=@p32,
BaseCustomerTypeCode=@p33, BaseCustomerCode=@p34,
BaseSubCurrAccID=@p35, BaseStoreCode=@p36,
LastUpdatedUserName=@p37, LastUpdatedDate=@p38,
SurplusOrderQtyToleranceRate=@p39,
WithHoldingTaxTypeCode=@p40, DOVCode=@p41
WHERE OrderLineID=@p42 AND ISNULL(IsClosed,0)=0`)
	if err != nil {
		return nil, err
	}
	defer updStmt.Close()

	// ======================================================
	// LOOP
	// ======================================================
	lineResults := make([]OrderLineResult, 0)
	variantCache := make(map[string]map[string]struct{})
	seenCombo := make(map[string]bool)

	for _, ln := range lines {
		comboKey := normalizeComboKey(safeNS(ln.ComboKey))
		if comboKey == "" {
			comboKey = makeComboKey(ln)
		}

		// Duplicate guard (SADECE aktif)
		if qtyValue(ln.Qty1) > 0 && comboKey != "" {
			if seenCombo[comboKey] {
				return nil, fmt.Errorf("Duplicate satır: %s", comboKey)
			}
			seenCombo[comboKey] = true
		}

		// Kapalı satır guard
		if ln.OrderLineID != "" && existingClosed[ln.OrderLineID] {
			continue
		}
		if comboKey != "" {
			if _, ok := existingClosedCombo[comboKey]; ok {
				continue
			}
		}

		// DELETE SIGNAL
		if ln.OrderLineID != "" && qtyValue(ln.Qty1) <= 0 {
			invoiced, err := isLineInvoiced(ln.OrderLineID)
			if err != nil {
				return nil, err
			}
			if invoiced {
				return nil, &models.ValidationError{
					Code:      "ORDER_LINE_INVOICED",
					Message:   fmt.Sprintf("Faturalanmis satir silinemez (OrderLineID=%s)", ln.OrderLineID),
					ClientKey: safeNS(ln.ClientKey),
					ItemCode:  strings.TrimSpace(safeNS(ln.ItemCode)),
					ColorCode: strings.TrimSpace(safeNS(ln.ColorCode)),
					Dim1:      strings.TrimSpace(safeNS(ln.ItemDim1Code)),
					Dim2:      strings.TrimSpace(safeNS(ln.ItemDim2Code)),
				}
			}
			if err := deleteOrSoftCloseLine(ln.OrderLineID); err != nil {
				return nil, err
			}

			delete(existingOpen, ln.OrderLineID)
			delete(existingOpenCombo, comboKey)
			continue
		}

		isNew := false

		// ID resolve: boşsa combo'dan yakala, yoksa yeni üret
		if ln.OrderLineID == "" {
			if dbID, ok := existingOpenCombo[comboKey]; ok {
				ln.OrderLineID = dbID
			} else {
				ln.OrderLineID = uuid.New().String()
				isNew = true
			}
		}

		// Edit akışında bazen stale OrderLineID gelebiliyor (silinmiş/eski satır ID'si).
		// Böyle bir ID ile UPDATE denemek yerine yeni satır olarak ele al.
		if !isNew && ln.OrderLineID != "" {
			if _, stillOpen := existingOpen[ln.OrderLineID]; !stillOpen {
				ln.OrderLineID = uuid.New().String()
				isNew = true
			}
		}

		// Eğer satır DB'deki değerlerle aynıysa, update/upsert yapma (performans)
		if !isNew && ln.OrderLineID != "" {
			if meta, ok := existingOpenMeta[ln.OrderLineID]; ok {
				trim := strings.TrimSpace
				floatEq := func(a, b float64) bool { return math.Abs(a-b) < 0.0001 }
				dateOnly := func(nt models.NullTime) string {
					if nt.Valid && !nt.Time.IsZero() {
						return nt.Time.Format("2006-01-02")
					}
					return ""
				}
				plannedStr := ""
				if ln.PlannedDateOfLading.Valid {
					plannedStr = strings.TrimSpace(ln.PlannedDateOfLading.String)
				}
				deliveryStr := dateOnly(ln.DeliveryDate)

				payloadHasDates := deliveryStr != "" && plannedStr != ""

				if payloadHasDates &&
					trim(meta.item) == trim(safeNS(ln.ItemCode)) &&
					trim(meta.color) == trim(safeNS(ln.ColorCode)) &&
					trim(meta.dim1) == trim(safeNS(ln.ItemDim1Code)) &&
					trim(meta.dim2) == trim(safeNS(ln.ItemDim2Code)) &&
					floatEq(meta.qty1, nf0(ln.Qty1)) &&
					floatEq(meta.price, nf0(ln.Price)) &&
					trim(meta.docCurrency) == trim(safeNS(ln.DocCurrencyCode)) &&
					floatEq(meta.vatRate, nf0(ln.VatRate)) &&
					trim(meta.lineDesc) == trim(safeNS(ln.LineDescription)) &&
					trim(meta.deliveryDate) == trim(deliveryStr) &&
					trim(meta.plannedDate) == trim(plannedStr) {

					// Bu satırı "işlendi" say, ama DB yazma yok
					delete(existingOpen, ln.OrderLineID)
					delete(existingOpenCombo, comboKey)
					continue
				}
			}
		}

		// Variant guard
		if qtyValue(ln.Qty1) > 0 {
			if err := ValidateItemVariantCached(tx, ln, variantCache); err != nil {
				return nil, err
			}
		}

		if isNew {
			_, err := insStmt.Exec(
				ln.OrderLineID, ln.SortOrder, ln.ItemTypeCode,
				nullableString(ln.ItemCode, ""), safeNS(ln.ColorCode),
				safeNS(ln.ItemDim1Code), safeNS(ln.ItemDim2Code),
				nullableString(ln.ItemDim3Code, ""),
				ln.Qty1, ln.Qty2, ln.CancelQty1, ln.CancelQty2,
				nullableString(ln.OrderCancelReasonCode, ""),
				nullableTime(ln.DeliveryDate, now),
				nullableDateString(ln.PlannedDateOfLading),
				nullableString(ln.LineDescription, ""),
				nullableString(ln.UsedBarcode, ""),
				nullableString(ln.CostCenterCode, ""),
				normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
				normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
				nf0(ln.LDisRate1), nf0(ln.LDisRate2),
				nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
				nullableString(ln.DocCurrencyCode, "TRY"),
				nullableString(ln.PriceCurrencyCode, "TRY"),
				nf0(ln.PriceExchangeRate), nf0(ln.Price),
				nullableString(ln.BaseProcessCode, ""),
				nullableString(ln.BaseOrderNumber, ""),
				ln.BaseCustomerTypeCode,
				nullableString(ln.BaseCustomerCode, ""),
				nullableUUID(ln.BaseSubCurrAccID),
				nullableString(ln.BaseStoreCode, ""),
				header.OrderHeaderID,
				v3User, now, v3User, now,
				nf0(ln.SurplusOrderQtyToleranceRate),
				nullableString(ln.WithHoldingTaxTypeCode, ""),
				nullableString(ln.DOVCode, ""),
			)
			if err != nil {
				return nil, err
			}
		} else {
			_, err := updStmt.Exec(
				ln.SortOrder, ln.ItemTypeCode,
				nullableString(ln.ItemCode, ""),
				safeNS(ln.ColorCode),
				safeNS(ln.ItemDim1Code),
				safeNS(ln.ItemDim2Code),
				nullableString(ln.ItemDim3Code, ""),
				nf0(ln.Qty1), nf0(ln.Qty2),
				nf0(ln.CancelQty1), nf0(ln.CancelQty2),
				nullableString(ln.OrderCancelReasonCode, ""),
				nullableTime(ln.DeliveryDate, now),
				nullableDateString(ln.PlannedDateOfLading),
				nullableString(ln.LineDescription, ""),
				nullableString(ln.UsedBarcode, ""),
				nullableString(ln.CostCenterCode, ""),
				normalizeVatCode(ln.VatCode), nf0(ln.VatRate),
				normalizePCTCode(ln.PCTCode), nf0(ln.PCTRate),
				nf0(ln.LDisRate1), nf0(ln.LDisRate2),
				nf0(ln.LDisRate3), nf0(ln.LDisRate4), nf0(ln.LDisRate5),
				nullableString(ln.DocCurrencyCode, "TRY"),
				nullableString(ln.PriceCurrencyCode, "TRY"),
				nf0(ln.PriceExchangeRate), nf0(ln.Price),
				nullableString(ln.BaseProcessCode, ""),
				nullableString(ln.BaseOrderNumber, ""),
				ln.BaseCustomerTypeCode,
				nullableString(ln.BaseCustomerCode, ""),
				nullableUUID(ln.BaseSubCurrAccID),
				nullableString(ln.BaseStoreCode, ""),
				v3User, now,
				nf0(ln.SurplusOrderQtyToleranceRate),
				nullableString(ln.WithHoldingTaxTypeCode, ""),
				nullableString(ln.DOVCode, ""),
				ln.OrderLineID,
			)
			if err != nil {
				return nil, err
			}
		}

		// ✅ Currency UPSERT (insert/update sonrası ortak)
		if err := upsertLineCurrency(
			tx,
			ln.OrderLineID,
			safeNS(ln.DocCurrencyCode),
			nf0(ln.PriceExchangeRate),
			nf0(ln.Price),
			nf0(ln.Qty1),
			nf0(ln.LDisRate1),
			0,               // TODO: header TDisRate toplamını istersen buraya bağlarız
			nf0(ln.VatRate), // satır vat oranı
			ln,
			v3User,
		); err != nil {
			return nil, err
		}

		// Bu satır işlendi -> existingOpen setinden düş
		delete(existingOpen, ln.OrderLineID)
		delete(existingOpenCombo, comboKey)

		// Sonuç mapping
		if ln.ClientKey.Valid {
			lineResults = append(lineResults, OrderLineResult{
				ClientKey:   ln.ClientKey.String,
				OrderLineID: ln.OrderLineID,
			})
		}
	}

	// =======================================================
	// Grid dışı kalan açık satırlar (payload'da yok -> sil)
	// =======================================================
	for id := range existingOpen {
		invoiced, err := isLineInvoiced(id)
		if err != nil {
			return nil, err
		}
		if invoiced {
			meta := existingOpenMeta[id]
			fmt.Printf("[ORDER_UPDATE] skip delete invoiced line id=%s item=%s color=%s dim1=%s dim2=%s\n",
				id, meta.item, meta.color, meta.dim1, meta.dim2)
			continue
		}
		if err := deleteOrSoftCloseLine(id); err != nil {
			return nil, err
		}
	}

	// =======================================================
	// COMMIT + RETURN
	// =======================================================
	if err := tx.Commit(); err != nil {
		return nil, err
	}

	return lineResults, nil
}
