package prepo

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

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/agg"

	"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"
)

const (
	defaultListLimit int64 = 50
	maxListLimit     int64 = 200
)

type listCursor struct {
	CreatedAt time.Time
	ID        bson.ObjectID
}

type paymentStore interface {
	EnsureIndexes(defs []*indexDefinition) error
	Create(ctx context.Context, doc *paymentDocument) error
	UpdateCAS(ctx context.Context, doc *paymentDocument, expectedVersion uint64) (bool, error)
	GetByPaymentRef(ctx context.Context, orgRef bson.ObjectID, paymentRef string) (*paymentDocument, error)
	GetByPaymentRefGlobal(ctx context.Context, paymentRef string) (*paymentDocument, error)
	GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (*paymentDocument, error)
	GetByID(ctx context.Context, orgRef bson.ObjectID, id bson.ObjectID) (*paymentDocument, error)
	ListByQuotationRef(ctx context.Context, orgRef bson.ObjectID, quotationRef string, cursor *listCursor, limit int64) ([]*paymentDocument, error)
	ListByState(ctx context.Context, orgRef bson.ObjectID, state agg.State, cursor *listCursor, limit int64) ([]*paymentDocument, error)
	ListByStateGlobal(ctx context.Context, state agg.State, cursor *listCursor, limit int64) ([]*paymentDocument, error)
}

type svc struct {
	logger mlogger.Logger
	store  paymentStore
	now    func() time.Time
}

func newWithStore(store paymentStore) (Repository, error) {
	return newWithStoreLogger(store, nil)
}

func newWithStoreLogger(store paymentStore, logger mlogger.Logger) (Repository, error) {
	if store == nil {
		return nil, merrors.InvalidArgument("payment repository v2: store is required")
	}
	if logger == nil {
		logger = zap.NewNop()
	}
	if err := store.EnsureIndexes(requiredIndexes()); err != nil {
		return nil, err
	}
	return &svc{
		logger: logger.Named("repository"),
		store:  store,
		now: func() time.Time {
			return time.Now().UTC()
		},
	}, nil
}

