package orchestrator

import (
	"strings"
	"time"

	"github.com/tech/sendico/payments/orchestrator/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1"
	fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1"
	paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
	oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
	orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent {
	if src == nil {
		return model.PaymentIntent{}
	}
	intent := model.PaymentIntent{
		Kind:        modelKindFromProto(src.GetKind()),
		Source:      endpointFromProto(src.GetSource()),
		Destination: endpointFromProto(src.GetDestination()),
		Amount:      cloneMoney(src.GetAmount()),
		RequiresFX:  src.GetRequiresFx(),
		FeePolicy:   src.GetFeePolicy(),
		Attributes:  cloneMetadata(src.GetAttributes()),
	}
	if src.GetFx() != nil {
		intent.FX = fxIntentFromProto(src.GetFx())
	}
	return intent
}

func endpointFromProto(src *orchestratorv1.PaymentEndpoint) model.PaymentEndpoint {
	if src == nil {
		return model.PaymentEndpoint{Type: model.EndpointTypeUnspecified}
	}
	result := model.PaymentEndpoint{
		Type:     model.EndpointTypeUnspecified,
		Metadata: cloneMetadata(src.GetMetadata()),
	}
	if ledger := src.GetLedger(); ledger != nil {
		result.Type = model.EndpointTypeLedger
		result.Ledger = &model.LedgerEndpoint{
			LedgerAccountRef:       strings.TrimSpace(ledger.GetLedgerAccountRef()),
			ContraLedgerAccountRef: strings.TrimSpace(ledger.GetContraLedgerAccountRef()),
		}
		return result
	}
	if managed := src.GetManagedWallet(); managed != nil {
		result.Type = model.EndpointTypeManagedWallet
		result.ManagedWallet = &model.ManagedWalletEndpoint{
			ManagedWalletRef: strings.TrimSpace(managed.GetManagedWalletRef()),
			Asset:            cloneAsset(managed.GetAsset()),
		}
		return result
	}
	if external := src.GetExternalChain(); external != nil {
		result.Type = model.EndpointTypeExternalChain
		result.ExternalChain = &model.ExternalChainEndpoint{
			Asset:   cloneAsset(external.GetAsset()),
			Address: strings.TrimSpace(external.GetAddress()),
			Memo:    strings.TrimSpace(external.GetMemo()),
		}
		return result
	}
	return result
}

func fxIntentFromProto(src *orchestratorv1.FXIntent) *model.FXIntent {
	if src == nil {
		return nil
	}
	return &model.FXIntent{
		Pair:              clonePair(src.GetPair()),
		Side:              src.GetSide(),
		Firm:              src.GetFirm(),
		TTLMillis:         src.GetTtlMs(),
		PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()),
		MaxAgeMillis:      src.GetMaxAgeMs(),
	}
}

func quoteSnapshotToModel(src *orchestratorv1.PaymentQuote) *model.PaymentQuoteSnapshot {
	if src == nil {
		return nil
	}
	return &model.PaymentQuoteSnapshot{
		DebitAmount:              cloneMoney(src.GetDebitAmount()),
		ExpectedSettlementAmount: cloneMoney(src.GetExpectedSettlementAmount()),
		ExpectedFeeTotal:         cloneMoney(src.GetExpectedFeeTotal()),
		FeeLines:                 cloneFeeLines(src.GetFeeLines()),
		FeeRules:                 cloneFeeRules(src.GetFeeRules()),
		FXQuote:                  cloneFXQuote(src.GetFxQuote()),
		NetworkFee:               cloneNetworkEstimate(src.GetNetworkFee()),
		FeeQuoteToken:            strings.TrimSpace(src.GetFeeQuoteToken()),
	}
}

func toProtoPayment(src *model.Payment) *orchestratorv1.Payment {
	if src == nil {
		return nil
	}
	payment := &orchestratorv1.Payment{
		PaymentRef:     src.PaymentRef,
		IdempotencyKey: src.IdempotencyKey,
		Intent:         protoIntentFromModel(src.Intent),
		State:          protoStateFromModel(src.State),
		FailureCode:    protoFailureFromModel(src.FailureCode),
		FailureReason:  src.FailureReason,
		LastQuote:      modelQuoteToProto(src.LastQuote),
		Execution:      protoExecutionFromModel(src.Execution),
		Metadata:       cloneMetadata(src.Metadata),
	}
	if src.CreatedAt.IsZero() {
		payment.CreatedAt = timestamppb.New(time.Now().UTC())
	} else {
		payment.CreatedAt = timestamppb.New(src.CreatedAt.UTC())
	}
	if src.UpdatedAt != (time.Time{}) {
		payment.UpdatedAt = timestamppb.New(src.UpdatedAt.UTC())
	}
	return payment
}

