package orchestrator

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

	"github.com/tech/sendico/pkg/discovery"
	"github.com/tech/sendico/pkg/mutil/mzap"

	"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/prepo"
	"github.com/tech/sendico/payments/orchestrator/internal/service/orchestrationv2/psvc"
	svcshared "github.com/tech/sendico/payments/orchestrator/internal/service/shared"
	"github.com/tech/sendico/payments/storage/model"
	cons "github.com/tech/sendico/pkg/messaging/consumer"
	paymentgatewaynotifications "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
	pmodel "github.com/tech/sendico/pkg/model"
	"github.com/tech/sendico/pkg/payments/rail"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"go.uber.org/zap"
)

const (
	observePollInterval   = 5 * time.Second
	observePollPageLimit  = int32(100)
	observePollMaxPerTick = 200
	observeStepCodeToken  = ".observe"

	failureCodeGatewayExecutionFailed  = "gateway.execution_failed"
	failureCodeGatewayExecutionCancel  = "gateway.execution_cancelled"
	failureCodeGatewayTransferFailed   = "gateway.transfer_failed"
	failureCodeGatewayTransferCanceled = "gateway.transfer_cancelled"
)

type runningObserveCandidate struct {
	stepRef           string
	transferRef       string
	operationRef      string
	gatewayInstanceID string
}

func (s *Service) startExternalRuntime() {
	if s == nil || s.v2 == nil || s.paymentRepo == nil {
		return
	}

	runCtx, cancel := context.WithCancel(context.Background())
	started := false

	if s.paymentGatewayBroker != nil {
		processor := paymentgatewaynotifications.NewPaymentGatewayExecutionProcessor(s.logger, s.onPaymentGatewayExecution)
		consumer, err := cons.NewConsumer(s.logger, s.paymentGatewayBroker, processor.GetSubject())
		if err != nil {
			s.logger.Warn("Failed to start payment gateway execution consumer", zap.Error(err))
		} else {
			s.gatewayConsumers = append(s.gatewayConsumers, consumer)
			go func() {
				if err := consumer.ConsumeMessages(processor.Process); err != nil && !errors.Is(err, context.Canceled) {
					s.logger.Warn("Payment gateway execution consumer stopped", zap.Error(err))
				}
			}()
			started = true
		}
	}

	if s.gatewayInvokeResolver != nil && s.gatewayRegistry != nil {
		go s.observePollLoop(runCtx)
		started = true
	} else {
		s.logger.Warn("Observe polling fallback disabled: gateway resolver or registry is missing")
	}

	if started {
		s.stopExternalWorkers = cancel
		return
	}
	cancel()
}

func (s *Service) onPaymentGatewayExecution(ctx context.Context, msg *pmodel.PaymentGatewayExecution) error {
	if s == nil || s.v2 == nil || s.paymentRepo == nil || msg == nil {
		return nil
	}

	paymentRef := strings.TrimSpace(msg.PaymentRef)
	if paymentRef == "" {
		s.logger.Debug("Skipping payment gateway execution event without payment_ref")
		return nil
	}

	payment, err := s.paymentRepo.GetByPaymentRefGlobal(ctx, paymentRef)
	if err != nil {
		if errors.Is(err, prepo.ErrPaymentNotFound) {
			s.logger.Debug("Skipping payment gateway execution event for unknown payment",
				zap.String("payment_ref", paymentRef),
			)
			return nil
		}
		return err
	}

	event, ok := buildGatewayExecutionEvent(payment, msg)
	if !ok {
		s.logger.Debug("Dropping payment gateway execution event",
			zap.String("payment_ref", paymentRef),
			zap.String("status", strings.TrimSpace(string(msg.Status))),
			zap.String("operation_ref", strings.TrimSpace(msg.OperationRef)),
			zap.String("transfer_ref", strings.TrimSpace(msg.TransferRef)),
			zap.String("drop_reason", gatewayExecutionDropReason(payment, msg)),
		)
		return nil
	}

	s.logger.Debug("Reconciling payment from gateway execution event",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		mzap.ObjRef("organization_ref", payment.OrganizationRef),
		zap.String("step_ref", strings.TrimSpace(event.StepRef)),
		zap.String("status", strings.TrimSpace(string(event.Status))),
		zap.String("transfer_ref", strings.TrimSpace(event.TransferRef)),
		zap.String("operation_ref", strings.TrimSpace(event.OperationRef)),
	)

	_, err = s.v2.ReconcileExternal(ctx, psvc.ReconcileExternalInput{
		OrganizationRef: payment.OrganizationRef.Hex(),
		PaymentRef:      payment.PaymentRef,
		Event:           erecon.Event{Gateway: event},
	})
	return err
}

