/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>
*/

package config

import (
	"time"

	"github.com/cgrates/birpc/context"
	"github.com/cgrates/cgrates/utils"
	"github.com/cgrates/rpcclient"
)

// NewDfltRPCConn returns the default value for a RPCConn
func NewDfltRPCConn() *RPCConn {
	return &RPCConn{Strategy: rpcclient.PoolFirst}
}

// RPCConns the config for all rpc pools
type RPCConns map[string]*RPCConn

// loadRPCConns loads the RPCConns section of the configuration
func (rC RPCConns) Load(ctx *context.Context, jsnCfg ConfigDB, _ *CGRConfig) (err error) {
	jsnRPCConns := make(RPCConnsJson)
	if err = jsnCfg.GetSection(ctx, RPCConnsJSON, &jsnRPCConns); err != nil {
		return
	}
	rC.loadFromJSONCfg(jsnRPCConns)
	return
}

func (rC RPCConns) loadFromJSONCfg(jsn RPCConnsJson) {
	// hardoded the *internal connection
	rC[utils.MetaInternal] = &RPCConn{
		Strategy: rpcclient.PoolFirst,
		PoolSize: 0,
		Conns: []*RemoteHost{{
			Address: utils.MetaInternal,
		}},
	}
	rC[rpcclient.BiRPCInternal] = &RPCConn{
		Strategy: rpcclient.PoolFirst,
		PoolSize: 0,
		Conns: []*RemoteHost{{
			Address: rpcclient.BiRPCInternal,
		}},
	}
	rC[utils.MetaLocalHost] = &RPCConn{
		Strategy: rpcclient.PoolFirst,
		PoolSize: 0,
		Conns: []*RemoteHost{{
			Address:   "127.0.0.1:2012",
			Transport: utils.MetaJSON,
		}},
	}
	rC[utils.MetaBiJSONLocalHost] = &RPCConn{
		Strategy: rpcclient.PoolFirst,
		PoolSize: 0,
		Conns: []*RemoteHost{{
			Address:   "127.0.0.1:2014",
			Transport: rpcclient.BiRPCJSON,
		}},
	}
	for key, val := range jsn {
		rC[key] = NewDfltRPCConn()
		rC[key].loadFromJSONCfg(val)
	}
}

// AsMapInterface returns the config as a map[string]any
func (rC RPCConns) AsMapInterface() any {
	rpcConns := make(map[string]any)
	for key, value := range rC {
		rpcConns[key] = value.AsMapInterface()
	}
	return rpcConns
}

func (RPCConns) SName() string            { return RPCConnsJSON }
func (rC RPCConns) CloneSection() Section { return rC.Clone() }

// Clone returns a deep copy of RPCConns
func (rC RPCConns) Clone() (cln RPCConns) {
	cln = make(RPCConns)
	for id, conn := range rC {
		cln[id] = conn.Clone()
	}
	return
}

// RPCConn the connection pool config
type RPCConn struct {
	Strategy     string
	PoolSize     int
	ReplyTimeout time.Duration
	Conns        []*RemoteHost
}

func (rC *RPCConn) loadFromJSONCfg(jsnCfg *RPCConnJson) (err error) {
	if jsnCfg == nil {
		return
	}
	if jsnCfg.Strategy != nil {
		rC.Strategy = *jsnCfg.Strategy
	}
	if jsnCfg.PoolSize != nil {
		rC.PoolSize = *jsnCfg.PoolSize
	}
	if jsnCfg.Reply_timeout != nil {
		if rC.ReplyTimeout, err = utils.ParseDurationWithNanosecs(*jsnCfg.Reply_timeout); err != nil {
			return
		}
	}
	if jsnCfg.Conns != nil {
		rC.Conns = make([]*RemoteHost, len(*jsnCfg.Conns))
		for idx, jsnHaCfg := range *jsnCfg.Conns {
			rC.Conns[idx] = getDftRemHstCfg()
			rC.Conns[idx].loadFromJSONCfg(jsnHaCfg) //To review if the function signature changes
		}
	}
	return
}

