package queries

import (
	"bssapp-backend/models"
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"math"
	"strings"
)

type FxDeltaPublishStats struct {
	RateDate string

	Queued   int
	Updated  int // sdprc rows updated/inserted
	Skipped  int // missing anchor or rule
	Failures int
}

type sdprcPublishRow struct {
	ProductCode string  `json:"product_code"`
	Currency    string  `json:"currency"`
	LevelNo     int     `json:"level_no"`
	Price       float64 `json:"price"`
}

func round2fx(v float64) float64 {
	if math.IsNaN(v) || math.IsInf(v, 0) {
		return 0
	}
	return math.Round(v*100) / 100
}

func roundDerivedWithRule(rule *PricingRuleRow, currency string, level int, raw float64) float64 {
	currency = strings.ToUpper(strings.TrimSpace(currency))
	if level < 1 || level > 6 {
		return 0
	}
	if rule == nil {
		// Fallback: keep a stable 2-decimal behavior when no rule exists.
		return round2fx(raw)
	}

	whStep := 0.0
	rtStep := 0.0
	rtMode := ""
	switch currency {
	case "TRY":
		whStep, rtStep, rtMode = rule.TryWholesaleStep, rule.TryRetailStep, rule.TryRetailMode
	case "USD":
		whStep, rtStep, rtMode = rule.UsdWholesaleStep, rule.UsdRetailStep, rule.UsdRetailMode
	case "EUR":
		whStep, rtStep, rtMode = rule.EurWholesaleStep, rule.EurRetailStep, rule.EurRetailMode
	default:
		return 0
	}

	// In our model: level 1-5 = wholesale rounding, level 6 = retail rounding.
	if level >= 6 {
		return applyRetailRounding(raw, whStep, rtStep, rtMode)
	}
	return roundUpStep(raw, whStep)
}

// PublishDerivedPricesFromAnchor recalculates derived currency tiers from the stored anchor tiers in sdprc.
// Rule selection determines anchor_mode (USD/TRY). Anchor tiers are never modified here.
func PublishDerivedPricesFromAnchor(ctx context.Context, pg *sql.DB, productCodes []string, rateDate string, forceFxRefresh bool) (int, int, error) {
	if len(productCodes) == 0 {
		return 0, 0, nil
	}
	rateRow, err := resolvePricingFxRateByDate(ctx, pg, rateDate, forceFxRefresh, true)
	if err != nil {
		return 0, 0, err
	}
	if rateRow.UsdTry <= 0 || rateRow.EurTry <= 0 || rateRow.UsdEur <= 0 {
		return 0, 0, fmt.Errorf("invalid fx rates for date %s", rateRow.RateDate)
	}

	// Load rule map once.
	ruleRows, err := ListPricingParameterRules(ctx, pg, PricingRuleOptionFilters{})
	if err != nil {
		return 0, 0, err
	}
	rulesByScope := make(map[string]PricingParameterRuleRow, len(ruleRows))
	for _, item := range ruleRows {
		rulesByScope[item.ScopeKey] = item
	}

	// Fetch product metadata (scope) from MSSQL.
	products, err := GetAllProductPricingRows(ctx, 1000, ProductPricingFilters{ProductCode: productCodes}, "productCode", false)
	if err != nil {
		return 0, 0, err
	}
	byCode := make(map[string]models.ProductPricing, len(products))
	for _, p := range products {
		code := strings.TrimSpace(p.ProductCode)
		if code == "" {
			continue
		}
		byCode[code] = p
	}

	derivedTargets := make([]sdprcPublishRow, 0, len(productCodes)*12) // derived: 2 currencies * 6 levels
	skipped := 0

	for _, codeRaw := range productCodes {
		code := strings.TrimSpace(codeRaw)
		if code == "" {
			continue
		}
		product, ok := byCode[code]
		if !ok {
			skipped++
			continue
		}

		scopeKey := pricingParameterScopeKey(pricingParameterRow{
			AskiliYan:     strings.TrimSpace(product.AskiliYan),
			Kategori:      strings.TrimSpace(product.Kategori),
			UrunIlkGrubu:  strings.TrimSpace(product.UrunIlkGrubu),
			UrunAnaGrubu:  strings.TrimSpace(product.UrunAnaGrubu),
			UrunAltGrubu:  strings.TrimSpace(product.UrunAltGrubu),
			Icerik:        strings.TrimSpace(product.Icerik),
			Marka:         strings.TrimSpace(product.Marka),
			BrandCode:     strings.TrimSpace(product.BrandCode),
			BrandGroupSec: strings.TrimSpace(product.BrandGroupSec),
		})
		ruleItem, ok := rulesByScope[scopeKey]
		if !ok || ruleItem.Rule == nil || !ruleItem.Rule.IsActive || !ruleItem.Rule.CalcEnabled {
			skipped++
			continue
		}
		anchorMode := strings.ToUpper(strings.TrimSpace(ruleItem.Rule.AnchorMode))
		if anchorMode != "USD" && anchorMode != "TRY" {
			anchorMode = "USD"
		}

		anchor, ok, err := loadLatestSdprcTiers(ctx, pg, code, anchorMode)
		if err != nil || !ok {
			skipped++
			continue
		}

		switch anchorMode {
		case "USD":
			for i := 0; i < 6; i++ {
				level := i + 1
				usd := anchor[i]
				tryV := roundDerivedWithRule(ruleItem.Rule, "TRY", level, usd*rateRow.UsdTry)
				eurV := roundDerivedWithRule(ruleItem.Rule, "EUR", level, usd*rateRow.UsdEur)
				if tryV > 0 {
					derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "TRY", LevelNo: level, Price: tryV})
				}
				if eurV > 0 {
					derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "EUR", LevelNo: level, Price: eurV})
				}
			}
		default: // TRY
			for i := 0; i < 6; i++ {
				level := i + 1
				tryV := anchor[i]
				usd := roundDerivedWithRule(ruleItem.Rule, "USD", level, tryV/rateRow.UsdTry)
				eurV := roundDerivedWithRule(ruleItem.Rule, "EUR", level, tryV/rateRow.EurTry)
				if usd > 0 {
					derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "USD", LevelNo: level, Price: usd})
				}
				if eurV > 0 {
					derivedTargets = append(derivedTargets, sdprcPublishRow{ProductCode: code, Currency: "EUR", LevelNo: level, Price: eurV})
				}
			}
		}
	}

	if len(derivedTargets) == 0 {
		return 0, skipped, nil
	}
	written, err := bulkUpsertSdprcDerived(ctx, pg, derivedTargets)
	return written, skipped, err
}

