package routes

import (
	"bytes"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"math"
	"net/http"
	"runtime/debug"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/mux"
	"github.com/jung-kurt/gofpdf"
)

/* ===========================================================
   1) SABİTLER / RENKLER / TİPLER
   =========================================================== */

// Baggi renkleri
var (
	baggiGoldR, baggiGoldG, baggiGoldB                   = 201, 162, 39
	baggiCreamR, baggiCreamG, baggiCreamB                = 255, 254, 249
	baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB = 187, 187, 187
)

// Beden kategorileri (frontend birebir)
const (
	catAyk    = "ayk"
	catAykGar = "ayk_garson"
	catYas    = "yas"
	catPan    = "pan"
	catGom    = "gom"
	catTak    = "tak"
	catAksbir = "aksbir"
)

var categoryOrder = []string{catTak, catAyk, catAykGar, catYas, catPan, catGom, catAksbir}

var categoryTitle = map[string]string{
	catTak:    "                                                TAKIM ELBİSE",
	catAyk:    "                                                AYAKKABI",
	catAykGar: "                                                AYAKKABI GARSON",
	catYas:    "                                                YAŞ",
	catPan:    "                                                PANTOLON",
	catGom:    "                                                GÖMLEK",
	catAksbir: "                                                AKSESUAR",
}

/* ===========================================================
   HEADER MODEL
   =========================================================== */

type OrderHeader struct {
	OrderHeaderID string
	OrderNumber   string
	CurrAccCode   string
	CurrAccName   string
	DocCurrency   string
	OrderDate     time.Time
	Description   string
	InternalDesc  string
	OfficeCode    string
	CreatedUser   string
	CustomerRep   string // 🆕 Müşteri Temsilcisi
}

/* ===========================================================
   RAW LINE MODEL
   =========================================================== */

type OrderLineRaw struct {
	OrderLineID         sql.NullString
	ItemCode            string
	ColorCode           string
	ItemDim1Code        sql.NullString
	ItemDim2Code        sql.NullString
	Qty1                sql.NullFloat64
	Price               sql.NullFloat64
	DocCurrencyCode     sql.NullString
	DeliveryDate        sql.NullTime
	LineDescription     sql.NullString
	UrunAnaGrubu        sql.NullString
	UrunAltGrubu        sql.NullString
	YetiskinGarson      sql.NullString
	IsClosed            sql.NullBool
	WithHoldingTaxType  sql.NullString
	DOVCode             sql.NullString
	PlannedDateOfLading sql.NullTime
	CostCenterCode      sql.NullString
	VatCode             sql.NullString
	VatRate             sql.NullFloat64
}

/* ===========================================================
   PDF SATIR MODELİ
   =========================================================== */

type PdfRow struct {
	Model          string
	Color          string
	GroupMain      string
	GroupSub       string
	YetiskinGarson string
	Description    string
	Category       string
	SizeQty        map[string]int
	TotalQty       int
	Price          float64
	Currency       string
	Amount         float64
	Termin         string
	IsClosed       bool
	OrderLineIDs   map[string]string

	ClosedSizes map[string]bool // 🆕 her beden için IsClosed bilgisi
}

/* ===========================================================
   PDF LAYOUT STRUCT
   =========================================================== */

type pdfLayout struct {
	PageW, PageH float64
	MarginL      float64
	MarginR      float64
	MarginT      float64
	MarginB      float64

	ColModelW    float64
	ColRenkW     float64
	ColGroupW    float64
	ColGroupW2   float64
	ColDescLeft  float64
	ColDescRight float64
	ColDescW     float64
	ColQtyW      float64
	ColPriceW    float64
	ColCurW      float64
	ColAmountW   float64
	ColTerminW   float64

	CentralW    float64
	HeaderMainH float64
	HeaderSizeH float64
	RowH        float64
}

/* genel cell padding */
const OcellPadX = 2

/*
===========================================================

	PDF LAYOUT OLUŞTURUCU
	===========================================================
*/

/*
===========================================================

	PDF LAYOUT OLUŞTURUCU — AÇIKLAMA TEK KOLON
	===========================================================
*/
func newPdfLayout(pdf *gofpdf.Fpdf) pdfLayout {
	pageW, pageH := pdf.GetPageSize()

	l := pdfLayout{
		PageW:   pageW,
		PageH:   pageH,
		MarginL: 10,
		MarginR: 10,
		MarginT: 10,
		MarginB: 12,

		RowH:        7,
		HeaderMainH: 8,
		HeaderSizeH: 6,
	}

	totalW := pageW - l.MarginL - l.MarginR

	/* --------------------------------------------------------
	   SOL BLOK GÜNCEL – MODEL/RENK ÇAKIŞMASI GİDERİLDİ
	-------------------------------------------------------- */
	l.ColModelW = 24 // eski 18 → genişletildi
	l.ColRenkW = 24  // eski 14 → biraz geniş
	l.ColGroupW = 20
	l.ColGroupW2 = 20

	/* --------------------------------------------------------
	   AÇIKLAMA = TEK GENİŞ KOLON (kategori listesi + açıklama içerir)
	-------------------------------------------------------- */
	l.ColDescLeft = 50 // açıklama başlığı + kategori alanı (artık tek kolon)
	l.ColDescRight = 0 // kullanılmıyor (0 bırakılmalı!)

	left := l.ColModelW + l.ColRenkW + l.ColGroupW + l.ColGroupW2 + l.ColDescLeft

	/* --------------------------------------------------------
	   SAĞ BLOK
	-------------------------------------------------------- */
	l.ColQtyW = 12
	l.ColPriceW = 16
	l.ColCurW = 10
	l.ColAmountW = 20
	l.ColTerminW = 20

	right := l.ColQtyW + l.ColPriceW + l.ColCurW + l.ColAmountW + l.ColTerminW

	/* --------------------------------------------------------
	   ORTA BLOK (BEDEN 16 KOLON)
	-------------------------------------------------------- */
	l.CentralW = totalW - left - right
	if l.CentralW < 70 {
		l.CentralW = 70
	}

	return l
}

/* ===========================================================
   HELPER FONKSİYONLAR
   =========================================================== */

func safeTrimUpper(s string) string {
	return strings.ToUpper(strings.TrimSpace(s))
}

func normalizeTextForMatchGo(s string) string {
	replacer := strings.NewReplacer(
		"ç", "c", "Ç", "C",
		"ğ", "g", "Ğ", "G",
		"ı", "i", "I", "I", "İ", "I",
		"ö", "o", "Ö", "O",
		"ş", "s", "Ş", "S",
		"ü", "u", "Ü", "U",
	)
	out := replacer.Replace(strings.TrimSpace(s))
	out = strings.ToUpper(out)
	out = strings.Join(strings.Fields(out), " ")
	return out
}

func f64(v sql.NullFloat64) float64 {
	if !v.Valid {
		return 0
	}
	return v.Float64
}

func s64(v sql.NullString) string {
	if !v.Valid {
		return ""
	}
	return v.String
}

func sOrEmpty(v sql.NullString) string {
	if !v.Valid {
		return ""
	}
	return strings.TrimSpace(v.String)
}