// AsMapInterface returns the config as a map[string]any
func (rC *RPCConn) AsMapInterface() (mp map[string]any) {
	mp = map[string]any{
		utils.StrategyCfg: rC.Strategy,
		utils.PoolSize:    rC.PoolSize,
	}
	if rC.ReplyTimeout != 0 {
		mp[utils.ReplyTimeoutCfg] = rC.ReplyTimeout
	}
	if rC.Conns != nil {
		conns := make([]map[string]any, len(rC.Conns))
		for i, item := range rC.Conns {
			conns[i] = item.AsMapInterface()
		}
		mp[utils.Conns] = conns
	}
	return
}

// Clone returns a deep copy of RPCConn
func (rC RPCConn) Clone() (cln *RPCConn) {
	cln = &RPCConn{
		Strategy:     rC.Strategy,
		PoolSize:     rC.PoolSize,
		ReplyTimeout: rC.ReplyTimeout,
	}
	if rC.Conns != nil {
		cln.Conns = make([]*RemoteHost, len(rC.Conns))
		for i, req := range rC.Conns {
			cln.Conns[i] = req.Clone()
		}
	}
	return
}

// RemoteHost connection config
type RemoteHost struct {
	ID                   string
	Address              string
	Transport            string
	ConnectAttempts      int
	Reconnects           int
	MaxReconnectInterval time.Duration
	ConnectTimeout       time.Duration
	ReplyTimeout         time.Duration
	TLS                  bool
	ClientKey            string
	ClientCertificate    string
	CaCertificate        string
}

func (rh *RemoteHost) loadFromJSONCfg(jsnCfg *RemoteHostJson) (err error) {
	if jsnCfg == nil {
		return
	}
	if jsnCfg.Id != nil {
		rh.ID = *jsnCfg.Id
		// ignore defaults if we have ID
		rh.Address = utils.EmptyString
		rh.Transport = utils.EmptyString
	}
	if jsnCfg.Address != nil {
		rh.Address = *jsnCfg.Address
	}
	if jsnCfg.Transport != nil {
		rh.Transport = *jsnCfg.Transport
	}
	if jsnCfg.Tls != nil {
		rh.TLS = *jsnCfg.Tls
	}
	if jsnCfg.Client_key != nil {
		rh.ClientKey = *jsnCfg.Client_key
	}
	if jsnCfg.Client_certificate != nil {
		rh.ClientCertificate = *jsnCfg.Client_certificate
	}
	if jsnCfg.Ca_certificate != nil {
		rh.CaCertificate = *jsnCfg.Ca_certificate
	}
	if jsnCfg.Connect_attempts != nil {
		rh.ConnectAttempts = *jsnCfg.Connect_attempts
	}
	if jsnCfg.Reconnects != nil {
		rh.Reconnects = *jsnCfg.Reconnects
	}
	if jsnCfg.Max_reconnect_interval != nil {
		if rh.MaxReconnectInterval, err = utils.ParseDurationWithNanosecs(*jsnCfg.Max_reconnect_interval); err != nil {
			return err
		}
	}
	if jsnCfg.Connect_timeout != nil {
		if rh.ConnectTimeout, err = utils.ParseDurationWithNanosecs(*jsnCfg.Connect_timeout); err != nil {
			return err
		}
	}
	if jsnCfg.Reply_timeout != nil {
		if rh.ReplyTimeout, err = utils.ParseDurationWithNanosecs(*jsnCfg.Reply_timeout); err != nil {
			return err
		}
	}
	return
}

// AsMapInterface returns the config as a map[string]any
func (rh *RemoteHost) AsMapInterface() (mp map[string]any) {
	mp = map[string]any{
		utils.AddressCfg:   rh.Address,
		utils.TransportCfg: rh.Transport,
	}
	if rh.ID != utils.EmptyString {
		mp[utils.IDCfg] = rh.ID
	}
	if rh.TLS {
		mp[utils.TLSNoCaps] = rh.TLS
	}
	if rh.ClientKey != utils.EmptyString {
		mp[utils.KeyPathCgr] = rh.ClientKey
	}
	if rh.ClientCertificate != utils.EmptyString {
		mp[utils.CertPathCgr] = rh.ClientCertificate
	}
	if rh.CaCertificate != utils.EmptyString {
		mp[utils.CAPathCgr] = rh.CaCertificate
	}
	if rh.ConnectAttempts != 0 {
		mp[utils.ConnectAttemptsCfg] = rh.ConnectAttempts
	}
	if rh.Reconnects != 0 {
		mp[utils.ReconnectsCfg] = rh.Reconnects
	}
	if rh.MaxReconnectInterval != 0 {
		mp[utils.MaxReconnectIntervalCfg] = rh.MaxReconnectInterval.String()
	}
	if rh.ConnectTimeout != 0 {
		mp[utils.ConnectTimeoutCfg] = rh.ConnectTimeout
	}
	if rh.ReplyTimeout != 0 {
		mp[utils.ReplyTimeoutCfg] = rh.ReplyTimeout
	}
	return
}

