package payments

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"time"

	"github.com/go-jet/jet/v2/postgres"
	"github.com/go-jet/jet/v2/qrm"
	"github.com/google/uuid"

	"scrabble/backend/internal/postgres/jet/payments/model"
	"scrabble/backend/internal/postgres/jet/payments/table"
)

// ErrProductTransacted is returned when a hard delete is attempted on a product an order or ledger
// row references — it must be archived instead, to keep the append-only ledger resolvable.
var ErrProductTransacted = errors.New("payments: product has transactions; archive instead of delete")

// adminCatalog lists products with their atoms, prices and transacted flag. includeInactive adds the
// archived products to the active ones; with it false only active products are returned.
func (s *Store) adminCatalog(ctx context.Context, includeInactive bool) ([]AdminProduct, error) {
	where := table.Product.Active.IS_TRUE()
	if includeInactive {
		where = postgres.Bool(true)
	}
	var prods []model.Product
	if err := postgres.SELECT(table.Product.AllColumns).
		FROM(table.Product).
		WHERE(where).
		ORDER_BY(table.Product.CreatedAt.ASC()).
		QueryContext(ctx, s.db, &prods); err != nil {
		return nil, fmt.Errorf("payments: admin list products: %w", err)
	}
	out := make([]AdminProduct, 0, len(prods))
	for _, p := range prods {
		atoms, prices, err := s.productComposition(ctx, p.ProductID)
		if err != nil {
			return nil, err
		}
		transacted, err := s.productTransacted(ctx, p.ProductID)
		if err != nil {
			return nil, err
		}
		out = append(out, AdminProduct{
			ID: p.ProductID, Title: p.Title, Active: p.Active,
			Atoms: atoms, Prices: prices, Transacted: transacted,
		})
	}
	return out, nil
}

// productComposition reads one product's atom lines and price rows.
func (s *Store) productComposition(ctx context.Context, id uuid.UUID) ([]AtomLine, []PriceLine, error) {
	var items []model.ProductItem
	if err := postgres.SELECT(table.ProductItem.AllColumns).
		FROM(table.ProductItem).
		WHERE(table.ProductItem.ProductID.EQ(postgres.UUID(id))).
		ORDER_BY(table.ProductItem.AtomType.ASC()).
		QueryContext(ctx, s.db, &items); err != nil {
		return nil, nil, fmt.Errorf("payments: load items %s: %w", id, err)
	}
	atoms := make([]AtomLine, len(items))
	for i, it := range items {
		atoms[i] = AtomLine{Atom: it.AtomType, Quantity: int(it.Quantity)}
	}
	var prs []model.ProductPrice
	if err := postgres.SELECT(table.ProductPrice.AllColumns).
		FROM(table.ProductPrice).
		WHERE(table.ProductPrice.ProductID.EQ(postgres.UUID(id))).
		QueryContext(ctx, s.db, &prs); err != nil {
		return nil, nil, fmt.Errorf("payments: load prices %s: %w", id, err)
	}
	prices := make([]PriceLine, len(prs))
	for i, pr := range prs {
		method := ""
		if pr.Method != nil {
			method = *pr.Method
		}
		prices[i] = PriceLine{Method: method, Currency: Currency(pr.Currency), Amount: pr.Amount}
	}
	return atoms, prices, nil
}

// productTransacted reports whether any order or ledger row references the product.
func (s *Store) productTransacted(ctx context.Context, id uuid.UUID) (bool, error) {
	var yes bool
	if err := s.db.QueryRowContext(ctx,
		`SELECT EXISTS(SELECT 1 FROM payments.orders WHERE product_id=$1)
		     OR EXISTS(SELECT 1 FROM payments.ledger WHERE product_id=$1)`, id).Scan(&yes); err != nil {
		return false, fmt.Errorf("payments: product transacted %s: %w", id, err)
	}
	return yes, nil
}

// productActive reads a product's archived flag, ErrProductNotFound when it is missing.
func (s *Store) productActive(ctx context.Context, id uuid.UUID) (bool, error) {
	var p model.Product
	err := postgres.SELECT(table.Product.Active).FROM(table.Product).
		WHERE(table.Product.ProductID.EQ(postgres.UUID(id))).LIMIT(1).
		QueryContext(ctx, s.db, &p)
	if errors.Is(err, qrm.ErrNoRows) {
		return false, ErrProductNotFound
	}
	if err != nil {
		return false, fmt.Errorf("payments: product active %s: %w", id, err)
	}
	return p.Active, nil
}