func normalizeBedenLabelGo(v string) string {
	// 1️⃣ NULL / boş / whitespace → " " (aksbir null kolonu)
	s := strings.TrimSpace(v)
	if s == "" {
		return " " // 🔥 NULL BEDEN → boş kolon
	}

	// 2️⃣ Uppercase
	s = strings.ToUpper(s)

	// Yas bedenleri: 2Y / 2YAS / 2YAŞ -> 2
	for _, suf := range []string{"YAS", "YAŞ", "Y"} {
		if strings.HasSuffix(s, suf) {
			num := strings.TrimSpace(strings.TrimSuffix(s, suf))
			if num != "" {
				if _, err := strconv.Atoi(num); err == nil {
					return num
				}
			}
		}
	}

	/* --------------------------------------------------
	   🔥 AKSBİR ÖZEL (STD eş anlamlıları)
	-------------------------------------------------- */
	switch s {
	case "STD", "STANDART", "STANDARD", "ONE SIZE", "ONESIZE":
		return "STD"
	}

	/* --------------------------------------------------
	   🔢 SADECE "CM" VARSA → NUMERİK KISMI AL
	   120CM / 120 CM → 120
	   ❌ 105 / 110 / 120 → DOKUNMA
	-------------------------------------------------- */
	if strings.HasSuffix(s, "CM") {
		num := strings.TrimSpace(strings.TrimSuffix(s, "CM"))
		if num != "" {
			return num
		}
	}

	/* --------------------------------------------------
	   HARF BEDENLER (DOKUNMA)
	-------------------------------------------------- */
	switch s {
	case "XS", "S", "M", "L", "XL",
		"2XL", "3XL", "4XL", "5XL", "6XL", "7XL":
		return s
	}

	// 4️⃣ Sayısal veya başka değerler → olduğu gibi
	return s
}

func parseNumericSize(v string) (int, bool) {
	s := strings.TrimSpace(strings.ToUpper(v))
	if s == "" {
		return 0, false
	}
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, false
	}
	return n, true
}

func deriveKategoriTokenGo(urunKategori, yetiskinGarson string) string {
	kat := normalizeTextForMatchGo(urunKategori)
	if strings.Contains(kat, "GARSON") {
		return "GARSON"
	}
	if strings.Contains(kat, "YETISKIN") {
		return "YETISKIN"
	}
	return ""
}

func normalizeRuleAltGroupGo(urunAltGrubu string) string {
	return normalizeTextForMatchGo(urunAltGrubu)
}

func pickBestGroupFromCandidatesGo(groupKeys, bedenList []string, schemas map[string][]string) string {
	if len(groupKeys) == 0 {
		return ""
	}
	if len(groupKeys) == 1 {
		return strings.TrimSpace(groupKeys[0])
	}

	normalizedBeden := make([]string, 0, len(bedenList))
	for _, b := range bedenList {
		n := normalizeBedenLabelGo(b)
		if strings.TrimSpace(n) == "" {
			n = " "
		}
		normalizedBeden = append(normalizedBeden, n)
	}

	if len(normalizedBeden) == 0 {
		return strings.TrimSpace(groupKeys[0])
	}

	bestKey := strings.TrimSpace(groupKeys[0])
	bestScore := -1
	for _, key := range groupKeys {
		k := strings.TrimSpace(key)
		if k == "" {
			continue
		}
		normalizedSchema := map[string]bool{}
		for _, sv := range schemas[k] {
			ns := normalizeBedenLabelGo(sv)
			if strings.TrimSpace(ns) == "" {
				ns = " "
			}
			normalizedSchema[ns] = true
		}
		score := 0
		for _, b := range normalizedBeden {
			if normalizedSchema[b] {
				score++
			}
		}
		if score > bestScore {
			bestScore = score
			bestKey = k
		}
	}
	return bestKey
}

func resolveGroupFromProductSizeMatchRulesGo(
	matchData *ProductSizeMatchResponse,
	bedenList []string,
	urunAnaGrubu, urunKategori, yetiskinGarson, urunAltGrubu string,
) string {
	if matchData == nil || len(matchData.Rules) == 0 {
		return ""
	}

	kategoriToken := deriveKategoriTokenGo(urunKategori, yetiskinGarson)
	ana := normalizeTextForMatchGo(urunAnaGrubu)
	alt := normalizeRuleAltGroupGo(urunAltGrubu)
	if kategoriToken == "" || ana == "" {
		return ""
	}

	candidateGroupKeys := make([]string, 0, 2)
	seen := map[string]bool{}

	for i := range matchData.Rules {
		rule := &matchData.Rules[i]
		if normalizeTextForMatchGo(rule.UrunAnaGrubu) != ana {
			continue
		}
		ruleKategori := normalizeTextForMatchGo(rule.Kategori)
		if ruleKategori != kategoriToken {
			continue
		}

		ruleAlt := normalizeTextForMatchGo(rule.UrunAltGrubu)
		if ruleAlt != alt {
			continue
		}
		for _, g := range rule.GroupKeys {
			key := strings.TrimSpace(g)
			if key == "" || seen[key] {
				continue
			}
			seen[key] = true
			candidateGroupKeys = append(candidateGroupKeys, key)
		}
	}

	if len(candidateGroupKeys) == 0 {
		return ""
	}
	return pickBestGroupFromCandidatesGo(candidateGroupKeys, bedenList, matchData.Schemas)
}

func detectBedenGroupGo(
	matchData *ProductSizeMatchResponse,
	bedenList []string,
	ana, alt, urunKategori, yetiskinGarson string,
) string {
	ruleBased := resolveGroupFromProductSizeMatchRulesGo(
		matchData,
		bedenList,
		ana,
		urunKategori,
		yetiskinGarson,
		alt,
	)
	if ruleBased != "" {
		return ruleBased
	}

	ana = normalizeTextForMatchGo(ana)
	alt = normalizeTextForMatchGo(alt)

	isYetiskin := strings.Contains(alt, "YETISKIN") || strings.Contains(alt, "YETISKIN/GARSON")
	isGomlekKlasikOrAtayaka := strings.Contains(ana, "GOMLEK KLASIK") ||
		strings.Contains(ana, "GOMLEK ATA YAKA") ||
		strings.Contains(ana, "GOMLEK ATAYAKA")
	// Özel kural:
	// Kategorisi YETISKIN ve ana grubu GOMLEK KLASIK/ATA YAKA olanlar "gom" grubunda raporlanır.
	if isYetiskin && isGomlekKlasikOrAtayaka {
		return catGom
	}

	// Beden seti çocuk yaş formatındaysa metadata beklemeden "yas" aç.
	yasNums := map[string]bool{"2": true, "4": true, "6": true, "8": true, "10": true, "12": true, "14": true}
	if len(bedenList) > 0 {
		allYas := true
		for _, b := range bedenList {
			x := normalizeBedenLabelGo(b)
			if !yasNums[x] {
				allYas = false
				break
			}
		}
		if allYas {
			return catYas
		}
	}

	hasGarson := strings.Contains(ana, "GARSON") || strings.Contains(alt, "GARSON") ||
		strings.Contains(ana, "YETISKIN/GARSON") || strings.Contains(alt, "YETISKIN/GARSON") ||
		strings.Contains(ana, "YETİSKIN/GARSON") || strings.Contains(alt, "YETİSKIN/GARSON") ||
		strings.Contains(ana, "YETİŞKIN/GARSON") || strings.Contains(alt, "YETİŞKIN/GARSON") ||
		strings.Contains(ana, "YETİŞKİN/GARSON") || strings.Contains(alt, "YETİŞKİN/GARSON")

	// Ayakkabi kurali garsondan once uygulanmali:
	// GARSON + AYAKKABI => ayk_garson, digerleri => ayk
	if strings.Contains(ana, "AYAKKABI") || strings.Contains(alt, "AYAKKABI") {
		if hasGarson {
			return catAykGar
		}
		return catAyk
	}

	// ✅ Garson → yaş (ürün tipi fark etmeksizin)
	if hasGarson {
		return catYas
	}

	// ✅ Harfli beden → gömlek
	for _, b := range bedenList {
		b = safeTrimUpper(b)
		switch b {
		case "XS", "S", "M", "L", "XL",
			"2XL", "3XL", "4XL", "5XL", "6XL", "7XL":
			return catGom
		}
	}

	// ✅ Aksesuar tespiti (aksbir)
	aksesuarGruplari := []string{
		"AKSESUAR", "KRAVAT", "PAPYON", "KEMER", "CORAP", "ÇORAP",
		"FULAR", "MENDIL", "MENDİL", "KASKOL", "ASKI",
		"YAKA", "KOL DUGMESI", "KOL DÜĞMESİ",
	}
	giyimGruplari := []string{"GOMLEK", "GÖMLEK", "CEKET", "PANTOLON", "MONT", "YELEK", "TAKIM", "TSHIRT", "TISORT", "TİŞÖRT"}
	isAksesuar := false
	for _, g := range aksesuarGruplari {
		if strings.Contains(ana, g) || strings.Contains(alt, g) {
			isAksesuar = true
			break
		}
	}
	if isAksesuar {
		for _, g := range giyimGruplari {
			if strings.Contains(ana, g) {
				isAksesuar = false
				break
			}
		}
	}
	if isAksesuar {
		return catAksbir
	}

	// ✅ Pantolon özel (yetişkin)
	if strings.Contains(ana, "PANTOLON") && (strings.Contains(alt, "YETISKIN") || strings.Contains(alt, "YETİŞKİN") || strings.Contains(alt, "YETİSKIN")) {
		return catPan
	}

	// ✅ Tam numeric ve 35–46 arası → ayakkabı
	allNumeric := true
	nums := make([]int, 0, len(bedenList))
	for _, b := range bedenList {
		n, ok := parseNumericSize(b)
		if !ok {
			allNumeric = false
			break
		}
		nums = append(nums, n)
	}
	if allNumeric && len(nums) > 0 {
		sort.Ints(nums)
		okSeq := true
		for i := 1; i < len(nums); i++ {
			if nums[i]-nums[i-1] != 1 {
				okSeq = false
				break
			}
		}
		if okSeq && nums[0] >= 35 && nums[len(nums)-1] <= 46 {
			return catAyk
		}
	}

	// ✅ Garson/çocuk alt grubu → yaş
	if strings.Contains(alt, "ÇOCUK") || strings.Contains(alt, "GARSON") {
		return catYas
	}

	return catTak
}

