package prepo

import (
	"context"
	"errors"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

type mongoStore struct {
	collection *mongo.Collection
}

func newMongoStore(collection *mongo.Collection) paymentStore {
	return &mongoStore{
		collection: collection,
	}
}

func (s *mongoStore) EnsureIndexes(defs []*indexDefinition) error {
	if s.collection == nil {
		return merrors.InvalidArgument("payment repository v2: mongo collection is required")
	}
	if len(defs) == 0 {
		return nil
	}
	models := make([]mongo.IndexModel, 0, len(defs))
	for i := range defs {
		def := defs[i]
		if def == nil || len(def.Keys) == 0 {
			continue
		}
		keys := bson.D{}
		for j := range def.Keys {
			key := def.Keys[j]
			name := strings.TrimSpace(key.Field)
			if name == "" {
				continue
			}
			switch key.Type {
			case "":
				keys = append(keys, bson.E{Key: name, Value: int32(key.Sort)})
			default:
				keys = append(keys, bson.E{Key: name, Value: string(key.Type)})
			}
		}
		if len(keys) == 0 {
			continue
		}
		opt := options.Index()
		if def.Name != "" {
			opt.SetName(def.Name)
		}
		if def.Unique {
			opt.SetUnique(true)
		}
		if def.Sparse {
			opt.SetSparse(true)
		}
		if def.TTL != nil {
			opt.SetExpireAfterSeconds(*def.TTL)
		}
		if def.PartialFilter != nil {
			opt.SetPartialFilterExpression(def.PartialFilter.BuildQuery())
		}
		models = append(models, mongo.IndexModel{
			Keys:    keys,
			Options: opt,
		})
	}
	if len(models) == 0 {
		return nil
	}
	_, err := s.collection.Indexes().CreateMany(context.Background(), models)
	return err
}

func (s *mongoStore) Create(ctx context.Context, doc *paymentDocument) error {
	if s.collection == nil {
		return merrors.InvalidArgument("payment repository v2: mongo collection is required")
	}
	if doc == nil {
		return merrors.InvalidArgument("payment repository v2: payment document is required")
	}
	_, err := s.collection.InsertOne(ctx, doc)
	if mongo.IsDuplicateKeyError(err) {
		return ErrDuplicatePayment
	}
	return err
}

func (s *mongoStore) UpdateCAS(ctx context.Context, doc *paymentDocument, expectedVersion uint64) (bool, error) {
	if s.collection == nil {
		return false, merrors.InvalidArgument("payment repository v2: mongo collection is required")
	}
	if doc == nil {
		return false, merrors.InvalidArgument("payment repository v2: payment document is required")
	}
	filter := bson.D{
		{Key: "_id", Value: doc.ID},
		{Key: "organizationRef", Value: doc.OrganizationRef},
		{Key: "version", Value: expectedVersion},
	}
	result, err := s.collection.ReplaceOne(ctx, filter, doc)
	if mongo.IsDuplicateKeyError(err) {
		return false, ErrDuplicatePayment
	}
	if err != nil {
		return false, err
	}
	return result.MatchedCount > 0, nil
}

func (s *mongoStore) GetByPaymentRef(ctx context.Context, orgRef bson.ObjectID, paymentRef string) (*paymentDocument, error) {
	return s.findOne(ctx, bson.D{
		{Key: "organizationRef", Value: orgRef},
		{Key: "paymentRef", Value: paymentRef},
	})
}

func (s *mongoStore) GetByPaymentRefGlobal(ctx context.Context, paymentRef string) (*paymentDocument, error) {
	return s.findOne(ctx, bson.D{
		{Key: "paymentRef", Value: paymentRef},
	})
}

func (s *mongoStore) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*paymentDocument, error) {
	return s.findOne(ctx, bson.D{
		{Key: "organizationRef", Value: orgRef},
		{Key: "idempotencyKey", Value: idempotencyKey},
	})
}

func (s *mongoStore) GetByID(ctx context.Context, orgRef bson.ObjectID, id bson.ObjectID) (*paymentDocument, error) {
	return s.findOne(ctx, bson.D{
		{Key: "_id", Value: id},
		{Key: "organizationRef", Value: orgRef},
	})
}

func (s *mongoStore) ListByQuotationRef(ctx context.Context, orgRef bson.ObjectID, quotationRef string, cursor *listCursor, limit int64) ([]*paymentDocument, error) {
	filter := bson.D{
		{Key: "organizationRef", Value: orgRef},
		{Key: "quotationRef", Value: quotationRef},
	}
	return s.list(ctx, filter, cursor, limit)
}

func (s *mongoStore) ListByState(ctx context.Context, orgRef bson.ObjectID, state agg.State, cursor *listCursor, limit int64) ([]*paymentDocument, error) {
	filter := bson.D{
		{Key: "organizationRef", Value: orgRef},
		{Key: "state", Value: state},
	}
	return s.list(ctx, filter, cursor, limit)
}

func (s *mongoStore) ListByStateGlobal(ctx context.Context, state agg.State, cursor *listCursor, limit int64) ([]*paymentDocument, error) {
	filter := bson.D{
		{Key: "state", Value: state},
	}
	return s.list(ctx, filter, cursor, limit)
}

func (s *mongoStore) findOne(ctx context.Context, filter bson.D) (*paymentDocument, error) {
	if s.collection == nil {
		return nil, merrors.InvalidArgument("payment repository v2: mongo collection is required")
	}
	doc := &paymentDocument{}
	err := s.collection.FindOne(ctx, filter).Decode(doc)
	if err == nil {
		return doc, nil
	}
	if errors.Is(err, mongo.ErrNoDocuments) {
		return nil, ErrPaymentNotFound
	}
	return nil, err
}

func (s *mongoStore) list(ctx context.Context, filter bson.D, cursor *listCursor, limit int64) ([]*paymentDocument, error) {
	if s.collection == nil {
		return nil, merrors.InvalidArgument("payment repository v2: mongo collection is required")
	}
	if cursor != nil {
		filter = append(filter, bson.E{
			Key: "$or",
			Value: bson.A{
				bson.D{{Key: "createdAt", Value: bson.D{{Key: "$lt", Value: cursor.CreatedAt}}}},
				bson.D{
					{Key: "createdAt", Value: cursor.CreatedAt},
					{Key: "_id", Value: bson.D{{Key: "$lt", Value: cursor.ID}}},
				},
			},
		})
	}
	opt := options.Find().
		SetSort(bson.D{
			{Key: "createdAt", Value: -1},
			{Key: "_id", Value: -1},
		}).
		SetLimit(limit)
	cur, err := s.collection.Find(ctx, filter, opt)
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = cur.Close(ctx)
	}()

	items := make([]*paymentDocument, 0)
	for cur.Next(ctx) {
		doc := &paymentDocument{}
		if err := cur.Decode(doc); err != nil {
			return nil, err
		}
		items = append(items, doc)
	}
	if err := cur.Err(); err != nil {
		return nil, err
	}
	return items, nil
}

var _ paymentStore = (*mongoStore)(nil)
