package queries

import (
	"bssapp-backend/auth"
	"bssapp-backend/db"
	"bssapp-backend/internal/authz"
	"bssapp-backend/models"
	"context"
	"database/sql"
	"fmt"
	"log"
	"sort"
	"strconv"
	"strings"
)

type mkCariBakiyeLine struct {
	CurrAccTypeCode  int
	CariKodu         string
	CariDoviz        string
	SirketKodu       int
	PislemTipi       string
	ParasalIslemTipi string
	YerelBakiye      float64
	Bakiye           float64
	VadeGun          float64
	VadeBelgeGun     float64
}

type cariMeta struct {
	CariDetay    string
	CariTip      string
	Kanal1       string
	Piyasa       string
	Temsilci     string
	Ulke         string
	Il           string
	Ilce         string
	TC           string
	RiskDurumu   string
	MuhasebeKodu string
	SirketDetay  string
}

type masterCariMeta struct {
	CariDetay  string
	Kanal1     string
	Piyasa     string
	Temsilci   string
	Ulke       string
	Il         string
	Ilce       string
	RiskDurumu string
}

type balanceFilters struct {
	cariIlkGrup map[string]struct{}
	piyasa      map[string]struct{}
	temsilci    map[string]struct{}
	riskDurumu  map[string]struct{}
	islemTipi   map[string]struct{}
	ulke        map[string]struct{}
	il          map[string]struct{}
	ilce        map[string]struct{}
}