func buildGatewayExecutionEvent(payment *agg.Payment, msg *pmodel.PaymentGatewayExecution) (*erecon.GatewayEvent, bool) {
	if payment == nil || msg == nil {
		return nil, false
	}

	status, ok := mapGatewayExecutionStatus(msg.Status)
	if !ok {
		return nil, false
	}

	operationRef := strings.TrimSpace(msg.OperationRef)
	transferRef := strings.TrimSpace(msg.TransferRef)
	stepRef, gatewayInstanceID, matched := matchExecutionStep(payment, msg)
	// Drop unmatched events that include correlation refs. This prevents
	// unrelated gateway events (for the same payment_ref) from being applied to
	// a running observe step via fallback inference.
	if !matched && (operationRef != "" || transferRef != "") {
		return nil, false
	}
	if stepRef == "" && operationRef == "" && transferRef == "" {
		return nil, false
	}

	event := &erecon.GatewayEvent{
		StepRef:           stepRef,
		OperationRef:      operationRef,
		TransferRef:       transferRef,
		GatewayInstanceID: gatewayInstanceID,
		Status:            status,
		ExecutedMoney:     svcshared.CloneMoneyTrimNonEmpty(msg.ExecutedMoney),
	}

	switch status {
	case erecon.GatewayStatusFailed:
		retryable := false
		event.Retryable = &retryable
		event.FailureCode = failureCodeGatewayExecutionFailed
		event.FailureMsg = strings.TrimSpace(msg.Error)
	case erecon.GatewayStatusCancelled:
		retryable := false
		event.Retryable = &retryable
		event.FailureCode = failureCodeGatewayExecutionCancel
		event.FailureMsg = strings.TrimSpace(msg.Error)
	default:
		event.FailureCode = ""
		event.FailureMsg = ""
	}

	return event, true
}

func mapGatewayExecutionStatus(status rail.OperationResult) (erecon.GatewayStatus, bool) {
	switch strings.ToLower(strings.TrimSpace(string(status))) {
	case string(rail.OperationResultSuccess):
		return erecon.GatewayStatusSuccess, true
	case string(rail.OperationResultFailed):
		return erecon.GatewayStatusFailed, true
	case string(rail.OperationResultCancelled):
		return erecon.GatewayStatusCancelled, true
	default:
		return erecon.GatewayStatusUnspecified, false
	}
}

