package pricing

import (
	"context"
	"errors"
	"fmt"
	"strings"

	"github.com/shopspring/decimal"

	"github.com/TaruLab/keluargamart-backend/internal/db"
)

var (
	ErrProductNotFound    = errors.New("product not found")
	ErrProductInactive    = errors.New("product is not active")
	ErrUnitNotFound       = errors.New("product unit not found")
	ErrUnitNotForProduct  = errors.New("product_unit does not belong to this product")
	ErrUnitInactive       = errors.New("product unit is not active")
	ErrQuantityInvalid    = errors.New("quantity must be greater than 0")
	ErrBelowMinPurchase   = errors.New("quantity is below the configured minimum purchase")
	ErrPriceNotConfigured = errors.New("no authoritative price is configured for this product unit")
)

// MembershipLookup resolves the active membership tier for a customer on the
// caller's transaction. ok=false means no active membership (no benefit).
// Implementations must fail the whole resolve on lookup error.
type MembershipLookup func(ctx context.Context, q db.DBTX, customerID int64) (tier string, ok bool, err error)

func (s *Service) WithMembershipLookup(fn MembershipLookup) *Service {
	s.membershipLookup = fn
	return s
}

var tierDiscountPercent = map[string]decimal.Decimal{
	"bronze":   decimal.NewFromInt(2),
	"gold":     decimal.NewFromInt(5),
	"platinum": decimal.NewFromInt(8),
}

var ErrUnknownTier = errors.New("unknown membership tier")

// TierBenefitResolver resolves the discount percentage for a confirmed tier
// from the database-backed membership_tiers table (STEP 0, business-rules.md
// §13.1). When wired, production pricing uses it instead of the in-code
// tierDiscountPercent fallback map, so the percentage stays DB-sourced.
type TierBenefitResolver func(ctx context.Context, q db.DBTX, tier string) (percent decimal.Decimal, err error)

func (s *Service) WithTierBenefitResolver(fn TierBenefitResolver) *Service {
	s.tierResolver = fn
	return s
}

// TierDiscountPercent maps a confirmed tier to its discount percentage of the line
// subtotal: bronze 2%, gold 5%, platinum 8%. Unknown/invalid tiers fail closed.
func TierDiscountPercent(tier string) (decimal.Decimal, error) {
	p, ok := tierDiscountPercent[strings.ToLower(strings.TrimSpace(tier))]
	if !ok {
		return decimal.Zero, ErrUnknownTier
	}
	return p, nil
}

// Service resolves authoritative prices. It holds no request state.
type Service struct {
	store            Store
	membershipLookup MembershipLookup
	tierResolver     TierBenefitResolver
}

// NewService builds a resolver. membershipLookup may be nil, in which case no
// membership benefit is applied (existing behavior for callers that do not
// supply a customer).
func NewService(store Store) *Service {
	return &Service{store: store}
}