// Clone returns a deep copy of RemoteHost
func (rh RemoteHost) Clone() (cln *RemoteHost) {
	return &RemoteHost{
		ID:                   rh.ID,
		Address:              rh.Address,
		Transport:            rh.Transport,
		TLS:                  rh.TLS,
		ClientKey:            rh.ClientKey,
		ClientCertificate:    rh.ClientCertificate,
		CaCertificate:        rh.CaCertificate,
		ConnectAttempts:      rh.ConnectAttempts,
		Reconnects:           rh.Reconnects,
		MaxReconnectInterval: rh.MaxReconnectInterval,
		ConnectTimeout:       rh.ConnectTimeout,
		ReplyTimeout:         rh.ReplyTimeout,
	}
}

// UpdateRPCCons will parse each conn and update only
// the conns that have the same ID
func UpdateRPCCons(rpcConns RPCConns, newHosts map[string]*RemoteHost) (connIDs utils.StringSet) {
	connIDs = make(utils.StringSet)
	for rpcKey, rpcPool := range rpcConns {
		for _, rh := range rpcPool.Conns {
			newHost, has := newHosts[rh.ID]
			if !has {
				continue
			}
			connIDs.Add(rpcKey)
			rh.Address = newHost.Address
			rh.Transport = newHost.Transport
			rh.TLS = newHost.TLS
			rh.ClientKey = newHost.ClientKey
			rh.ClientCertificate = newHost.ClientCertificate
			rh.CaCertificate = newHost.CaCertificate
			rh.ConnectAttempts = newHost.ConnectAttempts
			rh.Reconnects = newHost.Reconnects
			rh.MaxReconnectInterval = newHost.MaxReconnectInterval
			rh.ConnectTimeout = newHost.ConnectTimeout
			rh.ReplyTimeout = newHost.ReplyTimeout
		}
	}
	return
}

// RemoveRPCCons will parse each conn and reset only
// the conns that have the same ID
func RemoveRPCCons(rpcConns RPCConns, hosts utils.StringSet) (connIDs utils.StringSet) {
	connIDs = make(utils.StringSet)
	for rpcKey, rpcPool := range rpcConns {
		for _, rh := range rpcPool.Conns {
			if !hosts.Has(rh.ID) {
				continue
			}
			connIDs.Add(rpcKey)
			rh.Address = ""
			rh.Transport = ""
			rh.TLS = false
			rh.ClientKey = ""
			rh.ClientCertificate = ""
			rh.CaCertificate = ""
			rh.ConnectAttempts = 0
			rh.Reconnects = 0
			rh.MaxReconnectInterval = 0
			rh.ConnectTimeout = 0
			rh.ReplyTimeout = 0
		}
	}
	return
}

// Represents one connection instance towards a rater/cdrs server
type RemoteHostJson struct {
	Id                     *string
	Address                *string
	Transport              *string
	Connect_attempts       *int
	Reconnects             *int
	Max_reconnect_interval *string
	Connect_timeout        *string
	Reply_timeout          *string
	Tls                    *bool
	Client_certificate     *string
	Client_key             *string
	Ca_certificate         *string
}

