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/oobs"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/prepo"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/sexec"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/ssched"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/xplan"
	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/pkg/merrors"
	"go.uber.org/zap"
)

func (s *svc) runRuntime(ctx context.Context, payment *agg.Payment) (*agg.Payment, error) {
	logger := s.logger
	paymentRef := ""
	state := agg.StateUnspecified
	stepCount := 0
	if payment != nil {
		paymentRef = strings.TrimSpace(payment.PaymentRef)
		state = payment.State
		stepCount = len(payment.StepExecutions)
	}
	logger.Debug("Starting payment runtime loop",
		zap.String("payment_ref", paymentRef),
		zap.String("state", string(state)),
		zap.Int("steps_count", stepCount),
	)

	if payment == nil {
		return nil, merrors.InvalidArgument("payment is required")
	}
	if s.state.IsAggregateTerminal(payment.State) {
		logger.Debug("Skipping runtime loop because payment is already terminal",
			zap.String("payment_ref", paymentRef),
			zap.String("state", string(payment.State)),
		)
		return payment, nil
	}

	current := payment
	for tick := 0; tick < s.maxTicks; tick++ {
		graph, err := s.compileGraph(current)
		if err != nil {
			return nil, err
		}

		updated, changed, waitOnly, err := s.runTick(ctx, current, graph)
		if err != nil {
			return nil, err
		}
		logger.Debug("Processed runtime tick for payment",
			zap.String("payment_ref", paymentRef),
			zap.Int("tick", tick),
			zap.Bool("changed", changed),
			zap.Bool("wait_only", waitOnly),
			zap.String("state", string(updated.State)),
			zap.Uint64("version", updated.Version),
		)
		if !changed {
			return updated, nil
		}

		if s.state.IsAggregateTerminal(updated.State) {
			return updated, nil
		}
		if waitOnly {
			return updated, nil
		}
		current = updated
	}
	logger.Debug("Stopped runtime loop after reaching max ticks",
		zap.String("payment_ref", paymentRef),
		zap.Int("max_ticks", s.maxTicks),
		zap.String("state", string(current.State)),
		zap.Uint64("version", current.Version),
	)
	return current, nil
}

func (s *svc) runTick(ctx context.Context, payment *agg.Payment, graph *xplan.Graph) (*agg.Payment, bool, bool, error) {
	logger := s.logger
	expectedVersion := payment.Version
	previousAggregateState := payment.State

	scheduled, err := s.scheduler.Schedule(ssched.Input{
		Steps:          graph.Steps,
		StepExecutions: payment.StepExecutions,
		Retry:          s.retryPolicy,
	})
	if err != nil {
		return nil, false, false, err
	}
	logger.Debug("Calculated runnable, blocked, and skipped steps for tick",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		zap.Int("runnable_count", len(scheduled.Runnable)),
		zap.Int("blocked_count", len(scheduled.Blocked)),
		zap.Int("skipped_count", len(scheduled.Skipped)),
	)

	changed := mergeScheduledExecutions(payment, scheduled.StepExecutions)
	if changed {
		if err := s.recordScheduleTransitions(ctx, payment, scheduled.StepExecutions); err != nil {
			return nil, false, false, err
		}
	}

	for i := range scheduled.Runnable {
		stepChanged, runErr := s.executeRunnable(ctx, payment, graph, scheduled.Runnable[i])
		if runErr != nil {
			return nil, false, false, runErr
		}
		changed = changed || stepChanged
	}

	aggChanged, err := s.recomputeAggregateState(ctx, payment)
	if err != nil {
		return nil, false, false, err
	}
	changed = changed || aggChanged
	if !changed {
		return payment, false, len(scheduled.Runnable) == 0, nil
	}

	if err := s.repository.UpdateCAS(ctx, payment, expectedVersion); err != nil {
		if errors.Is(err, prepo.ErrVersionConflict) {
			logger.Debug("Detected version conflict while persisting tick; reloading payment",
				zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
				zap.Uint64("expected_version", expectedVersion),
			)
			fresh, reloadErr := s.repository.GetByPaymentRef(ctx, payment.OrganizationRef, payment.PaymentRef)
			if reloadErr != nil {
				return nil, false, false, reloadErr
			}
			return fresh, true, false, nil
		}
		return nil, false, false, err
	}
	logger.Debug("Persisted tick updates to payment",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		zap.Uint64("version", payment.Version),
		zap.String("state", string(payment.State)),
	)
	if aggChanged && payment.State != previousAggregateState {
		if err := s.statuses.Publish(ctx, paymentStatusPublishInput{
			Payment:       payment,
			PreviousState: previousAggregateState,
			CurrentState:  payment.State,
			OccurredAt:    s.nowUTC(),
			Event:         "state_changed",
		}); err != nil {
			logger.Warn("Failed to publish payment status update",
				zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
				zap.String("from_state", string(previousAggregateState)),
				zap.String("to_state", string(payment.State)),
				zap.Error(err),
			)
		}
	}
	return payment, true, len(scheduled.Runnable) == 0, nil
}

