package orchestrator

import (
	"context"
	"errors"
	"strings"

	"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"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"google.golang.org/protobuf/proto"
)

func validateMetaAndOrgRef(meta *orchestratorv1.RequestMeta) (string, primitive.ObjectID, error) {
	if meta == nil {
		return "", primitive.NilObjectID, merrors.InvalidArgument("meta is required")
	}
	orgRef := strings.TrimSpace(meta.GetOrganizationRef())
	if orgRef == "" {
		return "", primitive.NilObjectID, merrors.InvalidArgument("organization_ref is required")
	}
	orgID, err := primitive.ObjectIDFromHex(orgRef)
	if err != nil {
		return "", primitive.NilObjectID, merrors.InvalidArgument("organization_ref must be a valid objectID")
	}
	return orgRef, orgID, nil
}

func requireIdempotencyKey(k string) (string, error) {
	key := strings.TrimSpace(k)
	if key == "" {
		return "", merrors.InvalidArgument("idempotency_key is required")
	}
	return key, nil
}

func requirePaymentRef(ref string) (string, error) {
	val := strings.TrimSpace(ref)
	if val == "" {
		return "", merrors.InvalidArgument("payment_ref is required")
	}
	return val, nil
}

func requireNonNilIntent(intent *orchestratorv1.PaymentIntent) error {
	if intent == nil {
		return merrors.InvalidArgument("intent is required")
	}
	if intent.GetAmount() == nil {
		return merrors.InvalidArgument("intent.amount is required")
	}
	if strings.TrimSpace(intent.GetSettlementCurrency()) == "" {
		return merrors.InvalidArgument("intent.settlement_currency is required")
	}
	return nil
}

func ensurePaymentsStore(repo storage.Repository) (storage.PaymentsStore, error) {
	if repo == nil {
		return nil, errStorageUnavailable
	}
	store := repo.Payments()
	if store == nil {
		return nil, errStorageUnavailable
	}
	return store, nil
}

func ensureQuotesStore(repo storage.Repository) (storage.QuotesStore, error) {
	if repo == nil {
		return nil, errStorageUnavailable
	}
	store := repo.Quotes()
	if store == nil {
		return nil, errStorageUnavailable
	}
	return store, nil
}

func getPaymentByIdempotencyKey(ctx context.Context, store storage.PaymentsStore, orgID primitive.ObjectID, key string) (*model.Payment, error) {
	payment, err := store.GetByIdempotencyKey(ctx, orgID, key)
	if err != nil {
		return nil, err
	}
	return payment, nil
}

type quoteResolutionInput struct {
	OrgRef         string
	OrgID          primitive.ObjectID
	Meta           *orchestratorv1.RequestMeta
	Intent         *orchestratorv1.PaymentIntent
	QuoteRef       string
	IdempotencyKey string
}

type quoteResolutionError struct {
	code string
	err  error
}

func (e quoteResolutionError) Error() string { return e.err.Error() }

func (s *Service) resolvePaymentQuote(ctx context.Context, in quoteResolutionInput) (*orchestratorv1.PaymentQuote, *orchestratorv1.PaymentIntent, error) {
	if ref := strings.TrimSpace(in.QuoteRef); ref != "" {
		quotesStore, err := ensureQuotesStore(s.storage)
		if err != nil {
			return nil, nil, err
		}
		record, err := quotesStore.GetByRef(ctx, in.OrgID, ref)
		if err != nil {
			if errors.Is(err, storage.ErrQuoteNotFound) {
				return nil, nil, quoteResolutionError{code: "quote_not_found", err: merrors.InvalidArgument("quote_ref not found or expired")}
			}
			return nil, nil, err
		}
		if !record.ExpiresAt.IsZero() && s.clock.Now().After(record.ExpiresAt) {
			return nil, nil, quoteResolutionError{code: "quote_expired", err: merrors.InvalidArgument("quote_ref expired")}
		}
		intent, err := recordIntentFromQuote(record)
		if err != nil {
			return nil, nil, err
		}
		if in.Intent != nil && !proto.Equal(intent, in.Intent) {
			return nil, nil, quoteResolutionError{code: "quote_intent_mismatch", err: merrors.InvalidArgument("quote_ref does not match intent")}
		}
		quote, err := recordQuoteFromQuote(record)
		if err != nil {
			return nil, nil, err
		}
		quote.QuoteRef = ref
		return quote, intent, nil
	}

	if in.Intent == nil {
		return nil, nil, merrors.InvalidArgument("intent is required")
	}
	req := &orchestratorv1.QuotePaymentRequest{
		Meta:           in.Meta,
		IdempotencyKey: in.IdempotencyKey,
		Intent:         in.Intent,
		PreviewOnly:    false,
	}
	quote, _, err := s.buildPaymentQuote(ctx, in.OrgRef, req)
	if err != nil {
		return nil, nil, err
	}
	return quote, in.Intent, nil
}

func recordIntentFromQuote(record *model.PaymentQuoteRecord) (*orchestratorv1.PaymentIntent, error) {
	if record == nil {
		return nil, merrors.InvalidArgument("stored quote payload is incomplete")
	}
	if len(record.Intents) > 0 {
		if len(record.Intents) != 1 {
			return nil, merrors.InvalidArgument("stored quote payload is incomplete")
		}
		return protoIntentFromModel(record.Intents[0]), nil
	}
	if record.Intent.Amount == nil && (record.Intent.Kind == "" || record.Intent.Kind == model.PaymentKindUnspecified) {
		return nil, merrors.InvalidArgument("stored quote payload is incomplete")
	}
	return protoIntentFromModel(record.Intent), nil
}

func recordQuoteFromQuote(record *model.PaymentQuoteRecord) (*orchestratorv1.PaymentQuote, error) {
	if record == nil {
		return nil, merrors.InvalidArgument("stored quote is empty")
	}
	if record.Quote != nil {
		return modelQuoteToProto(record.Quote), nil
	}
	if len(record.Quotes) > 0 {
		if len(record.Quotes) != 1 {
			return nil, merrors.InvalidArgument("stored quote payload is incomplete")
		}
		return modelQuoteToProto(record.Quotes[0]), nil
	}
	return nil, merrors.InvalidArgument("stored quote is empty")
}

func newPayment(orgID primitive.ObjectID, intent *orchestratorv1.PaymentIntent, idempotencyKey string, metadata map[string]string, quote *orchestratorv1.PaymentQuote) *model.Payment {
	entity := &model.Payment{}
	entity.SetID(primitive.NewObjectID())
	entity.SetOrganizationRef(orgID)
	entity.PaymentRef = entity.GetID().Hex()
	entity.IdempotencyKey = idempotencyKey
	entity.State = model.PaymentStateAccepted
	entity.Intent = intentFromProto(intent)
	entity.Metadata = cloneMetadata(metadata)
	entity.LastQuote = quoteSnapshotToModel(quote)
	entity.Normalize()
	return entity
}

func paymentNotFoundResponder[T any](svc mservice.Type, logger mlogger.Logger, err error) gsresponse.Responder[T] {
	if errors.Is(err, storage.ErrPaymentNotFound) {
		return gsresponse.NotFound[T](logger, svc, err)
	}
	return gsresponse.Auto[T](logger, svc, err)
}
