package orchestrator

import (
	"context"
	"fmt"
	"strings"

	paymodel "github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	cons "github.com/tech/sendico/pkg/messaging/consumer"
	paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
	np "github.com/tech/sendico/pkg/messaging/notifications/processor"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/payments/rail"
	"go.uber.org/zap"
)

func (s *Service) startGatewayConsumers() {
	if s == nil || s.gatewayBroker == nil {
		s.logger.Warn("Missing broker. Gateway feedback consumer has NOT started")
		return
	}
	s.logger.Info("Gateway feedback consumer started")
	processor := paymentgateway.NewPaymentGatewayExecutionProcessor(s.logger, s.onGatewayExecution)
	s.consumeGatewayProcessor(processor)
}

func (s *Service) consumeGatewayProcessor(processor np.EnvelopeProcessor) {
	consumer, err := cons.NewConsumer(s.logger, s.gatewayBroker, processor.GetSubject())
	if err != nil {
		s.logger.Warn("Failed to create payment gateway consumer", zap.Error(err), zap.String("event", processor.GetSubject().ToString()))
		return
	}
	s.gatewayConsumers = append(s.gatewayConsumers, consumer)
	go func() {
		if err := consumer.ConsumeMessages(processor.Process); err != nil {
			s.logger.Warn("Payment gateway consumer stopped", zap.Error(err), zap.String("event", processor.GetSubject().ToString()))
		}
	}()
}

func executionPlanSucceeded(plan *paymodel.ExecutionPlan) bool {
	for _, s := range plan.Steps {
		if !s.IsTerminal() {
			return false
		}
		if s.State != paymodel.OperationStateSuccess {
			return false
		}
	}
	return true
}

func executionPlanFailed(plan *paymodel.ExecutionPlan) bool {
	hasFailed := false

	for _, s := range plan.Steps {
		if !s.IsTerminal() {
			return false
		}
		if s.State == paymodel.OperationStateFailed {
			hasFailed = true
		}
	}

	return hasFailed
}

func (s *Service) onGatewayExecution(ctx context.Context, exec *model.PaymentGatewayExecution) error {
	if exec == nil {
		return merrors.InvalidArgument("payment gateway execution is nil", "execution")
	}

	paymentRef := strings.TrimSpace(exec.PaymentRef)
	if paymentRef == "" {
		return merrors.InvalidArgument("payment_ref is required", "payment_ref")
	}

	store := s.storage.Payments()

	payment, err := store.GetByPaymentRef(ctx, paymentRef)
	if err != nil {
		s.logger.Warn("Failed to fetch payment from database", zap.Error(err))
		return err
	}

	// --- metadata
	if payment.Metadata == nil {
		payment.Metadata = map[string]string{}
	}
	payment.Metadata["gateway_operation_result"] = string(exec.Status)
	payment.Metadata["gateway_operation_ref"] = exec.OperationRef
	payment.Metadata["gateway_request_idempotency"] = exec.IdempotencyKey

	// --- update exactly ONE step

	if payment.State, err = updateExecutionStepsFromGatewayExecution(s.logger, payment, exec); err != nil {
		s.logger.Warn("No execution step matched gateway result",
			zap.String("payment_ref", paymentRef),
			zap.String("operation_ref", exec.OperationRef),
			zap.String("idempotency", exec.IdempotencyKey),
		)
	}

	if err := store.Update(ctx, payment); err != nil {
		return err
	}

	// reload unified state
	payment, err = store.GetByPaymentRef(ctx, paymentRef)
	if err != nil {
		return err
	}

	// --- if plan can continue — continue
	if payment.ExecutionPlan != nil && !executionPlanComplete(payment.ExecutionPlan) {
		return s.resumePaymentPlan(ctx, store, payment)
	}

	// --- plan is terminal: decide payment fate by aggregation
	if payment.ExecutionPlan != nil && executionPlanComplete(payment.ExecutionPlan) {
		switch {
		case executionPlanSucceeded(payment.ExecutionPlan):
			payment.State = paymodel.PaymentStateSettled

		case executionPlanFailed(payment.ExecutionPlan):
			payment.State = paymodel.PaymentStateFailed
			payment.FailureReason = "execution_plan_failed"
		}

		return store.Update(ctx, payment)
	}

	return nil
}

