package store

import (
	"context"
	"errors"

	"github.com/tech/sendico/payments/storage"
	"github.com/tech/sendico/pkg/auth"
	"github.com/tech/sendico/pkg/db/repository"
	ri "github.com/tech/sendico/pkg/db/repository/index"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	pkgmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mservice"
	mutil "github.com/tech/sendico/pkg/mutil/db"
	mauth "github.com/tech/sendico/pkg/mutil/db/auth"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

type PaymentMethods struct {
	logger        mlogger.Logger
	repo          repository.Repository
	enforcer      auth.Enforcer
	permissionRef bson.ObjectID
}

// NewPaymentMethods constructs a Mongo-backed payment-methods store.
func NewPaymentMethods(logger mlogger.Logger, repo repository.Repository, enforcer auth.Enforcer, permissionRef bson.ObjectID) (*PaymentMethods, error) {
	if repo == nil {
		return nil, merrors.InvalidArgument("paymentMethodsStore: repository is nil")
	}
	if enforcer == nil {
		return nil, merrors.InvalidArgument("paymentMethodsStore: enforcer is nil")
	}
	if permissionRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: permission reference is required")
	}

	indexes := []*ri.Definition{
		{
			Keys: []ri.Key{
				{Field: "organizationRef", Sort: ri.Asc},
				{Field: "recipientRef", Sort: ri.Asc},
			},
		},
		{
			Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}},
		},
	}

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

	return &PaymentMethods{
		logger:        logger.Named("payment_methods"),
		repo:          repo,
		enforcer:      enforcer,
		permissionRef: permissionRef,
	}, nil
}

func (p *PaymentMethods) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, method *pkgmodel.PaymentMethod) error {
	if method == nil {
		return merrors.InvalidArgument("paymentMethodsStore: nil payment method")
	}
	if accountRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if organizationRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: organization_ref is required")
	}
	if method.RecipientRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
	}

	if method.GetPermissionRef() == bson.NilObjectID {
		method.SetPermissionRef(p.permissionRef)
	}
	method.SetOrganizationRef(organizationRef)

	allowed, err := p.enforcer.Enforce(ctx, method.GetPermissionRef(), accountRef, organizationRef, bson.NilObjectID, pkgmodel.ActionCreate)
	if err != nil {
		return err
	}
	if !allowed {
		return merrors.AccessDenied(mservice.PaymentMethods, string(pkgmodel.ActionCreate), bson.NilObjectID)
	}

	return p.repo.Insert(ctx, method, nil)
}

func (p *PaymentMethods) Get(ctx context.Context, accountRef, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error) {
	if accountRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if methodRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
	}

	if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionRead); err != nil {
		return nil, err
	}

	method := &pkgmodel.PaymentMethod{}
	if err := p.repo.Get(ctx, methodRef, method); err != nil {
		return nil, err
	}
	return method, nil
}

func (p *PaymentMethods) GetPrivate(ctx context.Context, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error) {
	if methodRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
	}

	method := &pkgmodel.PaymentMethod{}
	if err := p.repo.Get(ctx, methodRef, method); err != nil {
		return nil, err
	}
	return method, nil
}

func (p *PaymentMethods) Update(ctx context.Context, accountRef bson.ObjectID, method *pkgmodel.PaymentMethod) error {
	if method == nil {
		return merrors.InvalidArgument("paymentMethodsStore: nil payment method")
	}
	if accountRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if method.GetID() == nil || method.GetID().IsZero() {
		return merrors.InvalidArgument("paymentMethodsStore: method id is required")
	}

	if err := p.enforceObject(ctx, accountRef, *method.GetID(), pkgmodel.ActionUpdate); err != nil {
		return err
	}

	return p.repo.Update(ctx, method)
}

func (p *PaymentMethods) Delete(ctx context.Context, accountRef, methodRef bson.ObjectID) error {
	if accountRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if methodRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
	}

	if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionDelete); err != nil {
		return err
	}

	return p.repo.Delete(ctx, methodRef)
}

func (p *PaymentMethods) DeleteCascade(ctx context.Context, accountRef, methodRef bson.ObjectID) error {
	return p.Delete(ctx, accountRef, methodRef)
}

func (p *PaymentMethods) SetArchived(ctx context.Context, accountRef, _ bson.ObjectID, methodRef bson.ObjectID, archived, _ bool) error {
	if accountRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if methodRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: method_ref is required")
	}

	if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionUpdate); err != nil {
		return err
	}

	patch := repository.Patch().Set(repository.Field("isArchived"), archived)
	return p.repo.Patch(ctx, methodRef, patch)
}

func (p *PaymentMethods) List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error) {
	if accountRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required")
	}
	if organizationRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: organization_ref is required")
	}
	if recipientRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
	}

	items, err := mauth.GetProtectedObjects[pkgmodel.PaymentMethod](
		ctx,
		p.logger,
		accountRef,
		organizationRef,
		pkgmodel.ActionRead,
		repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
		cursor,
		p.enforcer,
		p.repo,
	)
	if errors.Is(err, merrors.ErrNoData) {
		return []pkgmodel.PaymentMethod{}, nil
	}
	return items, err
}

func (p *PaymentMethods) ListPrivate(ctx context.Context, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error) {
	if organizationRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: organization_ref is required")
	}
	if recipientRef == bson.NilObjectID {
		return nil, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
	}

	items, err := mutil.GetObjects[pkgmodel.PaymentMethod](
		ctx,
		p.logger,
		repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)),
		cursor,
		p.repo,
	)
	if errors.Is(err, merrors.ErrNoData) {
		return []pkgmodel.PaymentMethod{}, nil
	}
	return items, err
}

func (p *PaymentMethods) SetArchivedByRecipient(ctx context.Context, recipientRef bson.ObjectID, archived bool) (int, error) {
	if recipientRef == bson.NilObjectID {
		return 0, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
	}

	filter := repository.Filter("recipientRef", recipientRef)
	patch := repository.Patch().Set(repository.Field("isArchived"), archived)
	return p.repo.PatchMany(ctx, filter, patch)
}

func (p *PaymentMethods) DeleteByRecipient(ctx context.Context, recipientRef bson.ObjectID) error {
	if recipientRef == bson.NilObjectID {
		return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required")
	}
	return p.repo.DeleteMany(ctx, repository.Filter("recipientRef", recipientRef))
}

func (p *PaymentMethods) enforceObject(ctx context.Context, accountRef, methodRef bson.ObjectID, action pkgmodel.Action) error {
	refs, err := p.repo.ListPermissionBound(ctx, repository.IDFilter(methodRef))
	if err != nil {
		if errors.Is(err, merrors.ErrNoData) {
			return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
		}
		return err
	}
	if len(refs) == 0 {
		return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
	}

	allowed, err := p.enforcer.Enforce(ctx, refs[0].GetPermissionRef(), accountRef, refs[0].GetOrganizationRef(), methodRef, action)
	if err != nil {
		return err
	}
	if !allowed {
		return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef)
	}
	return nil
}

var _ storage.PaymentMethodsStore = (*PaymentMethods)(nil)
