package agg

import (
	"strings"
	"time"

	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/db/storable"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	pm "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/mutil/mzap"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

const initialVersion uint64 = 1

type svc struct {
	logger mlogger.Logger
	now    func() time.Time
	newID  func() bson.ObjectID
}

func (s *svc) Create(in Input) (payment *Payment, err error) {
	logger := s.logger
	logger.Debug("Starting Create",
		mzap.ObjRef("organization_ref", in.OrganizationRef),
		zap.String("quotation_ref", strings.TrimSpace(in.QuotationRef)),
		zap.Int("steps_count", len(in.Steps)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if err != nil {
			logger.Warn("Failed to create", append(fields, zap.Error(err))...)
			return
		}
		if payment == nil {
			logger.Debug("Completed Create", append(fields, zap.Bool("payment_nil", true))...)
			return
		}
		fields = append(fields,
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("state", string(payment.State)),
			zap.Uint64("version", payment.Version),
		)
		logger.Debug("Completed Create", fields...)
	}(time.Now())

	if in.OrganizationRef.IsZero() {
		return nil, merrors.InvalidArgument("organization_id is required")
	}

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

	quotationRef := strings.TrimSpace(in.QuotationRef)
	if quotationRef == "" {
		return nil, merrors.InvalidArgument("quotation_ref is required")
	}

	if isEmptyIntentSnapshot(in.IntentSnapshot) {
		return nil, merrors.InvalidArgument("intent_snapshot is required")
	}

	if in.QuoteSnapshot == nil {
		return nil, merrors.InvalidArgument("quote_snapshot is required")
	}

	intentSnapshot, err := cloneIntentSnapshot(in.IntentSnapshot)
	if err != nil {
		return nil, err
	}
	quoteSnapshot, err := cloneQuoteSnapshot(in.QuoteSnapshot)
	if err != nil {
		return nil, err
	}
	if quoteSnapshot == nil {
		return nil, merrors.InvalidArgument("quote_snapshot is required")
	}

	if quoteRef := strings.TrimSpace(quoteSnapshot.QuoteRef); quoteRef == "" {
		quoteSnapshot.QuoteRef = quotationRef
	} else if quoteRef != quotationRef {
		return nil, merrors.InvalidArgument("quote_snapshot.quote_ref must match quotation_ref")
	}

	stepExecutions, err := buildInitialStepTelemetry(in.Steps)
	if err != nil {
		return nil, err
	}

	now := s.now().UTC()
	id := s.newID()

	payment = &Payment{
		Base: storable.Base{
			ID:        id,
			CreatedAt: now,
			UpdatedAt: now,
		},
		OrganizationBoundBase: pm.OrganizationBoundBase{
			OrganizationRef: in.OrganizationRef,
		},
		PaymentRef:       id.Hex(),
		IdempotencyKey:   idempotencyKey,
		QuotationRef:     quotationRef,
		ClientPaymentRef: strings.TrimSpace(in.ClientPaymentRef),
		IntentSnapshot:   intentSnapshot,
		QuoteSnapshot:    quoteSnapshot,
		State:            StateCreated,
		Version:          initialVersion,
		StepExecutions:   stepExecutions,
	}
	return payment, nil
}

func buildInitialStepTelemetry(shell []StepShell) ([]StepExecution, error) {
	if len(shell) == 0 {
		return nil, nil
	}

	seenRefs := make(map[string]struct{}, len(shell))
	out := make([]StepExecution, 0, len(shell))
	for i := range shell {
		stepRef := strings.TrimSpace(shell[i].StepRef)
		if stepRef == "" {
			return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].step_ref is required")
		}
		if _, exists := seenRefs[stepRef]; exists {
			return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].step_ref must be unique")
		}
		seenRefs[stepRef] = struct{}{}

		stepCode := strings.TrimSpace(shell[i].StepCode)
		if stepCode == "" {
			return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].step_code is required")
		}
		visibility := model.NormalizeReportVisibility(shell[i].ReportVisibility)
		if !model.IsValidReportVisibility(visibility) {
			return nil, merrors.InvalidArgument("steps[" + itoa(i) + "].report_visibility is invalid")
		}
		userLabel := strings.TrimSpace(shell[i].UserLabel)
		railValue := model.ParseRail(string(shell[i].Rail))
		gatewayID := strings.TrimSpace(shell[i].Gateway)
		instanceID := strings.TrimSpace(shell[i].InstanceID)

		out = append(out, StepExecution{
			StepRef:               stepRef,
			StepCode:              stepCode,
			Rail:                  railValue,
			Gateway:               gatewayID,
			InstanceID:            instanceID,
			ReportVisibility:      visibility,
			UserLabel:             userLabel,
			Comment:               strings.TrimSpace(shell[i].Comment),
			State:                 StepStatePending,
			Attempt:               1,
			PlannedMoney:          svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedMoney),
			PlannedConvertedMoney: svcshared.CloneMoneyTrimNonEmpty(shell[i].PlannedConvertedMoney),
		})
	}
	return out, nil
}

func cloneIntentSnapshot(src model.PaymentIntent) (model.PaymentIntent, error) {
	var dst model.PaymentIntent
	if err := bsonClone(src, &dst); err != nil {
		return model.PaymentIntent{}, err
	}
	return dst, nil
}

func cloneQuoteSnapshot(src *model.PaymentQuoteSnapshot) (*model.PaymentQuoteSnapshot, error) {
	if src == nil {
		return nil, nil //nolint:nilnil // nil snapshot means quote data is intentionally absent
	}
	dst := &model.PaymentQuoteSnapshot{}
	if err := bsonClone(src, dst); err != nil {
		return nil, err
	}
	return dst, nil
}

func bsonClone(src any, dst any) error {
	data, err := bson.Marshal(src)
	if err != nil {
		return err
	}
	return bson.Unmarshal(data, dst)
}

func isEmptyIntentSnapshot(intent model.PaymentIntent) bool {
	return intent.Amount == nil && (intent.Kind == "" || intent.Kind == model.PaymentKindUnspecified)
}

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:])
}
