package topicmock

import (
	"context"
	"fmt"
	"net"
	"reflect"
	"strconv"
	"time"

	"github.com/rekby/fixenv"
	"github.com/rekby/fixenv/sf"
	"github.com/ydb-platform/ydb-go-genproto/Ydb_Discovery_V1"
	"github.com/ydb-platform/ydb-go-genproto/Ydb_Topic_V1"
	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb"
	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Discovery"
	"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_Operations"
	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/anypb"
)

func GrpcMockTopicConnString(e fixenv.Env, topicServiceImpl Ydb_Topic_V1.TopicServiceServer) string {
	return GrpcMockTopicConnStringWithNodeIDs(e, topicServiceImpl, []uint32{1})
}

// GrpcMockTopicConnStringWithNodeIDs is like [GrpcMockTopicConnString] but discovery
// advertises the given node IDs (all on the same listener address).
func GrpcMockTopicConnStringWithNodeIDs(
	e fixenv.Env,
	topicServiceImpl Ydb_Topic_V1.TopicServiceServer,
	nodeIDs []uint32,
) string {
	v := reflect.ValueOf(topicServiceImpl)
	addr := v.Pointer()

	var f fixenv.GenericFixtureFunction[string] = func() (*fixenv.GenericResult[string], error) {
		listener := sf.LocalTCPListenerNamed(e, fmt.Sprintf("ydb-grpc-mock-topic-%v", addr))
		connString := fmt.Sprintf("grpc://%s/local", listener.Addr())

		mock, err := newGrpcMockWithNodeIDs(listener, topicServiceImpl, nodeIDs)
		if err != nil {
			return nil, fmt.Errorf("failed to create grpc mock: %w", err)
		}

		clean := func() {
			_ = mock.Close()
		}

		return fixenv.NewGenericResultWithCleanup(connString, clean), nil
	}

	return fixenv.CacheResult(e, f, fixenv.CacheOptions{CacheKey: addr})
}

type grpcMock struct {
	listener   net.Listener
	grpcServer *grpc.Server
	stopChan   chan error
}

func (m *grpcMock) Close() error {
	m.grpcServer.Stop()

	return m.listener.Close()
}

func newGrpcMockWithNodeIDs(
	listener net.Listener,
	topicServiceImpl Ydb_Topic_V1.TopicServiceServer,
	nodeIDs []uint32,
) (*grpcMock, error) {
	res := &grpcMock{
		listener:   listener,
		grpcServer: grpc.NewServer(),
		stopChan:   make(chan error, 1),
	}

	host, portS, err := net.SplitHostPort(listener.Addr().String())
	if err != nil {
		return nil, fmt.Errorf("failed to split host port addresses: %w", err)
	}

	port, err := strconv.ParseUint(portS, 10, 32)
	if err != nil {
		return nil, fmt.Errorf("failed convert port to int: %w", err)
	}
	discoveryService := newMockDiscoveryService(host, uint32(port), nodeIDs)

	Ydb_Discovery_V1.RegisterDiscoveryServiceServer(res.grpcServer, discoveryService)
	Ydb_Topic_V1.RegisterTopicServiceServer(res.grpcServer, topicServiceImpl)

	go func() {
		res.stopChan <- res.grpcServer.Serve(res.listener)
	}()

	select {
	case <-res.stopChan:
		return nil, err
	case <-time.After(time.Millisecond):
		return res, nil
	}
}

type mockDiscoveryService struct {
	Ydb_Discovery_V1.UnimplementedDiscoveryServiceServer

	host    string
	port    uint32
	nodeIDs []uint32
}

func newMockDiscoveryService(host string, port uint32, nodeIDs []uint32) *mockDiscoveryService {
	if len(nodeIDs) == 0 {
		nodeIDs = []uint32{1}
	}

	return &mockDiscoveryService{
		host:    host,
		port:    port,
		nodeIDs: nodeIDs,
	}
}

func (m mockDiscoveryService) ListEndpoints(
	ctx context.Context,
	request *Ydb_Discovery.ListEndpointsRequest,
) (*Ydb_Discovery.ListEndpointsResponse, error) {
	endpoints := make([]*Ydb_Discovery.EndpointInfo, 0, len(m.nodeIDs))
	for _, nodeID := range m.nodeIDs {
		endpoints = append(endpoints, &Ydb_Discovery.EndpointInfo{
			Address:    m.host,
			Port:       m.port,
			LoadFactor: 0,
			Ssl:        false,
			Service:    nil,
			Location:   "",
			NodeId:     nodeID,
			IpV4:       []string{"127.0.0.1"},
		})
	}

	res := &Ydb_Discovery.ListEndpointsResult{
		Endpoints:    endpoints,
		SelfLocation: "",
	}
	resp := &Ydb_Discovery.ListEndpointsResponse{
		Operation: &Ydb_Operations.Operation{
			Id:     "test-list-operation",
			Ready:  true,
			Status: Ydb.StatusIds_SUCCESS,
			Result: &anypb.Any{},
		},
	}
	err := resp.GetOperation().GetResult().MarshalFrom(res)

	return resp, err
}

func (m mockDiscoveryService) WhoAmI(
	ctx context.Context,
	request *Ydb_Discovery.WhoAmIRequest,
) (*Ydb_Discovery.WhoAmIResponse, error) {
	panic("unimplemented")
}
