package cart

import (
	"context"
	"crypto/sha256"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/TaruLab/keluargamart-backend/internal/db"
	"github.com/jackc/pgx/v5/pgtype"
	"github.com/shopspring/decimal"
)

var (
	ErrInvalidQuantity  = errors.New("quantity must be greater than 0")
	ErrEmptyCart        = errors.New("cart is empty")
	ErrNotConfigured    = errors.New("checkout is not configured")
	ErrCheckoutConflict = errors.New("checkout key was already used for a different request")
)

// CheckoutInput for POST /cart/checkout.
type CheckoutInput struct {
	IdempotencyKey  string  `json:"-"`
	WarehouseID     int64   `json:"warehouse_id"`
	PaymentMethod   string  `json:"payment_method"`
	ShippingAddress *string `json:"shipping_address"`
	Notes           *string `json:"notes"`
	// VoucherCode is forwarded to the sales order placer (sales.CheckoutInput)
	// and redeemed under the same §2.2 rules as POS.
	VoucherCode *string `json:"voucher_code"`
}

// OrderPlacer is injected from main: it turns cart items into a confirmed SO
// (mirrors the sales service PlaceCheckoutInTx wiring pattern). It runs on the
// caller's tx (q) so order creation, stock reservation and cart clearing form
// a single transaction boundary.
type OrderPlacer func(ctx context.Context, q db.DBTX, userID int64, userName, userEmail string, in CheckoutInput, items []PlacedItem) (*PlacedOrder, error)

// PlacedItem is a checkout line derived from the cart.
type PlacedItem struct {
	ProductID int64
	Quantity  decimal.Decimal
}

// LinePricer resolves membership-adjusted pricing for the given cart lines.
// Injected from main so the cart package stays free of the pricing engine's
// dependencies. nil = feature off, and GetCart returns raw list prices only.
type LinePricer func(ctx context.Context, userID int64, items []PlacedItem) ([]PricedCartLine, error)

// PricedCartLine is one line's authoritative pricing outcome.
type PricedCartLine struct {
	ProductID          int64
	MembershipDiscount decimal.Decimal // money off this line
	Subtotal           decimal.Decimal // net line total (list × qty − discount)
}

// PlacedOrder is the checkout result handed back to the buyer.
type PlacedOrder struct {
	ID              int64             `json:"id"`
	SoNumber        string            `json:"so_number"`
	CustomerID      int64             `json:"customer_id"`
	Status          string            `json:"status"`
	OrderType       string            `json:"order_type"`
	Subtotal        decimal.Decimal   `json:"subtotal"`
	TaxAmount       decimal.Decimal   `json:"tax_amount"`
	DiscountAmount  decimal.Decimal   `json:"discount_amount"`
	ShippingCost    decimal.Decimal   `json:"shipping_cost"`
	TotalAmount     decimal.Decimal   `json:"total_amount"`
	PaidAmount      decimal.Decimal   `json:"paid_amount"`
	RemainingAmount decimal.Decimal   `json:"remaining_amount"`
	Notes           *string           `json:"notes,omitempty"`
	Items           []PlacedOrderItem `json:"items,omitempty"`
}

type PlacedOrderItem struct {
	ID                 int64           `json:"id"`
	ProductID          int64           `json:"product_id"`
	Quantity           decimal.Decimal `json:"quantity"`
	UnitPrice          decimal.Decimal `json:"unit_price"`
	MembershipDiscount decimal.Decimal `json:"membership_discount"`
	Subtotal           decimal.Decimal `json:"subtotal"`
}

type Service struct {
	repo       Store
	placer     OrderPlacer
	linePricer LinePricer
	afterPlace func(ctx context.Context, orderID int64)
}

func NewService(repo Store) *Service {
	return &Service{repo: repo}
}

// WithOrderPlacer wires the checkout pipeline (called from main).
func (s *Service) WithOrderPlacer(fn OrderPlacer) *Service {
	s.placer = fn
	return s
}