func formatSizeQtyForLog(m map[string]int) string {
	if len(m) == 0 {
		return "{}"
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, fmt.Sprintf("%s:%d", k, m[k]))
	}
	return "{" + strings.Join(parts, ", ") + "}"
}
func defaultSizeListFor(cat string) []string {
	switch cat {
	case catAykGar:
		return []string{"22", "23", "24", "25", "26", "27", "28", "29", "30", "31", "32", "33", "34", "35", "STD"}
	case catAyk:
		return []string{"39", "40", "41", "42", "43", "44", "45"}
	case catYas:
		return []string{"2", "4", "6", "8", "10", "12", "14"}
	case catPan:
		return []string{"38", "40", "42", "44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68"}
	case catGom:
		return []string{"XS", "S", "M", "L", "XL", "2XL", "3XL", "4XL", "5XL", "6XL", "7XL"}
	case catTak:
		return []string{"44", "46", "48", "50", "52", "54", "56", "58", "60", "62", "64", "66", "68", "70", "72", "74"}
	case catAksbir:
		return []string{" ", "44", "STD", "110", "115", "120", "125", "130", "135"}
	}
	return []string{}
}

func contains(list []string, v string) bool {
	for _, x := range list {
		if x == v {
			return true
		}
	}
	return false
}

func formatPdfSizeLabel(cat, size string) string {
	s := strings.TrimSpace(size)
	if s == "" {
		return s
	}
	if cat == catYas {
		up := strings.ToUpper(s)
		if _, err := strconv.Atoi(up); err == nil {
			return up + "Y"
		}
	}
	return s
}

/* ===========================================================
   2) PDF OLUŞTURUCU (A4 YATAY + FOOTER)
   =========================================================== */

func newOrderPdf() (*gofpdf.Fpdf, error) {
	pdf := gofpdf.New("L", "mm", "A4", "")
	pdf.SetMargins(10, 10, 10)
	pdf.SetAutoPageBreak(false, 12)

	if err := registerDejavuFonts(pdf, "dejavu"); err != nil {
		return nil, err
	}

	// Footer: sayfa numarası
	pdf.AliasNbPages("")
	pdf.SetFooterFunc(func() {
		pdf.SetY(-10)
		pdf.SetFont("dejavu", "B", 8)
		txt := fmt.Sprintf("Sayfa %d/{nb}", pdf.PageNo())
		pdf.CellFormat(0, 10, txt, "", 0, "R", false, 0, "")
	})

	return pdf, nil
}

/* ===========================================================
   3)  DB FONKSİYONLARI (HEADER + LINES)
   =========================================================== */

// HEADER
func getOrderHeaderFromDB(db *sql.DB, orderID string) (*OrderHeader, error) {
	row := db.QueryRow(`
        SELECT 
            CAST(h.OrderHeaderID AS varchar(36)),
            h.OrderNumber,
            h.CurrAccCode,
            d.CurrAccDescription,
            h.DocCurrencyCode,
            h.OrderDate,
            h.Description,
            h.InternalDescription,
            h.OfficeCode,
            h.CreatedUserName,
            ISNULL((
                SELECT TOP (1) ca.AttributeDescription
                FROM BAGGI_V3.dbo.cdCurrAccAttributeDesc AS ca
                WHERE ca.CurrAccTypeCode   IN (1,3)
                  AND ca.AttributeTypeCode = 2      -- 🟡 Müşteri Temsilcisi
                  AND ca.AttributeCode     = f.CustomerAtt02
                  AND ca.LangCode         = 'TR'
            ), '') AS CustomerRep
        FROM BAGGI_V3.dbo.trOrderHeader AS h
        LEFT JOIN BAGGI_V3.dbo.cdCurrAccDesc AS d
            ON h.CurrAccCode = d.CurrAccCode
        LEFT JOIN BAGGI_V3.dbo.CustomerAttributesFilter AS f
            ON h.CurrAccCode = f.CurrAccCode
        WHERE h.OrderHeaderID = @p1
    `, orderID)

	var h OrderHeader
	var orderDate sql.NullTime
	var orderNumber, currAccCode, currAccName, docCurrency sql.NullString
	var description, internalDesc, officeCode, createdUser, customerRep sql.NullString

	err := row.Scan(
		&h.OrderHeaderID,
		&orderNumber,
		&currAccCode,
		&currAccName,
		&docCurrency,
		&orderDate,
		&description,
		&internalDesc,
		&officeCode,
		&createdUser,
		&customerRep, // 🆕 buradan geliyor
	)
	if err != nil {
		return nil, err
	}

	h.OrderNumber = sOrEmpty(orderNumber)
	h.CurrAccCode = sOrEmpty(currAccCode)
	h.CurrAccName = sOrEmpty(currAccName)
	h.DocCurrency = sOrEmpty(docCurrency)
	h.Description = sOrEmpty(description)
	h.InternalDesc = sOrEmpty(internalDesc)
	h.OfficeCode = sOrEmpty(officeCode)
	h.CreatedUser = sOrEmpty(createdUser)
	h.CustomerRep = sOrEmpty(customerRep)

	if orderDate.Valid {
		h.OrderDate = orderDate.Time
	}
	return &h, nil
}