// productInput reads a product's current editable content (title, atoms, prices).
func (s *Store) productInput(ctx context.Context, id uuid.UUID) (ProductInput, error) {
	var p model.Product
	err := postgres.SELECT(table.Product.AllColumns).FROM(table.Product).
		WHERE(table.Product.ProductID.EQ(postgres.UUID(id))).LIMIT(1).
		QueryContext(ctx, s.db, &p)
	if errors.Is(err, qrm.ErrNoRows) {
		return ProductInput{}, ErrProductNotFound
	}
	if err != nil {
		return ProductInput{}, fmt.Errorf("payments: product input %s: %w", id, err)
	}
	atoms, prices, err := s.productComposition(ctx, id)
	if err != nil {
		return ProductInput{}, err
	}
	return ProductInput{Title: p.Title, Atoms: atoms, Prices: prices}, nil
}

// createProduct inserts a product with its atoms and prices in one transaction, returning its id.
func (s *Store) createProduct(ctx context.Context, in ProductInput, active bool, now time.Time) (uuid.UUID, error) {
	id := uuid.New()
	if err := withTx(ctx, s.db, func(tx *sql.Tx) error {
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO payments.product (product_id, title, active, created_at, updated_at) VALUES ($1,$2,$3,$4,$4)`,
			id, in.Title, active, now); err != nil {
			return fmt.Errorf("insert product: %w", err)
		}
		return insertComposition(ctx, tx, id, in)
	}); err != nil {
		return uuid.Nil, fmt.Errorf("payments: create product: %w", err)
	}
	return id, nil
}

// updateProduct replaces a product's title, atoms and prices in one transaction (active unchanged).
func (s *Store) updateProduct(ctx context.Context, id uuid.UUID, in ProductInput, now time.Time) error {
	return withTx(ctx, s.db, func(tx *sql.Tx) error {
		res, err := tx.ExecContext(ctx,
			`UPDATE payments.product SET title=$2, updated_at=$3 WHERE product_id=$1`, id, in.Title, now)
		if err != nil {
			return fmt.Errorf("payments: update product %s: %w", id, err)
		}
		if n, _ := res.RowsAffected(); n == 0 {
			return ErrProductNotFound
		}
		if _, err := tx.ExecContext(ctx, `DELETE FROM payments.product_item WHERE product_id=$1`, id); err != nil {
			return fmt.Errorf("payments: clear items %s: %w", id, err)
		}
		if _, err := tx.ExecContext(ctx, `DELETE FROM payments.product_price WHERE product_id=$1`, id); err != nil {
			return fmt.Errorf("payments: clear prices %s: %w", id, err)
		}
		return insertComposition(ctx, tx, id, in)
	})
}

// insertComposition inserts a product's atom items and price rows inside tx (a value's CHIP price
// carries a NULL method).
func insertComposition(ctx context.Context, tx *sql.Tx, id uuid.UUID, in ProductInput) error {
	for _, a := range in.Atoms {
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO payments.product_item (product_id, atom_type, quantity) VALUES ($1,$2,$3)`,
			id, a.Atom, a.Quantity); err != nil {
			return fmt.Errorf("insert item %s: %w", a.Atom, err)
		}
	}
	for _, p := range in.Prices {
		var method any
		if p.Method != "" {
			method = p.Method
		}
		if _, err := tx.ExecContext(ctx,
			`INSERT INTO payments.product_price (product_id, method, currency, amount) VALUES ($1,$2,$3,$4)`,
			id, method, string(p.Currency), p.Amount); err != nil {
			return fmt.Errorf("insert price %s: %w", p.Currency, err)
		}
	}
	return nil
}

// setProductActive flips the archived flag.
func (s *Store) setProductActive(ctx context.Context, id uuid.UUID, active bool, now time.Time) error {
	res, err := s.db.ExecContext(ctx,
		`UPDATE payments.product SET active=$2, updated_at=$3 WHERE product_id=$1`, id, active, now)
	if err != nil {
		return fmt.Errorf("payments: set product active %s: %w", id, err)
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return ErrProductNotFound
	}
	return nil
}

// deleteProduct hard-deletes a never-transacted product (its items/prices cascade); a transacted
// product is refused with ErrProductTransacted.
func (s *Store) deleteProduct(ctx context.Context, id uuid.UUID) error {
	transacted, err := s.productTransacted(ctx, id)
	if err != nil {
		return err
	}
	if transacted {
		return ErrProductTransacted
	}
	res, err := s.db.ExecContext(ctx, `DELETE FROM payments.product WHERE product_id=$1`, id)
	if err != nil {
		return fmt.Errorf("payments: delete product %s: %w", id, err)
	}
	if n, _ := res.RowsAffected(); n == 0 {
		return ErrProductNotFound
	}
	return nil
}