func matchExecutionStep(payment *agg.Payment, msg *pmodel.PaymentGatewayExecution) (stepRef string, gatewayInstanceID string, matched bool) {
	if payment == nil || msg == nil {
		return "", "", false
	}

	transferRef := strings.TrimSpace(msg.TransferRef)
	if transferRef != "" {
		if stepRef, gatewayInstanceID, ok := findStepByExternalRef(payment, erecon.ExternalRefKindTransfer, transferRef); ok {
			return stepRef, gatewayInstanceID, true
		}
		if stepRef, gatewayInstanceID, ok := findStepByExternalRef(payment, erecon.ExternalRefKindCardPayout, transferRef); ok {
			return stepRef, gatewayInstanceID, true
		}
	}

	operationRef := strings.TrimSpace(msg.OperationRef)
	if operationRef != "" {
		if stepRef, gatewayInstanceID, ok := findStepByExternalRef(payment, erecon.ExternalRefKindOperation, operationRef); ok {
			return stepRef, gatewayInstanceID, true
		}
	}

	// Fallback inference is allowed only when the event has no refs at all.
	// If refs are present but unmatched, treat it as unrelated and skip.
	if transferRef != "" || operationRef != "" {
		return "", "", false
	}

	candidates := runningObserveCandidates(payment)
	if len(candidates) == 1 {
		return candidates[0].stepRef, candidates[0].gatewayInstanceID, true
	}
	return "", "", false
}

func gatewayExecutionDropReason(payment *agg.Payment, msg *pmodel.PaymentGatewayExecution) string {
	if msg == nil {
		return "nil_event"
	}
	if _, ok := mapGatewayExecutionStatus(msg.Status); !ok {
		return "unsupported_status"
	}
	operationRef := strings.TrimSpace(msg.OperationRef)
	transferRef := strings.TrimSpace(msg.TransferRef)
	_, _, matched := matchExecutionStep(payment, msg)
	if (operationRef != "" || transferRef != "") && !matched {
		return "unmatched_refs"
	}
	if operationRef == "" && transferRef == "" && !matched {
		return "missing_refs_and_no_observe_candidate"
	}
	return "not_accepted"
}

func findStepByExternalRef(payment *agg.Payment, kind, ref string) (stepRef string, gatewayInstanceID string, ok bool) {
	if payment == nil {
		return "", "", false
	}
	kind = strings.TrimSpace(kind)
	ref = strings.TrimSpace(ref)
	if kind == "" || ref == "" {
		return "", "", false
	}

	type match struct {
		stepRef           string
		state             agg.StepState
		gatewayInstanceID string
	}
	var matches []match
	for i := range payment.StepExecutions {
		step := payment.StepExecutions[i]
		for j := range step.ExternalRefs {
			externalRef := step.ExternalRefs[j]
			if !strings.EqualFold(strings.TrimSpace(externalRef.Kind), kind) {
				continue
			}
			if !strings.EqualFold(strings.TrimSpace(externalRef.Ref), ref) {
				continue
			}
			matches = append(matches, match{
				stepRef:           strings.TrimSpace(step.StepRef),
				state:             step.State,
				gatewayInstanceID: strings.TrimSpace(externalRef.GatewayInstanceID),
			})
			break
		}
	}

	if len(matches) == 0 {
		return "", "", false
	}
	for i := range matches {
		if matches[i].state == agg.StepStateRunning {
			return matches[i].stepRef, matches[i].gatewayInstanceID, true
		}
	}
	return matches[0].stepRef, matches[0].gatewayInstanceID, true
}

func (s *Service) observePollLoop(ctx context.Context) {
	ticker := time.NewTicker(observePollInterval)
	defer ticker.Stop()

	s.logger.Info("Started observe polling fallback",
		zap.Duration("interval", observePollInterval),
		zap.Int32("page_limit", observePollPageLimit),
		zap.Int("max_per_tick", observePollMaxPerTick),
	)
	defer s.logger.Info("Stopped observe polling fallback")

	for {
		select {
		case <-ctx.Done():
			return
		case <-ticker.C:
			if err := s.pollObserveCandidates(ctx); err != nil && !errors.Is(err, context.Canceled) {
				s.logger.Warn("Observe polling fallback tick failed", zap.Error(err))
			}
		}
	}
}