// LINES
func getOrderLinesFromDB(db *sql.DB, orderID string) ([]OrderLineRaw, error) {
	rows, err := db.Query(`
        SELECT
            CAST(L.OrderLineID AS varchar(36)),
            L.ItemCode,
            L.ColorCode,
            L.ItemDim1Code,
            L.ItemDim2Code,
            L.Qty1,
            ISNULL(CD.Price, 0) AS Price,
            ISNULL(CD.CurrencyCode, ISNULL(L.DocCurrencyCode, 'TRY')) AS DocCurrencyCode,
            L.DeliveryDate,
            L.LineDescription,
            P.ProductAtt01Desc,
            P.ProductAtt02Desc,
            P.ProductAtt44Desc,
            CASE
                WHEN ISNULL(L.IsClosed, 0) = 1
                  OR EXISTS (
                      SELECT 1
                      FROM BAGGI_V3.dbo.trInvoiceLine il
                      WHERE il.OrderLineID = L.OrderLineID
                  )
                    THEN CAST(1 AS bit)
                ELSE CAST(0 AS bit)
            END AS IsClosed,
            L.WithHoldingTaxTypeCode,
            L.DOVCode,
            L.PlannedDateOfLading,
            L.CostCenterCode,
            L.VatCode,
            L.VatRate
        FROM BAGGI_V3.dbo.trOrderLine AS L
        LEFT JOIN BAGGI_V3.dbo.trOrderLineCurrency AS CD
            ON CD.OrderLineID = L.OrderLineID
            AND CD.CurrencyCode = ISNULL(NULLIF(LTRIM(RTRIM(L.DocCurrencyCode)), ''), 'TRY')
        LEFT JOIN ProductFilterWithDescription('TR') AS P
            ON LTRIM(RTRIM(P.ProductCode)) = LTRIM(RTRIM(L.ItemCode))
        WHERE L.OrderHeaderID = @p1
        ORDER BY L.SortOrder, L.OrderLineID
    `, orderID)

	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var out []OrderLineRaw
	for rows.Next() {
		var l OrderLineRaw
		if err := rows.Scan(
			&l.OrderLineID,
			&l.ItemCode,
			&l.ColorCode,
			&l.ItemDim1Code,
			&l.ItemDim2Code,
			&l.Qty1,
			&l.Price,
			&l.DocCurrencyCode,
			&l.DeliveryDate,
			&l.LineDescription,
			&l.UrunAnaGrubu,
			&l.UrunAltGrubu,
			&l.YetiskinGarson,
			&l.IsClosed,
			&l.WithHoldingTaxType,
			&l.DOVCode,
			&l.PlannedDateOfLading,
			&l.CostCenterCode,
			&l.VatCode,
			&l.VatRate,
		); err != nil {
			return nil, err
		}
		out = append(out, l)
	}
	return out, rows.Err()
}

/* ===========================================================
   4) NORMALIZE + CATEGORY MAP
   =========================================================== */

func normalizeOrderLinesForPdf(lines []OrderLineRaw, matchData *ProductSizeMatchResponse) []PdfRow {
	type comboKey struct {
		Model, Color, Color2 string
	}

	merged := make(map[comboKey]*PdfRow)

	for _, raw := range lines {

		// ❌ ARTIK KAPALI SATIRLARI ATMAYACAĞIZ
		// if raw.IsClosed.Valid && raw.IsClosed.Bool {
		// 	continue
		// }

		model := safeTrimUpper(raw.ItemCode)
		color := safeTrimUpper(raw.ColorCode)
		color2 := safeTrimUpper(s64(raw.ItemDim2Code))
		displayColor := color
		if color2 != "" {
			displayColor = fmt.Sprintf("%s-%s", color, color2)
		}

		key := comboKey{model, color, color2}

		if _, ok := merged[key]; !ok {
			merged[key] = &PdfRow{
				Model:          model,
				Color:          displayColor,
				GroupMain:      s64(raw.UrunAnaGrubu),
				GroupSub:       s64(raw.UrunAltGrubu),
				YetiskinGarson: s64(raw.YetiskinGarson),
				Description:    s64(raw.LineDescription),
				SizeQty:        make(map[string]int),
				Currency:       s64(raw.DocCurrencyCode),
				Price:          f64(raw.Price),
				OrderLineIDs:   make(map[string]string),
				ClosedSizes:    make(map[string]bool), // 🆕
			}
		}
		row := merged[key]

		// beden
		rawBeden := s64(raw.ItemDim1Code)
		if raw.ItemDim1Code.Valid {
			rawBeden = raw.ItemDim1Code.String
		}
		normalized := normalizeBedenLabelGo(rawBeden)

		qty := int(math.Round(f64(raw.Qty1)))
		if qty > 0 {
			row.SizeQty[normalized] += qty
			row.TotalQty += qty

			// 🆕 Bu beden kapalı satırdan geldiyse işaretle
			if raw.IsClosed.Valid && raw.IsClosed.Bool {
				row.ClosedSizes[normalized] = true
			}
		}

		// OrderLineID eşleştirmesi
		if raw.OrderLineID.Valid {
			row.OrderLineIDs[normalized] = raw.OrderLineID.String
		}

		// Termin
		if row.Termin == "" && raw.DeliveryDate.Valid {
			row.Termin = raw.DeliveryDate.Time.Format("02.01.2006")
		}
	}

	// finalize
	out := make([]PdfRow, 0, len(merged))
	for _, r := range merged {
		var sizes []string
		for s := range r.SizeQty {
			sizes = append(sizes, s)
		}
		r.Category = detectBedenGroupGo(matchData, sizes, r.GroupMain, r.GroupSub, r.YetiskinGarson, r.YetiskinGarson)
		r.Amount = float64(r.TotalQty) * r.Price
		out = append(out, *r)
	}

	// Sıralama: Model → Renk → Category
	sort.Slice(out, func(i, j int) bool {
		if out[i].Model != out[j].Model {
			return out[i].Model < out[j].Model
		}
		if out[i].Color != out[j].Color {
			return out[i].Color < out[j].Color
		}
		return out[i].Category < out[j].Category
	})

	return out
}

/* ===========================================================
   5) CATEGORY → SIZE MAP (HEADER İÇİN)
   =========================================================== */

type CategorySizeMap map[string][]string

// kategori beden map (global – TÜM GRİD İÇİN TEK HEADER)
func buildCategorySizeMap(rows []PdfRow) CategorySizeMap {
	cm := make(CategorySizeMap)

	// Her kategori için sabit default listeleri kullan
	for _, cat := range categoryOrder {
		base := defaultSizeListFor(cat)
		if len(base) > 0 {
			cm[cat] = append([]string{}, base...)
		}
	}

	// İstersen ekstra bedenler varsa ekle (opsiyonel)
	for _, r := range rows {
		c := r.Category
		if c == "" {
			c = catTak
		}
		// AKSESUAR başlığını sabit tut: satırlardan gelen 2,4,6... gibi ekstra bedenleri ekleme.
		if c == catAksbir {
			continue
		}
		if _, ok := cm[c]; !ok {
			cm[c] = []string{}
		}
		for size := range r.SizeQty {
			if !contains(cm[c], size) {
				cm[c] = append(cm[c], size)
			}
		}
	}

	return cm
}

/* ===========================================================
   ORDER HEADER (Logo + Gold Label + Sağ Bilgi Kutusu)
   =========================================================== */