func loadLatestSdprcTiers(ctx context.Context, pg *sql.DB, productCode string, currency string) ([6]float64, bool, error) {
	var out [6]float64
	productCode = strings.TrimSpace(productCode)
	currency = strings.ToUpper(strings.TrimSpace(currency))
	if productCode == "" {
		return out, false, nil
	}
	if currency != "USD" && currency != "TRY" {
		return out, false, nil
	}

	rows, err := pg.QueryContext(ctx, `
WITH latest AS (
  SELECT DISTINCT ON (sdprc.sdprcgrp_id)
    sdprc.sdprcgrp_id AS grp,
    COALESCE(sdprc.prc, 0)::float8 AS prc
  FROM sdprc
  JOIN mmitem ON mmitem.id = sdprc.mmitem_id
  WHERE mmitem.code = $1
    AND sdprc.crn = $2
    AND sdprc.sdprcgrp_id BETWEEN 1 AND 6
    AND sdprc.prc IS NOT NULL
    AND sdprc.prc > 0
  ORDER BY sdprc.sdprcgrp_id, sdprc.zlins_dttm DESC
)
SELECT grp, prc FROM latest ORDER BY grp;
`, productCode, currency)
	if err != nil {
		return out, false, err
	}
	defer rows.Close()

	found := 0
	for rows.Next() {
		var grp int
		var prc float64
		if err := rows.Scan(&grp, &prc); err != nil {
			return out, false, err
		}
		if grp >= 1 && grp <= 6 && prc > 0 {
			out[grp-1] = prc
			found++
		}
	}
	if err := rows.Err(); err != nil {
		return out, false, err
	}
	return out, found == 6, nil
}