func (s *Service) pollObserveCandidates(ctx context.Context) error {
	if s == nil || s.paymentRepo == nil {
		return nil
	}

	cursor := (*prepo.ListCursor)(nil)
	processed := 0
	for processed < observePollMaxPerTick {
		page, err := s.paymentRepo.ListByStateGlobal(ctx, prepo.ListByStateGlobalInput{
			State:  agg.StateExecuting,
			Limit:  observePollPageLimit,
			Cursor: cursor,
		})
		if err != nil {
			return err
		}
		if page == nil || len(page.Items) == 0 {
			return nil
		}

		for i := range page.Items {
			payment := page.Items[i]
			candidates := runningObserveCandidates(payment)
			for j := range candidates {
				if processed >= observePollMaxPerTick {
					return nil
				}
				processed++
				s.pollObserveCandidate(ctx, payment, candidates[j])
			}
		}

		if page.NextCursor == nil {
			return nil
		}
		cursor = page.NextCursor
	}
	return nil
}

func runningObserveCandidates(payment *agg.Payment) []runningObserveCandidate {
	if payment == nil || len(payment.StepExecutions) == 0 {
		return nil
	}
	out := make([]runningObserveCandidate, 0, len(payment.StepExecutions))
	for i := range payment.StepExecutions {
		step := payment.StepExecutions[i]
		if step.State != agg.StepStateRunning {
			continue
		}
		if !isObserveStepCode(step.StepCode) {
			continue
		}
		candidate, ok := buildObserveCandidate(step)
		if !ok {
			continue
		}
		out = append(out, candidate)
	}
	return out
}

func isObserveStepCode(stepCode string) bool {
	code := strings.ToLower(strings.TrimSpace(stepCode))
	return strings.Contains(code, observeStepCodeToken)
}

func buildObserveCandidate(step agg.StepExecution) (runningObserveCandidate, bool) {
	candidate := runningObserveCandidate{
		stepRef: strings.TrimSpace(step.StepRef),
	}
	for i := range step.ExternalRefs {
		ref := step.ExternalRefs[i]
		kind := strings.TrimSpace(ref.Kind)
		value := strings.TrimSpace(ref.Ref)
		if kind == "" || value == "" {
			continue
		}
		switch {
		case strings.EqualFold(kind, erecon.ExternalRefKindTransfer):
			if candidate.transferRef == "" {
				candidate.transferRef = value
				candidate.gatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID)
			}
		case strings.EqualFold(kind, erecon.ExternalRefKindCardPayout):
			if candidate.transferRef == "" {
				candidate.transferRef = value
				candidate.gatewayInstanceID = strings.TrimSpace(ref.GatewayInstanceID)
			}
		case strings.EqualFold(kind, erecon.ExternalRefKindOperation):
			if candidate.operationRef == "" {
				candidate.operationRef = value
			}
		}
	}
	if candidate.gatewayInstanceID == "" {
		candidate.gatewayInstanceID = strings.TrimSpace(step.InstanceID)
	}
	if candidate.stepRef == "" || candidate.transferRef == "" {
		return runningObserveCandidate{}, false
	}
	return candidate, true
}