func drawOrderHeader(pdf *gofpdf.Fpdf, h *OrderHeader, showDesc bool) float64 {
	pageW, _ := pdf.GetPageSize()
	marginL := 10.0
	y := 8.0
	shorten := func(s string, max int) string {
		r := []rune(strings.TrimSpace(s))
		if len(r) <= max {
			return string(r)
		}
		if max < 3 {
			return string(r[:max])
		}
		return string(r[:max-3]) + "..."
	}

	/* ----------------------------------------------------
	   1) LOGO
	---------------------------------------------------- */
	if logoPath, err := resolvePdfImagePath("Baggi-Tekstil-A.s-Logolu.jpeg"); err == nil {
		pdf.ImageOptions(logoPath, marginL, y, 32, 0, false, gofpdf.ImageOptions{}, 0, "")
	} else {
		pdf.SetFont("dejavu", "B", 12)
		pdf.SetTextColor(149, 113, 22)
		pdf.SetXY(marginL, y+6)
		pdf.CellFormat(32, 6, "BAGGI", "", 0, "L", false, 0, "")
		pdf.SetTextColor(0, 0, 0)
	}

	/* ----------------------------------------------------
	   2) ALTIN BAŞLIK BAR
	---------------------------------------------------- */
	titleW := 150.0
	titleX := marginL + 40
	titleY := y + 2

	pdf.SetFillColor(149, 113, 22) // Baggi altın
	pdf.Rect(titleX, titleY, titleW, 10, "F")

	pdf.SetFont("dejavu", "B", 13)
	pdf.SetTextColor(255, 255, 255)
	pdf.SetXY(titleX+4, titleY+2)
	pdf.CellFormat(titleW-8, 6, "BAGGI TEKSTİL - SİPARİŞ FORMU", "", 0, "L", false, 0, "")

	/* ----------------------------------------------------
	   3) SAĞ TARAF BİLGİ KUTUSU
	---------------------------------------------------- */
	boxW := 78.0
	boxX := pageW - marginL - boxW
	boxY := y - 2

	pdf.SetFont("dejavu", "B", 9)
	pdf.SetTextColor(149, 113, 22)
	rep := strings.TrimSpace(h.CustomerRep)
	if rep == "" {
		rep = strings.TrimSpace(h.CreatedUser)
	}
	desc := strings.TrimSpace(h.Description)
	if desc == "" {
		desc = strings.TrimSpace(h.InternalDesc)
	}

	info := []string{
		"Formun Basılma Tarihi: " + time.Now().Format("02.01.2006"),
		"Sipariş Tarihi: " + h.OrderDate.Format("02.01.2006"),
		"Sipariş No: " + h.OrderNumber,
		"Müşteri Temsilcisi: " + rep, // 🔥 YENİ EKLENDİ
		"Cari Kod: " + h.CurrAccCode,
		"Müşteri: " + h.CurrAccName,
	}
	if desc != "" {
		info = append(info, "Sipariş Açıklaması: "+shorten(desc, 52))
	}

	lineH := 4.5
	boxH := float64(len(info))*lineH + 3
	if boxH < 30 {
		boxH = 30
	}

	pdf.SetDrawColor(180, 180, 180)
	pdf.Rect(boxX, boxY, boxW, boxH, "")

	iy := boxY + 3
	for _, line := range info {
		pdf.SetXY(boxX+3, iy)
		pdf.CellFormat(boxW-6, lineH, line, "", 0, "L", false, 0, "")
		iy += lineH
	}

	/* ----------------------------------------------------
	   4) ALT AYIRICI ÇİZGİ
	---------------------------------------------------- */
	lineY := boxY + boxH + 3
	pdf.SetDrawColor(120, 120, 120)
	pdf.Line(marginL, lineY, pageW-marginL, lineY)
	pdf.SetDrawColor(200, 200, 200)

	y = lineY + 4

	/* ----------------------------------------------------
	   5) AÇIKLAMA (Varsa)
	---------------------------------------------------- */
	if showDesc && desc != "" {
		text := desc

		pdf.SetFont("dejavu", "", 8) // wrap’te kullanılacak font
		lineH := 4.0
		textW := pageW - marginL*2 - 52

		// Kaç satır olacağını hesapla
		lines := pdf.SplitLines([]byte(text), textW)
		descBoxH := float64(len(lines))*lineH + 4 // min boşluk

		if descBoxH < 10 {
			descBoxH = 10
		}

		pdf.SetDrawColor(210, 210, 210)
		pdf.Rect(marginL, y, pageW-marginL*2, descBoxH, "")

		// Başlık
		pdf.SetFont("dejavu", "B", 8)
		pdf.SetTextColor(149, 113, 22)
		pdf.SetXY(marginL+3, y+2)
		pdf.CellFormat(40, 4, "Sipariş Genel Açıklaması:", "", 0, "L", false, 0, "")

		// Metin
		pdf.SetFont("dejavu", "", 8)
		pdf.SetTextColor(30, 30, 30)
		pdf.SetXY(marginL+48, y+2)
		pdf.MultiCell(textW, lineH, text, "", "L", false)

		y += descBoxH + 3
	}

	return y
}

/* ===========================================================
   GRID HEADER — 2 katmanlı + 16 beden kolonlu
   =========================================================== */

/*
===========================================================

	GRID HEADER — AÇIKLAMA içinde kategori listesi + 16 beden kolonu
	===========================================================
*/
func drawGridHeader(pdf *gofpdf.Fpdf, layout pdfLayout, startY float64, catSizes CategorySizeMap) float64 {
	pdf.SetFont("dejavu", "B", 6)
	pdf.SetDrawColor(baggiGrayBorderR, baggiGrayBorderG, baggiGrayBorderB)
	pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
	pdf.SetTextColor(20, 20, 20) // 🟣 TÜM HEADER YAZILARI SİYAH

	y := startY
	x := layout.MarginL

	totalHeaderH := float64(len(categoryOrder)) * layout.HeaderSizeH

	centerLabel := func(h float64) float64 {
		return y + (h/2.0 - 3)
	}

	/* ----------------------------------------------------
	   SOL BLOK (Model – Renk – Grup1 – Grup2)
	---------------------------------------------------- */
	cols := []struct {
		w float64
		t string
	}{
		{layout.ColModelW, "MODEL"},
		{layout.ColRenkW, "RENK"},
		{layout.ColGroupW, "ÜRÜN ANA"},
		{layout.ColGroupW2, "ÜRÜN ALT"},
	}

	for _, c := range cols {
		pdf.Rect(x, y, c.w, totalHeaderH, "DF")
		pdf.SetXY(x, centerLabel(totalHeaderH))
		pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "")
		x += c.w
	}

	/* ----------------------------------------------------
	   AÇIKLAMA BAŞLIĞI — TEK HÜCRE & DİKEY ORTALAMA
	---------------------------------------------------- */
	descX := x
	descW := layout.ColDescLeft

	pdf.Rect(descX, y, descW, totalHeaderH, "DF")

	// AÇIKLAMA yazısı ortalanacak
	pdf.SetXY(descX, y+(totalHeaderH/2-3))
	pdf.CellFormat(descW, 6, "AÇIKLAMA", "", 0, "C", false, 0, "")

	/* ----------------------------------------------------
	   AÇIKLAMA sağında kategori listesi dikey şekilde
	---------------------------------------------------- */
	catX := descX + 1
	catY := y + 2

	pdf.SetFont("dejavu", "", 6.2)
	for _, cat := range categoryOrder {
		label := categoryTitle[cat]
		pdf.SetXY(catX+2, catY)
		pdf.CellFormat(descW-4, 4.8, label, "", 0, "L", false, 0, "")
		catY += layout.HeaderSizeH
	}

	/* ----------------------------------------------------
	   16’lı BEDEN BLOĞU
	---------------------------------------------------- */
	x = descX + descW
	colW := layout.CentralW / 16.0

	cy := y

	for _, cat := range categoryOrder {

		// Arka plan
		pdf.SetFillColor(baggiCreamR, baggiCreamG, baggiCreamB)
		pdf.Rect(x, cy, layout.CentralW, layout.HeaderSizeH, "DF")

		sizes := catSizes[cat]
		if len(sizes) == 0 {
			sizes = defaultSizeListFor(cat)
		}
		if cat == catAksbir {
			pdf.SetFont("dejavu", "", 5) // sadece aksesuar için küçük font
		} else {
			pdf.SetFont("dejavu", "", 6) // diğer tüm kategoriler normal font
		}

		xx := x
		for i := 0; i < 16; i++ {
			pdf.Rect(xx, cy, colW, layout.HeaderSizeH, "")
			if i < len(sizes) {
				label := formatPdfSizeLabel(cat, sizes[i])
				pdf.SetXY(xx, cy+1)
				pdf.CellFormat(colW, layout.HeaderSizeH-2, label, "", 0, "C", false, 0, "")
			}
			xx += colW
		}

		cy += layout.HeaderSizeH
	}

	/* ----------------------------------------------------
	   SAĞ BLOK (ADET – FİYAT – PB – TUTAR – TERMİN)
	---------------------------------------------------- */
	rightX := x + 16*colW

	rightCols := []struct {
		w float64
		t string
	}{
		{layout.ColQtyW, "ADET"},
		{layout.ColPriceW, "FİYAT"},
		{layout.ColCurW, "PB"},
		{layout.ColAmountW, "TUTAR"},
		{layout.ColTerminW, "TERMİN"},
	}

	for _, c := range rightCols {
		pdf.Rect(rightX, y, c.w, totalHeaderH, "DF")
		pdf.SetXY(rightX, centerLabel(totalHeaderH))
		pdf.CellFormat(c.w, 6, c.t, "", 0, "C", false, 0, "")
		rightX += c.w
	}

	return y + totalHeaderH
}

