package store

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

	"github.com/tech/sendico/gateway/chsettle/storage"
	"github.com/tech/sendico/gateway/chsettle/storage/model"
	"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"
	mutil "github.com/tech/sendico/pkg/mutil/db"
	"go.mongodb.org/mongo-driver/v2/mongo"
	"go.uber.org/zap"
)

const (
	pendingConfirmationsCollection = "pending_confirmations"
	fieldPendingRequestID          = "requestId"
	fieldPendingMessageID          = "messageId"
	fieldPendingExpiresAt          = "expiresAt"
)

type PendingConfirmations struct {
	logger mlogger.Logger
	repo   repository.Repository
}

func NewPendingConfirmations(logger mlogger.Logger, db *mongo.Database) (*PendingConfirmations, error) {
	if db == nil {
		return nil, merrors.InvalidArgument("mongo database is nil")
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("pending_confirmations").With(zap.String("collection", pendingConfirmationsCollection))

	repo := repository.CreateMongoRepository(db, pendingConfirmationsCollection)
	if err := repo.CreateIndex(&ri.Definition{
		Keys:   []ri.Key{{Field: fieldPendingRequestID, Sort: ri.Asc}},
		Unique: true,
	}); err != nil {
		logger.Error("Failed to create pending confirmations request_id index", zap.Error(err), zap.String("index_field", fieldPendingRequestID))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys: []ri.Key{{Field: fieldPendingMessageID, Sort: ri.Asc}},
	}); err != nil {
		logger.Error("Failed to create pending confirmations message_id index", zap.Error(err), zap.String("index_field", fieldPendingMessageID))
		return nil, err
	}
	if err := repo.CreateIndex(&ri.Definition{
		Keys: []ri.Key{{Field: fieldPendingExpiresAt, Sort: ri.Asc}},
	}); err != nil {
		logger.Error("Failed to create pending confirmations expires_at index", zap.Error(err), zap.String("index_field", fieldPendingExpiresAt))
		return nil, err
	}

	p := &PendingConfirmations{
		logger: logger,
		repo:   repo,
	}
	return p, nil
}

func (p *PendingConfirmations) Upsert(ctx context.Context, record *model.PendingConfirmation) error {
	if record == nil {
		return merrors.InvalidArgument("pending confirmation is nil", "record")
	}
	record.RequestID = strings.TrimSpace(record.RequestID)
	record.MessageID = strings.TrimSpace(record.MessageID)
	record.TargetChatID = strings.TrimSpace(record.TargetChatID)
	record.SourceService = strings.TrimSpace(record.SourceService)
	record.Rail = strings.TrimSpace(record.Rail)
	if record.RequestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	if record.TargetChatID == "" {
		return merrors.InvalidArgument("target_chat_id is required", "target_chat_id")
	}
	if record.ExpiresAt.IsZero() {
		return merrors.InvalidArgument("expires_at is required", "expires_at")
	}

	filter := repository.Filter(fieldPendingRequestID, record.RequestID)
	err := p.repo.Insert(ctx, record, filter)
	if errors.Is(err, merrors.ErrDataConflict) {
		patch := repository.Patch().
			Set(repository.Field(fieldPendingMessageID), record.MessageID).
			Set(repository.Field("targetChatId"), record.TargetChatID).
			Set(repository.Field("acceptedUserIds"), record.AcceptedUserIDs).
			Set(repository.Field("requestedMoney"), record.RequestedMoney).
			Set(repository.Field("sourceService"), record.SourceService).
			Set(repository.Field("rail"), record.Rail).
			Set(repository.Field("clarified"), record.Clarified).
			Set(repository.Field(fieldPendingExpiresAt), record.ExpiresAt)
		_, err = p.repo.PatchMany(ctx, filter, patch)
	}
	if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
		p.logger.Warn("Failed to upsert pending confirmation", zap.Error(err), zap.String("request_id", record.RequestID))
	}
	return err
}

func (p *PendingConfirmations) FindByRequestID(ctx context.Context, requestID string) (*model.PendingConfirmation, error) {
	requestID = strings.TrimSpace(requestID)
	if requestID == "" {
		return nil, merrors.InvalidArgument("request_id is required", "request_id")
	}
	var result model.PendingConfirmation
	err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingRequestID, requestID), &result)
	if errors.Is(err, merrors.ErrNoData) {
		return nil, nil //nolint:nilnil // nil means pending confirmation does not exist
	}
	if err != nil {
		return nil, err
	}
	return &result, nil
}

func (p *PendingConfirmations) FindByMessageID(ctx context.Context, messageID string) (*model.PendingConfirmation, error) {
	messageID = strings.TrimSpace(messageID)
	if messageID == "" {
		return nil, merrors.InvalidArgument("message_id is required", "message_id")
	}
	var result model.PendingConfirmation
	err := p.repo.FindOneByFilter(ctx, repository.Filter(fieldPendingMessageID, messageID), &result)
	if errors.Is(err, merrors.ErrNoData) {
		return nil, nil //nolint:nilnil // nil means pending confirmation does not exist
	}
	if err != nil {
		return nil, err
	}
	return &result, nil
}

func (p *PendingConfirmations) MarkClarified(ctx context.Context, requestID string) error {
	requestID = strings.TrimSpace(requestID)
	if requestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	patch := repository.Patch().
		Set(repository.Field("clarified"), true)
	_, err := p.repo.PatchMany(ctx, repository.Filter(fieldPendingRequestID, requestID), patch)
	return err
}

func (p *PendingConfirmations) AttachMessage(ctx context.Context, requestID string, messageID string) error {
	requestID = strings.TrimSpace(requestID)
	messageID = strings.TrimSpace(messageID)
	if requestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	if messageID == "" {
		return merrors.InvalidArgument("message_id is required", "message_id")
	}

	filter := repository.Filter(fieldPendingRequestID, requestID).And(
		repository.Query().Or(
			repository.Exists(repository.Field(fieldPendingMessageID), false),
			repository.Filter(fieldPendingMessageID, ""),
			repository.Filter(fieldPendingMessageID, messageID),
		),
	)
	patch := repository.Patch().
		Set(repository.Field(fieldPendingMessageID), messageID)
	updated, err := p.repo.PatchMany(ctx, filter, patch)
	if err != nil {
		return err
	}
	if updated == 0 {
		return merrors.NoData("pending confirmation not found")
	}
	return nil
}

func (p *PendingConfirmations) DeleteByRequestID(ctx context.Context, requestID string) error {
	requestID = strings.TrimSpace(requestID)
	if requestID == "" {
		return merrors.InvalidArgument("request_id is required", "request_id")
	}
	return p.repo.DeleteMany(ctx, repository.Filter(fieldPendingRequestID, requestID))
}

func (p *PendingConfirmations) ListExpired(ctx context.Context, now time.Time, limit int64) ([]model.PendingConfirmation, error) {
	if limit <= 0 {
		limit = 100
	}
	query := repository.Query().
		Comparison(repository.Field(fieldPendingExpiresAt), builder.Lte, now).
		Sort(repository.Field(fieldPendingExpiresAt), true).
		Limit(&limit)

	items, err := mutil.GetObjects[model.PendingConfirmation](ctx, p.logger, query, nil, p.repo)
	if err != nil && !errors.Is(err, merrors.ErrNoData) {
		return nil, err
	}
	return items, nil
}

var _ storage.PendingConfirmationsStore = (*PendingConfirmations)(nil)