func (s *Service) pollObserveCandidate(ctx context.Context, payment *agg.Payment, candidate runningObserveCandidate) {
	if s == nil || payment == nil || s.v2 == nil {
		return
	}

	gateway, err := s.resolveObserveGateway(ctx, payment, candidate)
	if err != nil {
		s.logger.Debug("Observe polling skipped: gateway resolution failed",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", candidate.stepRef),
			zap.String("transfer_ref", candidate.transferRef),
			zap.Error(err),
		)
		return
	}

	client, err := s.gatewayInvokeResolver.Resolve(ctx, strings.TrimSpace(gateway.InvokeURI))
	if err != nil {
		s.logger.Warn("Observe polling failed to resolve gateway client",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", candidate.stepRef),
			zap.String("gateway_instance_id", strings.TrimSpace(gateway.InstanceID)),
			zap.Error(err),
		)
		return
	}

	transferResp, err := client.GetTransfer(ctx, &chainv1.GetTransferRequest{TransferRef: candidate.transferRef})
	if err != nil {
		s.logger.Warn("Observe polling transfer status call failed",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", candidate.stepRef),
			zap.String("transfer_ref", candidate.transferRef),
			zap.String("gateway_instance_id", strings.TrimSpace(gateway.InstanceID)),
			zap.Error(err),
		)
		return
	}
	transfer := transferResp.GetTransfer()
	if transfer == nil {
		s.logger.Warn("Observe polling transfer status response is empty",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", candidate.stepRef),
			zap.String("transfer_ref", candidate.transferRef),
		)
		return
	}

	status, terminal, ok := mapTransferStatus(transfer.GetStatus())
	if !ok || !terminal {
		return
	}

	event := erecon.GatewayEvent{
		StepRef:           candidate.stepRef,
		OperationRef:      firstNonEmpty(strings.TrimSpace(transfer.GetOperationRef()), candidate.operationRef),
		TransferRef:       strings.TrimSpace(candidate.transferRef),
		GatewayInstanceID: resolvedObserveGatewayID(candidate.gatewayInstanceID, gateway),
		Status:            status,
		ExecutedMoney:     transferExecutedMoney(transfer),
	}
	switch status {
	case erecon.GatewayStatusFailed:
		retryable := false
		event.Retryable = &retryable
		event.FailureCode = failureCodeGatewayTransferFailed
		event.FailureMsg = strings.TrimSpace(transfer.GetFailureReason())
	case erecon.GatewayStatusCancelled:
		retryable := false
		event.Retryable = &retryable
		event.FailureCode = failureCodeGatewayTransferCanceled
		event.FailureMsg = strings.TrimSpace(transfer.GetFailureReason())
	}

	s.logger.Debug("Reconciling payment from observe polling result",
		zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
		mzap.ObjRef("organization_ref", payment.OrganizationRef),
		zap.String("step_ref", candidate.stepRef),
		zap.String("status", strings.TrimSpace(string(event.Status))),
		zap.String("transfer_ref", candidate.transferRef),
		zap.String("operation_ref", strings.TrimSpace(event.OperationRef)),
		zap.String("gateway_instance_id", strings.TrimSpace(event.GatewayInstanceID)),
	)

	_, err = s.v2.ReconcileExternal(ctx, psvc.ReconcileExternalInput{
		OrganizationRef: payment.OrganizationRef.Hex(),
		PaymentRef:      strings.TrimSpace(payment.PaymentRef),
		Event:           erecon.Event{Gateway: &event},
	})
	if err != nil {
		s.logger.Warn("Observe polling reconciliation failed",
			zap.String("payment_ref", strings.TrimSpace(payment.PaymentRef)),
			zap.String("step_ref", candidate.stepRef),
			zap.String("transfer_ref", candidate.transferRef),
			zap.Error(err),
		)
	}
}

func (s *Service) resolveObserveGateway(ctx context.Context, payment *agg.Payment, candidate runningObserveCandidate) (*model.GatewayInstanceDescriptor, error) {
	if s == nil || s.gatewayRegistry == nil {
		return nil, errors.New("observe polling: gateway registry is unavailable")
	}
	items, err := s.gatewayRegistry.List(ctx)
	if err != nil {
		return nil, err
	}
	hint, hasHint := observeStepGatewayHint(payment, candidate.stepRef)
	expectedRail := model.Rail(discovery.RailUnspecified)
	if hasHint {
		expectedRail = hint.rail
	}
	if gatewayID := strings.TrimSpace(candidate.gatewayInstanceID); gatewayID != "" {
		if item := findEnabledGatewayDescriptor(items, gatewayID, expectedRail); item != nil {
			return item, nil
		}
	}
	if hasHint {
		if item := findEnabledGatewayDescriptor(items, hint.instanceID, hint.rail); item != nil {
			return item, nil
		}
		if item := findEnabledGatewayDescriptor(items, hint.gatewayID, hint.rail); item != nil {
			return item, nil
		}
	}
	if s.logger != nil {
		s.logger.Debug("Gateway for polling not found", zap.String("instance_id", hint.instanceID),
			zap.String("expected_rail", string(expectedRail)), zap.String("hint_rail", string(hint.rail)))
	}
	return nil, errors.New("observe polling: gateway instance not found")
}