/* ===========================================================
   AÇIKLAMA WRAP + DİNAMİK SATIR YÜKSEKLİĞİ
   =========================================================== */

// Açıklama kolonunu çok satırlı yazan helper
func drawWrappedCell(pdf *gofpdf.Fpdf, text string, x, y, w, h float64) {
	txt := strings.TrimSpace(text)
	if txt == "" {
		return
	}

	lineH := 3.2
	lines := pdf.SplitLines([]byte(txt), w-2*OcellPadX)
	cy := y + 1

	for _, ln := range lines {
		if cy+lineH > y+h {
			break
		}
		pdf.SetXY(x+OcellPadX, cy)
		pdf.CellFormat(w-2*OcellPadX, lineH, string(ln), "", 0, "L", false, 0, "")
		cy += lineH
	}
}

// Açıklamaya göre satır yüksekliği hesaplar
func calcRowHeight(pdf *gofpdf.Fpdf, layout pdfLayout, row PdfRow) float64 {
	base := layout.RowH
	desc := strings.TrimSpace(row.Description)
	if desc == "" {
		return base
	}

	// Açıklama tek kolonda (ColDescLeft) render ediliyor.
	// ColDescW set edilmediği için 0 kalabiliyor; bu durumda SplitLines patlayabiliyor.
	descW := layout.ColDescLeft
	if descW <= float64(2*OcellPadX) {
		descW = layout.ColDescLeft + layout.ColDescRight
	}
	if descW <= float64(2*OcellPadX) {
		return base
	}

	lines := pdf.SplitLines([]byte(desc), descW-float64(2*OcellPadX))
	lineH := 3.2
	h := float64(len(lines))*lineH + 2

	if h < base {
		h = base
	}
	return h
}

/* ===========================================================
   SATIR ÇİZİMİ — 16 kolonlu beden dizilimi (sola yaslı)
   =========================================================== */

/*
===========================================================

	SATIR ÇİZİMİ — Tek açıklama sütunu + 16 beden kolonu
	===========================================================
*/
func drawPdfRow(pdf *gofpdf.Fpdf, layout pdfLayout, y float64, row PdfRow, catSizes CategorySizeMap, rowH float64) float64 {

	pdf.SetFont("dejavu", "", 7)
	pdf.SetDrawColor(200, 200, 200)
	pdf.SetLineWidth(0.15)
	pdf.SetTextColor(0, 0, 0)

	x := layout.MarginL
	h := rowH

	centerY := func() float64 {
		return y + (h-3.5)/2
	}

	/* ----------------------------------------------------
	   1) SOL BLOK
	      MODEL – RENK – ÜRÜN ANA – ÜRÜN ALT
	---------------------------------------------------- */

	cols := []struct {
		w float64
		v string
	}{
		{layout.ColModelW, row.Model},
		{layout.ColRenkW, row.Color},
		{layout.ColGroupW, row.GroupMain},
		{layout.ColGroupW2, row.GroupSub},
	}

	for _, c := range cols {
		pdf.Rect(x, y, c.w, h, "")
		pdf.SetXY(x+1.3, centerY())
		pdf.CellFormat(c.w-2.6, 3.5, c.v, "", 0, "L", false, 0, "")
		x += c.w
	}

	/* ----------------------------------------------------
	   2) AÇIKLAMA (TEK BÜYÜK KOLON)
	---------------------------------------------------- */

	descW := layout.ColDescLeft // açıklama = sadece sol kolon

	pdf.Rect(x, y, descW, h, "")
	drawWrappedCell(pdf, row.Description, x, y, descW, h)

	x += descW

	/* ----------------------------------------------------
	   3) 16 BEDEN KOLONU
	---------------------------------------------------- */

	colW := layout.CentralW / 16.0

	// kategorinin beden listesi
	sizes := catSizes[row.Category]

	if len(sizes) == 0 {
		tmp := make([]string, 0, len(row.SizeQty))
		for s := range row.SizeQty {
			tmp = append(tmp, s)
		}
		sort.Strings(tmp)
		sizes = tmp
	}

	xx := x
	for i := 0; i < 16; i++ {
		if i < len(sizes) {
			lbl := sizes[i]

			if q, ok := row.SizeQty[lbl]; ok && q > 0 {
				// 🔍 Bu beden kapalı mı?
				isClosedSize := row.ClosedSizes != nil && row.ClosedSizes[lbl]

				if isClosedSize {
					// Gri dolgu + beyaz yazı
					pdf.SetFillColor(220, 220, 220) // açık gri zemin
					pdf.Rect(xx, y, colW, h, "DF")  // doldurulmuş hücre
					pdf.SetTextColor(255, 255, 255) // beyaz text
				} else {
					// Normal hücre: sadece border
					pdf.Rect(xx, y, colW, h, "")
					pdf.SetTextColor(0, 0, 0) // siyah text
				}

				pdf.SetXY(xx, centerY())
				pdf.CellFormat(colW, 3.5, fmt.Sprintf("%d", q), "", 0, "C", false, 0, "")

				// Sonraki işler için text rengini resetle
				if isClosedSize {
					pdf.SetTextColor(0, 0, 0)
				}
			} else {
				// Bu beden kolonunda quantity yoksa normal boş hücre
				pdf.Rect(xx, y, colW, h, "")
			}
		} else {
			// 16 kolonun kalanları (header'da var ama bu kategoride kullanılmayan bedenler)
			pdf.Rect(xx, y, colW, h, "")
		}

		xx += colW
	}
	x = xx

	/* ----------------------------------------------------
	   4) SAĞ BLOK: ADET – FİYAT – PB – TUTAR – TERMİN
	---------------------------------------------------- */

	rightCols := []struct {
		w   float64
		v   string
		alg string
	}{
		{layout.ColQtyW, fmt.Sprintf("%d", row.TotalQty), "C"},
		{layout.ColPriceW, fmt.Sprintf("%.2f", row.Price), "R"},
		{layout.ColCurW, row.Currency, "C"},
		{layout.ColAmountW, fmt.Sprintf("%.2f", row.Amount), "R"},
		{layout.ColTerminW, row.Termin, "C"},
	}

	for _, c := range rightCols {
		pdf.Rect(x, y, c.w, h, "")
		pdf.SetXY(x+0.6, centerY())
		pdf.CellFormat(c.w-1.2, 3.5, c.v, "", 0, c.alg, false, 0, "")
		x += c.w
	}

	return y + h
}

