package mongo

import (
	"context"
	"time"

	"github.com/tech/sendico/payments/storage"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/payments/storage/mongo/store"
	quotestorage "github.com/tech/sendico/payments/storage/quote"
	quotemongo "github.com/tech/sendico/payments/storage/quote/mongo"
	"github.com/tech/sendico/pkg/auth"
	"github.com/tech/sendico/pkg/db"
	"github.com/tech/sendico/pkg/db/repository"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.mongodb.org/mongo-driver/v2/mongo"
)

// Store implements storage.Repository backed by MongoDB.
type Store struct {
	logger mlogger.Logger
	ping   func(context.Context) error

	database *mongo.Database
	payments storage.PaymentsStore
	methods  storage.PaymentMethodsStore
	quotes   quotestorage.QuotesStore
	routes   storage.RoutesStore
}

type paymentMethodsConfig struct {
	enforcer      auth.Enforcer
	permissionRef bson.ObjectID
}

type options struct {
	quoteRetention     time.Duration
	paymentMethodsAuth *paymentMethodsConfig
}

// Option configures the Mongo-backed payments repository.
type Option func(*options)

// WithQuoteRetention sets how long payment quote records are retained after expiry.
func WithQuoteRetention(retention time.Duration) Option {
	return func(opts *options) {
		opts.quoteRetention = retention
	}
}

// WithPaymentMethodsAuth enables the payment-methods store and permission checks.
func WithPaymentMethodsAuth(enforcer auth.Enforcer, permissionRef bson.ObjectID) Option {
	return func(opts *options) {
		opts.paymentMethodsAuth = &paymentMethodsConfig{
			enforcer:      enforcer,
			permissionRef: permissionRef,
		}
	}
}

// New constructs a Mongo-backed payments repository from a Mongo connection.
func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Store, error) {
	if conn == nil {
		return nil, merrors.InvalidArgument("payments.storage.mongo: connection is nil")
	}
	paymentsRepo := repository.CreateMongoRepository(conn.Database(), (&model.Payment{}).Collection())
	quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection())
	routesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentRoute{}).Collection())
	methodsRepo := repository.CreateMongoRepository(conn.Database(), mservice.PaymentMethods)

	return newWithRepository(logger, conn.Ping, conn.Database(), paymentsRepo, methodsRepo, quotesRepo, routesRepo, opts...)
}

// NewWithRepository constructs a payments repository using the provided primitives.
func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository, quotesRepo repository.Repository, routesRepo repository.Repository, opts ...Option) (*Store, error) {
	return newWithRepository(logger, ping, nil, paymentsRepo, nil, quotesRepo, routesRepo, opts...)
}

func newWithRepository(
	logger mlogger.Logger,
	ping func(context.Context) error,
	database *mongo.Database,
	paymentsRepo, methodsRepo, quotesRepo, routesRepo repository.Repository,
	opts ...Option,
) (*Store, error) {
	if ping == nil {
		return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
	}
	if paymentsRepo == nil {
		return nil, merrors.InvalidArgument("payments.storage.mongo: payments repository is nil")
	}
	if quotesRepo == nil {
		return nil, merrors.InvalidArgument("payments.storage.mongo: quotes repository is nil")
	}
	if routesRepo == nil {
		return nil, merrors.InvalidArgument("payments.storage.mongo: routes repository is nil")
	}

	cfg := options{}
	for _, opt := range opts {
		if opt != nil {
			opt(&cfg)
		}
	}

	childLogger := logger.Named("storage").Named("mongo")
	paymentsStore, err := store.NewPayments(childLogger, paymentsRepo)
	if err != nil {
		return nil, err
	}
	quotesRepoStore, err := quotemongo.NewWithRepository(childLogger, ping, quotesRepo, quotemongo.WithQuoteRetention(cfg.quoteRetention))
	if err != nil {
		return nil, err
	}
	routesStore, err := store.NewRoutes(childLogger, routesRepo)
	if err != nil {
		return nil, err
	}

	var methodsStore storage.PaymentMethodsStore
	if cfg.paymentMethodsAuth != nil {
		if methodsRepo == nil {
			return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods repository is nil")
		}
		if cfg.paymentMethodsAuth.enforcer == nil {
			return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods enforcer is nil")
		}
		if cfg.paymentMethodsAuth.permissionRef == bson.NilObjectID {
			return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods permission reference is required")
		}

		methodsStore, err = store.NewPaymentMethods(childLogger, methodsRepo, cfg.paymentMethodsAuth.enforcer, cfg.paymentMethodsAuth.permissionRef)
		if err != nil {
			return nil, err
		}
	}

	result := &Store{
		logger:   childLogger,
		ping:     ping,
		database: database,
		payments: paymentsStore,
		methods:  methodsStore,
		quotes:   quotesRepoStore.Quotes(),
		routes:   routesStore,
	}

	return result, nil
}

// Ping verifies connectivity with the backing database.
func (s *Store) Ping(ctx context.Context) error {
	if s.ping == nil {
		return merrors.InvalidArgument("payments.storage.mongo: ping func is nil")
	}
	return s.ping(ctx)
}

// Payments returns the payments store.
func (s *Store) Payments() storage.PaymentsStore {
	return s.payments
}

// PaymentMethods returns the payment-methods store.
func (s *Store) PaymentMethods() storage.PaymentMethodsStore {
	return s.methods
}

// Quotes returns the quotes store.
func (s *Store) Quotes() quotestorage.QuotesStore {
	return s.quotes
}

// Routes returns the routing store.
func (s *Store) Routes() storage.RoutesStore {
	return s.routes
}

// MongoDatabase returns underlying Mongo database when available.
func (s *Store) MongoDatabase() *mongo.Database {
	if s == nil {
		return nil
	}
	return s.database
}

var _ storage.Repository = (*Store)(nil)
