package cart

import (
	"context"
	"encoding/json"
	"errors"

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

var (
	ErrNotFound        = errors.New("cart not found")
	ErrItemNotFound    = errors.New("cart item not found")
	ErrProductNotFound = errors.New("product not found")
)

type Repository struct {
	pool *pgxpool.Pool
	q    *db.Queries
}

func NewRepository(pool *pgxpool.Pool) *Repository {
	return &Repository{pool: pool, q: db.New(pool)}
}

// GetOrCreateCart retrieves or creates a cart for the given user.
func (r *Repository) GetOrCreateCart(ctx context.Context, userID int64) (db.Cart, error) {
	return r.q.GetOrCreateCart(ctx, userID)
}

// GetCartByUserID retrieves a cart row for the user.
func (r *Repository) GetCartByUserID(ctx context.Context, userID int64) (db.Cart, error) {
	c, err := r.q.GetCartByUserID(ctx, userID)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return db.Cart{}, ErrNotFound
		}
		return db.Cart{}, err
	}
	return c, nil
}

func (r *Repository) LockCartTx(ctx context.Context, q db.DBTX, userID int64) (db.Cart, error) {
	var c db.Cart
	err := q.QueryRow(ctx, `SELECT id, user_id, created_at, updated_at FROM carts WHERE user_id=$1 FOR UPDATE`, userID).Scan(&c.ID, &c.UserID, &c.CreatedAt, &c.UpdatedAt)
	if errors.Is(err, pgx.ErrNoRows) {
		return c, ErrNotFound
	}
	return c, err
}

func (r *Repository) CheckoutReceiptTx(ctx context.Context, q db.DBTX, userID int64, key, fingerprint string) (*PlacedOrder, error) {
	var stored string
	var body []byte
	err := q.QueryRow(ctx, `SELECT fingerprint, response FROM checkout_receipts WHERE user_id=$1 AND idempotency_key=$2`, userID, key).Scan(&stored, &body)
	if errors.Is(err, pgx.ErrNoRows) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	if stored != fingerprint {
		return nil, ErrCheckoutConflict
	}
	var order PlacedOrder
	if err := json.Unmarshal(body, &order); err != nil {
		return nil, err
	}
	return &order, nil
}

func (r *Repository) SaveCheckoutReceiptTx(ctx context.Context, q db.DBTX, userID int64, key, fingerprint string, order *PlacedOrder) error {
	body, err := json.Marshal(order)
	if err != nil {
		return err
	}
	_, err = q.Exec(ctx, `INSERT INTO checkout_receipts (user_id, idempotency_key, fingerprint, sales_order_id, response) VALUES ($1,$2,$3,$4,$5)`, userID, key, fingerprint, order.ID, body)
	return err
}

// ListCartItems lists cart lines with product info, ordered by insertion.
func (r *Repository) ListCartItems(ctx context.Context, cartID int64) ([]db.ListCartItemsByCartIDRow, error) {
	return r.q.ListCartItemsByCartID(ctx, cartID)
}

// ListCartItemsTx reads cart lines inside the caller's tx (used by checkout so
// the read, order placement and cart clear share one transaction).
func (r *Repository) ListCartItemsTx(ctx context.Context, q db.DBTX, cartID int64) ([]db.ListCartItemsByCartIDRow, error) {
	return db.New(q).ListCartItemsByCartID(ctx, cartID)
}

// ClearCartTx removes all items from the cart inside the caller's tx.
func (r *Repository) ClearCartTx(ctx context.Context, q db.DBTX, cartID int64) error {
	return db.New(q).ClearCartItems(ctx, cartID)
}

// Begin opens a physical transaction (checkout boundary).
func (r *Repository) Begin(ctx context.Context) (Tx, error) {
	tx, err := r.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	return tx, nil
}

func (r *Repository) beginCartMutation(ctx context.Context, cartID int64) (pgx.Tx, error) {
	tx, err := r.pool.Begin(ctx)
	if err != nil {
		return nil, err
	}
	if _, err := tx.Exec(ctx, `SELECT id FROM carts WHERE id=$1 FOR UPDATE`, cartID); err != nil {
		_ = tx.Rollback(ctx)
		return nil, err
	}
	return tx, nil
}

// AddOrUpdateItem adds a product to the cart or updates quantity if it exists.
func (r *Repository) AddOrUpdateItem(ctx context.Context, cartID, productID int64, quantity decimal.Decimal) (db.CartItem, error) {
	tx, err := r.beginCartMutation(ctx, cartID)
	if err != nil {
		return db.CartItem{}, err
	}
	defer tx.Rollback(ctx)
	exists, err := db.New(tx).ProductExistsByID(ctx, productID)
	if err != nil {
		return db.CartItem{}, err
	}
	if !exists {
		return db.CartItem{}, ErrProductNotFound
	}
	item, err := db.New(tx).AddOrUpdateCartItem(ctx, db.AddOrUpdateCartItemParams{
		CartID: cartID, ProductID: productID, Quantity: quantity,
	})
	if err != nil {
		return db.CartItem{}, err
	}
	if err := tx.Commit(ctx); err != nil {
		return db.CartItem{}, err
	}
	return item, nil
}

// UpdateItemQuantity sets the quantity for an existing cart item. Zero rows
// affected means the (cart_id, product_id) pair does not exist — answering
// success there would lie to the caller (R2-4).
func (r *Repository) UpdateItemQuantity(ctx context.Context, cartID, productID int64, quantity decimal.Decimal) error {
	tx, err := r.beginCartMutation(ctx, cartID)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)
	n, err := db.New(tx).UpdateCartItemQuantity(ctx, db.UpdateCartItemQuantityParams{
		CartID:    cartID,
		ProductID: productID,
		Quantity:  quantity,
	})
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrItemNotFound
	}
	return tx.Commit(ctx)
}

// RemoveItem removes a product from the cart. Zero rows affected means the
// pair does not exist (R2-4).
func (r *Repository) RemoveItem(ctx context.Context, cartID, productID int64) error {
	tx, err := r.beginCartMutation(ctx, cartID)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)
	n, err := db.New(tx).DeleteCartItem(ctx, db.DeleteCartItemParams{
		CartID:    cartID,
		ProductID: productID,
	})
	if err != nil {
		return err
	}
	if n == 0 {
		return ErrItemNotFound
	}
	return tx.Commit(ctx)
}

// ClearCart removes all items from the cart.
func (r *Repository) ClearCart(ctx context.Context, cartID int64) error {
	tx, err := r.beginCartMutation(ctx, cartID)
	if err != nil {
		return err
	}
	defer tx.Rollback(ctx)
	if err := db.New(tx).ClearCartItems(ctx, cartID); err != nil {
		return err
	}
	return tx.Commit(ctx)
}

// GetProductBySKU retrieves a product by its code (SKU).
func (r *Repository) GetProductBySKU(ctx context.Context, sku string) (int64, error) {
	id, err := r.q.ProductIDBySKU(ctx, sku)
	if err != nil {
		if errors.Is(err, pgx.ErrNoRows) {
			return 0, ErrProductNotFound
		}
		return 0, err
	}
	return id, nil
}