/* ===========================================================
   FORMATLAYICI: TR PARA BİÇİMİ (1.234.567,89)
   =========================================================== */
/* ===========================================================
   TOPLAM TUTAR / KDV / KDV DAHİL TOPLAM KUTUSU
   (Sipariş Genel Açıklaması ile grid header arasında)
=========================================================== */

/* ===========================================================
   PREMIUM BAGGI GOLD TOTALS BOX
   (Sipariş Açıklaması ile Grid Header arasındaki kutu)
=========================================================== */

func drawTotalsBox(
	pdf *gofpdf.Fpdf,
	layout pdfLayout,
	startY float64,
	subtotal float64,
	hasVat bool,
	vatRate float64,
	vatAmount float64,
	totalWithVat float64,
	currency string,
) float64 {

	x := layout.MarginL
	w := layout.PageW - layout.MarginL - layout.MarginR

	lineH := 6.0
	rows := 1
	if hasVat {
		rows = 3
	}

	boxH := float64(rows)*lineH + 5

	/* ----------------------------------------------------
	   ARKA PLAN + ALTIN ÇERÇEVE
	---------------------------------------------------- */
	pdf.SetFillColor(255, 253, 245) // yumuşak krem
	pdf.SetDrawColor(149, 113, 22)  // Baggi gold
	pdf.SetLineWidth(0.4)
	pdf.Rect(x, startY, w, boxH, "DF")

	/* ----------------------------------------------------
	   Genel metin stilleri
	---------------------------------------------------- */
	labelX := x + 4
	valueX := x + w - 70 // değerlerin sağda hizalanacağı kolon

	pdf.SetTextColor(149, 113, 22) // Sol başlık gold
	pdf.SetFont("dejavu", "B", 8.5)

	y := startY + 2

	/* ----------------------------------------------------
	   1️⃣ TOPLAM TUTAR
	---------------------------------------------------- */
	pdf.SetXY(labelX, y)
	pdf.CellFormat(80, lineH, "TOPLAM TUTAR", "", 0, "L", false, 0, "")

	pdf.SetTextColor(201, 162, 39)
	pdf.SetFont("dejavu", "B", 9)

	pdf.SetXY(valueX, y)
	pdf.CellFormat(65, lineH,
		fmt.Sprintf("%s %s", formatCurrencyTR(subtotal), currency),
		"", 0, "R", false, 0, "")

	y += lineH

	/* ----------------------------------------------------
	   2️⃣ KDV (opsiyonel)
	---------------------------------------------------- */
	if hasVat {

		pdf.SetTextColor(149, 113, 22) // gold başlık
		pdf.SetFont("dejavu", "B", 8.5)

		pdf.SetXY(labelX, y)
		pdf.CellFormat(80, lineH,
			fmt.Sprintf("KDV (%%%g)", vatRate),
			"", 0, "L", false, 0, "")

		pdf.SetTextColor(20, 20, 20)
		pdf.SetFont("dejavu", "B", 9)

		pdf.SetXY(valueX, y)
		pdf.CellFormat(65, lineH,
			fmt.Sprintf("%s %s", formatCurrencyTR(vatAmount), currency),
			"", 0, "R", false, 0, "")

		y += lineH

		/* ----------------------------------------------------
		   3️⃣ KDV DAHİL TOPLAM
		---------------------------------------------------- */
		pdf.SetTextColor(201, 162, 39)
		pdf.SetFont("dejavu", "B", 8.5)

		pdf.SetXY(labelX, y)
		pdf.CellFormat(80, lineH, "KDV DAHİL TOPLAM TUTAR", "", 0, "L", false, 0, "")

		pdf.SetTextColor(20, 20, 20)
		pdf.SetFont("dejavu", "B", 9)

		pdf.SetXY(valueX, y)
		pdf.CellFormat(65, lineH,
			fmt.Sprintf("%s %s", formatCurrencyTR(totalWithVat), currency),
			"", 0, "R", false, 0, "")
	}

	/* ----------------------------------------------------
	   Kutu altı boşluk
	---------------------------------------------------- */
	return startY + boxH + 4
}

/* ===========================================================
   GRUP TOPLAM BAR (Frontend tarzı sarı bar)
   =========================================================== */

func drawGroupSummaryBar(pdf *gofpdf.Fpdf, layout pdfLayout, groupName string, totalQty int, totalAmount float64, currency string) float64 {
	y := pdf.GetY()
	x := layout.MarginL
	w := layout.PageW - layout.MarginL - layout.MarginR
	h := 7.0

	// Açık sarı zemin
	pdf.SetFillColor(255, 249, 205) // #fff9cd benzeri
	pdf.SetDrawColor(214, 192, 106)
	pdf.Rect(x, y, w, h, "DF")

	pdf.SetFont("dejavu", "B", 8.5)
	pdf.SetTextColor(20, 20, 20)

	leftTxt := strings.ToUpper(strings.TrimSpace(groupName))
	if leftTxt == "" {
		leftTxt = "GENEL"
	}
	pdf.SetXY(x+2, y+1.2)
	pdf.CellFormat(w*0.40, h-2.4, leftTxt, "", 0, "L", false, 0, "")

	rightTxt := fmt.Sprintf(
		"TOPLAM %s ADET: %d   TOPLAM %s TUTAR: %s %s",
		leftTxt, totalQty,
		leftTxt, formatCurrencyTR(totalAmount), currency,
	)

	pdf.SetXY(x+w*0.35, y+1.2)
	pdf.CellFormat(w*0.60-2, h-2.4, rightTxt, "", 0, "R", false, 0, "")

	// reset
	pdf.SetTextColor(0, 0, 0)
	pdf.SetDrawColor(201, 162, 39)

	pdf.SetY(y + h)
	return y + h
}

/* ===========================================================
   MULTIPAGE RENDER ENGINE (Header + GridHeader + Rows)
   =========================================================== */