func GetCustomerBalanceList(ctx context.Context, params models.CustomerBalanceListParams) ([]models.CustomerBalanceListRow, error) {
	if strings.TrimSpace(params.SelectedDate) == "" {
		return nil, fmt.Errorf("selected_date is required")
	}

	lines, err := loadBalanceLines(ctx, params.SelectedDate, params.CariSearch)
	if err != nil {
		return nil, err
	}

	metaMap, err := loadCariMetaMap(ctx, lines)
	if err != nil {
		log.Printf("customer_balance_list: cari meta query failed, fallback without meta: %v", err)
		metaMap = map[string]cariMeta{}
	}

	masterMetaMap, err := loadMasterCariMetaMap(ctx, lines)
	if err != nil {
		log.Printf("customer_balance_list: master cari meta query failed, fallback without master meta: %v", err)
		masterMetaMap = map[string]masterCariMeta{}
	}

	companyMap, err := loadCompanyMap(ctx)
	if err != nil {
		return nil, err
	}

	glMap, err := loadGLAccountMap(ctx, lines)
	if err != nil {
		return nil, err
	}

	rateMap, err := loadNearestTryRates(ctx)
	if err != nil {
		return nil, err
	}

	usdTry := rateMap["USD"]
	if usdTry <= 0 {
		usdTry = 1
	}

	filters := buildFilters(params)
	agg := make(map[string]*models.CustomerBalanceListRow, len(lines))

	for _, ln := range lines {
		cari := strings.TrimSpace(ln.CariKodu)
		if cari == "" {
			continue
		}

		curr := strings.ToUpper(strings.TrimSpace(ln.CariDoviz))
		if curr == "" {
			curr = "TRY"
		}

		meta := metaMap[metaKey(ln.CurrAccTypeCode, cari)]
		meta.MuhasebeKodu = glMap[glKey(ln.CurrAccTypeCode, cari, ln.SirketKodu)]
		meta.SirketDetay = companyMap[ln.SirketKodu]
		master := deriveMasterCari(cari)
		mm := masterMetaMap[master]

		if strings.TrimSpace(mm.Kanal1) != "" {
			meta.Kanal1 = mm.Kanal1
		}
		if strings.TrimSpace(mm.Piyasa) != "" {
			meta.Piyasa = mm.Piyasa
		}
		if strings.TrimSpace(mm.Temsilci) != "" {
			meta.Temsilci = mm.Temsilci
		}
		if strings.TrimSpace(mm.Ulke) != "" {
			meta.Ulke = mm.Ulke
		}
		if strings.TrimSpace(mm.Il) != "" {
			meta.Il = mm.Il
		}
		if strings.TrimSpace(mm.Ilce) != "" {
			meta.Ilce = mm.Ilce
		}
		if strings.TrimSpace(mm.RiskDurumu) != "" {
			meta.RiskDurumu = mm.RiskDurumu
		}

		if !filters.matchLine(ln.PislemTipi, meta) {
			continue
		}

		key := strconv.Itoa(ln.CurrAccTypeCode) + "|" + cari + "|" + curr + "|" + strconv.Itoa(ln.SirketKodu)
		row, ok := agg[key]
		if !ok {
			row = &models.CustomerBalanceListRow{
				CariIlkGrup:  meta.Kanal1,
				Piyasa:       meta.Piyasa,
				Temsilci:     meta.Temsilci,
				Sirket:       strconv.Itoa(ln.SirketKodu),
				AnaCariKodu:  master,
				AnaCariAdi:   firstNonEmpty(mm.CariDetay, meta.CariDetay),
				CariKodu:     cari,
				CariDetay:    meta.CariDetay,
				CariTip:      meta.CariTip,
				Kanal1:       meta.Kanal1,
				Ozellik03:    meta.RiskDurumu,
				Ozellik05:    meta.Ulke,
				Ozellik06:    meta.Il,
				Ozellik07:    meta.Ilce,
				Il:           meta.Il,
				Ilce:         meta.Ilce,
				MuhasebeKodu: meta.MuhasebeKodu,
				TC:           meta.TC,
				RiskDurumu:   meta.RiskDurumu,
				SirketDetay:  meta.SirketDetay,
				CariDoviz:    curr,
			}
			agg[key] = row
		}

		usd := toUSD(ln.Bakiye, curr, usdTry, rateMap)
		add12, add13 := resolveBalanceBuckets(ln)
		if add12 {
			row.Bakiye12 += ln.Bakiye
			row.TLBakiye12 += ln.YerelBakiye
			row.USDBakiye12 += usd
		}
		if add13 {
			row.Bakiye13 += ln.Bakiye
			row.TLBakiye13 += ln.YerelBakiye
			row.USDBakiye13 += usd
		}
	}

	out := make([]models.CustomerBalanceListRow, 0, len(agg))
	for _, v := range agg {
		out = append(out, *v)
	}

	sort.Slice(out, func(i, j int) bool {
		if out[i].AnaCariKodu == out[j].AnaCariKodu {
			if out[i].CariKodu == out[j].CariKodu {
				return out[i].CariDoviz < out[j].CariDoviz
			}
			return out[i].CariKodu < out[j].CariKodu
		}
		return out[i].AnaCariKodu < out[j].AnaCariKodu
	})

	return out, nil
}

func loadMasterCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]masterCariMeta, error) {
	masters := make(map[string]struct{})
	for _, ln := range lines {
		m := strings.TrimSpace(deriveMasterCari(ln.CariKodu))
		if m != "" {
			masters[m] = struct{}{}
		}
	}
	if len(masters) == 0 {
		return map[string]masterCariMeta{}, nil
	}

	query := fmt.Sprintf(`
WITH BaseCari AS
(
    SELECT
        CB.CurrAccCode,
        CB.CurrAccTypeCode,
        MasterCari = LEFT(CB.CurrAccCode, 8),
        rn = ROW_NUMBER() OVER
        (
            PARTITION BY LEFT(CB.CurrAccCode, 8)
            ORDER BY CB.CurrAccCode
        )
    FROM cdCurrAcc CB
    WHERE CB.CurrAccTypeCode IN (1,3)
      AND LEFT(CB.CurrAccCode, 8) IN (%s)
),
FirstCari AS
(
    SELECT *
    FROM BaseCari
    WHERE rn = 1
)
SELECT
    CariKodu = F.MasterCari,
    CariDetay = ISNULL(cd.CurrAccDescription, ''),
    KANAL_1 = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt08Desc ELSE CDesc.CustomerAtt08Desc END, ''),
    PIYASA = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt01Desc ELSE CDesc.CustomerAtt01Desc END, ''),
    CARI_TEMSILCI = ISNULL(
        CASE
            WHEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END,'') = ''
            THEN ISNULL(CASE WHEN F.CurrAccTypeCode = 1 THEN VAttr.VendorAtt09 ELSE CAttr.CustomerAtt09 END,'')
            ELSE CASE WHEN F.CurrAccTypeCode = 1 THEN VDesc.VendorAtt02Desc ELSE CDesc.CustomerAtt02Desc END
        END,''
    ),
    ULKE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt05Desc ELSE CDesc.CustomerAtt05Desc END, ''),
    IL = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt06Desc ELSE CDesc.CustomerAtt06Desc END, ''),
    ILCE = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt07Desc ELSE CDesc.CustomerAtt07Desc END, ''),
    Risk_Durumu = ISNULL(CASE WHEN F.CurrAccTypeCode=1 THEN VDesc.VendorAtt03Desc ELSE CDesc.CustomerAtt03Desc END, '')
FROM FirstCari F
LEFT JOIN cdCurrAccDesc cd
    ON cd.CurrAccTypeCode = F.CurrAccTypeCode
   AND cd.CurrAccCode     = F.CurrAccCode
   AND cd.LangCode        = 'TR'
LEFT JOIN VendorAttributeDescriptions('TR') VDesc
    ON VDesc.CurrAccCode     = F.CurrAccCode
   AND VDesc.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN CustomerAttributeDescriptions('TR') CDesc
    ON CDesc.CurrAccCode     = F.CurrAccCode
   AND CDesc.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN VendorAttributes VAttr
    ON VAttr.CurrAccCode     = F.CurrAccCode
   AND VAttr.CurrAccTypeCode = F.CurrAccTypeCode
LEFT JOIN CustomerAttributes CAttr
    ON CAttr.CurrAccCode     = F.CurrAccCode
   AND CAttr.CurrAccTypeCode = F.CurrAccTypeCode
ORDER BY F.MasterCari;
`, quotedInList(masters))

	rows, err := db.MssqlDB.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("master cari meta query error: %w", err)
	}
	defer rows.Close()

	out := make(map[string]masterCariMeta, len(masters))
	for rows.Next() {
		var master string
		var m masterCariMeta
		if err := rows.Scan(
			&master,
			&m.CariDetay,
			&m.Kanal1,
			&m.Piyasa,
			&m.Temsilci,
			&m.Ulke,
			&m.Il,
			&m.Ilce,
			&m.RiskDurumu,
		); err != nil {
			return nil, err
		}
		out[strings.TrimSpace(master)] = m
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func loadBalanceLines(ctx context.Context, selectedDate, cariSearch string) ([]mkCariBakiyeLine, error) {
	piyasaScope, err := buildPiyasaExistsForCariCode(ctx, "CariKodu")
	if err != nil {
		return nil, err
	}

	queryTemplate := `
	SELECT
		CurrAccTypeCode,
		CariKodu,
		CariDoviz,
		SirketKodu,
		PislemTipi,
		%s
		YerelBakiye,
		Bakiye,
		CAST(0 AS DECIMAL(18,4)) AS Vade_Gun,
		CAST(0 AS DECIMAL(18,4)) AS Vade_BelgeTarihi_Gun
	FROM dbo.MK_CARI_BAKIYE_LIST(@SonTarih)
	WHERE (@CariSearch = '' OR CariKodu LIKE '%%' + @CariSearch + '%%')
	  AND %s
	`

	selectParasalCandidates := make([]string, 0, 7)
	if expr := strings.TrimSpace(resolveParasalIslemSelectExpr(ctx, "SELECT * FROM dbo.MK_CARI_BAKIYE_LIST('2000-01-01')")); expr != "" {
		selectParasalCandidates = append(selectParasalCandidates, expr)
	}
	selectParasalCandidates = append(selectParasalCandidates,
		"CAST(ATAtt01 AS varchar(16)) AS ParasalIslemTipi,",
		"CAST(ParasalIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
		"CAST(ParislemTipi AS varchar(16)) AS ParasalIslemTipi,",
		"CAST(ParIslemTipi AS varchar(16)) AS ParasalIslemTipi,",
		"CAST('' AS varchar(16)) AS ParasalIslemTipi,",
	)

	var rows *sql.Rows
	for i, sel := range selectParasalCandidates {
		query := fmt.Sprintf(queryTemplate, sel, piyasaScope)
		rows, err = db.MssqlDB.QueryContext(ctx, query,
			sql.Named("SonTarih", selectedDate),
			sql.Named("CariSearch", strings.TrimSpace(cariSearch)),
		)
		if err == nil {
			break
		}
		if i < len(selectParasalCandidates)-1 && isInvalidColumnError(err) {
			continue
		}
		return nil, fmt.Errorf("MK_CARI_BAKIYE_LIST query error: %w", err)
	}
	defer rows.Close()

	out := make([]mkCariBakiyeLine, 0, 4096)
	for rows.Next() {
		var r mkCariBakiyeLine
		if err := rows.Scan(
			&r.CurrAccTypeCode,
			&r.CariKodu,
			&r.CariDoviz,
			&r.SirketKodu,
			&r.PislemTipi,
			&r.ParasalIslemTipi,
			&r.YerelBakiye,
			&r.Bakiye,
			&r.VadeGun,
			&r.VadeBelgeGun,
		); err != nil {
			return nil, err
		}
		out = append(out, r)
	}

	if err := rows.Err(); err != nil {
		return nil, err
	}

	return out, nil
}

func loadCariMetaMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]cariMeta, error) {
	vendorCodes := make(map[string]struct{})
	customerCodes := make(map[string]struct{})

	for _, ln := range lines {
		code := strings.TrimSpace(ln.CariKodu)
		if code == "" {
			continue
		}
		if ln.CurrAccTypeCode == 1 {
			vendorCodes[code] = struct{}{}
		} else if ln.CurrAccTypeCode == 3 {
			customerCodes[code] = struct{}{}
		}
	}

	if len(vendorCodes) == 0 && len(customerCodes) == 0 {
		return map[string]cariMeta{}, nil
	}

	whereParts := make([]string, 0, 2)
	if len(vendorCodes) > 0 {
		whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=1 AND c.CurrAccCode IN (%s))", quotedInList(vendorCodes)))
	}
	if len(customerCodes) > 0 {
		whereParts = append(whereParts, fmt.Sprintf("(c.CurrAccTypeCode=3 AND c.CurrAccCode IN (%s))", quotedInList(customerCodes)))
	}

	query := fmt.Sprintf(`
	SELECT
		c.CurrAccTypeCode,
		c.CurrAccCode,
		CariDetay = ISNULL(d.CurrAccDescription, ''),
		CariTip = CASE WHEN c.CurrAccTypeCode = 1 THEN N'Satıcı' ELSE N'Müşteri' END,
		KANAL_1 = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt08Desc ELSE cad.CustomerAtt08Desc END, ''),
		PIYASA = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt01Desc ELSE cad.CustomerAtt01Desc END, ''),
		CARI_TEMSILCI = ISNULL(
			CASE
				WHEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END, '') = ''
				THEN ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN va.VendorAtt09 ELSE ca.CustomerAtt09 END, '')
				ELSE CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt02Desc ELSE cad.CustomerAtt02Desc END
			END,
		''),
		ULKE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt05Desc ELSE cad.CustomerAtt05Desc END, ''),
		IL = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt06Desc ELSE cad.CustomerAtt06Desc END, ''),
		ILCE = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt07Desc ELSE cad.CustomerAtt07Desc END, ''),
		TC = ISNULL(c.IdentityNum, ''),
		Risk_Durumu = ISNULL(CASE WHEN c.CurrAccTypeCode=1 THEN vad.VendorAtt03Desc ELSE cad.CustomerAtt03Desc END, '')
	FROM cdCurrAcc c
	LEFT JOIN cdCurrAccDesc d
		ON d.CurrAccTypeCode = c.CurrAccTypeCode
	   AND d.CurrAccCode = c.CurrAccCode
	   AND d.LangCode = 'TR'
	LEFT JOIN VendorAttributes va
		ON va.CurrAccTypeCode = c.CurrAccTypeCode
	   AND va.CurrAccCode = c.CurrAccCode
	LEFT JOIN VendorAttributeDescriptions('TR') vad
		ON vad.CurrAccTypeCode = c.CurrAccTypeCode
	   AND vad.CurrAccCode = c.CurrAccCode
	LEFT JOIN CustomerAttributes ca
		ON ca.CurrAccTypeCode = c.CurrAccTypeCode
	   AND ca.CurrAccCode = c.CurrAccCode
	LEFT JOIN CustomerAttributeDescriptions('TR') cad
		ON cad.CurrAccTypeCode = c.CurrAccTypeCode
	   AND cad.CurrAccCode = c.CurrAccCode
	WHERE c.CurrAccTypeCode IN (1,3)
	  AND (%s)
	`, strings.Join(whereParts, " OR "))

	rows, err := db.MssqlDB.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("cari meta query error: %w", err)
	}
	defer rows.Close()

	out := make(map[string]cariMeta, len(lines))
	for rows.Next() {
		var t int
		var code string
		var m cariMeta
		if err := rows.Scan(
			&t,
			&code,
			&m.CariDetay,
			&m.CariTip,
			&m.Kanal1,
			&m.Piyasa,
			&m.Temsilci,
			&m.Ulke,
			&m.Il,
			&m.Ilce,
			&m.TC,
			&m.RiskDurumu,
		); err != nil {
			return nil, err
		}
		out[metaKey(t, code)] = m
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return out, nil
}

