package quotation

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

	chainpkg "github.com/tech/sendico/pkg/chain"
	"github.com/tech/sendico/pkg/merrors"
	paymenttypes "github.com/tech/sendico/pkg/payments/types"
	chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

type managedWalletNetworkResolver struct {
	resolver              ChainGatewayResolver
	gatewayRegistry       GatewayRegistry
	gatewayInvokeResolver GatewayInvokeResolver
	logger                *zap.Logger
}

type discoveredGatewayCandidate struct {
	gatewayID  string
	instanceID string
	network    string
	invokeURI  string
}

func newManagedWalletNetworkResolver(core *Service) *managedWalletNetworkResolver {
	if core == nil {
		return nil
	}
	logger := core.logger
	if logger == nil {
		logger = zap.NewNop()
	}
	return &managedWalletNetworkResolver{
		resolver:              core.deps.gateway.resolver,
		gatewayRegistry:       core.deps.gatewayRegistry,
		gatewayInvokeResolver: core.deps.gatewayInvokeResolver,
		logger:                logger,
	}
}

func (r *managedWalletNetworkResolver) ResolveManagedWalletNetwork(ctx context.Context, managedWalletRef string) (string, error) {
	asset, err := r.ResolveManagedWalletAsset(ctx, managedWalletRef)
	if err != nil {
		return "", err
	}
	network := strings.ToUpper(strings.TrimSpace(asset.GetChain()))
	if network == "" {
		return "", merrors.NoData("managed wallet network is missing")
	}
	return network, nil
}

func (r *managedWalletNetworkResolver) ResolveManagedWalletAsset(ctx context.Context, managedWalletRef string) (*paymenttypes.Asset, error) {
	if r == nil {
		return nil, merrors.NoData("chain gateway unavailable")
	}
	walletRef := strings.TrimSpace(managedWalletRef)
	if walletRef == "" {
		return nil, merrors.InvalidArgument("managed_wallet_ref is required")
	}

	var discoveryErr error
	if r.gatewayRegistry != nil && r.gatewayInvokeResolver != nil {
		asset, err := r.resolveAssetFromDiscoveredGateways(ctx, walletRef)
		if err == nil {
			return asset, nil
		}
		discoveryErr = err
		if r.logger != nil {
			r.logger.Warn("Managed wallet asset lookup via discovery failed",
				zap.String("wallet_ref", walletRef),
				zap.Error(err),
			)
		}
	}

	if r.resolver == nil {
		if discoveryErr != nil {
			return nil, discoveryErr
		}
		return nil, merrors.NoData("chain gateway unavailable")
	}

	client, err := r.resolver.Resolve(ctx, "")
	if err != nil {
		return nil, err
	}
	if client == nil {
		return nil, merrors.NoData("chain gateway unavailable")
	}
	resp, err := client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: walletRef})
	if err != nil {
		return nil, err
	}
	return managedWalletAssetFromResponse(resp)
}

func (r *managedWalletNetworkResolver) resolveAssetFromDiscoveredGateways(ctx context.Context, walletRef string) (*paymenttypes.Asset, error) {
	candidates, err := r.listDiscoveredGatewayCandidates(ctx)
	if err != nil {
		return nil, err
	}
	if len(candidates) == 0 {
		return nil, merrors.NoData("chain gateway unavailable")
	}

	var firstErr error
	for _, candidate := range candidates {
		client, resolveErr := r.gatewayInvokeResolver.Resolve(ctx, candidate.invokeURI)
		if resolveErr != nil {
			if firstErr == nil {
				firstErr = resolveErr
			}
			continue
		}
		resp, lookupErr := client.GetManagedWallet(ctx, &chainv1.GetManagedWalletRequest{WalletRef: walletRef})
		if lookupErr != nil {
			if isManagedWalletNotFound(lookupErr) {
				continue
			}
			if firstErr == nil {
				firstErr = lookupErr
			}
			continue
		}
		asset, extractErr := managedWalletAssetFromResponse(resp)
		if extractErr != nil {
			if firstErr == nil {
				firstErr = extractErr
			}
			continue
		}
		if r.logger != nil {
			r.logger.Debug("Resolved managed wallet network from discovered gateway",
				zap.String("wallet_ref", walletRef),
				zap.String("gateway_id", candidate.gatewayID),
				zap.String("instance_id", candidate.instanceID),
				zap.String("gateway_network", candidate.network),
				zap.String("resolved_network", asset.GetChain()),
			)
		}
		return asset, nil
	}

	if firstErr != nil {
		return nil, firstErr
	}
	return nil, merrors.NoData("managed wallet not found in discovered gateways")
}

func (r *managedWalletNetworkResolver) listDiscoveredGatewayCandidates(ctx context.Context) ([]discoveredGatewayCandidate, error) {
	entries, err := r.gatewayRegistry.List(ctx)
	if err != nil {
		return nil, err
	}

	candidates := make([]discoveredGatewayCandidate, 0, len(entries))
	seenInvokeURI := map[string]struct{}{}
	for _, entry := range entries {
		if entry == nil || !entry.IsEnabled || entry.Rail != discovery.RailCrypto {
			continue
		}
		invokeURI := strings.TrimSpace(entry.InvokeURI)
		if invokeURI == "" {
			continue
		}
		key := strings.ToLower(invokeURI)
		if _, exists := seenInvokeURI[key]; exists {
			continue
		}
		seenInvokeURI[key] = struct{}{}
		candidates = append(candidates, discoveredGatewayCandidate{
			gatewayID:  strings.TrimSpace(entry.ID),
			instanceID: strings.TrimSpace(entry.InstanceID),
			network:    strings.ToUpper(strings.TrimSpace(entry.Network)),
			invokeURI:  invokeURI,
		})
	}

	sort.Slice(candidates, func(i, j int) bool {
		if candidates[i].gatewayID != candidates[j].gatewayID {
			return candidates[i].gatewayID < candidates[j].gatewayID
		}
		if candidates[i].instanceID != candidates[j].instanceID {
			return candidates[i].instanceID < candidates[j].instanceID
		}
		return candidates[i].invokeURI < candidates[j].invokeURI
	})
	return candidates, nil
}

func managedWalletAssetFromResponse(resp *chainv1.GetManagedWalletResponse) (*paymenttypes.Asset, error) {
	wallet := resp.GetWallet()
	if wallet == nil || wallet.GetAsset() == nil {
		return nil, merrors.NoData("managed wallet asset is missing")
	}
	network := strings.ToUpper(strings.TrimSpace(chainpkg.NetworkAlias(wallet.GetAsset().GetChain())))
	if network == "" || network == "UNSPECIFIED" {
		return nil, merrors.NoData("managed wallet network is missing")
	}
	return &paymenttypes.Asset{
		Chain:           network,
		TokenSymbol:     strings.ToUpper(strings.TrimSpace(wallet.GetAsset().GetTokenSymbol())),
		ContractAddress: strings.TrimSpace(wallet.GetAsset().GetContractAddress()),
	}, nil
}

func isManagedWalletNotFound(err error) bool {
	if err == nil {
		return false
	}
	if errors.Is(err, merrors.ErrNoData) {
		return true
	}
	if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
		return true
	}
	msg := strings.ToLower(strings.TrimSpace(err.Error()))
	return strings.Contains(msg, "not_found")
}