type observeStepHint struct {
	rail       model.Rail
	gatewayID  string
	instanceID string
}

func observeStepGatewayHint(payment *agg.Payment, stepRef string) (observeStepHint, bool) {
	if payment == nil {
		return observeStepHint{}, false
	}
	key := strings.TrimSpace(stepRef)
	if key == "" {
		return observeStepHint{}, false
	}
	for i := range payment.StepExecutions {
		step := payment.StepExecutions[i]
		if !strings.EqualFold(strings.TrimSpace(step.StepRef), key) {
			continue
		}
		hint := observeStepHint{
			rail:       model.ParseRail(string(step.Rail)),
			gatewayID:  strings.TrimSpace(step.Gateway),
			instanceID: strings.TrimSpace(step.InstanceID),
		}
		if hint.gatewayID == "" && hint.instanceID == "" {
			return observeStepHint{}, false
		}
		return hint, true
	}
	return observeStepHint{}, false
}

func findEnabledGatewayDescriptor(items []*model.GatewayInstanceDescriptor, identifier string, rail model.Rail) *model.GatewayInstanceDescriptor {
	key := strings.TrimSpace(identifier)
	if key == "" {
		return nil
	}
	for i := range items {
		item := items[i]
		if item == nil || !item.IsEnabled || strings.TrimSpace(item.InvokeURI) == "" {
			continue
		}
		if rail != model.Rail(discovery.RailUnspecified) && model.ParseRail(string(item.Rail)) != rail {
			continue
		}
		if strings.EqualFold(strings.TrimSpace(item.ID), key) || strings.EqualFold(strings.TrimSpace(item.InstanceID), key) {
			return item
		}
	}
	return nil
}

func resolvedObserveGatewayID(candidateGatewayID string, gateway *model.GatewayInstanceDescriptor) string {
	candidateID := strings.TrimSpace(candidateGatewayID)
	if candidateID != "" && gatewayIdentifierMatches(gateway, candidateID) {
		return candidateID
	}
	if gateway == nil {
		return ""
	}
	return firstNonEmpty(strings.TrimSpace(gateway.InstanceID), strings.TrimSpace(gateway.ID))
}

func gatewayIdentifierMatches(gateway *model.GatewayInstanceDescriptor, identifier string) bool {
	if gateway == nil {
		return false
	}
	key := strings.TrimSpace(identifier)
	if key == "" {
		return false
	}
	return strings.EqualFold(strings.TrimSpace(gateway.ID), key) || strings.EqualFold(strings.TrimSpace(gateway.InstanceID), key)
}

func mapTransferStatus(status chainv1.TransferStatus) (gatewayStatus erecon.GatewayStatus, terminal bool, ok bool) {
	switch status {
	case chainv1.TransferStatus_TRANSFER_CREATED:
		return erecon.GatewayStatusCreated, false, true
	case chainv1.TransferStatus_TRANSFER_PROCESSING:
		return erecon.GatewayStatusProcessing, false, true
	case chainv1.TransferStatus_TRANSFER_WAITING:
		return erecon.GatewayStatusWaiting, false, true
	case chainv1.TransferStatus_TRANSFER_SUCCESS:
		return erecon.GatewayStatusSuccess, true, true
	case chainv1.TransferStatus_TRANSFER_FAILED:
		return erecon.GatewayStatusFailed, true, true
	case chainv1.TransferStatus_TRANSFER_CANCELLED:
		return erecon.GatewayStatusCancelled, true, true
	default:
		return erecon.GatewayStatusUnspecified, false, false
	}
}