func loadGLAccountMap(ctx context.Context, lines []mkCariBakiyeLine) (map[string]string, error) {
	vendorCodes := make(map[string]struct{})
	customerCodes := make(map[string]struct{})
	companyCodes := make(map[int]struct{})

	for _, ln := range lines {
		code := strings.TrimSpace(ln.CariKodu)
		if code == "" {
			continue
		}
		companyCodes[ln.SirketKodu] = struct{}{}
		if ln.CurrAccTypeCode == 1 {
			vendorCodes[code] = struct{}{}
		} else if ln.CurrAccTypeCode == 3 {
			customerCodes[code] = struct{}{}
		}
	}

	if len(companyCodes) == 0 || (len(vendorCodes) == 0 && len(customerCodes) == 0) {
		return map[string]string{}, nil
	}

	whereParts := make([]string, 0, 2)
	if len(vendorCodes) > 0 {
		whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=1 AND CurrAccCode IN (%s))", quotedInList(vendorCodes)))
	}
	if len(customerCodes) > 0 {
		whereParts = append(whereParts, fmt.Sprintf("(CurrAccTypeCode=3 AND CurrAccCode IN (%s))", quotedInList(customerCodes)))
	}

	query := fmt.Sprintf(`
	SELECT CurrAccTypeCode, CurrAccCode, CompanyCode, GLAccCode
	FROM prCurrAccGLAccount
	WHERE PostAccTypeCode = 100
	  AND CompanyCode IN (%s)
	  AND (%s)
	`, intInList(companyCodes), strings.Join(whereParts, " OR "))

	rows, err := db.MssqlDB.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("gl account query error: %w", err)
	}
	defer rows.Close()

	out := make(map[string]string)
	for rows.Next() {
		var t int
		var code string
		var company int
		var gl sql.NullString
		if err := rows.Scan(&t, &code, &company, &gl); err != nil {
			return nil, err
		}
		out[glKey(t, code, company)] = strings.TrimSpace(gl.String)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return out, nil
}

