package orchestrator

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	"github.com/tech/sendico/pkg/mutil/mzap"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
	"google.golang.org/protobuf/proto"
)

type quotePaymentCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

var (
	errIdempotencyRequired      = errors.New("idempotency key is required")
	errPreviewWithIdempotency   = errors.New("preview requests must not use idempotency key")
	errIdempotencyParamMismatch = errors.New("idempotency key reuse with different parameters")
)

type quoteCtx struct {
	orgID          string
	orgRef         bson.ObjectID
	intent         *orchestratorv1.PaymentIntent
	previewOnly    bool
	idempotencyKey string
	hash           string
}

func (h *quotePaymentCommand) Execute(
	ctx context.Context,
	req *orchestratorv1.QuotePaymentRequest,
) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] {

	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}

	qc, err := h.prepareQuoteCtx(req)
	if err != nil {
		return h.mapQuoteErr(err)
	}

	quotesStore, err := ensureQuotesStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	quoteProto, err := h.quotePayment(ctx, quotesStore, qc, req)
	if err != nil {
		return h.mapQuoteErr(err)
	}

	return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{
		IdempotencyKey: req.GetIdempotencyKey(),
		Quote:          quoteProto,
	})
}

func (h *quotePaymentCommand) prepareQuoteCtx(req *orchestratorv1.QuotePaymentRequest) (*quoteCtx, error) {
	orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
	if err != nil {
		return nil, err
	}
	if err := requireNonNilIntent(req.GetIntent()); err != nil {
		return nil, err
	}

	intent := req.GetIntent()
	preview := req.GetPreviewOnly()
	idem := strings.TrimSpace(req.GetIdempotencyKey())

	if preview && idem != "" {
		return nil, errPreviewWithIdempotency
	}
	if !preview && idem == "" {
		return nil, errIdempotencyRequired
	}

	return &quoteCtx{
		orgID:          orgRef,
		orgRef:         orgID,
		intent:         intent,
		previewOnly:    preview,
		idempotencyKey: idem,
		hash:           hashQuoteRequest(req),
	}, nil
}

func (h *quotePaymentCommand) quotePayment(
	ctx context.Context,
	quotesStore storage.QuotesStore,
	qc *quoteCtx,
	req *orchestratorv1.QuotePaymentRequest,
) (*orchestratorv1.PaymentQuote, error) {

	if qc.previewOnly {
		quote, _, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
		if err != nil {
			h.logger.Warn("Failed to build preview payment quote", zap.Error(err), zap.String("org_ref", qc.orgID))
			return nil, err
		}
		quote.QuoteRef = bson.NewObjectID().Hex()
		return quote, nil
	}

	existing, err := quotesStore.GetByIdempotencyKey(ctx, qc.orgRef, qc.idempotencyKey)
	if err != nil && !errors.Is(err, storage.ErrQuoteNotFound) && !errors.Is(err, merrors.ErrNoData) {
		h.logger.Warn("Failed to lookup quote by idempotency key", zap.Error(err),
			mzap.ObjRef("org_ref", qc.orgRef), zap.String("idempotency_key", qc.idempotencyKey),
		)
		return nil, err
	}
	if existing != nil {
		if existing.Hash != qc.hash {
			return nil, errIdempotencyParamMismatch
		}
		h.logger.Debug(
			"Idempotent quote reused",
			mzap.ObjRef("org_ref", qc.orgRef),
			zap.String("idempotency_key", qc.idempotencyKey),
			zap.String("quote_ref", existing.QuoteRef),
		)
		return modelQuoteToProto(existing.Quote), nil
	}

	quote, expiresAt, err := h.engine.BuildPaymentQuote(ctx, qc.orgID, req)
	if err != nil {
		h.logger.Warn(
			"Failed to build payment quote",
			zap.Error(err),
			mzap.ObjRef("org_ref", qc.orgRef),
			zap.String("idempotency_key", qc.idempotencyKey),
		)
		return nil, err
	}

	quoteRef := bson.NewObjectID().Hex()
	quote.QuoteRef = quoteRef

	record := &model.PaymentQuoteRecord{
		QuoteRef:       quoteRef,
		IdempotencyKey: qc.idempotencyKey,
		Hash:           qc.hash,
		Intent:         intentFromProto(qc.intent),
		Quote:          quoteSnapshotToModel(quote),
		ExpiresAt:      expiresAt,
	}
	record.SetID(bson.NewObjectID())
	record.SetOrganizationRef(qc.orgRef)

	if err := quotesStore.Create(ctx, record); err != nil {
		if errors.Is(err, storage.ErrDuplicateQuote) {
			existing, getErr := quotesStore.GetByIdempotencyKey(ctx, qc.orgRef, qc.idempotencyKey)
			if getErr == nil && existing != nil {
				if existing.Hash != qc.hash {
					return nil, errIdempotencyParamMismatch
				}
				return modelQuoteToProto(existing.Quote), nil
			}
		}
		return nil, err
	}

	h.logger.Info(
		"Stored payment quote",
		zap.String("quote_ref", quoteRef),
		mzap.ObjRef("org_ref", qc.orgRef),
		zap.String("idempotency_key", qc.idempotencyKey),
		zap.String("kind", qc.intent.GetKind().String()),
	)

	return quote, nil
}

