package psvc

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

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

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/oobs"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/pkg/merrors"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

func (s *svc) ReconcileExternal(ctx context.Context, in ReconcileExternalInput) (out *ReconcileExternalOutput, err error) {
	logger := s.logger
	logger.Debug("Starting Reconcile external",
		zap.String("organization_ref", strings.TrimSpace(in.OrganizationRef)),
		zap.String("payment_ref", strings.TrimSpace(in.PaymentRef)),
		zap.String("event_source", externalEventSource(in.Event)),
		zap.String("event_status", externalEventStatus(in.Event)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil && out.Payment != nil {
			fields = append(fields,
				zap.String("state", out.Payment.GetState().String()),
				zap.Uint64("version", out.Payment.GetVersion()),
			)
		}
		if err != nil {
			logger.Warn("Failed to reconcile external", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Reconcile external", fields...)
	}(time.Now())

	orgRef := strings.TrimSpace(in.OrganizationRef)
	if orgRef == "" {
		return nil, merrors.InvalidArgument("organization_ref is required")
	}
	orgID, err := bson.ObjectIDFromHex(orgRef)
	if err != nil {
		return nil, merrors.InvalidArgument("organization_ref must be a valid objectID")
	}
	paymentRef, err := parsePaymentRef(in.PaymentRef)
	if err != nil {
		return nil, err
	}

	payment, err := s.repository.GetByPaymentRef(ctx, orgID, paymentRef)
	if err != nil {
		return nil, err
	}
	if payment == nil {
		return nil, prepo.ErrPaymentNotFound
	}

	reconOut, err := s.reconciler.Reconcile(erecon.Input{
		Payment: payment,
		Event:   in.Event,
	})
	if err != nil {
		return nil, err
	}
	if reconOut == nil || reconOut.Payment == nil {
		return nil, merrors.Internal("reconciler returned nil payment")
	}

	if err := s.recordExternal(ctx, reconOut.Payment, in.Event, reconOut.MatchedStepRef); err != nil {
		return nil, err
	}
	if reconOut.StepChanged || reconOut.AggregateChanged {
		if err := s.repository.UpdateCAS(ctx, reconOut.Payment, payment.Version); err != nil {
			if errors.Is(err, prepo.ErrVersionConflict) {
				fresh, reloadErr := s.repository.GetByPaymentRef(ctx, orgID, paymentRef)
				if reloadErr != nil {
					return nil, reloadErr
				}
				reconOut.Payment = fresh
			} else {
				return nil, err
			}
		}
	}

	advanced, err := s.runRuntime(ctx, reconOut.Payment)
	if err != nil {
		return nil, err
	}
	mapped, err := s.mapPayment(advanced)
	if err != nil {
		return nil, err
	}
	out = &ReconcileExternalOutput{Payment: mapped}
	return out, nil
}

func (s *svc) recordExternal(ctx context.Context, payment *agg.Payment, event erecon.Event, matchedStepRef string) error {
	input, ok := buildExternalRecordInput(payment, event, matchedStepRef)
	if !ok {
		return nil
	}
	return s.observer.RecordExternal(ctx, input)
}

func buildExternalRecordInput(payment *agg.Payment, event erecon.Event, matchedStepRef string) (oobs.RecordExternalInput, bool) {
	if payment == nil {
		return oobs.RecordExternalInput{}, false
	}
	stepRef := strings.TrimSpace(matchedStepRef)
	if stepRef == "" {
		if event.Gateway != nil {
			stepRef = strings.TrimSpace(event.Gateway.StepRef)
		} else if event.Ledger != nil {
			stepRef = strings.TrimSpace(event.Ledger.StepRef)
		} else if event.Card != nil {
			stepRef = strings.TrimSpace(event.Card.StepRef)
		}
	}
	if stepRef == "" {
		return oobs.RecordExternalInput{}, false
	}
	attempt := stepAttempt(payment.StepExecutions, stepRef)
	if attempt == 0 {
		attempt = 1
	}

	in := oobs.RecordExternalInput{
		PaymentRef: payment.PaymentRef,
		StepRef:    stepRef,
		Attempt:    attempt,
	}
	switch {
	case event.Gateway != nil:
		in.Source = oobs.ExternalSourceGateway
		in.Status = strings.TrimSpace(string(event.Gateway.Status))
		in.RefKind = erecon.ExternalRefKindOperation
		in.Ref = firstNonEmpty(event.Gateway.OperationRef, event.Gateway.TransferRef)
		if strings.TrimSpace(event.Gateway.TransferRef) != "" {
			in.RefKind = erecon.ExternalRefKindTransfer
		}
		in.Message = strings.TrimSpace(event.Gateway.FailureMsg)
	case event.Ledger != nil:
		in.Source = oobs.ExternalSourceLedger
		in.Status = strings.TrimSpace(string(event.Ledger.Status))
		in.RefKind = erecon.ExternalRefKindLedger
		in.Ref = strings.TrimSpace(event.Ledger.EntryRef)
		in.Message = strings.TrimSpace(event.Ledger.FailureMsg)
	case event.Card != nil:
		in.Source = oobs.ExternalSourceCard
		in.Status = strings.TrimSpace(string(event.Card.Status))
		in.RefKind = erecon.ExternalRefKindCardPayout
		in.Ref = strings.TrimSpace(event.Card.PayoutRef)
		in.Message = strings.TrimSpace(event.Card.FailureMsg)
	default:
		return oobs.RecordExternalInput{}, false
	}
	return in, true
}

func stepAttempt(steps []agg.StepExecution, stepRef string) uint32 {
	ref := strings.TrimSpace(stepRef)
	for i := range steps {
		if strings.TrimSpace(steps[i].StepRef) == ref {
			return steps[i].Attempt
		}
	}
	return 0
}

func firstNonEmpty(values ...string) string {
	for i := range values {
		if val := strings.TrimSpace(values[i]); val != "" {
			return val
		}
	}
	return ""
}

func externalEventSource(event erecon.Event) string {
	switch {
	case event.Gateway != nil:
		return "gateway"
	case event.Ledger != nil:
		return "ledger"
	case event.Card != nil:
		return "card"
	default:
		return "unknown"
	}
}

func externalEventStatus(event erecon.Event) string {
	switch {
	case event.Gateway != nil:
		return strings.TrimSpace(string(event.Gateway.Status))
	case event.Ledger != nil:
		return strings.TrimSpace(string(event.Ledger.Status))
	case event.Card != nil:
		return strings.TrimSpace(string(event.Card.Status))
	default:
		return ""
	}
}