func loadCompanyMap(ctx context.Context) (map[int]string, error) {
	rows, err := db.MssqlDB.QueryContext(ctx, `SELECT CompanyCode, CompanyName FROM cdCompany`)
	if err != nil {
		return nil, fmt.Errorf("company map query error: %w", err)
	}
	defer rows.Close()

	out := make(map[int]string)
	for rows.Next() {
		var code int
		var name sql.NullString
		if err := rows.Scan(&code, &name); err != nil {
			return nil, err
		}
		out[code] = strings.TrimSpace(name.String)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func loadNearestTryRates(ctx context.Context) (map[string]float64, error) {
	query := `
	WITH Ranked AS (
		SELECT
			CurrencyCode,
			Rate,
			rn = ROW_NUMBER() OVER (
				PARTITION BY CurrencyCode
				ORDER BY ABS(DATEDIFF(DAY, Date, GETDATE())), Date DESC
			)
		FROM AllExchangeRates
		WHERE RelationCurrencyCode = 'TRY'
		  AND ExchangeTypeCode = 6
		  AND Rate > 0
	)
	SELECT CurrencyCode, Rate
	FROM Ranked
	WHERE rn = 1
	`

	rows, err := db.MssqlDB.QueryContext(ctx, query)
	if err != nil {
		return nil, fmt.Errorf("exchange rates query error: %w", err)
	}
	defer rows.Close()

	out := map[string]float64{"TRY": 1}
	for rows.Next() {
		var code string
		var rate float64
		if err := rows.Scan(&code, &rate); err != nil {
			return nil, err
		}
		code = strings.ToUpper(strings.TrimSpace(code))
		if code != "" && rate > 0 {
			out[code] = rate
		}
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}

	return out, nil
}

func toUSD(amount float64, currency string, usdTry float64, rateMap map[string]float64) float64 {
	if usdTry <= 0 {
		return 0
	}

	switch currency {
	case "USD":
		return amount
	case "TRY":
		return amount / usdTry
	default:
		currTry := rateMap[currency]
		if currTry <= 0 {
			return 0
		}
		return (amount * currTry) / usdTry
	}
}

func deriveMasterCari(cari string) string {
	cari = strings.TrimSpace(cari)
	if cari == "" {
		return ""
	}

	base := cari
	if idx := strings.Index(base, "/"); idx > 0 {
		base = base[:idx]
	}

	base = strings.TrimSpace(base)
	if len(base) >= 8 {
		return strings.TrimSpace(base[:8])
	}

	return base
}

func buildFilters(params models.CustomerBalanceListParams) balanceFilters {
	return balanceFilters{
		cariIlkGrup: parseCSVSet(params.CariIlkGrup),
		piyasa:      parseCSVSet(params.Piyasa),
		temsilci:    parseCSVSet(params.Temsilci),
		riskDurumu:  parseCSVSet(params.RiskDurumu),
		islemTipi:   parseIslemTipiSet(params.IslemTipi),
		ulke:        parseCSVSet(params.Ulke),
		il:          parseCSVSet(params.Il),
		ilce:        parseCSVSet(params.Ilce),
	}
}

func (f balanceFilters) matchLine(islemTipi string, m cariMeta) bool {
	if !matchSet(f.islemTipi, islemTipi) {
		return false
	}
	if !matchSet(f.cariIlkGrup, m.Kanal1) {
		return false
	}
	if !matchSet(f.piyasa, m.Piyasa) {
		return false
	}
	if !matchSet(f.temsilci, m.Temsilci) {
		return false
	}
	if !matchSet(f.riskDurumu, m.RiskDurumu) {
		return false
	}
	if !matchSet(f.ulke, m.Ulke) {
		return false
	}
	if !matchSet(f.il, m.Il) {
		return false
	}
	if !matchSet(f.ilce, m.Ilce) {
		return false
	}
	return true
}

func matchSet(set map[string]struct{}, value string) bool {
	if len(set) == 0 {
		return true
	}
	trimmed := strings.TrimSpace(value)
	if trimmed == "" {
		return true
	}
	_, ok := set[trimmed]
	return ok
}

func parseCSVSet(v string) map[string]struct{} {
	out := make(map[string]struct{})
	for _, p := range strings.Split(v, ",") {
		t := strings.TrimSpace(p)
		if t == "" {
			continue
		}
		out[t] = struct{}{}
	}
	return out
}

func parseIslemTipiSet(v string) map[string]struct{} {
	raw := parseCSVSet(v)
	if len(raw) == 0 {
		return raw
	}

	out := make(map[string]struct{}, 2)
	for token := range raw {
		switch strings.ToLower(strings.TrimSpace(token)) {
		case "1_2", "prbr_1_2", "usd_1_2", "try_1_2", "tl_1_2", "usd_bakiye_1_2", "tl_bakiye_1_2":
			out["1_2"] = struct{}{}
		case "1_3", "prbr_1_3", "usd_1_3", "try_1_3", "tl_1_3", "usd_bakiye_1_3", "tl_bakiye_1_3":
			out["1_3"] = struct{}{}
		}
	}

	if len(out) == 0 {
		return raw
	}
	return out
}

func getAuthorizedPiyasaCodes(ctx context.Context) ([]string, error) {
	claims, ok := auth.GetClaimsFromContext(ctx)
	if !ok || claims == nil {
		return nil, fmt.Errorf("unauthorized: claims not found")
	}
	if claims.IsAdmin() {
		return nil, nil
	}

	rawCodes := authz.GetPiyasaCodesFromCtx(ctx)
	if len(rawCodes) == 0 {
		return []string{}, nil
	}

	unique := make(map[string]struct{}, len(rawCodes))
	out := make([]string, 0, len(rawCodes))
	for _, code := range rawCodes {
		norm := strings.ToUpper(strings.TrimSpace(code))
		if norm == "" {
			continue
		}
		if _, exists := unique[norm]; exists {
			continue
		}
		unique[norm] = struct{}{}
		out = append(out, norm)
	}
	if len(out) == 0 {
		return []string{}, nil
	}
	return out, nil
}

func buildPiyasaWhereClause(codes []string, column string) string {
	if len(codes) == 0 {
		return "1=1"
	}
	return authz.BuildINClause(column, codes)
}

func metaKey(currType int, code string) string {
	return strconv.Itoa(currType) + "|" + strings.TrimSpace(code)
}

func glKey(currType int, code string, company int) string {
	return strconv.Itoa(currType) + "|" + strings.TrimSpace(code) + "|" + strconv.Itoa(company)
}

func quotedInList(set map[string]struct{}) string {
	vals := make([]string, 0, len(set))
	for v := range set {
		esc := strings.ReplaceAll(strings.TrimSpace(v), "'", "''")
		if esc != "" {
			vals = append(vals, "'"+esc+"'")
		}
	}
	if len(vals) == 0 {
		return "''"
	}
	sort.Strings(vals)
	return strings.Join(vals, ",")
}

func intInList(set map[int]struct{}) string {
	vals := make([]int, 0, len(set))
	for v := range set {
		vals = append(vals, v)
	}
	if len(vals) == 0 {
		return "0"
	}
	sort.Ints(vals)
	parts := make([]string, 0, len(vals))
	for _, v := range vals {
		parts = append(parts, strconv.Itoa(v))
	}
	return strings.Join(parts, ",")
}

func firstNonEmpty(v ...string) string {
	for _, s := range v {
		if strings.TrimSpace(s) != "" {
			return s
		}
	}
	return ""
}

func isInvalidColumnError(err error) bool {
	if err == nil {
		return false
	}
	msg := strings.ToLower(err.Error())
	return strings.Contains(msg, "invalid column name")
}

func shouldSkipBalanceLine(ln mkCariBakiyeLine) bool {
	add12, add13 := resolveBalanceBuckets(ln)
	p := strings.TrimSpace(ln.PislemTipi)
	if p == "1_2" {
		return !add12
	}
	if p == "1_3" {
		return !add13
	}
	return false
}

func resolveBalanceBuckets(ln mkCariBakiyeLine) (add12 bool, add13 bool) {
	p := strings.TrimSpace(ln.PislemTipi)
	t := normalizeParasalIslemTipi(ln.ParasalIslemTipi)
	switch t {
	case "1":
		return true, true
	case "2", "1_2":
		return true, false
	case "3", "1_3":
		return false, true
	}

	// Parasal tip yoksa eski davranis: PislemTipi'ne gore ayir.
	if p == "1_2" {
		return true, false
	}
	if p == "1_3" {
		return false, true
	}
	return false, false
}

func normalizeParasalIslemTipi(v string) string {
	s := strings.TrimSpace(v)
	if s == "" {
		return ""
	}

	lower := strings.ToLower(s)
	compact := strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(lower, " ", ""), "-", "_"), "/", "_")
	if strings.Contains(compact, "1_2") {
		return "1_2"
	}
	if strings.Contains(compact, "1_3") {
		return "1_3"
	}

	// "1,2" / "1,3" gibi liste formatlarini dogrudan yakala.
	tokenized := strings.NewReplacer(" ", "", ";", ",", "|", ",", "/", ",", "-", ",", "_", ",").Replace(lower)
	parts := strings.Split(tokenized, ",")
	has1 := false
	has2 := false
	has3 := false
	for _, p := range parts {
		t := strings.TrimSpace(p)
		switch t {
		case "1":
			has1 = true
		case "2":
			has2 = true
		case "3":
			has3 = true
		}
	}
	if has1 && has2 {
		return "1_2"
	}
	if has1 && has3 {
		return "1_3"
	}
	if has2 && !has1 && !has3 {
		return "2"
	}
	if has3 && !has1 && !has2 {
		return "3"
	}
	if has1 && !has2 && !has3 {
		return "1"
	}

	// "2.00", "2,00", " 2 " gibi varyasyonlari tek tipe indir.
	s = strings.ReplaceAll(s, ",", ".")
	if n, err := strconv.ParseFloat(s, 64); err == nil {
		return strconv.Itoa(int(n))
	}

	// Metinsel geldiyse ilk rakam bloğunu al.
	start := -1
	end := -1
	for i, r := range s {
		if r >= '0' && r <= '9' {
			if start == -1 {
				start = i
			}
			end = i
			continue
		}
		if start != -1 {
			break
		}
	}
	if start == -1 || end < start {
		return s
	}
	return s[start : end+1]
}

