package store

import (
	"context"
	"errors"
	"strconv"
	"strings"
	"time"

	"github.com/tech/sendico/payments/storage/model"
	quotestorage "github.com/tech/sendico/payments/storage/quote"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/db/repository/builder"
	ri "github.com/tech/sendico/pkg/db/repository/index"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type Quotes struct {
	logger    mlogger.Logger
	repo      repository.Repository
	retention time.Duration
}

const defaultPaymentQuoteRetention = 72 * time.Hour

// NewQuotes constructs a Mongo-backed quotes store.
func NewQuotes(logger mlogger.Logger, repo repository.Repository, retention time.Duration) (*Quotes, error) {
	if repo == nil {
		return nil, merrors.InvalidArgument("quotesStore: repository is nil")
	}
	if retention <= 0 {
		logger.Info("Using default retention duration", zap.Duration("default_retention", defaultPaymentQuoteRetention))
		retention = defaultPaymentQuoteRetention
	}
	logger.Info("Using retention duration", zap.Duration("retention", retention))

	indexes := []*ri.Definition{
		{
			Keys:   []ri.Key{{Field: "quoteRef", Sort: ri.Asc}},
			Unique: true,
		},
		{
			Keys: []ri.Key{
				{Field: "organizationRef", Sort: ri.Asc},
				{Field: "idempotencyKey", Sort: ri.Asc},
			},
			Unique:        true,
			Name:          "payment_quotes_org_idempotency_key",
			PartialFilter: repository.Query().Comparison(repository.Field("idempotencyKey"), builder.Exists, true),
		},
		{
			Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}},
		},
		{
			Keys: []ri.Key{{Field: "purgeAt", Sort: ri.Asc}},
			TTL:  int32Ptr(0),
			Name: "payment_quotes_purge_at_ttl",
		},
	}

	for _, def := range indexes {
		if err := repo.CreateIndex(def); err != nil {
			logger.Error("Failed to ensure quotes index", zap.Error(err), zap.String("collection", repo.Collection()))
			return nil, err
		}
	}

	return &Quotes{
		logger:    logger.Named("quotes"),
		repo:      repo,
		retention: retention,
	}, nil
}

func (q *Quotes) Create(ctx context.Context, quote *model.PaymentQuoteRecord) error {
	if quote == nil {
		return merrors.InvalidArgument("quotesStore: nil quote")
	}
	quote.QuoteRef = strings.TrimSpace(quote.QuoteRef)
	if quote.QuoteRef == "" {
		return merrors.InvalidArgument("quotesStore: empty quoteRef")
	}
	if quote.OrganizationRef == bson.NilObjectID {
		return merrors.InvalidArgument("quotesStore: organization_ref is required")
	}
	quote.IdempotencyKey = strings.TrimSpace(quote.IdempotencyKey)
	if quote.IdempotencyKey == "" {
		return merrors.InvalidArgument("quotesStore: idempotency key is required")
	}
	quote.RequestShape = model.QuoteRequestShape(strings.TrimSpace(string(quote.RequestShape)))
	if quote.RequestShape == "" || quote.RequestShape == model.QuoteRequestShapeUnspecified {
		return merrors.InvalidArgument("quotesStore: request shape is required")
	}
	if len(quote.Items) == 0 {
		return merrors.InvalidArgument("quotesStore: items are required")
	}
	if quote.RequestShape == model.QuoteRequestShapeSingle && len(quote.Items) != 1 {
		return merrors.InvalidArgument("quotesStore: single shape requires exactly one item")
	}
	if quote.ExpiresAt.IsZero() {
		return merrors.InvalidArgument("quotesStore: expires_at is required")
	}
	if quote.PurgeAt.IsZero() || quote.PurgeAt.Before(quote.ExpiresAt) {
		quote.PurgeAt = quote.ExpiresAt.Add(q.retention)
	}
	for i := range quote.Items {
		item := quote.Items[i]
		if item == nil {
			return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "] is required")
		}
		if item.Intent == nil {
			return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].intent is required")
		}
		if item.Quote == nil {
			return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].quote is required")
		}
		if item.Status == nil {
			return merrors.InvalidArgument("quotesStore: items[" + strconv.Itoa(i) + "].status is required")
		}
		if item.Intent.Attributes != nil {
			for k, v := range item.Intent.Attributes {
				item.Intent.Attributes[k] = strings.TrimSpace(v)
			}
		}
	}
	quote.Update()

	filter := repository.OrgFilter(quote.OrganizationRef).And(
		repository.Filter("quoteRef", quote.QuoteRef),
	)

	if err := q.repo.Insert(ctx, quote, filter); err != nil {
		if errors.Is(err, merrors.ErrDataConflict) {
			return quotestorage.ErrDuplicateQuote
		}
		q.logger.Warn("Failed to insert quote", mzap.ObjRef("org_ref", quote.OrganizationRef), zap.String("quote_ref", quote.QuoteRef), zap.Error(err))
		return err
	}
	return nil
}

func (q *Quotes) GetByRef(ctx context.Context, orgRef bson.ObjectID, quoteRef string) (*model.PaymentQuoteRecord, error) {
	quoteRef = strings.TrimSpace(quoteRef)
	if quoteRef == "" {
		return nil, merrors.InvalidArgument("quotesStore: empty quoteRef")
	}
	if orgRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("quotesStore: organization_ref is required")
	}
	entity := &model.PaymentQuoteRecord{}
	query := repository.OrgFilter(orgRef).And(repository.Filter("quoteRef", quoteRef))
	if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			q.logger.Debug("Quote not found by ref", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef))
			return nil, quotestorage.ErrQuoteNotFound
		}
		q.logger.Warn("Failed to fetch quote by ref", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef), zap.Error(err))
		return nil, err
	}
	if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
		q.logger.Debug("Quote expired by idempotency key", zap.String("quote_ref", quoteRef), mzap.ObjRef("org_ref", orgRef), zap.Time("expires_at", entity.ExpiresAt))
		return nil, quotestorage.ErrQuoteNotFound
	}
	return entity, nil
}

func (q *Quotes) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*model.PaymentQuoteRecord, error) {
	idempotencyKey = strings.TrimSpace(idempotencyKey)
	if idempotencyKey == "" {
		return nil, merrors.InvalidArgument("quotesStore: empty idempotency key")
	}
	if orgRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("quotesStore: organization_ref is required")
	}
	entity := &model.PaymentQuoteRecord{}
	query := repository.OrgFilter(orgRef).And(repository.Filter("idempotencyKey", idempotencyKey))
	if err := q.repo.FindOneByFilter(ctx, query, entity); err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			q.logger.Debug("Quote not found by idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef))
			return nil, quotestorage.ErrQuoteNotFound
		}
		q.logger.Warn("Failed to fetch quoteby idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef))
		return nil, err
	}
	if !entity.ExpiresAt.IsZero() && time.Now().After(entity.ExpiresAt) {
		q.logger.Debug("Quote expired by idempotency key", zap.String("idempotency_key", idempotencyKey), mzap.ObjRef("org_ref", orgRef), zap.Time("expires_at", entity.ExpiresAt))
		return nil, quotestorage.ErrQuoteNotFound
	}
	return entity, nil
}

var _ quotestorage.QuotesStore = (*Quotes)(nil)

func int32Ptr(v int32) *int32 {
	return &v
}