// WithAfterPlace wires a callback run after the checkout tx has committed
// (invoice + delivery order creation). It must not run inside the tx: those
// steps read the order through other connections, which cannot see the
// uncommitted row.
func (s *Service) WithAfterPlace(fn func(ctx context.Context, orderID int64)) *Service {
	s.afterPlace = fn
	return s
}

// WithLinePricer wires membership-adjusted cart pricing (called from main).
func (s *Service) WithLinePricer(fn LinePricer) *Service {
	s.linePricer = fn
	return s
}

// GetCart retrieves the user's cart with all items.
func (s *Service) GetCart(ctx context.Context, userID int64) (*CartView, error) {
	cart, err := s.repo.GetCartByUserID(ctx, userID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			// Return empty cart if none exists yet
			cart, err = s.repo.GetOrCreateCart(ctx, userID)
			if err != nil {
				return nil, err
			}
			return &CartView{
				ID:        cart.ID,
				UserID:    cart.UserID,
				Items:     []ItemView{},
				CreatedAt: ts(cart.CreatedAt),
				UpdatedAt: ts(cart.UpdatedAt),
			}, nil
		}
		return nil, err
	}
	return s.viewCart(ctx, cart)
}

func (s *Service) viewCart(ctx context.Context, cart db.Cart) (*CartView, error) {
	rows, err := s.repo.ListCartItems(ctx, cart.ID)
	if err != nil {
		return nil, err
	}
	items := make([]ItemView, 0, len(rows))
	for _, rw := range rows {
		img := ""
		if rw.Image.Valid {
			img = rw.Image.String
		}
		items = append(items, ItemView{
			ID:        rw.ID,
			CartID:    rw.CartID,
			ProductID: rw.ProductID,
			Quantity:  rw.Quantity,
			Product: &ProductInfo{
				ID:           rw.ID_2,
				Code:         rw.Code,
				Name:         rw.Name,
				SellingPrice: rw.SellingPrice,
				Image:        &img,
			},
		})
	}
	view := &CartView{
		ID:        cart.ID,
		UserID:    cart.UserID,
		Items:     items,
		CreatedAt: ts(cart.CreatedAt),
		UpdatedAt: ts(cart.UpdatedAt),
	}
	s.applyPricing(ctx, cart.UserID, view)
	return view, nil
}

// applyPricing enriches each line and the cart summary with the customer's
// membership-adjusted totals. Best-effort: if the pricer is not wired or fails,
// the cart is still returned with raw list prices (Summary stays nil) so a
// pricing hiccup never blocks the buyer from seeing their cart.
func (s *Service) applyPricing(ctx context.Context, userID int64, view *CartView) {
	if s.linePricer == nil || len(view.Items) == 0 {
		return
	}
	placed := make([]PlacedItem, 0, len(view.Items))
	for _, it := range view.Items {
		placed = append(placed, PlacedItem{ProductID: it.ProductID, Quantity: it.Quantity})
	}
	priced, err := s.linePricer(ctx, userID, placed)
	if err != nil || len(priced) == 0 {
		return
	}
	byProduct := make(map[int64]PricedCartLine, len(priced))
	for _, p := range priced {
		byProduct[p.ProductID] = p
	}
	gross, memberDisc, net := decimal.Zero, decimal.Zero, decimal.Zero
	for i := range view.Items {
		it := &view.Items[i]
		lineGross := decimal.Zero
		if it.Product != nil {
			lineGross = it.Product.SellingPrice.Mul(it.Quantity)
		}
		gross = gross.Add(lineGross)
		p, ok := byProduct[it.ProductID]
		if !ok {
			continue
		}
		md, sub := p.MembershipDiscount, p.Subtotal
		it.MembershipDiscount = &md
		it.Subtotal = &sub
		memberDisc = memberDisc.Add(md)
		net = net.Add(sub)
	}
	view.Summary = &CartSummary{
		Subtotal:           gross,
		MembershipDiscount: memberDisc,
		Total:              net,
	}
}

func ts(t pgtype.Timestamptz) string {
	if !t.Valid {
		return ""
	}
	return t.Time.Format(time.RFC3339)
}

