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/merrors"
	"github.com/tech/sendico/pkg/mlogger"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"go.uber.org/zap"
)

type paymentExecutor struct {
	deps   *serviceDependencies
	logger mlogger.Logger
	svc    *Service
}

func newPaymentExecutor(deps *serviceDependencies, logger mlogger.Logger, svc *Service) *paymentExecutor {
	return &paymentExecutor{deps: deps, logger: logger, svc: svc}
}

func (p *paymentExecutor) executePayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, quote *orchestratorv1.PaymentQuote) error {
	if store == nil {
		return errStorageUnavailable
	}
	if p.svc == nil {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "service_unavailable", errStorageUnavailable)
	}
	if p.svc.storage == nil {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "routes_store_unavailable", errStorageUnavailable)
	}
	routeStore := p.svc.storage.Routes()
	if routeStore == nil {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "routes_store_unavailable", errStorageUnavailable)
	}
	planTemplates := p.svc.storage.PlanTemplates()
	if planTemplates == nil {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "plan_templates_store_unavailable", errStorageUnavailable)
	}
	builder := p.svc.deps.planBuilder
	if builder == nil {
		builder = &defaultPlanBuilder{}
	}
	plan, err := builder.Build(ctx, payment, quote, routeStore, planTemplates, p.svc.deps.gatewayRegistry)
	if err != nil {
		p.logPlanBuilderFailure(payment, err)
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, strings.TrimSpace(err.Error()), err)
	}
	if plan == nil || len(plan.Steps) == 0 {
		return p.failPayment(ctx, store, payment, model.PaymentFailureCodePolicy, "payment_plan_empty", merrors.InvalidArgument("payment plan is required"))
	}
	payment.PaymentPlan = plan

	return p.executePaymentPlan(ctx, store, payment, quote)
}

func (p *paymentExecutor) logPlanBuilderFailure(payment *model.Payment, err error) {
	if p == nil || payment == nil {
		return
	}
	intent := payment.Intent
	sourceRail, sourceNetwork, sourceErr := railFromEndpoint(intent.Source, intent.Attributes, true)
	destRail, destNetwork, destErr := railFromEndpoint(intent.Destination, intent.Attributes, false)

	fields := []zap.Field{
		zap.Error(err),
		zap.String("payment_ref", payment.PaymentRef),
		zap.String("org_ref", payment.OrganizationRef.Hex()),
		zap.String("idempotency_key", payment.IdempotencyKey),
		zap.String("source_rail", string(sourceRail)),
		zap.String("destination_rail", string(destRail)),
		zap.String("source_network", sourceNetwork),
		zap.String("destination_network", destNetwork),
		zap.String("source_endpoint_type", string(intent.Source.Type)),
		zap.String("destination_endpoint_type", string(intent.Destination.Type)),
	}

	missing := make([]string, 0, 2)
	if sourceErr != nil || sourceRail == model.RailUnspecified {
		missing = append(missing, "source")
		if sourceErr != nil {
			fields = append(fields, zap.String("source_rail_error", sourceErr.Error()))
		}
	}
	if destErr != nil || destRail == model.RailUnspecified {
		missing = append(missing, "destination")
		if destErr != nil {
			fields = append(fields, zap.String("destination_rail_error", destErr.Error()))
		}
	}
	if len(missing) > 0 {
		fields = append(fields, zap.String("missing_rails", strings.Join(missing, ",")))
		p.logger.Warn("Payment rail resolution failed", fields...)
		return
	}

	routeNetwork, routeErr := resolveRouteNetwork(intent.Attributes, sourceNetwork, destNetwork)
	if routeErr != nil {
		fields = append(fields, zap.String("route_network_error", routeErr.Error()))
	} else if routeNetwork != "" {
		fields = append(fields, zap.String("route_network", routeNetwork))
	}
	p.logger.Warn("Payment route missing for rails", fields...)
}

