package idem

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

	"github.com/tech/sendico/payments/storage"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.uber.org/zap"
)

const reqHashMetaKey = "_orchestrator_v2_req_hash"

type svc struct {
	logger mlogger.Logger
}

func (s *svc) TryReuse(
	ctx context.Context,
	store Store,
	in ReuseInput,
) (payment *model.Payment, reused bool, err error) {
	logger := s.logger
	logger.Debug("Starting Try reuse",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.Bool("has_idempotency_key", strings.TrimSpace(in.IdempotencyKey) != ""),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.Bool("reused", reused),
		}
		if payment != nil {
			fields = append(fields, zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)))
		}
		if err != nil {
			logger.Warn("Failed to try reuse", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Try reuse", fields...)
	}(time.Now())

	if store == nil {
		return nil, false, merrors.InvalidArgument("payments store is required")
	}

	idempotencyKey, fingerprint, err := validateReuseInput(in)
	if err != nil {
		return nil, false, err
	}

	payment, err = store.GetByIdempotencyKey(ctx, in.OrganizationRef, idempotencyKey)
	if err != nil {
		if errors.Is(err, storage.ErrPaymentNotFound) || errors.Is(err, merrors.ErrNoData) {
			return nil, false, nil
		}
		return nil, false, err
	}
	if payment == nil {
		return nil, false, nil
	}

	if paymentReqHash(payment) != fingerprint {
		return nil, false, ErrIdempotencyParamMismatch
	}

	return payment, true, nil
}

func (s *svc) CreateOrReuse(
	ctx context.Context,
	store Store,
	in CreateInput,
) (payment *model.Payment, reused bool, err error) {
	logger := s.logger
	logger.Debug("Starting Create or reuse",
		zap.String("organization_ref", in.Reuse.OrganizationRef.Hex()),
		zap.Bool("has_payment", in.Payment != nil),
		zap.Bool("has_idempotency_key", strings.TrimSpace(in.Reuse.IdempotencyKey) != ""),
	)
	defer func(start time.Time) {
		fields := []zap.Field{
			zap.Int64("duration_ms", time.Since(start).Milliseconds()),
			zap.Bool("reused", reused),
		}
		if payment != nil {
			fields = append(fields, zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)))
		}
		if err != nil {
			logger.Warn("Failed to create or reuse", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Create or reuse", fields...)
	}(time.Now())

	if store == nil {
		return nil, false, merrors.InvalidArgument("payments store is required")
	}
	if in.Payment == nil {
		return nil, false, merrors.InvalidArgument("payment is required")
	}

	_, fingerprint, err := validateReuseInput(in.Reuse)
	if err != nil {
		return nil, false, err
	}
	setPaymentReqHash(in.Payment, fingerprint)

	if createErr := store.Create(ctx, in.Payment); createErr != nil {
		if !errors.Is(createErr, storage.ErrDuplicatePayment) {
			return nil, false, createErr
		}

		payment, reused, err = s.TryReuse(ctx, store, in.Reuse)
		if err != nil {
			return nil, false, err
		}
		if reused {
			return payment, true, nil
		}
		return nil, false, createErr
	}

	return in.Payment, false, nil
}

func validateReuseInput(in ReuseInput) (string, string, error) {
	if in.OrganizationRef.IsZero() {
		return "", "", merrors.InvalidArgument("organization_id is required")
	}

	idempotencyKey := strings.TrimSpace(in.IdempotencyKey)
	if idempotencyKey == "" {
		return "", "", merrors.InvalidArgument("idempotency_key is required")
	}

	fingerprint := strings.TrimSpace(in.Fingerprint)
	if fingerprint == "" {
		return "", "", merrors.InvalidArgument("fingerprint is required")
	}

	return idempotencyKey, fingerprint, nil
}

func paymentReqHash(payment *model.Payment) string {
	if payment == nil || payment.Metadata == nil {
		return ""
	}
	return strings.TrimSpace(payment.Metadata[reqHashMetaKey])
}

func setPaymentReqHash(payment *model.Payment, hash string) {
	if payment == nil {
		return
	}
	hash = strings.TrimSpace(hash)
	if hash == "" {
		return
	}
	if payment.Metadata == nil {
		payment.Metadata = map[string]string{}
	}
	payment.Metadata[reqHashMetaKey] = hash
}
