package erecon

import (
	"strings"
	"time"

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

	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ostate"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xerr"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.mongodb.org/mongo-driver/v2/bson"
	"go.uber.org/zap"
)

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

func defaultNow() time.Time {
	return time.Now().UTC()
}

func (s *svc) Reconcile(in Input) (out *Output, err error) {
	logger := s.logger
	paymentRef := ""
	if in.Payment != nil {
		paymentRef = strings.TrimSpace(in.Payment.PaymentRef)
	}
	logger.Debug("Starting Reconcile",
		zap.String("payment_ref", paymentRef),
		zap.String("event_source", eventSource(in.Event)),
	)
	defer func(start time.Time) {
		fields := []zap.Field{zap.Int64("duration_ms", time.Since(start).Milliseconds())}
		if out != nil {
			fields = append(fields,
				zap.String("matched_step_ref", strings.TrimSpace(out.MatchedStepRef)),
				zap.Bool("step_changed", out.StepChanged),
				zap.Bool("aggregate_changed", out.AggregateChanged),
			)
			if out.Payment != nil {
				fields = append(fields,
					zap.String("payment_state", string(out.Payment.State)),
					zap.Uint64("version", out.Payment.Version),
				)
			}
		}
		if err != nil {
			logger.Warn("Failed to reconcile", append(fields, zap.Error(err))...)
			return
		}
		logger.Debug("Completed Reconcile", fields...)
	}(time.Now())

	if in.Payment == nil {
		return nil, merrors.InvalidArgument("payment is required")
	}
	if len(in.Payment.StepExecutions) == 0 {
		return nil, merrors.InvalidArgument("payment.step_executions are required")
	}

	event, err := normalizeEvent(in.Event)
	if err != nil {
		return nil, err
	}

	payment, err := clonePayment(in.Payment)
	if err != nil {
		return nil, err
	}

	idx, err := findStepIndex(payment, event)
	if err != nil {
		return nil, err
	}

	sm := ostate.New(ostate.Dependencies{Logger: logger.Named("ostate")})
	stepChanged, err := s.applyStepEvent(&payment.StepExecutions[idx], event, sm)
	if err != nil {
		return nil, err
	}

	aggregateChanged, err := reduceAggregateState(payment, event, sm)
	if err != nil {
		return nil, err
	}

	if stepChanged || aggregateChanged {
		payment.Version++
		payment.UpdatedAt = s.now().UTC()
	}

	out = &Output{
		Payment:          payment,
		MatchedStepRef:   payment.StepExecutions[idx].StepRef,
		StepChanged:      stepChanged,
		AggregateChanged: aggregateChanged,
	}
	return out, nil
}

func (s *svc) applyStepEvent(step *agg.StepExecution, event *normalizedEvent, sm ostate.StateMachine) (bool, error) {
	if step == nil || event == nil {
		return false, nil
	}

	changed := false
	out := *step

	refs, refsChanged := mergeExternalRefs(out.ExternalRefs, event.appendRefs)
	if refsChanged {
		out.ExternalRefs = refs
		changed = true
	}

	target := event.targetState
	if target == agg.StepStateUnspecified {
		*step = out
		return changed, nil
	}

	if out.State == target {
		changed = s.applyStepDiagnostics(&out, event) || changed
		changed = applyExecutedMoney(&out, event) || changed
		*step = out
		return changed, nil
	}

	if sm.IsStepTerminal(out.State) {
		*step = out
		return changed, nil
	}

	next, transitionChanged, err := transitionStepState(out, target, sm)
	if err != nil {
		return false, err
	}
	out = next
	changed = changed || transitionChanged
	changed = s.applyStepDiagnostics(&out, event) || changed
	changed = applyExecutedMoney(&out, event) || changed

	*step = out
	return changed, nil
}

func applyExecutedMoney(step *agg.StepExecution, event *normalizedEvent) bool {
	if step == nil || event == nil {
		return false
	}
	money := normalizeEventMoney(event.executedMoney)
	if money == nil {
		return false
	}
	if normalizeEventMoney(step.ExecutedMoney) != nil {
		return false
	}
	if stepMoneyEqual(step.ExecutedMoney, money) {
		return false
	}
	step.ExecutedMoney = money
	return true
}

func stepMoneyEqual(left, right *paymenttypes.Money) bool {
	left = normalizeEventMoney(left)
	right = normalizeEventMoney(right)
	switch {
	case left == nil && right == nil:
		return true
	case left == nil || right == nil:
		return false
	default:
		return left.Amount == right.Amount && left.Currency == right.Currency
	}
}

func transitionStepState(step agg.StepExecution, target agg.StepState, sm ostate.StateMachine) (agg.StepExecution, bool, error) {
	if step.State == target {
		return step, false, nil
	}

	if sm.EnsureStepTransition(step.State, target) == nil {
		step.State = target
		return step, true, nil
	}

	original := step.State
	bridge := []agg.StepState{agg.StepStateRunning, target}
	for i := range bridge {
		next := bridge[i]
		if step.State == next {
			continue
		}
		if sm.EnsureStepTransition(step.State, next) != nil {
			return step, false, xerr.Wrapf(ErrStepTransitionInvalid, "%s -> %s", original, target)
		}
		step.State = next
	}
	return step, step.State != original, nil
}

func (s *svc) applyStepDiagnostics(step *agg.StepExecution, event *normalizedEvent) bool {
	if step == nil || event == nil {
		return false
	}

	now := s.now().UTC()
	at := now
	if eventAt := event.occurredAtValue(); eventAt != nil {
		at = eventAt.UTC()
	}

	changed := false
	switch step.State {
	case agg.StepStateRunning:
		if step.StartedAt == nil {
			step.StartedAt = &at
			changed = true
		}
		if step.CompletedAt != nil {
			step.CompletedAt = nil
			changed = true
		}
		if step.FailureCode != "" || step.FailureMsg != "" {
			step.FailureCode = ""
			step.FailureMsg = ""
			changed = true
		}

	case agg.StepStateCompleted:
		if step.StartedAt == nil {
			step.StartedAt = &at
			changed = true
		}
		if step.CompletedAt == nil || !step.CompletedAt.Equal(at) {
			step.CompletedAt = &at
			changed = true
		}
		if step.FailureCode != "" || step.FailureMsg != "" {
			step.FailureCode = ""
			step.FailureMsg = ""
			changed = true
		}

	case agg.StepStateFailed, agg.StepStateNeedsAttention:
		if step.StartedAt == nil {
			step.StartedAt = &at
			changed = true
		}
		if step.CompletedAt == nil || !step.CompletedAt.Equal(at) {
			step.CompletedAt = &at
			changed = true
		}
		fc := event.failureCodeValue()
		fm := event.failureMsgValue()
		if step.FailureCode != fc || step.FailureMsg != fm {
			step.FailureCode = fc
			step.FailureMsg = fm
			changed = true
		}
	}
	return changed
}

func clonePayment(payment *agg.Payment) (*agg.Payment, error) {
	data, err := bson.Marshal(payment)
	if err != nil {
		return nil, merrors.Internal("payment clone failed")
	}
	var out agg.Payment
	if err := bson.Unmarshal(data, &out); err != nil {
		return nil, merrors.Internal("payment clone failed")
	}
	return &out, nil
}

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