func (p *paymentExecutor) applyFX(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine, description string, metadata map[string]string, exec *model.ExecutionRefs) error {
	intent := payment.Intent
	source := intent.Source.Ledger
	destination := intent.Destination.Ledger
	if source == nil || destination == nil {
		return merrors.InvalidArgument("ledger: fx conversion requires ledger source and destination")
	}
	fq := quote.GetFxQuote()
	if fq == nil {
		return merrors.InvalidArgument("ledger: fx quote missing")
	}
	fxSide := fxv1.Side_SIDE_UNSPECIFIED
	if intent.FX != nil {
		fxSide = fxSideToProto(intent.FX.Side)
	}
	fromMoney, toMoney := resolveTradeAmounts(protoMoney(intent.Amount), fq, fxSide)
	if fromMoney == nil {
		fromMoney = protoMoney(intent.Amount)
	}
	if toMoney == nil {
		toMoney = cloneProtoMoney(quote.GetExpectedSettlementAmount())
	}
	rate := ""
	if fq.GetPrice() != nil {
		rate = fq.GetPrice().GetValue()
	}
	req := &ledgerv1.FXRequest{
		IdempotencyKey:       payment.IdempotencyKey,
		OrganizationRef:      payment.OrganizationRef.Hex(),
		FromLedgerAccountRef: strings.TrimSpace(source.LedgerAccountRef),
		ToLedgerAccountRef:   strings.TrimSpace(destination.LedgerAccountRef),
		FromMoney:            fromMoney,
		ToMoney:              toMoney,
		Rate:                 rate,
		Description:          description,
		Charges:              charges,
		Metadata:             metadata,
	}
	resp, err := p.deps.ledger.client.ApplyFXWithCharges(ctx, req)
	if err != nil {
		return err
	}
	exec.FXEntryRef = strings.TrimSpace(resp.GetJournalEntryRef())
	payment.Execution = exec
	return nil
}

func (p *paymentExecutor) persistPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error {
	if store == nil {
		return errStorageUnavailable
	}
	return store.Update(ctx, payment)
}

func (p *paymentExecutor) failPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, code model.PaymentFailureCode, reason string, err error) error {
	payment.State = model.PaymentStateFailed
	payment.FailureCode = code
	payment.FailureReason = strings.TrimSpace(reason)
	if store != nil {
		if updateErr := store.Update(ctx, payment); updateErr != nil {
			p.logger.Error("failed to persist payment failure", zap.Error(updateErr), zap.String("payment_ref", payment.PaymentRef))
		}
	}
	if err != nil {
		return err
	}
	return merrors.Internal(reason)
}

func paymentDescription(payment *model.Payment) string {
	if payment == nil {
		return ""
	}
	if val := strings.TrimSpace(payment.Intent.Attributes["description"]); val != "" {
		return val
	}
	if payment.Metadata != nil {
		if val := strings.TrimSpace(payment.Metadata["description"]); val != "" {
			return val
		}
	}
	return payment.PaymentRef
}

func applyTransferStatus(event *chainv1.TransferStatusChangedEvent, payment *model.Payment) {
	if payment.Execution == nil {
		payment.Execution = &model.ExecutionRefs{}
	}
	if event == nil || event.GetTransfer() == nil {
		return
	}
	transfer := event.GetTransfer()
	payment.Execution.ChainTransferRef = strings.TrimSpace(transfer.GetTransferRef())
	reason := strings.TrimSpace(event.GetReason())
	if reason == "" {
		reason = strings.TrimSpace(transfer.GetFailureReason())
	}
	switch transfer.GetStatus() {
	case chainv1.TransferStatus_TRANSFER_CONFIRMED:
		payment.State = model.PaymentStateSettled
		payment.FailureCode = model.PaymentFailureCodeUnspecified
		payment.FailureReason = ""
	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_SIGNING,
		chainv1.TransferStatus_TRANSFER_PENDING,
		chainv1.TransferStatus_TRANSFER_SUBMITTED:
		payment.State = model.PaymentStateSubmitted
	default:
		// retain previous state
	}
}