// Resolve returns the authoritative pricing for one line of intent. q is the
// caller's transaction, or nil to read through the pool.
//
// Price precedence: the configured product-unit price wins; otherwise the
// product selling price is scaled by the unit conversion factor, so a box of 12
// inherits 12x the piece price rather than silently selling at the piece price.
func (s *Service) Resolve(ctx context.Context, q db.DBTX, req Request) (*Resolved, error) {
	if req.Quantity.LessThanOrEqual(decimal.Zero) {
		return nil, ErrQuantityInvalid
	}
	product, err := s.store.PricingProduct(ctx, q, req.ProductID)
	if err != nil {
		if errors.Is(err, ErrProductNotFound) {
			return nil, ErrProductNotFound
		}
		return nil, err
	}
	if !product.IsActive {
		return nil, ErrProductInactive
	}

	unit, err := s.resolveUnit(ctx, q, product, req.ProductUnitID)
	if err != nil {
		return nil, err
	}

	out := &Resolved{
		ProductID:   product.ID,
		ProductCode: product.Code,
		ProductName: product.Name,
		Quantity:    req.Quantity,
	}

	if unit == nil {
		out.ConversionFactor = decimal.NewFromInt(1)
		out.MinPurchase = decimal.NewFromInt(1)
		out.UnitPrice = product.SellingPrice.Round(2)
		out.UnitName = product.UnitName
		out.UnitSymbol = product.UnitSymbol
		if product.UnitID.Valid {
			id := product.UnitID.Int64
			out.UnitID = &id
		}
	} else {
		out.ConversionFactor = unit.ConversionFactor
		out.MinPurchase = unit.MinPurchase
		out.UnitName = unit.UnitName
		out.UnitSymbol = unit.UnitSymbol
		puID := unit.ID
		out.ProductUnitID = &puID
		unitID := unit.UnitID
		out.UnitID = &unitID
		if unit.SellingPrice.Valid {
			out.UnitPrice = unit.SellingPrice.Decimal.Round(2)
		} else {
			out.UnitPrice = product.SellingPrice.Mul(unit.ConversionFactor).Round(2)
		}
	}

	if req.Quantity.LessThan(out.MinPurchase) {
		return nil, fmt.Errorf("%w: %s required, %s requested",
			ErrBelowMinPurchase, out.MinPurchase.String(), req.Quantity.String())
	}

	out.BaseQuantity = req.Quantity.Mul(out.ConversionFactor).Round(4)
	out.LineSubtotal = out.UnitPrice.Mul(req.Quantity).Round(2)

	// Membership benefit: a pricing stage AFTER unit-price resolution and
	// BEFORE any future promotion/voucher stage. Triggers only when a customer
	// is supplied and a resolver is wired. The line subtotal is preserved; the
	// benefit is recorded separately so the snapshot stays explicit.
	//
	// CustomerID 0 is the walk-in sentinel: the sales service maps it to the
	// seeded "Pelanggan Umum" customer for persistence, but the membership
	// benefit is a perk of a customer the cashier actually picked — a stray
	// membership row on the placeholder would otherwise silently discount
	// every walk-in sale (bug sweep R3-1). Fail-safe: skip the lookup, price
	// at list.
	out.FinalSubtotal = out.LineSubtotal
	if req.CustomerID != nil && *req.CustomerID != 0 && s.membershipLookup != nil {
		tier, ok, lerr := s.membershipLookup(ctx, q, *req.CustomerID)
		if lerr != nil {
			return nil, lerr
		}
		if ok {
			var pct decimal.Decimal
			var perr error
			if s.tierResolver != nil {
				pct, perr = s.tierResolver(ctx, q, tier)
			} else {
				pct, perr = TierDiscountPercent(tier)
			}
			if perr != nil {
				return nil, perr // unknown tier fails closed
			}
			disc := out.LineSubtotal.Mul(pct).Div(decimal.NewFromInt(100)).Round(2)
			out.MembershipTier = tier
			out.MembershipDiscountPercent = pct
			out.MembershipDiscountAmount = disc
			out.FinalSubtotal = out.LineSubtotal.Sub(disc)
		}
	}
	return out, nil
}

// ResolveAll resolves every line, failing the whole batch on the first error so
// a transaction never commits a partially priced sale.
func (s *Service) ResolveAll(ctx context.Context, q db.DBTX, reqs []Request) ([]Resolved, error) {
	out := make([]Resolved, 0, len(reqs))
	for i, req := range reqs {
		r, err := s.Resolve(ctx, q, req)
		if err != nil {
			return nil, fmt.Errorf("line %d: %w", i+1, err)
		}
		out = append(out, *r)
	}
	return out, nil
}

// resolveUnit picks the explicit unit, else the product's active base unit, and
// returns nil when the product has no configured units at all.
func (s *Service) resolveUnit(ctx context.Context, q db.DBTX, product db.GetPricingProductRow, productUnitID *int64) (*db.GetPricingProductUnitRow, error) {
	if productUnitID != nil && *productUnitID != 0 {
		unit, err := s.store.PricingProductUnit(ctx, q, *productUnitID)
		if err != nil {
			if errors.Is(err, ErrUnitNotFound) {
				return nil, ErrUnitNotFound
			}
			return nil, err
		}
		if unit.ProductID != product.ID {
			return nil, ErrUnitNotForProduct
		}
		if !unit.IsActive {
			return nil, ErrUnitInactive
		}
		return &unit, nil
	}

	base, err := s.store.PricingBaseUnit(ctx, q, product.ID)
	if err != nil {
		if errors.Is(err, ErrUnitNotFound) {
			return nil, nil
		}
		return nil, err
	}
	unit := db.GetPricingProductUnitRow(base)
	return &unit, nil
}
