package orchestrator

import (
	"context"
	"strings"

	"github.com/tech/sendico/payments/orchestrator/storage"
	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/api/routers/gsresponse"
	"github.com/tech/sendico/pkg/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	"github.com/tech/sendico/pkg/mservice"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	mntxv1 "github.com/tech/sendico/pkg/proto/gateway/mntx/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
)

type paymentEventHandler struct {
	repo             storage.Repository
	ensureRepo       func(ctx context.Context) error
	logger           mlogger.Logger
	submitCardPayout func(ctx context.Context, payment *model.Payment) error
	resumePlan       func(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error
	releaseHold      func(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error
}

func newPaymentEventHandler(repo storage.Repository, ensure func(ctx context.Context) error, logger mlogger.Logger, submitCardPayout func(ctx context.Context, payment *model.Payment) error, resumePlan func(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error, releaseHold func(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error) *paymentEventHandler {
	return &paymentEventHandler{
		repo:             repo,
		ensureRepo:       ensure,
		logger:           logger,
		submitCardPayout: submitCardPayout,
		resumePlan:       resumePlan,
		releaseHold:      releaseHold,
	}
}

func (h *paymentEventHandler) processTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) gsresponse.Responder[orchestratorv1.ProcessTransferUpdateResponse] {
	if err := h.ensureRepo(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil || req.GetEvent() == nil || req.GetEvent().GetTransfer() == nil {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("transfer event is required"))
	}
	transfer := req.GetEvent().GetTransfer()
	transferRef := strings.TrimSpace(transfer.GetTransferRef())
	if transferRef == "" {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("transfer_ref is required"))
	}
	store := h.repo.Payments()
	if store == nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, errStorageUnavailable)
	}
	payment, err := store.GetByChainTransferRef(ctx, transferRef)
	if err != nil {
		return paymentNotFoundResponder[orchestratorv1.ProcessTransferUpdateResponse](mservice.PaymentOrchestrator, h.logger, err)
	}
	if payment.PaymentPlan != nil && len(payment.PaymentPlan.Steps) > 0 {
		if payment.ExecutionPlan == nil || len(payment.ExecutionPlan.Steps) != len(payment.PaymentPlan.Steps) {
			ensureExecutionPlanForPlan(payment, payment.PaymentPlan)
		}
		updateExecutionStepFromTransfer(payment.ExecutionPlan, req.GetEvent())
		if payment.Execution == nil {
			payment.Execution = &model.ExecutionRefs{}
		}
		if payment.Execution.ChainTransferRef == "" {
			payment.Execution.ChainTransferRef = transferRef
		}
		reason := transferFailureReason(req.GetEvent())
		switch transfer.GetStatus() {
		case chainv1.TransferStatus_TRANSFER_FAILED:
			payment.State = model.PaymentStateFailed
			payment.FailureCode = model.PaymentFailureCodeChain
			payment.FailureReason = reason
			if err := store.Update(ctx, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
			return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
		case chainv1.TransferStatus_TRANSFER_CANCELLED:
			payment.State = model.PaymentStateCancelled
			payment.FailureCode = model.PaymentFailureCodePolicy
			payment.FailureReason = reason
			if err := store.Update(ctx, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
			return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
		case chainv1.TransferStatus_TRANSFER_CONFIRMED:
			if h.resumePlan != nil {
				if err := h.resumePlan(ctx, store, payment); err != nil {
					return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
				}
			} else {
				payment.State = model.PaymentStateSubmitted
				if err := store.Update(ctx, payment); err != nil {
					return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
				}
			}
			return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
		case chainv1.TransferStatus_TRANSFER_SIGNING,
			chainv1.TransferStatus_TRANSFER_PENDING,
			chainv1.TransferStatus_TRANSFER_SUBMITTED:
			if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
				payment.State = model.PaymentStateSubmitted
			}
			if err := store.Update(ctx, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
			return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
		default:
			if err := store.Update(ctx, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
			return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
		}
	}

	updateExecutionStepFromTransfer(payment.ExecutionPlan, req.GetEvent())
	if payment.Intent.Destination.Type == model.EndpointTypeCard {
		if payment.Execution == nil {
			payment.Execution = &model.ExecutionRefs{}
		}
		if payment.Execution.ChainTransferRef == "" {
			payment.Execution.ChainTransferRef = transferRef
		}
		reason := transferFailureReason(req.GetEvent())
		switch transfer.GetStatus() {
		case chainv1.TransferStatus_TRANSFER_FAILED:
			payment.State = model.PaymentStateFailed
			payment.FailureCode = model.PaymentFailureCodeChain
			payment.FailureReason = reason
		case chainv1.TransferStatus_TRANSFER_CANCELLED:
			payment.State = model.PaymentStateCancelled
			payment.FailureCode = model.PaymentFailureCodePolicy
			payment.FailureReason = reason
		case chainv1.TransferStatus_TRANSFER_CONFIRMED:
			if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
				if cardPayoutDependenciesConfirmed(payment.PaymentPlan, payment.ExecutionPlan) {
					if payment.Execution.CardPayoutRef != "" {
						payment.State = model.PaymentStateSubmitted
					} else {
						payment.State = model.PaymentStateFundsReserved
						if h.submitCardPayout == nil {
							h.logger.Warn("card payout execution skipped", zap.String("payment_ref", payment.PaymentRef))
						} else if err := h.submitCardPayout(ctx, payment); err != nil {
							payment.State = model.PaymentStateFailed
							payment.FailureCode = model.PaymentFailureCodePolicy
							payment.FailureReason = strings.TrimSpace(err.Error())
							h.logger.Warn("card payout execution failed", zap.Error(err), zap.String("payment_ref", payment.PaymentRef))
						} else {
							payment.State = model.PaymentStateSubmitted
						}
					}
				} else {
					payment.State = model.PaymentStateSubmitted
				}
			}
		case chainv1.TransferStatus_TRANSFER_SIGNING,
			chainv1.TransferStatus_TRANSFER_PENDING,
			chainv1.TransferStatus_TRANSFER_SUBMITTED:
			if payment.State != model.PaymentStateFailed && payment.State != model.PaymentStateCancelled && payment.State != model.PaymentStateSettled {
				payment.State = model.PaymentStateSubmitted
			}
		default:
			// keep current state
		}
		if err := store.Update(ctx, payment); err != nil {
			return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
		h.logger.Info("transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
		return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
	}

	applyTransferStatus(req.GetEvent(), payment)
	if err := store.Update(ctx, payment); err != nil {
		return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	h.logger.Info("transfer update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("transfer_ref", transferRef), zap.Any("state", payment.State))
	return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)})
}

func (h *paymentEventHandler) processDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) gsresponse.Responder[orchestratorv1.ProcessDepositObservedResponse] {
	if err := h.ensureRepo(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil || req.GetEvent() == nil {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("deposit event is required"))
	}
	event := req.GetEvent()
	walletRef := strings.TrimSpace(event.GetWalletRef())
	if walletRef == "" {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("wallet_ref is required"))
	}
	store := h.repo.Payments()
	if store == nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, errStorageUnavailable)
	}
	filter := &model.PaymentFilter{
		States:         []model.PaymentState{model.PaymentStateSubmitted, model.PaymentStateFundsReserved},
		DestinationRef: walletRef,
	}
	result, err := store.List(ctx, filter)
	if err != nil {
		return gsresponse.Auto[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	for _, payment := range result.Items {
		if payment.Intent.Destination.Type != model.EndpointTypeManagedWallet {
			continue
		}
		if !moneyEquals(payment.Intent.Amount, event.GetAmount()) {
			continue
		}
		payment.State = model.PaymentStateSettled
		payment.FailureCode = model.PaymentFailureCodeUnspecified
		payment.FailureReason = ""
		if payment.Execution == nil {
			payment.Execution = &model.ExecutionRefs{}
		}
		if payment.Execution.ChainTransferRef == "" {
			payment.Execution.ChainTransferRef = strings.TrimSpace(event.GetTransactionHash())
		}
		if err := store.Update(ctx, payment); err != nil {
			return gsresponse.Auto[orchestratorv1.ProcessDepositObservedResponse](h.logger, mservice.PaymentOrchestrator, err)
		}
		h.logger.Info("deposit observed matched payment", zap.String("payment_ref", payment.PaymentRef), zap.String("wallet_ref", walletRef))
		return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{Payment: toProtoPayment(payment)})
	}
	return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{})
}