// AddItem adds a product to the cart by SKU.
func (s *Service) AddItem(ctx context.Context, userID int64, sku string, quantity decimal.Decimal) (*CartView, error) {
	if quantity.LessThanOrEqual(decimal.Zero) {
		return nil, ErrInvalidQuantity
	}

	productID, err := s.repo.GetProductBySKU(ctx, sku)
	if err != nil {
		return nil, err
	}

	cart, err := s.repo.GetOrCreateCart(ctx, userID)
	if err != nil {
		return nil, err
	}

	_, err = s.repo.AddOrUpdateItem(ctx, cart.ID, productID, quantity)
	if err != nil {
		return nil, err
	}

	return s.GetCart(ctx, userID)
}

// UpdateQuantity updates the quantity of a cart item.
func (s *Service) UpdateQuantity(ctx context.Context, userID int64, productID int64, quantity decimal.Decimal) (*CartView, error) {
	if quantity.LessThanOrEqual(decimal.Zero) {
		return nil, ErrInvalidQuantity
	}

	cart, err := s.repo.GetCartByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}

	err = s.repo.UpdateItemQuantity(ctx, cart.ID, productID, quantity)
	if err != nil {
		return nil, err
	}

	return s.GetCart(ctx, userID)
}

// RemoveItem removes a product from the cart.
func (s *Service) RemoveItem(ctx context.Context, userID int64, productID int64) (*CartView, error) {
	cart, err := s.repo.GetCartByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}

	err = s.repo.RemoveItem(ctx, cart.ID, productID)
	if err != nil {
		return nil, err
	}

	return s.GetCart(ctx, userID)
}

// ClearCart removes all items from the user's cart.
func (s *Service) ClearCart(ctx context.Context, userID int64) error {
	cart, err := s.repo.GetCartByUserID(ctx, userID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return nil // Nothing to clear
		}
		return err
	}

	return s.repo.ClearCart(ctx, cart.ID)
}

// Checkout converts the user's cart into a confirmed sales order via the
// injected placer, then clears the cart. Order creation, stock reservation and
// cart clearing all run inside one transaction boundary (§17.7): the placer
// receives the same tx the cart service uses to clear items, so a failure at
// any point rolls everything back.
func (s *Service) Checkout(ctx context.Context, userID int64, userName, userEmail string, in CheckoutInput) (*PlacedOrder, error) {
	if s.placer == nil {
		return nil, ErrNotConfigured
	}
	tx, err := s.repo.Begin(ctx)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback(ctx)

	cart, err := s.repo.LockCartTx(ctx, tx, userID)
	if err != nil {
		if errors.Is(err, ErrNotFound) {
			return nil, ErrEmptyCart
		}
		return nil, err
	}
	fingerprintBytes, err := json.Marshal(in)
	if err != nil {
		return nil, err
	}
	fingerprint := fmt.Sprintf("%x", sha256.Sum256(fingerprintBytes))
	if in.IdempotencyKey != "" {
		previous, err := s.repo.CheckoutReceiptTx(ctx, tx, userID, in.IdempotencyKey, fingerprint)
		if err != nil {
			return nil, err
		}
		if previous != nil {
			return previous, nil
		}
	}
	rows, err := s.repo.ListCartItemsTx(ctx, tx, cart.ID)
	if err != nil {
		return nil, err
	}
	if len(rows) == 0 {
		return nil, ErrEmptyCart
	}
	items := make([]PlacedItem, 0, len(rows))
	for _, rw := range rows {
		items = append(items, PlacedItem{ProductID: rw.ProductID, Quantity: rw.Quantity})
	}
	order, err := s.placer(ctx, tx, userID, userName, userEmail, in, items)
	if err != nil {
		return nil, err
	}
	if err := s.repo.ClearCartTx(ctx, tx, cart.ID); err != nil {
		return nil, err
	}
	if in.IdempotencyKey != "" {
		if err := s.repo.SaveCheckoutReceiptTx(ctx, tx, userID, in.IdempotencyKey, fingerprint, order); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(ctx); err != nil {
		return nil, err
	}
	if s.afterPlace != nil {
		s.afterPlace(ctx, order.ID)
	}
	return order, nil
}