func bulkUpsertSdprcDerived(ctx context.Context, pg *sql.DB, targets []sdprcPublishRow) (int, error) {
	raw, err := json.Marshal(targets)
	if err != nil {
		return 0, err
	}
	tx, err := pg.BeginTx(ctx, nil)
	if err != nil {
		return 0, err
	}
	defer tx.Rollback()

	if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(2003, 1)`); err != nil {
		return 0, err
	}

	q := `
WITH input AS (
  SELECT *
  FROM jsonb_to_recordset($1::jsonb) AS x(product_code text, currency text, level_no int, price float8)
),
norm AS (
  SELECT
    NULLIF(BTRIM(product_code), '') AS product_code,
    UPPER(NULLIF(BTRIM(currency), '')) AS currency,
    COALESCE(level_no, 0) AS level_no,
    COALESCE(price, 0) AS price
  FROM input
),
  -- Only PG's authoritative variant dimension table may drive delta writes.
  -- Do not union sdprc/cache dimensions; stale rows there can re-create wrong variant keys.
  dims AS (
    SELECT
      norm.product_code AS product_code,
      md.val1::bigint AS dim1,
      CASE
        WHEN md.val3 IS NULL OR md.val3 = 0 THEN NULL
        ELSE md.val3::bigint
      END AS dim3
    FROM norm
    JOIN mmitem mm
      ON mm.code = norm.product_code
    JOIN mmitem_dim md
      ON md.mmitem_id = mm.id
     AND COALESCE(md.is_active, TRUE) = TRUE
    WHERE md.val1 IS NOT NULL
      AND md.val1 > 0
    GROUP BY norm.product_code, md.val1, md.val3
  ),
mapped AS (
  SELECT
    mm.id AS mmitem_id,
    m.sdprcgrp_id AS sdprcgrp_id,
    norm.currency AS crn,
    d.dim1 AS dim1,
    d.dim3 AS dim3,
    norm.price AS prc
  FROM norm
  JOIN dims d
    ON d.product_code = norm.product_code
  JOIN mk_price_target_map_pg m
    ON m.is_active = TRUE
   AND m.currency = norm.currency
   AND m.level_no = norm.level_no
  JOIN mmitem mm
    ON mm.code = norm.product_code
  WHERE norm.product_code IS NOT NULL
    AND norm.currency IN ('USD','EUR','TRY')
    AND norm.level_no BETWEEN 1 AND 6
    AND norm.price > 0
    AND m.sdprcgrp_id IS NOT NULL
),
latest AS (
  SELECT DISTINCT ON (s.mmitem_id, s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0))
    s.id,
    s.mmitem_id,
    s.sdprcgrp_id,
    s.crn,
    s.dim1,
    s.dim3
  FROM sdprc s
  JOIN mapped m
    ON m.mmitem_id = s.mmitem_id
   AND m.sdprcgrp_id = s.sdprcgrp_id
   AND m.crn = s.crn
   AND m.dim1 = s.dim1
   AND ((m.dim3 IS NULL AND s.dim3 IS NULL) OR (m.dim3 = s.dim3))
  ORDER BY s.mmitem_id, s.sdprcgrp_id, s.crn, s.dim1, COALESCE(s.dim3, 0), s.zlins_dttm DESC
),
updated AS (
  UPDATE sdprc s
  SET prc = m.prc,
      zlins_dttm = now()
  FROM latest l
  JOIN mapped m
    ON m.mmitem_id=l.mmitem_id AND m.sdprcgrp_id=l.sdprcgrp_id AND m.crn=l.crn
   AND m.dim1 = l.dim1 AND ((m.dim3 IS NULL AND l.dim3 IS NULL) OR (m.dim3 = l.dim3))
  WHERE s.id = l.id
    AND s.prc IS DISTINCT FROM m.prc
  RETURNING 1
),
inserted AS (
  INSERT INTO sdprc (mmitem_id, sdprcgrp_id, crn, dim1, dim3, prc, zlins_dttm)
  SELECT m.mmitem_id, m.sdprcgrp_id, m.crn, m.dim1, m.dim3, m.prc, now()
  FROM mapped m
  LEFT JOIN latest l
    ON l.mmitem_id=m.mmitem_id AND l.sdprcgrp_id=m.sdprcgrp_id AND l.crn=m.crn
   AND l.dim1 = m.dim1 AND ((l.dim3 IS NULL AND m.dim3 IS NULL) OR (l.dim3 = m.dim3))
  WHERE l.id IS NULL
  RETURNING 1
)
SELECT (SELECT COUNT(*) FROM updated)::int + (SELECT COUNT(*) FROM inserted)::int;
`
	var written int
	if err := tx.QueryRowContext(ctx, q, raw).Scan(&written); err != nil {
		return 0, err
	}
	if err := tx.Commit(); err != nil {
		return 0, err
	}
	return written, nil
}