func (h *paymentEventHandler) processCardPayoutUpdate(ctx context.Context, req *orchestratorv1.ProcessCardPayoutUpdateRequest) gsresponse.Responder[orchestratorv1.ProcessCardPayoutUpdateResponse] {
	if err := h.ensureRepo(ctx); err != nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
	}
	if req == nil || req.GetEvent() == nil || req.GetEvent().GetPayout() == nil {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("event is required"))
	}
	payout := req.GetEvent().GetPayout()
	paymentRef := strings.TrimSpace(payout.GetPayoutId())
	if paymentRef == "" {
		return gsresponse.InvalidArgument[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("payout_id is required"))
	}

	store := h.repo.Payments()
	if store == nil {
		return gsresponse.Unavailable[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, errStorageUnavailable)
	}
	payment, err := store.GetByPaymentRef(ctx, paymentRef)
	if err != nil {
		return paymentNotFoundResponder[orchestratorv1.ProcessCardPayoutUpdateResponse](mservice.PaymentOrchestrator, h.logger, err)
	}

	applyCardPayoutUpdate(payment, payout)
	switch payout.GetStatus() {
	case mntxv1.PayoutStatus_PAYOUT_STATUS_PROCESSED:
		if h.resumePlan != nil && payment.PaymentPlan != nil && len(payment.PaymentPlan.Steps) > 0 {
			if err := h.resumePlan(ctx, store, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
		}
	case mntxv1.PayoutStatus_PAYOUT_STATUS_FAILED:
		if h.releaseHold != nil && payment.PaymentPlan != nil && len(payment.PaymentPlan.Steps) > 0 {
			if err := h.releaseHold(ctx, store, payment); err != nil {
				return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
			}
		}
	}

	if err := store.Update(ctx, payment); err != nil {
		return gsresponse.Auto[orchestratorv1.ProcessCardPayoutUpdateResponse](h.logger, mservice.PaymentOrchestrator, err)
	}

	h.logger.Info("card payout update applied", zap.String("payment_ref", payment.PaymentRef), zap.String("payout_id", paymentRef), zap.Any("state", payment.State))
	return gsresponse.Success(&orchestratorv1.ProcessCardPayoutUpdateResponse{
		Payment: toProtoPayment(payment),
	})
}

func transferFailureReason(event *chainv1.TransferStatusChangedEvent) string {
	if event == nil || event.GetTransfer() == nil {
		return ""
	}
	reason := strings.TrimSpace(event.GetReason())
	if reason != "" {
		return reason
	}
	return strings.TrimSpace(event.GetTransfer().GetFailureReason())
}