func (s *svc) compileGraph(payment *agg.Payment) (*xplan.Graph, error) {
	return s.planner.Compile(xplan.Input{
		IntentSnapshot: payment.IntentSnapshot,
		QuoteSnapshot:  payment.QuoteSnapshot,
	})
}

func mergeScheduledExecutions(payment *agg.Payment, updated []agg.StepExecution) bool {
	if payment == nil {
		return false
	}
	if len(updated) == 0 {
		return false
	}
	changed := false
	index := stepIndexByRef(payment.StepExecutions)
	for i := range updated {
		step := updated[i]
		idx, ok := index[strings.TrimSpace(step.StepRef)]
		if !ok {
			continue
		}
		if !stepExecutionEqual(payment.StepExecutions[idx], step) {
			payment.StepExecutions[idx] = step
			changed = true
		}
	}
	return changed
}

func (s *svc) recordScheduleTransitions(ctx context.Context, payment *agg.Payment, current []agg.StepExecution) error {
	_ = current
	for i := range payment.StepExecutions {
		step := payment.StepExecutions[i]
		if step.State != agg.StepStateSkipped && step.State != agg.StepStateNeedsAttention {
			continue
		}
		event := oobs.StepEventSkipped
		if step.State == agg.StepStateNeedsAttention {
			event = oobs.StepEventBlocked
		}
		if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
			PaymentRef: payment.PaymentRef,
			Step:       step,
			Event:      event,
		}); err != nil {
			return err
		}
	}
	return nil
}

func (s *svc) executeRunnable(ctx context.Context, payment *agg.Payment, graph *xplan.Graph, runnable ssched.RunnableStep) (bool, error) {
	logger := s.logger
	logger.Debug("Starting step execution attempt",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		zap.String("step_ref", strings.TrimSpace(runnable.StepRef)),
		zap.String("step_code", strings.TrimSpace(runnable.StepCode)),
		zap.Uint32("attempt", runnable.Attempt),
	)

	idx, ok := findStepExecution(payment.StepExecutions, runnable.StepRef)
	if !ok {
		return false, merrors.InvalidArgument("step execution not found: " + runnable.StepRef)
	}
	stepExecution := payment.StepExecutions[idx]
	stepExecution.Attempt = runnable.Attempt

	if stepExecution.State != agg.StepStateRunning {
		if err := s.state.EnsureStepTransition(stepExecution.State, agg.StepStateRunning); err != nil {
			stepExecution.State = agg.StepStateNeedsAttention
			stepExecution.FailureCode = "step.transition_invalid"
			stepExecution.FailureMsg = err.Error()
		} else {
			stepExecution.State = agg.StepStateRunning
		}
		now := s.nowUTC()
		stepExecution.StartedAt = &now
		stepExecution.CompletedAt = nil
		if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
			PaymentRef: payment.PaymentRef,
			Step:       stepExecution,
			Event:      oobs.StepEventStarted,
		}); err != nil {
			return false, err
		}
	}

	step, ok := findGraphStep(graph, runnable.StepRef)
	if !ok {
		return false, merrors.InvalidArgument("graph step not found: " + runnable.StepRef)
	}
	out, err := s.executors.Execute(ctx, sexec.ExecuteInput{
		Payment:       payment,
		Step:          step,
		StepExecution: stepExecution,
	})
	if err != nil {
		logger.Warn("Step executor returned error; marking step as failed",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", strings.TrimSpace(runnable.StepRef)),
			zap.Uint32("attempt", runnable.Attempt),
			zap.Error(err),
		)
		failed := markStepFailed(stepExecution, "step.executor_error", err.Error(), s.nowUTC())
		payment.StepExecutions[idx] = failed
		if obsErr := s.observer.RecordStep(ctx, oobs.RecordStepInput{
			PaymentRef: payment.PaymentRef,
			Step:       failed,
			Event:      oobs.StepEventFailed,
		}); obsErr != nil {
			return false, obsErr
		}
		return true, nil
	}

	next := normalizeExecutorOutput(stepExecution, out, s.nowUTC())
	payment.StepExecutions[idx] = next
	logger.Debug("Completed step execution attempt",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		zap.String("step_ref", strings.TrimSpace(next.StepRef)),
		zap.String("state", string(next.State)),
		zap.Uint32("attempt", next.Attempt),
	)
	if next.State == agg.StepStateCompleted || next.State == agg.StepStateFailed || next.State == agg.StepStateNeedsAttention {
		event := oobs.StepEventCompleted
		if next.State != agg.StepStateCompleted {
			event = oobs.StepEventFailed
		}
		if err := s.observer.RecordStep(ctx, oobs.RecordStepInput{
			PaymentRef: payment.PaymentRef,
			Step:       next,
			Event:      event,
			Duration:   stepDuration(next),
		}); err != nil {
			return false, err
		}
	}
	return true, nil
}