func resolveParasalIslemSelectExpr(ctx context.Context, sampleQuery string) string {
	sampleQuery = strings.TrimSpace(sampleQuery)
	if sampleQuery == "" {
		return ""
	}

	metaQuery := `
SELECT name
FROM sys.dm_exec_describe_first_result_set(@tsql, NULL, 0)
WHERE error_number IS NULL
  AND name IS NOT NULL
`
	rows, err := db.MssqlDB.QueryContext(ctx, metaQuery, sql.Named("tsql", sampleQuery))
	if err != nil {
		return ""
	}
	defer rows.Close()

	type candidate struct {
		key  string
		expr string
	}
	priority := []candidate{
		{key: "ata tt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
		{key: "atatt01", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
		{key: "parasalislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
		{key: "parislemtipi", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
		{key: "parislemtur", expr: "CAST(%s AS varchar(16)) AS ParasalIslemTipi,"},
	}

	available := make(map[string]string)
	for rows.Next() {
		var col sql.NullString
		if err := rows.Scan(&col); err != nil {
			return ""
		}
		name := strings.TrimSpace(col.String)
		if name == "" {
			continue
		}
		normalized := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(name, "_", ""), " ", ""))
		available[normalized] = name
	}
	if err := rows.Err(); err != nil {
		return ""
	}

	for _, c := range priority {
		key := strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(c.key, "_", ""), " ", ""))
		if col, ok := available[key]; ok {
			return fmt.Sprintf(c.expr, quoteSQLIdent(col))
		}
	}

	return ""
}

func quoteSQLIdent(ident string) string {
	return "[" + strings.ReplaceAll(strings.TrimSpace(ident), "]", "]]") + "]"
}