func (h *quotePaymentCommand) mapQuoteErr(err error) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] {
	if errors.Is(err, errIdempotencyRequired) ||
		errors.Is(err, errPreviewWithIdempotency) ||
		errors.Is(err, errIdempotencyParamMismatch) {
		return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
}

// TODO: temprorarary hashing function, replace with a proper solution later
func hashQuoteRequest(req *orchestratorv1.QuotePaymentRequest) string {
	cloned := proto.Clone(req).(*orchestratorv1.QuotePaymentRequest)
	cloned.Meta = nil
	cloned.IdempotencyKey = ""
	cloned.PreviewOnly = false

	b, err := proto.MarshalOptions{Deterministic: true}.Marshal(cloned)
	if err != nil {
		sum := sha256.Sum256([]byte("marshal_error"))
		return hex.EncodeToString(sum[:])
	}

	sum := sha256.Sum256(b)
	return hex.EncodeToString(sum[:])
}

type quotePaymentsCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

var (
	errBatchIdempotencyRequired      = errors.New("idempotency key is required")
	errBatchPreviewWithIdempotency   = errors.New("preview requests must not use idempotency key")
	errBatchIdempotencyParamMismatch = errors.New("idempotency key reuse with different parameters")
	errBatchIdempotencyShapeMismatch = errors.New("idempotency key already used for a different quote shape")
)

type quotePaymentsCtx struct {
	orgID          string
	orgRef         bson.ObjectID
	previewOnly    bool
	idempotencyKey string
	hash           string
	intentCount    int
}

func (h *quotePaymentsCommand) Execute(
	ctx context.Context,
	req *orchestratorv1.QuotePaymentsRequest,
) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {

	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}

	qc, intents, err := h.prepare(req)
	if err != nil {
		return h.mapErr(err)
	}

	quotesStore, err := ensureQuotesStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if qc.previewOnly {
		quotes, expires, err := h.buildQuotes(ctx, req.GetMeta(), qc.idempotencyKey, intents, true)
		if err != nil {
			return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
		aggregate, expiresAt, err := h.aggregate(quotes, expires)
		if err != nil {
			return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
		_ = expiresAt
		return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
			QuoteRef:  "",
			Aggregate: aggregate,
			Quotes:    quotes,
		})
	}

	if rec, ok, err := h.tryReuse(ctx, quotesStore, qc); err != nil {
		return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	} else if ok {
		return gsresponse.Success(h.responseFromRecord(rec))
	}

	quotes, expires, err := h.buildQuotes(ctx, req.GetMeta(), qc.idempotencyKey, intents, false)
	if err != nil {
		return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	aggregate, expiresAt, err := h.aggregate(quotes, expires)
	if err != nil {
		return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	quoteRef := bson.NewObjectID().Hex()
	for _, q := range quotes {
		if q != nil {
			q.QuoteRef = quoteRef
		}
	}

	rec, err := h.storeBatch(ctx, quotesStore, qc, quoteRef, intents, quotes, expiresAt)
	if err != nil {
		return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if rec != nil {
		return gsresponse.Success(h.responseFromRecord(rec))
	}

	h.logger.Info(
		"Stored payment quotes",
		h.logFields(qc, quoteRef, expiresAt, len(quotes))...,
	)

	return gsresponse.Success(&orchestratorv1.QuotePaymentsResponse{
		IdempotencyKey: req.GetIdempotencyKey(),
		QuoteRef:       quoteRef,
		Aggregate:      aggregate,
		Quotes:         quotes,
	})
}

func (h *quotePaymentsCommand) prepare(req *orchestratorv1.QuotePaymentsRequest) (*quotePaymentsCtx, []*orchestratorv1.PaymentIntent, error) {
	orgRefStr, orgID, err := validateMetaAndOrgRef(req.GetMeta())
	if err != nil {
		return nil, nil, err
	}

	intents := req.GetIntents()
	if len(intents) == 0 {
		return nil, nil, merrors.InvalidArgument("intents are required")
	}
	for _, intent := range intents {
		if err := requireNonNilIntent(intent); err != nil {
			return nil, nil, err
		}
	}

	preview := req.GetPreviewOnly()
	idem := strings.TrimSpace(req.GetIdempotencyKey())

	if preview && idem != "" {
		return nil, nil, errBatchPreviewWithIdempotency
	}
	if !preview && idem == "" {
		return nil, nil, errBatchIdempotencyRequired
	}

	hash, err := hashQuotePaymentsIntents(intents)
	if err != nil {
		return nil, nil, err
	}

	return &quotePaymentsCtx{
		orgID:          orgRefStr,
		orgRef:         orgID,
		previewOnly:    preview,
		idempotencyKey: idem,
		hash:           hash,
		intentCount:    len(intents),
	}, intents, nil
}

func (h *quotePaymentsCommand) tryReuse(
	ctx context.Context,
	quotesStore storage.QuotesStore,
	qc *quotePaymentsCtx,
) (*model.PaymentQuoteRecord, bool, error) {

	rec, err := quotesStore.GetByIdempotencyKey(ctx, qc.orgRef, qc.idempotencyKey)
	if err != nil {
		if errors.Is(err, storage.ErrQuoteNotFound) {
			return nil, false, nil
		}
		h.logger.Warn(
			"Failed to lookup payment quotes by idempotency key",
			h.logFields(qc, "", time.Time{}, 0)...,
		)
		return nil, false, err
	}

	if len(rec.Quotes) == 0 {
		return nil, false, errBatchIdempotencyShapeMismatch
	}
	if rec.Hash != qc.hash {
		return nil, false, errBatchIdempotencyParamMismatch
	}

	h.logger.Debug(
		"Idempotent payment quotes reused",
		h.logFields(qc, rec.QuoteRef, rec.ExpiresAt, len(rec.Quotes))...,
	)

	return rec, true, nil
}

func (h *quotePaymentsCommand) buildQuotes(
	ctx context.Context,
	meta *orchestratorv1.RequestMeta,
	baseKey string,
	intents []*orchestratorv1.PaymentIntent,
	preview bool,
) ([]*orchestratorv1.PaymentQuote, []time.Time, error) {

	quotes := make([]*orchestratorv1.PaymentQuote, 0, len(intents))
	expires := make([]time.Time, 0, len(intents))

	for i, intent := range intents {
		req := &orchestratorv1.QuotePaymentRequest{
			Meta:           meta,
			IdempotencyKey: perIntentIdempotencyKey(baseKey, i, len(intents)),
			Intent:         intent,
			PreviewOnly:    preview,
		}
		q, exp, err := h.engine.BuildPaymentQuote(ctx, meta.GetOrganizationRef(), req)
		if err != nil {
			h.logger.Warn(
				"Failed to build payment quote (batch item)",
				zap.Int("idx", i),
				zap.Error(err),
			)
			return nil, nil, err
		}
		quotes = append(quotes, q)
		expires = append(expires, exp)
	}

	return quotes, expires, nil
}

func (h *quotePaymentsCommand) aggregate(
	quotes []*orchestratorv1.PaymentQuote,
	expires []time.Time,
) (*orchestratorv1.PaymentQuoteAggregate, time.Time, error) {

	agg, err := aggregatePaymentQuotes(quotes)
	if err != nil {
		return nil, time.Time{}, merrors.InternalWrap(err, "quote aggregation failed")
	}

	expiresAt, ok := minQuoteExpiry(expires)
	if !ok {
		return nil, time.Time{}, merrors.Internal("quote expiry missing")
	}

	return agg, expiresAt, nil
}

func (h *quotePaymentsCommand) storeBatch(
	ctx context.Context,
	quotesStore storage.QuotesStore,
	qc *quotePaymentsCtx,
	quoteRef string,
	intents []*orchestratorv1.PaymentIntent,
	quotes []*orchestratorv1.PaymentQuote,
	expiresAt time.Time,
) (*model.PaymentQuoteRecord, error) {

	record := &model.PaymentQuoteRecord{
		QuoteRef:       quoteRef,
		IdempotencyKey: qc.idempotencyKey,
		Hash:           qc.hash,
		Intents:        intentsFromProto(intents),
		Quotes:         quoteSnapshotsFromProto(quotes),
		ExpiresAt:      expiresAt,
	}
	record.SetID(bson.NewObjectID())
	record.SetOrganizationRef(qc.orgRef)

	if err := quotesStore.Create(ctx, record); err != nil {
		if errors.Is(err, storage.ErrDuplicateQuote) {
			rec, ok, reuseErr := h.tryReuse(ctx, quotesStore, qc)
			if reuseErr != nil {
				return nil, reuseErr
			}
			if ok {
				return rec, nil
			}
			return nil, err
		}
		return nil, err
	}

	return nil, nil
}

func (h *quotePaymentsCommand) responseFromRecord(rec *model.PaymentQuoteRecord) *orchestratorv1.QuotePaymentsResponse {
	quotes := modelQuotesToProto(rec.Quotes)
	for _, q := range quotes {
		if q != nil {
			q.QuoteRef = rec.QuoteRef
		}
	}
	aggregate, _ := aggregatePaymentQuotes(quotes)

	return &orchestratorv1.QuotePaymentsResponse{
		QuoteRef:  rec.QuoteRef,
		Aggregate: aggregate,
		Quotes:    quotes,
	}
}

func (h *quotePaymentsCommand) logFields(qc *quotePaymentsCtx, quoteRef string, expiresAt time.Time, quoteCount int) []zap.Field {
	fields := []zap.Field{
		mzap.ObjRef("org_ref", qc.orgRef),
		zap.String("org_ref_str", qc.orgID),
		zap.String("idempotency_key", qc.idempotencyKey),
		zap.String("hash", qc.hash),
		zap.Bool("preview_only", qc.previewOnly),
		zap.Int("intent_count", qc.intentCount),
	}
	if quoteRef != "" {
		fields = append(fields, zap.String("quote_ref", quoteRef))
	}
	if !expiresAt.IsZero() {
		fields = append(fields, zap.Time("expires_at", expiresAt))
	}
	if quoteCount > 0 {
		fields = append(fields, zap.Int("quote_count", quoteCount))
	}
	return fields
}

func (h *quotePaymentsCommand) mapErr(err error) gsresponse.Responder[orchestratorv1.QuotePaymentsResponse] {
	if errors.Is(err, errBatchIdempotencyRequired) ||
		errors.Is(err, errBatchPreviewWithIdempotency) ||
		errors.Is(err, errBatchIdempotencyParamMismatch) ||
		errors.Is(err, errBatchIdempotencyShapeMismatch) {
		return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	return gsresponse.Auto[orchestratorv1.QuotePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
}

func modelQuotesToProto(snaps []*model.PaymentQuoteSnapshot) []*orchestratorv1.PaymentQuote {
	if len(snaps) == 0 {
		return nil
	}
	out := make([]*orchestratorv1.PaymentQuote, 0, len(snaps))
	for _, s := range snaps {
		out = append(out, modelQuoteToProto(s))
	}
	return out
}

func hashQuotePaymentsIntents(intents []*orchestratorv1.PaymentIntent) (string, error) {
	type item struct {
		Idx int
		H   [32]byte
	}
	items := make([]item, 0, len(intents))

	for i, intent := range intents {
		b, err := proto.MarshalOptions{Deterministic: true}.Marshal(intent)
		if err != nil {
			return "", err
		}
		items = append(items, item{Idx: i, H: sha256.Sum256(b)})
	}

	sort.Slice(items, func(i, j int) bool { return items[i].Idx < items[j].Idx })

	h := sha256.New()
	h.Write([]byte("quote-payments-fp/v1"))
	h.Write([]byte{0})
	for _, it := range items {
		h.Write(it.H[:])
		h.Write([]byte{0})
	}

	return hex.EncodeToString(h.Sum(nil)), nil
}

type initiatePaymentsCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

func (h *initiatePaymentsCommand) Execute(ctx context.Context, req *orchestratorv1.InitiatePaymentsRequest) gsresponse.Responder[orchestratorv1.InitiatePaymentsResponse] {
	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}
	_, orgRef, err := validateMetaAndOrgRef(req.GetMeta())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	idempotencyKey, err := requireIdempotencyKey(req.GetIdempotencyKey())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	quoteRef := strings.TrimSpace(req.GetQuoteRef())
	if quoteRef == "" {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("quote_ref is required"))
	}

	quotesStore, err := ensureQuotesStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	record, err := quotesStore.GetByRef(ctx, orgRef, quoteRef)
	if err != nil {
		if errors.Is(err, storage.ErrQuoteNotFound) {
			return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, "quote_not_found", merrors.InvalidArgument("quote_ref not found or expired"))
		}
		return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	intents := record.Intents
	quotes := record.Quotes
	if len(intents) == 0 && record.Intent.Kind != "" && record.Intent.Kind != model.PaymentKindUnspecified {
		intents = []model.PaymentIntent{record.Intent}
	}
	if len(quotes) == 0 && record.Quote != nil {
		quotes = []*model.PaymentQuoteSnapshot{record.Quote}
	}
	if len(intents) == 0 || len(quotes) == 0 || len(intents) != len(quotes) {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote payload is incomplete"))
	}

	store, err := ensurePaymentsStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	payments := make([]*orchestratorv1.Payment, 0, len(intents))
	for i := range intents {
		intentProto := protoIntentFromModel(intents[i])
		if err := requireNonNilIntent(intentProto); err != nil {
			return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
		quoteProto := modelQuoteToProto(quotes[i])
		if quoteProto == nil {
			return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("stored quote is empty"))
		}
		quoteProto.QuoteRef = quoteRef

		perKey := perIntentIdempotencyKey(idempotencyKey, i, len(intents))
		if existing, err := getPaymentByIdempotencyKey(ctx, store, orgRef, perKey); err == nil && existing != nil {
			payments = append(payments, toProtoPayment(existing))
			continue
		} else if err != nil && !errors.Is(err, storage.ErrPaymentNotFound) {
			return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}

		entity := newPayment(orgRef, intentProto, perKey, req.GetMetadata(), quoteProto)
		if err = store.Create(ctx, entity); err != nil {
			if errors.Is(err, storage.ErrDuplicatePayment) {
				return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists"))
			}
			return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}

		if err := h.engine.ExecutePayment(ctx, store, entity, quoteProto); err != nil {
			return gsresponse.Auto[orchestratorv1.InitiatePaymentsResponse](h.logger, mservice.PaymentOrchestrator, err)
		}

		payments = append(payments, toProtoPayment(entity))
	}

	h.logger.Info(
		"Payments initiated",
		mzap.ObjRef("org_ref", orgRef),
		zap.String("quote_ref", quoteRef),
		zap.String("idempotency_key", idempotencyKey),
		zap.Int("payment_count", len(payments)),
	)
	return gsresponse.Success(&orchestratorv1.InitiatePaymentsResponse{Payments: payments})
}

type initiatePaymentCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

func (h *initiatePaymentCommand) Execute(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) gsresponse.Responder[orchestratorv1.InitiatePaymentResponse] {
	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}
	orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	intent := req.GetIntent()
	quoteRef := strings.TrimSpace(req.GetQuoteRef())
	hasIntent := intent != nil
	hasQuote := quoteRef != ""
	switch {
	case !hasIntent && !hasQuote:
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent or quote_ref is required"))
	case hasIntent && hasQuote:
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent and quote_ref are mutually exclusive"))
	}
	if hasIntent {
		if err := requireNonNilIntent(intent); err != nil {
			return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
	}
	idempotencyKey, err := requireIdempotencyKey(req.GetIdempotencyKey())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	h.logger.Debug(
		"Initiate payment request accepted",
		mzap.ObjRef("org_ref", orgID),
		zap.String("idempotency_key", idempotencyKey),
		zap.String("quote_ref", quoteRef),
		zap.Bool("has_intent", hasIntent),
	)

	store, err := ensurePaymentsStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if existing, err := getPaymentByIdempotencyKey(ctx, store, orgID, idempotencyKey); err == nil && existing != nil {
		h.logger.Debug(
			"idempotent payment request reused",
			zap.String("payment_ref", existing.PaymentRef),
			mzap.ObjRef("org_ref", orgID),
			zap.String("idempotency_key", idempotencyKey),
			zap.String("quote_ref", quoteRef),
		)
		return gsresponse.Success(&orchestratorv1.InitiatePaymentResponse{Payment: toProtoPayment(existing)})
	} else if err != nil && !errors.Is(err, storage.ErrPaymentNotFound) {
		return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	quoteSnapshot, resolvedIntent, err := h.engine.ResolvePaymentQuote(ctx, quoteResolutionInput{
		OrgRef:         orgRef,
		OrgID:          orgID,
		Meta:           req.GetMeta(),
		Intent:         intent,
		QuoteRef:       quoteRef,
		IdempotencyKey: req.GetIdempotencyKey(),
	})
	if err != nil {
		if qerr, ok := err.(quoteResolutionError); ok {
			switch qerr.code {
			case "quote_not_found":
				return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.code, qerr.err)
			case "quote_expired":
				return gsresponse.FailedPrecondition[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.code, qerr.err)
			case "quote_intent_mismatch":
				return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.err)
			default:
				return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, qerr.err)
			}
		}
		return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if quoteSnapshot == nil {
		quoteSnapshot = &orchestratorv1.PaymentQuote{}
	}
	if err := requireNonNilIntent(resolvedIntent); err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	h.logger.Debug(
		"Payment quote resolved",
		mzap.ObjRef("org_ref", orgID),
		zap.String("quote_ref", quoteRef),
		zap.Bool("quote_ref_used", quoteRef != ""),
	)

	entity := newPayment(orgID, resolvedIntent, idempotencyKey, req.GetMetadata(), quoteSnapshot)

	if err = store.Create(ctx, entity); err != nil {
		if errors.Is(err, storage.ErrDuplicatePayment) {
			return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists"))
		}
		return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if err := h.engine.ExecutePayment(ctx, store, entity, quoteSnapshot); err != nil {
		return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	h.logger.Info(
		"Payment initiated",
		zap.String("payment_ref", entity.PaymentRef),
		mzap.ObjRef("org_ref", orgID),
		zap.String("kind", resolvedIntent.GetKind().String()),
		zap.String("quote_ref", quoteSnapshot.GetQuoteRef()),
		zap.String("idempotency_key", idempotencyKey),
	)
	return gsresponse.Success(&orchestratorv1.InitiatePaymentResponse{
		Payment: toProtoPayment(entity),
	})
}

type cancelPaymentCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

func (h *cancelPaymentCommand) Execute(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) gsresponse.Responder[orchestratorv1.CancelPaymentResponse] {
	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}
	paymentRef, err := requirePaymentRef(req.GetPaymentRef())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	store, err := ensurePaymentsStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	payment, err := store.GetByPaymentRef(ctx, paymentRef)
	if err != nil {
		return paymentNotFoundResponder[orchestratorv1.CancelPaymentResponse](mservice.PaymentOrchestrator, h.logger, err)
	}
	if payment.State != model.PaymentStateAccepted {
		reason := merrors.InvalidArgument("payment cannot be cancelled in current state")
		return gsresponse.FailedPrecondition[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, "payment_not_cancellable", reason)
	}
	payment.State = model.PaymentStateCancelled
	payment.FailureCode = model.PaymentFailureCodePolicy
	payment.FailureReason = strings.TrimSpace(req.GetReason())
	if err := store.Update(ctx, payment); err != nil {
		return gsresponse.Auto[orchestratorv1.CancelPaymentResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	h.logger.Info("Payment cancelled", zap.String("payment_ref", payment.PaymentRef), zap.String("org_ref", payment.OrganizationRef.Hex()))
	return gsresponse.Success(&orchestratorv1.CancelPaymentResponse{Payment: toProtoPayment(payment)})
}

type initiateConversionCommand struct {
	engine paymentEngine
	logger mlogger.Logger
}

func (h *initiateConversionCommand) Execute(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) gsresponse.Responder[orchestratorv1.InitiateConversionResponse] {
	if err := h.engine.EnsureRepository(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request"))
	}
	orgRef, orgID, err := validateMetaAndOrgRef(req.GetMeta())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	idempotencyKey, err := requireIdempotencyKey(req.GetIdempotencyKey())
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req.GetSource() == nil || req.GetSource().GetLedger() == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("source ledger endpoint is required"))
	}
	if req.GetDestination() == nil || req.GetDestination().GetLedger() == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("destination ledger endpoint is required"))
	}
	fxIntent := req.GetFx()
	if fxIntent == nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("fx intent is required"))
	}

	store, err := ensurePaymentsStore(h.engine.Repository())
	if err != nil {
		return gsresponse.Unavailable[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if existing, err := getPaymentByIdempotencyKey(ctx, store, orgID, idempotencyKey); err == nil && existing != nil {
		h.logger.Debug("Idempotent conversion request reused", zap.String("payment_ref", existing.PaymentRef), mzap.ObjRef("org_ref", orgID))
		return gsresponse.Success(&orchestratorv1.InitiateConversionResponse{Conversion: toProtoPayment(existing)})
	} else if err != nil && !errors.Is(err, storage.ErrPaymentNotFound) {
		return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	amount, err := conversionAmountFromMetadata(req.GetMetadata(), fxIntent)
	if err != nil {
		return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	intentProto := &orchestratorv1.PaymentIntent{
		Ref:                uuid.New().String(),
		Kind:               orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION,
		Source:             req.GetSource(),
		Destination:        req.GetDestination(),
		Amount:             amount,
		RequiresFx:         true,
		Fx:                 fxIntent,
		FeePolicy:          req.GetFeePolicy(),
		SettlementCurrency: strings.TrimSpace(amount.GetCurrency()),
	}

	quote, _, err := h.engine.BuildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{
		Meta:           req.GetMeta(),
		IdempotencyKey: req.GetIdempotencyKey(),
		Intent:         intentProto,
	})
	if err != nil {
		return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	entity := newPayment(orgID, intentProto, idempotencyKey, req.GetMetadata(), quote)

	if err = store.Create(ctx, entity); err != nil {
		if errors.Is(err, storage.ErrDuplicatePayment) {
			return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists"))
		}
		return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	if err := h.engine.ExecutePayment(ctx, store, entity, quote); err != nil {
		return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	h.logger.Info("Conversion initiated", zap.String("payment_ref", entity.PaymentRef), mzap.ObjRef("org_ref", orgID))
	return gsresponse.Success(&orchestratorv1.InitiateConversionResponse{
		Conversion: toProtoPayment(entity),
	})
}