func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent {
	intent := &orchestratorv1.PaymentIntent{
		Kind:        protoKindFromModel(src.Kind),
		Source:      protoEndpointFromModel(src.Source),
		Destination: protoEndpointFromModel(src.Destination),
		Amount:      cloneMoney(src.Amount),
		RequiresFx:  src.RequiresFX,
		FeePolicy:   src.FeePolicy,
		Attributes:  cloneMetadata(src.Attributes),
	}
	if src.FX != nil {
		intent.Fx = protoFXIntentFromModel(src.FX)
	}
	return intent
}

func protoEndpointFromModel(src model.PaymentEndpoint) *orchestratorv1.PaymentEndpoint {
	endpoint := &orchestratorv1.PaymentEndpoint{
		Metadata: cloneMetadata(src.Metadata),
	}
	switch src.Type {
	case model.EndpointTypeLedger:
		if src.Ledger != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{
				Ledger: &orchestratorv1.LedgerEndpoint{
					LedgerAccountRef:       src.Ledger.LedgerAccountRef,
					ContraLedgerAccountRef: src.Ledger.ContraLedgerAccountRef,
				},
			}
		}
	case model.EndpointTypeManagedWallet:
		if src.ManagedWallet != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{
				ManagedWallet: &orchestratorv1.ManagedWalletEndpoint{
					ManagedWalletRef: src.ManagedWallet.ManagedWalletRef,
					Asset:            cloneAsset(src.ManagedWallet.Asset),
				},
			}
		}
	case model.EndpointTypeExternalChain:
		if src.ExternalChain != nil {
			endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{
				ExternalChain: &orchestratorv1.ExternalChainEndpoint{
					Asset:   cloneAsset(src.ExternalChain.Asset),
					Address: src.ExternalChain.Address,
					Memo:    src.ExternalChain.Memo,
				},
			}
		}
	default:
		// leave unspecified
	}
	return endpoint
}

func protoFXIntentFromModel(src *model.FXIntent) *orchestratorv1.FXIntent {
	if src == nil {
		return nil
	}
	return &orchestratorv1.FXIntent{
		Pair:              clonePair(src.Pair),
		Side:              src.Side,
		Firm:              src.Firm,
		TtlMs:             src.TTLMillis,
		PreferredProvider: src.PreferredProvider,
		MaxAgeMs:          src.MaxAgeMillis,
	}
}

func protoExecutionFromModel(src *model.ExecutionRefs) *orchestratorv1.ExecutionRefs {
	if src == nil {
		return nil
	}
	return &orchestratorv1.ExecutionRefs{
		DebitEntryRef:    src.DebitEntryRef,
		CreditEntryRef:   src.CreditEntryRef,
		FxEntryRef:       src.FXEntryRef,
		ChainTransferRef: src.ChainTransferRef,
	}
}

func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote {
	if src == nil {
		return nil
	}
	return &orchestratorv1.PaymentQuote{
		DebitAmount:              cloneMoney(src.DebitAmount),
		ExpectedSettlementAmount: cloneMoney(src.ExpectedSettlementAmount),
		ExpectedFeeTotal:         cloneMoney(src.ExpectedFeeTotal),
		FeeLines:                 cloneFeeLines(src.FeeLines),
		FeeRules:                 cloneFeeRules(src.FeeRules),
		FxQuote:                  cloneFXQuote(src.FXQuote),
		NetworkFee:               cloneNetworkEstimate(src.NetworkFee),
		FeeQuoteToken:            src.FeeQuoteToken,
	}
}

func filterFromProto(req *orchestratorv1.ListPaymentsRequest) *model.PaymentFilter {
	if req == nil {
		return &model.PaymentFilter{}
	}
	filter := &model.PaymentFilter{
		SourceRef:      strings.TrimSpace(req.GetSourceRef()),
		DestinationRef: strings.TrimSpace(req.GetDestinationRef()),
	}
	if req.GetPage() != nil {
		filter.Cursor = strings.TrimSpace(req.GetPage().GetCursor())
		filter.Limit = req.GetPage().GetLimit()
	}
	if len(req.GetFilterStates()) > 0 {
		filter.States = make([]model.PaymentState, 0, len(req.GetFilterStates()))
		for _, st := range req.GetFilterStates() {
			filter.States = append(filter.States, modelStateFromProto(st))
		}
	}
	return filter
}

func protoKindFromModel(kind model.PaymentKind) orchestratorv1.PaymentKind {
	switch kind {
	case model.PaymentKindPayout:
		return orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT
	case model.PaymentKindInternalTransfer:
		return orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER
	case model.PaymentKindFXConversion:
		return orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION
	default:
		return orchestratorv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED
	}
}

func modelKindFromProto(kind orchestratorv1.PaymentKind) model.PaymentKind {
	switch kind {
	case orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT:
		return model.PaymentKindPayout
	case orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER:
		return model.PaymentKindInternalTransfer
	case orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION:
		return model.PaymentKindFXConversion
	default:
		return model.PaymentKindUnspecified
	}
}