func renderOrderGrid(pdf *gofpdf.Fpdf, header *OrderHeader, rows []PdfRow, hasVat bool, vatRate float64) {
	layout := newPdfLayout(pdf)
	catSizes := buildCategorySizeMap(rows)

	normalizeYetiskinGarsonTokenGo := func(v string) string {
		s := strings.ToUpper(strings.TrimSpace(v))
		if strings.Contains(s, "GARSON") {
			return "GARSON"
		}
		if strings.Contains(s, "YETISKIN") || strings.Contains(s, "YETİSKİN") {
			return "YETISKIN"
		}
		return "GENEL"
	}

	// Grup: ÜRÜN ANA GRUBU
	type group struct {
		Name  string
		Rows  []PdfRow
		Adet  int
		Tutar float64
	}

	groups := map[string]*group{}
	var order []string

	for _, r := range rows {
		ana := strings.TrimSpace(r.GroupMain)
		if ana == "" {
			ana = "GENEL"
		}
		ana = strings.ToUpper(ana)
		yg := normalizeYetiskinGarsonTokenGo(r.YetiskinGarson) // fallback
		kategori := strings.Join(strings.Fields(strings.TrimSpace(r.YetiskinGarson)), " ")
		if kategori == "" {
			kategori = yg
		}
		name := strings.TrimSpace(fmt.Sprintf("%s %s", kategori, ana))
		groupKey := fmt.Sprintf("%s::%s", kategori, ana)

		g, ok := groups[groupKey]
		if !ok {
			g = &group{Name: name}
			groups[groupKey] = g
			order = append(order, groupKey)
		}
		g.Rows = append(g.Rows, r)
		g.Adet += r.TotalQty
		g.Tutar += r.Amount
	}

	groupSummaryH := 7.0

	// 🔹 Genel toplam (grid içindeki satırlardan)
	var subtotal float64
	for _, r := range rows {
		subtotal += r.Amount
	}

	// 🔹 KDV hesapla (hasVat ve vatRate, OrderPDFHandler'dan geliyor)
	var vatAmount float64
	totalWithVat := subtotal

	if hasVat && vatRate > 0 {
		vatAmount = subtotal * vatRate / 100.0
		totalWithVat = subtotal + vatAmount
	}

	var y float64
	firstPage := true

	newPage := func(showDesc bool, showTotals bool) {
		pdf.AddPage()
		// Üst header (logo + sağ kutu + genel açıklama)
		y = drawOrderHeader(pdf, header, showDesc)

		// 🔸 İlk sayfada, header ile grid arasında TOPLAM kutusu
		if showTotals {
			y = drawTotalsBox(
				pdf,
				layout,
				y,
				subtotal,
				hasVat,
				vatRate,
				vatAmount,
				totalWithVat,
				header.DocCurrency,
			)
		}

		// Grid header
		y = drawGridHeader(pdf, layout, y, catSizes)
		y += 1
	}

	// İlk sayfa: açıklama + toplam kutusu
	newPage(firstPage, true)
	firstPage = false

	for _, key := range order {
		g := groups[key]

		for _, row := range g.Rows {
			rh := calcRowHeight(pdf, layout, row)

			if y+rh+groupSummaryH+2 > layout.PageH-layout.MarginB {
				// Sonraki sayfalarda: açıklama yok, toplam kutusu yok
				newPage(false, false)
			}
			y = drawPdfRow(pdf, layout, y, row, catSizes, rh)
			pdf.SetY(y) // 🔹 satırın altına imleci getir
		}

		// Grup toplam barı için yer kontrolü
		if y+groupSummaryH+2 > layout.PageH-layout.MarginB {
			newPage(false, false)
		}
		y = drawGroupSummaryBar(pdf, layout, g.Name, g.Adet, g.Tutar, header.DocCurrency)
		y += 1
	}

	// ⚠️ Eski alttaki "Genel Toplam" yazdırma kaldırıldı; toplam kutusu artık üstte.
}

/* ===========================================================
   HTTP HANDLER → /api/order/pdf/{id}
   =========================================================== */

func OrderPDFHandler(db *sql.DB, pgDB *sql.DB) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		orderID := mux.Vars(r)["id"]
		log.Printf("📄 OrderPDFHandler start orderID=%s", orderID)
		defer func() {
			if rec := recover(); rec != nil {
				log.Printf("❌ PANIC OrderPDFHandler orderID=%s: %v", orderID, rec)
				debug.PrintStack()
				http.Error(w, fmt.Sprintf("order pdf panic: %v", rec), http.StatusInternalServerError)
			}
		}()

		if orderID == "" {
			log.Printf("❌ OrderPDFHandler missing order id")
			http.Error(w, "missing order id", http.StatusBadRequest)
			return
		}
		if db == nil {
			log.Printf("❌ OrderPDFHandler db is nil")
			http.Error(w, "db not initialized", http.StatusInternalServerError)
			return
		}

		// Header
		header, err := getOrderHeaderFromDB(db, orderID)
		if err != nil {
			log.Printf("❌ OrderPDF header error orderID=%s: %v", orderID, err)
			if errors.Is(err, sql.ErrNoRows) {
				http.Error(w, "order not found", http.StatusNotFound)
				return
			}
			http.Error(w, "header not found: "+err.Error(), http.StatusInternalServerError)
			return
		}

		// Lines
		lines, err := getOrderLinesFromDB(db, orderID)
		if err != nil {
			log.Printf("❌ OrderPDF lines error orderID=%s: %v", orderID, err)
			http.Error(w, "lines not found: "+err.Error(), http.StatusInternalServerError)
			return
		}
		log.Printf("📄 OrderPDF lines loaded orderID=%s lineCount=%d", orderID, len(lines))
		// 🔹 Satırlardan KDV bilgisi yakala (ilk pozitif orana göre)
		hasVat := false
		var vatRate float64

		for _, l := range lines {
			if l.VatRate.Valid && l.VatRate.Float64 > 0 {
				hasVat = true
				vatRate = l.VatRate.Float64
				break
			}
		}

		// Normalize
		var sizeMatchData *ProductSizeMatchResponse
		if pgDB == nil {
			http.Error(w, "product-size-match db not initialized", http.StatusInternalServerError)
			return
		}
		if m, err := loadProductSizeMatchData(pgDB); err != nil {
			log.Printf("❌ OrderPDF product-size-match load failed orderID=%s: %v", orderID, err)
			http.Error(w, "product-size-match load failed: "+err.Error(), http.StatusInternalServerError)
			return
		} else {
			sizeMatchData = m
		}
		rows := normalizeOrderLinesForPdf(lines, sizeMatchData)
		unmapped := make([]string, 0)
		for i := range rows {
			if strings.TrimSpace(rows[i].Category) != "" {
				continue
			}
			bedenList := make([]string, 0, len(rows[i].SizeQty))
			for s := range rows[i].SizeQty {
				bedenList = append(bedenList, s)
			}
			rows[i].Category = detectBedenGroupGo(
				sizeMatchData,
				bedenList,
				rows[i].GroupMain,
				rows[i].GroupSub,
				rows[i].YetiskinGarson,
				rows[i].YetiskinGarson,
			)
			if strings.TrimSpace(rows[i].Category) == "" {
				rows[i].Category = catTak
			}
			if strings.TrimSpace(rows[i].Category) == "" {
				unmapped = append(unmapped, fmt.Sprintf("%s/%s/%s", rows[i].Model, rows[i].GroupMain, rows[i].GroupSub))
			}
		}
		if len(unmapped) > 0 {
			log.Printf("⚠️ OrderPDF unmapped rows fallback failed orderID=%s rows=%v", orderID, unmapped)
		}
		log.Printf("📄 OrderPDF normalized rows orderID=%s rowCount=%d", orderID, len(rows))
		for i, rr := range rows {
			if i >= 30 {
				break
			}
			log.Printf(
				"📄 OrderPDF row[%d] model=%s color=%s groupMain=%q groupSub=%q category=%s totalQty=%d sizeQty=%s",
				i,
				rr.Model,
				rr.Color,
				rr.GroupMain,
				rr.GroupSub,
				rr.Category,
				rr.TotalQty,
				formatSizeQtyForLog(rr.SizeQty),
			)
		}

		// PDF
		pdf, err := newOrderPdf()
		if err != nil {
			log.Printf("❌ OrderPDF init error orderID=%s: %v", orderID, err)
			http.Error(w, "pdf init error: "+err.Error(), http.StatusInternalServerError)
			return
		}
		renderOrderGrid(pdf, header, rows, hasVat, vatRate)
		if err := pdf.Error(); err != nil {
			log.Printf("❌ OrderPDF render error orderID=%s: %v", orderID, err)
			http.Error(w, "pdf render error: "+err.Error(), http.StatusInternalServerError)
			return
		}

		var buf bytes.Buffer
		if err := pdf.Output(&buf); err != nil {
			log.Printf("❌ OrderPDF output error orderID=%s: %v", orderID, err)
			http.Error(w, "pdf output error: "+err.Error(), http.StatusInternalServerError)
			return
		}
		log.Printf("✅ OrderPDF success orderID=%s bytes=%d", orderID, buf.Len())

		w.Header().Set("Content-Type", "application/pdf")
		w.Header().Set(
			"Content-Disposition",
			fmt.Sprintf("inline; filename=\"ORDER_%s.pdf\"", header.OrderNumber),
		)
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write(buf.Bytes())
	})
}