func (s *svc) Create(ctx context.Context, payment *agg.Payment) (err error) {
	logger := s.logger
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	logger.Debug("Starting Create", zap.String("payment_ref", paymentRef))
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", paymentRef),
		}
		if payment != nil {
			fields = append(fields,
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			logger.Warn("Failed to create", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Create", fields...)
	}(time.Now())

	doc, err := prepareCreate(payment, s.now().UTC())
	if err != nil {
		return err
	}
	if err := s.store.Create(ctx, doc); err != nil {
		if isDuplicate(err) {
			return ErrDuplicatePayment
		}
		return err
	}
	out, err := fromDocument(doc)
	if err != nil {
		return err
	}
	*payment = *out
	paymentRef = strings.TrimSpace(payment.PaymentRef)
	return nil
}

func (s *svc) UpdateCAS(ctx context.Context, payment *agg.Payment, expectedVersion uint64) (err error) {
	logger := s.logger
	paymentRef := ""
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
	}
	logger.Debug("Starting Update cas",
		zap.String("payment_ref", paymentRef),
		zap.Uint64("expected_version", expectedVersion),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", paymentRef),
			zap.Uint64("expected_version", expectedVersion),
		}
		if payment != nil {
			fields = append(fields,
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			logger.Warn("Failed to update cas", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Update cas", fields...)
	}(time.Now())

	doc, err := prepareUpdate(payment, expectedVersion, s.now().UTC())
	if err != nil {
		return err
	}

	updated, err := s.store.UpdateCAS(ctx, doc, expectedVersion)
	if err != nil {
		if isDuplicate(err) {
			return ErrDuplicatePayment
		}
		return err
	}
	if !updated {
		if _, findErr := s.store.GetByID(ctx, doc.OrganizationRef, doc.ID); findErr != nil {
			if errors.Is(findErr, ErrPaymentNotFound) {
				return ErrPaymentNotFound
			}
			return findErr
		}
		return ErrVersionConflict
	}

	out, err := fromDocument(doc)
	if err != nil {
		return err
	}
	*payment = *out
	paymentRef = strings.TrimSpace(payment.PaymentRef)
	return nil
}

func (s *svc) GetByPaymentRef(ctx context.Context, orgRef bson.ObjectID, paymentRef string) (payment *agg.Payment, err error) {
	logger := s.logger
	requestPaymentRef := strings.TrimSpace(paymentRef)
	logger.Debug("Starting Get by payment ref",
		mzap.ObjRef("organization_ref", orgRef),
		zap.String("payment_ref", requestPaymentRef),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("payment_ref", requestPaymentRef),
		}
		if payment != nil {
			fields = append(fields,
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			logger.Warn("Failed to get by payment ref", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Get by payment ref", fields...)
	}(time.Now())

	if orgRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	paymentRef = strings.TrimSpace(paymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	doc, err := s.store.GetByPaymentRef(ctx, orgRef, paymentRef)
	if err != nil {
		return nil, err
	}
	payment, err = fromDocument(doc)
	return payment, err
}

func (s *svc) GetByPaymentRefGlobal(ctx context.Context, paymentRef string) (payment *agg.Payment, err error) {
	logger := s.logger
	requestPaymentRef := strings.TrimSpace(paymentRef)
	logger.Debug("Starting Get by payment ref global",
		zap.String("payment_ref", requestPaymentRef),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.String("payment_ref", requestPaymentRef),
		}
		if payment != nil {
			fields = append(fields,
				mzap.ObjRef("organization_ref", payment.OrganizationRef),
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			logger.Warn("Failed to get by payment ref global", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Get by payment ref global", fields...)
	}(time.Now())

	paymentRef = strings.TrimSpace(paymentRef)
	if paymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	doc, err := s.store.GetByPaymentRefGlobal(ctx, paymentRef)
	if err != nil {
		return nil, err
	}
	payment, err = fromDocument(doc)
	return payment, err
}

func (s *svc) GetByIdempotencyKey(ctx context.Context, orgRef bson.ObjectID, idempotencyKey string) (payment *agg.Payment, err error) {
	logger := s.logger
	hasKey := strings.TrimSpace(idempotencyKey) != ""
	logger.Debug("Starting Get by idempotency key",
		mzap.ObjRef("organization_ref", orgRef),
		zap.Bool("has_idempotency_key", hasKey),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			mzap.ObjRef("organization_ref", orgRef),
			zap.String("idempotency_key", idempotencyKey),
		}
		if payment != nil {
			fields = append(fields,
				zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
				zap.String("state", string(payment.State)),
				zap.Uint64("version", payment.Version),
			)
		}
		if err != nil {
			if errors.Is(err, ErrPaymentNotFound) {
				logger.Debug("Completed Get by idempotency key", append(fields, zap.Bool("found", false))...)
				return
			}
			logger.Warn("Failed to get by idempotency key", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Get by idempotency key", append(fields, zap.Bool("found", true))...)
	}(time.Now())

	if orgRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	idempotencyKey = strings.TrimSpace(idempotencyKey)
	if idempotencyKey == "" {
		return nil, merrors.InvalidArgument("idempotency_key is required")
	}
	doc, err := s.store.GetByIdempotencyKey(ctx, orgRef, idempotencyKey)
	if err != nil {
		return nil, err
	}
	payment, err = fromDocument(doc)
	return payment, err
}

func (s *svc) ListByQuotationRef(ctx context.Context, in ListByQuotationRefInput) (out *ListOutput, err error) {
	logger := s.logger
	logger.Debug("Starting List by quotation ref",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
		zap.Int32("limit", in.Limit),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
		}
		if err != nil {
			logger.Warn("Failed to list by quotation ref", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed List by quotation ref", fields...)
	}(time.Now())

	if in.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	in.QuotationRef = strings.TrimSpace(in.QuotationRef)
	if in.QuotationRef == "" {
		return nil, merrors.InvalidArgument("quotation_ref is required")
	}
	cursor, err := normalizeCursor(in.Cursor)
	if err != nil {
		return nil, err
	}
	out, err = s.list(ctx, listQuery{
		limit: sanitizeLimit(in.Limit),
		run: func(limit int64) ([]*paymentDocument, error) {
			return s.store.ListByQuotationRef(ctx, in.OrganizationRef, in.QuotationRef, cursor, limit)
		},
	})
	return out, err
}

func (s *svc) ListByState(ctx context.Context, in ListByStateInput) (out *ListOutput, err error) {
	logger := s.logger
	logger.Debug("Starting List by state",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("state", string(in.State)),
		zap.Int32("limit", in.Limit),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
		}
		if err != nil {
			logger.Warn("Failed to list by state", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed List by state", fields...)
	}(time.Now())

	if in.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	state, ok := normalizeAggregateState(in.State)
	if !ok {
		return nil, merrors.InvalidArgument("state is invalid")
	}
	cursor, err := normalizeCursor(in.Cursor)
	if err != nil {
		return nil, err
	}
	out, err = s.list(ctx, listQuery{
		limit: sanitizeLimit(in.Limit),
		run: func(limit int64) ([]*paymentDocument, error) {
			return s.store.ListByState(ctx, in.OrganizationRef, state, cursor, limit)
		},
	})
	return out, err
}

func (s *svc) ListByStateGlobal(ctx context.Context, in ListByStateGlobalInput) (out *ListOutput, err error) {
	logger := s.logger
	logger.Debug("Starting List by state global",
		zap.String("state", string(in.State)),
		zap.Int32("limit", in.Limit),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields, zap.Int("items_count", len(out.Items)))
		}
		if err != nil {
			logger.Warn("Failed to list by state global", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed List by state global", fields...)
	}(time.Now())

	state, ok := normalizeAggregateState(in.State)
	if !ok {
		return nil, merrors.InvalidArgument("state is invalid")
	}
	cursor, err := normalizeCursor(in.Cursor)
	if err != nil {
		return nil, err
	}
	out, err = s.list(ctx, listQuery{
		limit: sanitizeLimit(in.Limit),
		run: func(limit int64) ([]*paymentDocument, error) {
			return s.store.ListByStateGlobal(ctx, state, cursor, limit)
		},
	})
	return out, err
}

type listQuery struct {
	limit int64
	run   func(limit int64) ([]*paymentDocument, error)
}

func (s *svc) list(_ context.Context, query listQuery) (*ListOutput, error) {
	fetchLimit := query.limit + 1
	docs, err := query.run(fetchLimit)
	if err != nil {
		return nil, err
	}
	if len(docs) == 0 {
		return &ListOutput{}, nil
	}

	nextCursor := (*ListCursor)(nil)
	if int64(len(docs)) == fetchLimit {
		docs = docs[:len(docs)-1]
		last := docs[len(docs)-1]
		nextCursor = &ListCursor{
			CreatedAt: last.CreatedAt.UTC(),
			ID:        last.ID,
		}
	}

	items := make([]*agg.Payment, 0, len(docs))
	for i := range docs {
		entity, convErr := fromDocument(docs[i])
		if convErr != nil {
			return nil, convErr
		}
		items = append(items, entity)
	}
	return &ListOutput{
		Items:      items,
		NextCursor: nextCursor,
	}, nil
}

func prepareCreate(payment *agg.Payment, now time.Time) (*paymentDocument, error) {
	doc, err := normalizePayment(payment, false)
	if err != nil {
		return nil, err
	}
	if doc.ID.IsZero() {
		doc.ID = bson.NewObjectID()
	}
	if doc.PaymentRef == "" {
		doc.PaymentRef = doc.ID.Hex()
	}
	if doc.CreatedAt.IsZero() {
		doc.CreatedAt = now
	}
	doc.UpdatedAt = now
	if doc.Version == 0 {
		doc.Version = 1
	}
	return doc, nil
}

func prepareUpdate(payment *agg.Payment, expectedVersion uint64, now time.Time) (*paymentDocument, error) {
	if expectedVersion == 0 {
		return nil, merrors.InvalidArgument("expected_version is required")
	}
	doc, err := normalizePayment(payment, true)
	if err != nil {
		return nil, err
	}
	if doc.ID.IsZero() {
		return nil, merrors.InvalidArgument("payment id is required")
	}
	if doc.CreatedAt.IsZero() {
		return nil, merrors.InvalidArgument("payment.created_at is required")
	}
	nextVersion := expectedVersion + 1
	if doc.Version != 0 && doc.Version != expectedVersion && doc.Version != nextVersion {
		return nil, merrors.InvalidArgument("payment.version must equal expected_version or expected_version + 1")
	}
	doc.Version = nextVersion
	doc.UpdatedAt = now
	return doc, nil
}

func normalizePayment(payment *agg.Payment, requirePaymentRef bool) (*paymentDocument, error) {
	doc, err := toDocument(payment)
	if err != nil {
		return nil, err
	}
	if doc == nil {
		return nil, merrors.InvalidArgument("payment is required")
	}
	doc.PaymentRef = strings.TrimSpace(doc.PaymentRef)
	doc.IdempotencyKey = strings.TrimSpace(doc.IdempotencyKey)
	doc.QuotationRef = strings.TrimSpace(doc.QuotationRef)
	doc.ClientPaymentRef = strings.TrimSpace(doc.ClientPaymentRef)

	if doc.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	if requirePaymentRef && doc.PaymentRef == "" {
		return nil, merrors.InvalidArgument("payment_ref is required")
	}
	if doc.IdempotencyKey == "" {
		return nil, merrors.InvalidArgument("idempotency_key is required")
	}
	if doc.QuotationRef == "" {
		return nil, merrors.InvalidArgument("quotation_ref is required")
	}
	if doc.QuoteSnapshot == nil {
		return nil, merrors.InvalidArgument("quote_snapshot is required")
	}
	state, ok := normalizeAggregateState(doc.State)
	if !ok {
		return nil, merrors.InvalidArgument("state is invalid")
	}
	doc.State = state

	for i := range doc.StepExecutions {
		step := &doc.StepExecutions[i]
		step.StepRef = strings.TrimSpace(step.StepRef)
		step.StepCode = strings.TrimSpace(step.StepCode)
		step.Comment = strings.TrimSpace(step.Comment)
		step.FailureCode = strings.TrimSpace(step.FailureCode)
		step.FailureMsg = strings.TrimSpace(step.FailureMsg)
		if step.StepRef == "" {
			return nil, merrors.InvalidArgument("stepExecutions[" + itoa(i) + "].step_ref is required")
		}
		if step.StepCode == "" {
			step.StepCode = step.StepRef
		}
		if step.Attempt == 0 {
			step.Attempt = 1
		}
		ss, ok := normalizeStepState(step.State)
		if !ok {
			return nil, merrors.InvalidArgument("stepExecutions[" + itoa(i) + "].state is invalid")
		}
		step.State = ss
	}
	return doc, nil
}

func normalizeAggregateState(state agg.State) (agg.State, bool) {
	switch strings.ToLower(strings.TrimSpace(string(state))) {
	case string(agg.StateCreated):
		return agg.StateCreated, true
	case string(agg.StateExecuting):
		return agg.StateExecuting, true
	case string(agg.StateNeedsAttention):
		return agg.StateNeedsAttention, true
	case string(agg.StateSettled):
		return agg.StateSettled, true
	case string(agg.StateFailed):
		return agg.StateFailed, true
	default:
		return agg.StateUnspecified, false
	}
}

func normalizeStepState(state agg.StepState) (agg.StepState, bool) {
	switch strings.ToLower(strings.TrimSpace(string(state))) {
	case string(agg.StepStatePending):
		return agg.StepStatePending, true
	case string(agg.StepStateRunning):
		return agg.StepStateRunning, true
	case string(agg.StepStateCompleted):
		return agg.StepStateCompleted, true
	case string(agg.StepStateFailed):
		return agg.StepStateFailed, true
	case string(agg.StepStateNeedsAttention):
		return agg.StepStateNeedsAttention, true
	case string(agg.StepStateSkipped):
		return agg.StepStateSkipped, true
	default:
		return agg.StepStateUnspecified, false
	}
}

func normalizeCursor(cursor *ListCursor) (*listCursor, error) {
	if cursor == nil {
		return nil, nil //nolint:nilnil // nil cursor means pagination starts from first page
	}
	if cursor.ID.IsZero() {
		return nil, merrors.InvalidArgument("cursor.id is required")
	}
	if cursor.CreatedAt.IsZero() {
		return nil, merrors.InvalidArgument("cursor.created_at is required")
	}
	return &listCursor{
		CreatedAt: cursor.CreatedAt.UTC(),
		ID:        cursor.ID,
	}, nil
}

func sanitizeLimit(limit int32) int64 {
	if limit <= 0 {
		return defaultListLimit
	}
	if limit > int32(maxListLimit) {
		return maxListLimit
	}
	return int64(limit)
}

func isDuplicate(err error) bool {
	return errors.Is(err, ErrDuplicatePayment)
}

func itoa(v int) string {
	if v == 0 {
		return "0"
	}
	var buf [20]byte
	i := len(buf)
	for v > 0 {
		i--
		buf[i] = byte('0' + v%10)
		v /= 10
	}
	return string(buf[i:])
}