func protoStateFromModel(state model.PaymentState) orchestratorv1.PaymentState {
	switch state {
	case model.PaymentStateAccepted:
		return orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED
	case model.PaymentStateFundsReserved:
		return orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED
	case model.PaymentStateSubmitted:
		return orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED
	case model.PaymentStateSettled:
		return orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED
	case model.PaymentStateFailed:
		return orchestratorv1.PaymentState_PAYMENT_STATE_FAILED
	case model.PaymentStateCancelled:
		return orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED
	default:
		return orchestratorv1.PaymentState_PAYMENT_STATE_UNSPECIFIED
	}
}

func modelStateFromProto(state orchestratorv1.PaymentState) model.PaymentState {
	switch state {
	case orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED:
		return model.PaymentStateAccepted
	case orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED:
		return model.PaymentStateFundsReserved
	case orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED:
		return model.PaymentStateSubmitted
	case orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED:
		return model.PaymentStateSettled
	case orchestratorv1.PaymentState_PAYMENT_STATE_FAILED:
		return model.PaymentStateFailed
	case orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED:
		return model.PaymentStateCancelled
	default:
		return model.PaymentStateUnspecified
	}
}

func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.PaymentFailureCode {
	switch code {
	case model.PaymentFailureCodeBalance:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_BALANCE
	case model.PaymentFailureCodeLedger:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_LEDGER
	case model.PaymentFailureCodeFX:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FX
	case model.PaymentFailureCodeChain:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_CHAIN
	case model.PaymentFailureCodeFees:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FEES
	case model.PaymentFailureCodePolicy:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_POLICY
	default:
		return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_UNSPECIFIED
	}
}

func cloneAsset(asset *gatewayv1.Asset) *gatewayv1.Asset {
	if asset == nil {
		return nil
	}
	return &gatewayv1.Asset{
		Chain:           asset.GetChain(),
		TokenSymbol:     asset.GetTokenSymbol(),
		ContractAddress: asset.GetContractAddress(),
	}
}

func clonePair(pair *fxv1.CurrencyPair) *fxv1.CurrencyPair {
	if pair == nil {
		return nil
	}
	return &fxv1.CurrencyPair{
		Base:  pair.GetBase(),
		Quote: pair.GetQuote(),
	}
}

func cloneFXQuote(quote *oraclev1.Quote) *oraclev1.Quote {
	if quote == nil {
		return nil
	}
	if cloned, ok := proto.Clone(quote).(*oraclev1.Quote); ok {
		return cloned
	}
	return nil
}

func cloneNetworkEstimate(resp *gatewayv1.EstimateTransferFeeResponse) *gatewayv1.EstimateTransferFeeResponse {
	if resp == nil {
		return nil
	}
	if cloned, ok := proto.Clone(resp).(*gatewayv1.EstimateTransferFeeResponse); ok {
		return cloned
	}
	return nil
}

func protoFailureToModel(code orchestratorv1.PaymentFailureCode) model.PaymentFailureCode {
	switch code {
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_BALANCE:
		return model.PaymentFailureCodeBalance
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_LEDGER:
		return model.PaymentFailureCodeLedger
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FX:
		return model.PaymentFailureCodeFX
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_CHAIN:
		return model.PaymentFailureCodeChain
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FEES:
		return model.PaymentFailureCodeFees
	case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_POLICY:
		return model.PaymentFailureCodePolicy
	default:
		return model.PaymentFailureCodeUnspecified
	}
}

func applyProtoPaymentToModel(src *orchestratorv1.Payment, dst *model.Payment) error {
	if src == nil || dst == nil {
		return merrors.InvalidArgument("payment payload is required")
	}
	dst.PaymentRef = strings.TrimSpace(src.GetPaymentRef())
	dst.IdempotencyKey = strings.TrimSpace(src.GetIdempotencyKey())
	dst.Intent = intentFromProto(src.GetIntent())
	dst.State = modelStateFromProto(src.GetState())
	dst.FailureCode = protoFailureToModel(src.GetFailureCode())
	dst.FailureReason = strings.TrimSpace(src.GetFailureReason())
	dst.Metadata = cloneMetadata(src.GetMetadata())
	dst.LastQuote = quoteSnapshotToModel(src.GetLastQuote())
	dst.Execution = executionFromProto(src.GetExecution())
	return nil
}

func executionFromProto(src *orchestratorv1.ExecutionRefs) *model.ExecutionRefs {
	if src == nil {
		return nil
	}
	return &model.ExecutionRefs{
		DebitEntryRef:    strings.TrimSpace(src.GetDebitEntryRef()),
		CreditEntryRef:   strings.TrimSpace(src.GetCreditEntryRef()),
		FXEntryRef:       strings.TrimSpace(src.GetFxEntryRef()),
		ChainTransferRef: strings.TrimSpace(src.GetChainTransferRef()),
	}
}

func ensurePageRequest(req *orchestratorv1.ListPaymentsRequest) *paginationv1.CursorPageRequest {
	if req == nil {
		return &paginationv1.CursorPageRequest{}
	}
	if req.GetPage() == nil {
		return &paginationv1.CursorPageRequest{}
	}
	return req.GetPage()
}