func normalizeExecutorOutput(current agg.StepExecution, out *sexec.ExecuteOutput, now time.Time) agg.StepExecution {
	if out == nil {
		next := current
		next.State = agg.StepStateCompleted
		next.CompletedAt = &now
		return next
	}
	next := current
	if out.StepExecution.StepRef != "" {
		next.StepRef = out.StepExecution.StepRef
	}
	if out.StepExecution.StepCode != "" {
		next.StepCode = out.StepExecution.StepCode
	}
	if out.StepExecution.Attempt != 0 {
		next.Attempt = out.StepExecution.Attempt
	}
	next.Comment = strings.TrimSpace(out.StepExecution.Comment)
	next.ExternalRefs = out.StepExecution.ExternalRefs
	next.ExecutedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ExecutedMoney)
	next.ConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.ConvertedMoney)
	next.PlannedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedMoney)
	next.PlannedConvertedMoney = svcshared.CloneMoneyTrimNonEmpty(out.StepExecution.PlannedConvertedMoney)
	next.FailureCode = strings.TrimSpace(out.StepExecution.FailureCode)
	next.FailureMsg = strings.TrimSpace(out.StepExecution.FailureMsg)

	switch out.StepExecution.State {
	case agg.StepStateCompleted, agg.StepStateFailed, agg.StepStateNeedsAttention, agg.StepStateSkipped:
		next.State = out.StepExecution.State
	case agg.StepStateRunning:
		next.State = agg.StepStateRunning
	default:
		if out.Async {
			next.State = agg.StepStateRunning
		} else {
			next.State = agg.StepStateCompleted
		}
	}

	if next.StartedAt == nil {
		next.StartedAt = &now
	}
	if next.State == agg.StepStateRunning {
		next.CompletedAt = nil
	} else if next.CompletedAt == nil {
		next.CompletedAt = &now
	}
	return next
}

func markStepFailed(step agg.StepExecution, code, message string, now time.Time) agg.StepExecution {
	step.State = agg.StepStateFailed
	step.FailureCode = strings.TrimSpace(code)
	step.FailureMsg = strings.TrimSpace(message)
	if step.StartedAt == nil {
		step.StartedAt = &now
	}
	step.CompletedAt = &now
	return step
}

func stepDuration(step agg.StepExecution) time.Duration {
	if step.StartedAt == nil || step.CompletedAt == nil {
		return 0
	}
	if step.CompletedAt.Before(*step.StartedAt) {
		return 0
	}
	return step.CompletedAt.Sub(*step.StartedAt)
}

func stepIndexByRef(steps []agg.StepExecution) map[string]int {
	out := make(map[string]int, len(steps))
	for i := range steps {
		out[strings.TrimSpace(steps[i].StepRef)] = i
	}
	return out
}

func findStepExecution(steps []agg.StepExecution, stepRef string) (int, bool) {
	ref := strings.TrimSpace(stepRef)
	for i := range steps {
		if strings.TrimSpace(steps[i].StepRef) == ref {
			return i, true
		}
	}
	return 0, false
}

func findGraphStep(graph *xplan.Graph, stepRef string) (xplan.Step, bool) {
	if graph == nil {
		return xplan.Step{}, false
	}
	ref := strings.TrimSpace(stepRef)
	for i := range graph.Steps {
		if strings.TrimSpace(graph.Steps[i].StepRef) == ref {
			return graph.Steps[i], true
		}
	}
	return xplan.Step{}, false
}

func stepExecutionEqual(left, right agg.StepExecution) bool {
	if left.StepRef != right.StepRef || left.StepCode != right.StepCode {
		return false
	}
	if left.Rail != right.Rail {
		return false
	}
	if strings.TrimSpace(left.Gateway) != strings.TrimSpace(right.Gateway) {
		return false
	}
	if strings.TrimSpace(left.InstanceID) != strings.TrimSpace(right.InstanceID) {
		return false
	}
	if left.State != right.State || left.Attempt != right.Attempt {
		return false
	}
	if strings.TrimSpace(left.FailureCode) != strings.TrimSpace(right.FailureCode) {
		return false
	}
	if strings.TrimSpace(left.FailureMsg) != strings.TrimSpace(right.FailureMsg) {
		return false
	}
	if strings.TrimSpace(left.Comment) != strings.TrimSpace(right.Comment) {
		return false
	}
	if !timePtrEqual(left.StartedAt, right.StartedAt) || !timePtrEqual(left.CompletedAt, right.CompletedAt) {
		return false
	}
	if len(left.ExternalRefs) != len(right.ExternalRefs) {
		return false
	}
	for i := range left.ExternalRefs {
		if left.ExternalRefs[i] != right.ExternalRefs[i] {
			return false
		}
	}
	if !stepMoneyEqual(left.ExecutedMoney, right.ExecutedMoney) {
		return false
	}
	if !stepMoneyEqual(left.ConvertedMoney, right.ConvertedMoney) {
		return false
	}
	if !stepMoneyEqual(left.PlannedMoney, right.PlannedMoney) {
		return false
	}
	if !stepMoneyEqual(left.PlannedConvertedMoney, right.PlannedConvertedMoney) {
		return false
	}
	return true
}

func timePtrEqual(left *time.Time, right *time.Time) bool {
	if left == nil && right == nil {
		return true
	}
	if left == nil || right == nil {
		return false
	}
	return left.UTC().Equal(right.UTC())
}
