package server

import (
	"fmt"
	"math/rand"
	"net/rpc"
	"sync"
	"time"
)

type RPCClient struct {
	mu      sync.Mutex
	clients map[string]*rpc.Client
	random  *rand.Rand
}

func NewRPCClient() (*RPCClient, error) {
	if serverInstance == nil {
		return nil, errorf(errServerNotInit)
	}
	if serverInstance.serverManager == nil {
		return nil, errorf(errServerManagerNotInit)
	}
	return &RPCClient{
		clients: make(map[string]*rpc.Client),
		random:  rand.New(rand.NewSource(time.Now().UnixNano())),
	}, nil
}

func (a *RPCClient) rpcCallWithReconnect(key string, client *rpc.Client, addr string, serverMethod string, args interface{}, reply interface{}) error {
	err := client.Call(serverMethod, args, reply)
	if err == rpc.ErrShutdown {
		Log.Warnf("rpc %s connection shut down, trying to reconnect...", addr)
		client, err = rpc.Dial("tcp", addr)
		if err != nil {
			return err
		}
		a.clients[key] = client
		return client.Call(serverMethod, args, reply)
	}
	return err
}

// Call RPC call with reconnect and retry.
func (a *RPCClient) Call(severName string, serverMethod string, args interface{}, reply interface{}) error {
	a.mu.Lock()
	defer a.mu.Unlock()
	addrs, err := serverInstance.serverManager.GetServerHosts(severName, FlagRPCHost)
	if err != nil {
		return err
	}
	// pick a random start index for round robin
	total := len(addrs)
	start := a.random.Intn(total)

	for idx := 0; idx < total; idx++ {
		addr := addrs[(start+idx)%total]
		mapkey := fmt.Sprintf("%s[%s]", severName, addr)
		if a.clients[mapkey] == nil {
			a.clients[mapkey], err = rpc.Dial("tcp", addr)
			if err != nil {
				Log.Warnf("RPC dial error : %s", err)
				continue
			}
		}
		err = a.rpcCallWithReconnect(mapkey, a.clients[mapkey], addr, serverMethod, args, reply)
		if err != nil {
			continue
		}

		return nil
	}
	return errorf(err.Error())
}

// CallHost RPC call by host
func (a *RPCClient) CallHost(host string, serverMethod string, args interface{}, reply interface{}) error {
	if a.clients[host] == nil {
		var err error
		a.clients[host], err = rpc.Dial("tcp", host)
		if err != nil {
			Log.Errorf("RPC dial error : %s", err)
			return err
		}
	}
	return a.rpcCallWithReconnect(host, a.clients[host], host, serverMethod, args, reply)
}