func diffRemoteHostJson(v1, v2 *RemoteHost) (d *RemoteHostJson) {
	d = new(RemoteHostJson)
	if v1.ID != v2.ID {
		d.Id = utils.StringPointer(v2.ID)
	}
	if v1.Address != v2.Address {
		d.Address = utils.StringPointer(v2.Address)
	}
	if v1.Transport != v2.Transport {
		d.Transport = utils.StringPointer(v2.Transport)
	}
	if v1.TLS != v2.TLS {
		d.Tls = utils.BoolPointer(v2.TLS)
	}
	if v1.ClientKey != v2.ClientKey {
		d.Client_key = utils.StringPointer(v2.ClientKey)
	}
	if v1.ClientCertificate != v2.ClientCertificate {
		d.Client_certificate = utils.StringPointer(v2.ClientCertificate)
	}
	if v1.ClientCertificate != v2.ClientCertificate {
		d.Ca_certificate = utils.StringPointer(v2.CaCertificate)
	}
	if v1.ConnectAttempts != v2.ConnectAttempts {
		d.Connect_attempts = utils.IntPointer(v2.ConnectAttempts)
	}
	if v1.Reconnects != v2.Reconnects {
		d.Reconnects = utils.IntPointer(v2.Reconnects)
	}
	if v1.MaxReconnectInterval != v2.MaxReconnectInterval {
		d.Max_reconnect_interval = utils.StringPointer(v2.MaxReconnectInterval.String())
	}
	if v1.ConnectTimeout != v2.ConnectTimeout {
		d.Connect_timeout = utils.StringPointer(v2.ConnectTimeout.String())
	}
	if v1.ReplyTimeout != v2.ReplyTimeout {
		d.Reply_timeout = utils.StringPointer(v2.ReplyTimeout.String())
	}
	return
}

type RPCConnJson struct {
	Strategy      *string
	PoolSize      *int
	Reply_timeout *string
	Conns         *[]*RemoteHostJson
}

func diffRPCConnJson(d *RPCConnJson, v1, v2 *RPCConn) *RPCConnJson {
	if d == nil {
		d = new(RPCConnJson)
	}
	if v1.Strategy != v2.Strategy {
		d.Strategy = utils.StringPointer(v2.Strategy)
	}
	if v1.PoolSize != v2.PoolSize {
		d.PoolSize = utils.IntPointer(v2.PoolSize)
	}
	if v1.ReplyTimeout != v2.ReplyTimeout {
		d.Reply_timeout = utils.StringPointer(v2.ReplyTimeout.String())
	}
	if v2.Conns != nil {
		conns := make([]*RemoteHostJson, len(v2.Conns))
		dft := getDftRemHstCfg()
		for i, conn := range v2.Conns {
			conns[i] = diffRemoteHostJson(dft, conn)
		}
		d.Conns = &conns
	}
	return d
}

func equalsRemoteHosts(v1, v2 []*RemoteHost) bool {
	if len(v1) != len(v2) {
		return false
	}
	for i := range v2 {
		if v1[i].ID != v2[i].ID ||
			v1[i].Address != v2[i].Address ||
			v1[i].Transport != v2[i].Transport ||
			v1[i].TLS != v2[i].TLS ||
			v1[i].ClientKey != v2[i].ClientKey ||
			v1[i].ClientCertificate != v2[i].ClientCertificate ||
			v1[i].CaCertificate != v2[i].CaCertificate ||
			v1[i].ConnectAttempts != v2[i].ConnectAttempts ||
			v1[i].Reconnects != v2[i].Reconnects ||
			v1[i].MaxReconnectInterval != v2[i].MaxReconnectInterval ||
			v1[i].ConnectTimeout != v2[i].ConnectTimeout ||
			v1[i].ReplyTimeout != v2[i].ReplyTimeout {
			return false
		}
	}
	return true
}

func equalsRPCConn(v1, v2 *RPCConn) bool {
	return v1 == nil && v2 == nil ||
		v1 != nil && v2 != nil &&
			v1.Strategy == v2.Strategy &&
			v1.PoolSize == v2.PoolSize &&
			v1.ReplyTimeout == v2.ReplyTimeout &&
			equalsRemoteHosts(v1.Conns, v2.Conns)
}

type RPCConnsJson map[string]*RPCConnJson

func diffRPCConnsJson(d RPCConnsJson, v1, v2 RPCConns) RPCConnsJson {
	if d == nil {
		d = make(RPCConnsJson)
	}
	dft := NewDfltRPCConn()
	for k, val := range v2 {
		if !equalsRPCConn(v1[k], val) {
			d[k] = diffRPCConnJson(d[k], dft, val)
		}
	}
	return d
}
