package quotation

import (
	"context"
	"github.com/tech/sendico/pkg/discovery"
	"sort"
	"strings"

	"github.com/shopspring/decimal"
	chainclient "github.com/tech/sendico/gateway/chain/client"
	"github.com/tech/sendico/payments/quotation/internal/service/plan"
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	"go.uber.org/zap"
)

func (s *Service) resolveChainGatewayClient(ctx context.Context, network string, amount *paymenttypes.Money, actions []model.RailOperation, instanceID string, paymentRef string) (chainclient.Client, *model.GatewayInstanceDescriptor, error) {
	if s.deps.gatewayRegistry != nil && s.deps.gatewayInvokeResolver != nil {
		entry, err := selectGatewayForActions(ctx, s.deps.gatewayRegistry, discovery.RailCrypto, network, amount, actions, instanceID, sendDirectionForRail(discovery.RailCrypto))
		if err != nil {
			return nil, nil, err
		}
		invokeURI := strings.TrimSpace(entry.InvokeURI)
		if invokeURI == "" {
			return nil, nil, merrors.InvalidArgument("chain gateway: invoke uri is required")
		}
		client, err := s.deps.gatewayInvokeResolver.Resolve(ctx, invokeURI)
		if err != nil {
			return nil, nil, err
		}
		if s.logger != nil {
			fields := []zap.Field{
				zap.String("gateway_id", entry.ID),
				zap.String("instance_id", entry.InstanceID),
				zap.String("rail", string(entry.Rail)),
				zap.String("network", entry.Network),
				zap.String("invoke_uri", invokeURI),
			}
			if paymentRef != "" {
				fields = append(fields, zap.String("payment_ref", paymentRef))
			}
			if len(actions) > 0 {
				fields = append(fields, zap.Strings("actions", railActionNames(actions)))
			}
			s.logger.Info("Chain gateway selected", fields...)
		}
		return client, entry, nil
	}
	if s.deps.gateway.resolver != nil {
		client, err := s.deps.gateway.resolver.Resolve(ctx, network)
		if err != nil {
			return nil, nil, err
		}
		return client, nil, nil
	}
	return nil, nil, merrors.NoData("chain gateway unavailable")
}

func selectGatewayForActions(ctx context.Context, registry GatewayRegistry, rail model.Rail, network string, amount *paymenttypes.Money, actions []model.RailOperation, instanceID string, dir plan.SendDirection) (*model.GatewayInstanceDescriptor, error) {
	if registry == nil {
		return nil, merrors.NoData("gateway registry unavailable")
	}
	all, err := registry.List(ctx)
	if err != nil {
		return nil, err
	}
	if len(all) == 0 {
		return nil, merrors.NoData("no gateway instances available")
	}
	if len(actions) == 0 {
		actions = []model.RailOperation{discovery.RailOperationSend}
	}

	currency := ""
	amt := decimal.Zero
	if amount != nil && strings.TrimSpace(amount.GetAmount()) != "" {
		amt, err = decimalFromMoney(amount)
		if err != nil {
			return nil, err
		}
		currency = strings.ToUpper(strings.TrimSpace(amount.GetCurrency()))
	}
	network = strings.ToUpper(strings.TrimSpace(network))

	eligible := make([]*model.GatewayInstanceDescriptor, 0)
	for _, entry := range all {
		if entry == nil || !entry.IsEnabled {
			continue
		}
		if entry.Rail != rail {
			continue
		}
		ok := true
		for _, action := range actions {
			if err := isGatewayEligible(entry, rail, network, currency, action, dir, amt); err != nil {
				ok = false
				break
			}
		}
		if !ok {
			continue
		}
		eligible = append(eligible, entry)
	}

	if len(eligible) == 0 {
		var action model.RailOperation = discovery.RailOperationUnspecified
		if len(actions) > 0 {
			action = actions[0]
		}
		return nil, merrors.NoData(model.NoEligibleGatewayMessage(network, currency, action, toGatewayDirection(dir)))
	}
	sort.Slice(eligible, func(i, j int) bool {
		return eligible[i].ID < eligible[j].ID
	})
	if instanceID != "" {
		for _, entry := range eligible {
			if strings.EqualFold(strings.TrimSpace(entry.InstanceID), strings.TrimSpace(instanceID)) {
				return entry, nil
			}
		}
	}
	return eligible[0], nil
}

func toGatewayDirection(dir plan.SendDirection) model.GatewayDirection {
	switch dir {
	case plan.SendDirectionOut:
		return model.GatewayDirectionOut
	case plan.SendDirectionIn:
		return model.GatewayDirectionIn
	default:
		return model.GatewayDirectionAny
	}
}

func railActionNames(actions []model.RailOperation) []string {
	if len(actions) == 0 {
		return nil
	}
	names := make([]string, 0, len(actions))
	for _, action := range actions {
		name := strings.TrimSpace(string(action))
		if name == "" {
			continue
		}
		names = append(names, name)
	}
	if len(names) == 0 {
		return nil
	}
	return names
}