func updateExecutionStepsFromGatewayExecution(
	logger mlogger.Logger,
	payment *paymodel.Payment,
	exec *model.PaymentGatewayExecution,
) (paymodel.PaymentState, error) {

	log := logger.With(
		zap.String("payment_ref", payment.PaymentRef),
		zap.String("operation_ref", strings.TrimSpace(exec.OperationRef)),
		zap.String("gateway_status", string(exec.Status)),
	)

	log.Debug("gateway execution received")

	if payment == nil || payment.PaymentPlan == nil || exec == nil {
		log.Warn("invalid input: payment/plan/exec is nil")
		return paymodel.PaymentStateSubmitted,
			merrors.DataConflict("payment is missing plan or execution step")
	}

	operationRef := strings.TrimSpace(exec.OperationRef)
	if operationRef == "" {
		log.Warn("empty operation_ref from gateway")
		return paymodel.PaymentStateSubmitted,
			merrors.InvalidArgument("no operation reference provided")
	}

	execPlan := ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
	if execPlan == nil {
		log.Warn("Execution plan missing")
		return paymodel.PaymentStateSubmitted, merrors.InvalidArgument("execution plan missing")
	}

	status := executionStepStatusFromGatewayStatus(exec.Status)
	if status == "" {
		log.Warn("Unknown gateway status")
		return paymodel.PaymentStateSubmitted,
			merrors.DataConflict(fmt.Sprintf("unknown gateway status: %s", exec.Status))
	}

	var matched bool

	for idx, execStep := range execPlan.Steps {
		if execStep == nil {
			continue
		}

		if strings.EqualFold(strings.TrimSpace(execStep.OperationRef), operationRef) {

			log.Debug("Execution step matched",
				zap.Int("step_index", idx),
				zap.String("step_code", execStep.Code),
				zap.String("prev_state", string(execStep.State)),
			)

			if execStep.TransferRef == "" && exec.TransferRef != "" {
				execStep.TransferRef = strings.TrimSpace(exec.TransferRef)
				log.Debug("Transfer_ref attached to step", zap.String("transfer_ref", execStep.TransferRef))
			}

			setExecutionStepStatus(execStep, status)

			log.Debug("Execution step state updated",
				zap.Int("step_index", idx),
				zap.String("step_code", execStep.Code),
				zap.String("new_state", string(execStep.State)),
			)

			matched = true
			break
		}
	}

	if !matched {
		log.Warn("No execution step found for operation_ref")
		return paymodel.PaymentStateSubmitted,
			merrors.InvalidArgument(
				fmt.Sprintf("execution step not found for operation reference: %s", operationRef),
			)
	}

	// -------- GLOBAL REDUCTION --------

	var (
		hasSuccess bool
		allDone    = true
	)

	for idx, step := range execPlan.Steps {
		if step == nil {
			continue
		}

		log.Debug("Evaluating step for payment state",
			zap.Int("step_index", idx),
			zap.String("step_code", step.Code),
			zap.String("step_state", string(step.State)),
		)

		switch step.State {
		case paymodel.OperationStateFailed:
			payment.FailureReason = step.Error
			log.Info("Payment marked as FAILED due to step failure",
				zap.String("failed_step_code", step.Code),
				zap.String("error", step.Error),
			)
			return paymodel.PaymentStateFailed, nil

		case paymodel.OperationStateSuccess:
			hasSuccess = true

		case paymodel.OperationStateSkipped:
			// ok

		default:
			allDone = false
		}
	}

	if hasSuccess && allDone {
		log.Info("Payment marked as SUCCESS (all steps completed)")
		return paymodel.PaymentStateSuccess, nil
	}

	log.Info("Payment still PROCESSING (steps not finished)")
	return paymodel.PaymentStateSubmitted, nil
}

func executionStepStatusFromGatewayStatus(status rail.OperationResult) paymodel.OperationState {
	switch status {

	case rail.OperationResultSuccess:
		return paymodel.OperationStateSuccess

	case rail.OperationResultFailed:
		return paymodel.OperationStateFailed

	case rail.OperationResultCancelled:
		return paymodel.OperationStateCancelled

	default:
		return paymodel.OperationStateFailed
	}
}

func (s *Service) Shutdown() {
	if s == nil {
		return
	}
	for _, consumer := range s.gatewayConsumers {
		if consumer != nil {
			consumer.Close()
		}
	}
}
