package getsvc

import (
	"context"
	"encoding/hex"
	"fmt"

	"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
	clientCore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/client"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object_manager/placement"
	objectV2 "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/rpc"
	rpcclient "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/rpc/client"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/session"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/signature"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
	"go.uber.org/zap"
)

// GetRangeHash calls internal service and returns v2 response.
func (s *Service) GetRangeHash(ctx context.Context, req *objectV2.GetRangeHashRequest) (*objectV2.GetRangeHashResponse, error) {
	forward, err := s.needToForwardGetRangeHashRequest(req)
	if err != nil {
		return nil, err
	}
	if forward.needToForward {
		return s.forwardGetRangeHashRequest(ctx, req, forward)
	}
	p, err := s.toHashRangePrm(req)
	if err != nil {
		return nil, err
	}

	res, err := s.svc.GetRangeHash(ctx, *p)
	if err != nil {
		return nil, err
	}

	return toHashResponse(req.GetBody().GetType(), res), nil
}

type getRangeForwardParams struct {
	needToForward  bool
	containerNodes []netmapSDK.NodeInfo
	address        oid.Address
}

func (s *Service) needToForwardGetRangeHashRequest(req *objectV2.GetRangeHashRequest) (getRangeForwardParams, error) {
	if req.GetMetaHeader().GetTTL() <= 1 {
		return getRangeForwardParams{}, nil
	}

	var result getRangeForwardParams
	addrV2 := req.GetBody().GetAddress()
	if addrV2 == nil {
		return result, errMissingObjAddress
	}

	var addr oid.Address
	err := addr.ReadFromV2(*addrV2)
	if err != nil {
		return result, errInvalidObjAddress(err)
	}
	result.address = addr

	cont, err := s.contSource.Get(addr.Container())
	if err != nil {
		return result, fmt.Errorf("(%T) could not get container: %w", s, err)
	}

	epoch, err := s.netmapSource.Epoch()
	if err != nil {
		return result, fmt.Errorf("(%T) could not get epoch: %w", s, err)
	}

	nm, err := s.netmapSource.GetNetMapByEpoch(epoch)
	if err != nil {
		return result, fmt.Errorf("(%T) could not get netmap: %w", s, err)
	}

	builder := placement.NewNetworkMapBuilder(nm)

	objectID := addr.Object()
	nodesVector, err := builder.BuildPlacement(addr.Container(), &objectID, cont.Value.PlacementPolicy())
	if err != nil {
		return result, fmt.Errorf("(%T) could not build object placement: %w", s, err)
	}
	result.containerNodes = distinctBy(placement.FlattenNodes(nodesVector), func(n netmapSDK.NodeInfo) string { return hex.EncodeToString(n.PublicKey()) })

	for _, node := range result.containerNodes {
		if s.announcedKeys.IsLocalKey(node.PublicKey()) {
			return result, nil
		}
	}
	result.needToForward = true
	return result, nil
}

func (s *Service) forwardGetRangeHashRequest(ctx context.Context, req *objectV2.GetRangeHashRequest, params getRangeForwardParams) (*objectV2.GetRangeHashResponse, error) {
	key, err := s.keyStorage.GetKey(nil)
	if err != nil {
		return nil, err
	}

	metaHdr := new(session.RequestMetaHeader)
	metaHdr.SetTTL(req.GetMetaHeader().GetTTL() - 1)
	metaHdr.SetOrigin(req.GetMetaHeader())
	writeCurrentVersion(metaHdr)
	req.SetMetaHeader(metaHdr)

	if err := signature.SignServiceMessage(key, req); err != nil {
		return nil, err
	}

	var firstErr error
	for _, node := range params.containerNodes {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}

		var addrGr network.AddressGroup
		if err := addrGr.FromIterator(network.NodeEndpointsIterator(node)); err != nil {
			s.log.Warn(ctx, logs.GetSvcV2FailedToParseNodeEndpoints, zap.String("node_public_key", hex.EncodeToString(node.PublicKey())))
			continue
		}

		var extAddr network.AddressGroup
		if len(node.ExternalAddresses()) > 0 {
			if err := extAddr.FromStringSlice(node.ExternalAddresses()); err != nil {
				s.log.Warn(ctx, logs.GetSvcV2FailedToParseNodeExternalAddresses, zap.String("node_public_key", hex.EncodeToString(node.PublicKey())))
				continue
			}
		}

		var info clientCore.NodeInfo
		clientCore.NodeInfoFromNetmapElement(&info, placement.NewNode(addrGr, extAddr, node.PublicKey()))

		resp, err := s.performGetRangeHashOnNode(ctx, req, info)
		if err == nil {
			if err := verifyResponse(resp, info.PublicKey()); err != nil {
				return nil, err
			}
			return resp, nil
		}
		if firstErr == nil {
			firstErr = err
		}
		s.log.Debug(ctx, logs.GetSvcV2FailedToGetRangeHashFromNode,
			zap.String("node_public_key", hex.EncodeToString(node.PublicKey())),
			zap.Stringer("address", params.address),
			zap.Error(err))
	}
	s.log.Debug(ctx, logs.GetSvcV2FailedToGetRangeHashFromAllOfContainerNodes, zap.Stringer("address", params.address), zap.Error(firstErr))
	if firstErr != nil {
		return nil, firstErr
	}
	return nil, new(apistatus.ObjectNotFound)
}

func (s *Service) performGetRangeHashOnNode(ctx context.Context, req *objectV2.GetRangeHashRequest, info clientCore.NodeInfo) (*objectV2.GetRangeHashResponse, error) {
	cl, err := s.clientSource.Get(info)
	if err != nil {
		return nil, err
	}

	var firstErr error
	var resp *objectV2.GetRangeHashResponse
	info.AddressGroup().IterateAddresses(func(a network.Address) bool {
		resp, err = s.performGetRangeHashOnAddress(ctx, req, cl, a)
		if err != nil {
			if firstErr == nil {
				firstErr = err
			}
			return false
		}
		return true
	})
	if firstErr != nil {
		return nil, firstErr
	}
	if resp == nil {
		return nil, new(apistatus.ObjectNotFound)
	}
	return resp, nil
}

func (s *Service) performGetRangeHashOnAddress(ctx context.Context, req *objectV2.GetRangeHashRequest, cl clientCore.MultiAddressClient,
	a network.Address,
) (*objectV2.GetRangeHashResponse, error) {
	var resp *objectV2.GetRangeHashResponse
	var rpcErr error
	err := cl.RawForAddress(ctx, a, func(cli *rpcclient.Client) error {
		resp, rpcErr = rpc.HashObjectRange(cli, req, rpcclient.WithContext(ctx))
		return rpcErr
	})
	if err != nil {
		return nil, err
	}
	return resp, err
}

func distinctBy[T any, K comparable](source []T, keySelector func(v T) K) []T {
	var result []T
	dict := make(map[K]struct{})
	for _, v := range source {
		key := keySelector(v)
		if _, exists := dict[key]; !exists {
			result = append(result, v)
			dict[key] = struct{}{}
		}
	}
	return result
}