/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package deliverservice

import (
	"encoding/pem"
	"os"
	"slices"
	"time"

	"github.com/hyperledger/fabric/common/deliverclient/orderers"
	"github.com/hyperledger/fabric/core/config"
	"github.com/hyperledger/fabric/internal/pkg/comm"
	"github.com/pkg/errors"
	"github.com/spf13/viper"
)

const (
	DefaultReConnectBackoffThreshold   = time.Hour * 1
	DefaultReConnectTotalTimeThreshold = time.Second * 60 * 60
	DefaultConnectionTimeout           = time.Second * 3
	DefaultBlockCensorshipTimeoutKey   = time.Second * 30
	DefaultMinimalReconnectInterval    = time.Millisecond * 100
	DefaultPolicy                      = "cluster"
)

// DeliverServiceConfig is the struct that defines the deliverservice configuration.
type DeliverServiceConfig struct {
	// PeerTLSEnabled enables/disables Peer TLS.
	PeerTLSEnabled bool
	// BlockGossipEnabled enables block forwarding via gossip
	BlockGossipEnabled bool
	// ReConnectBackoffThreshold sets the delivery service maximal delay between consecutive retries.
	ReConnectBackoffThreshold time.Duration
	// ReconnectTotalTimeThreshold sets the total time the delivery service may spend in reconnection attempts
	// until its retry logic gives up and returns an error.
	ReconnectTotalTimeThreshold time.Duration
	// ConnectionTimeout sets the delivery service <-> ordering service node connection timeout
	ConnectionTimeout time.Duration
	// Keepalive option for deliveryservice
	KeepaliveOptions comm.KeepaliveOptions
	// SecOpts provides the TLS info for connections
	SecOpts comm.SecureOptions
	// If a certain header from a header receiver is in front of the block receiver for more that this time, a
	// censorship event is declared and the block source is changed.
	BlockCensorshipTimeoutKey time.Duration
	// The initial value of the actual retry interval, which is increased on every failed retry
	MinimalReconnectInterval time.Duration
	// OrdererEndpointOverrides is a map of orderer addresses which should be
	// re-mapped to a different orderer endpoint.
	OrdererEndpointOverrides map[string]*orderers.Endpoint
	// Determines which delivery client will be used when consensus type is "BFT"
	// (when consensus type is "etcdraft" this key is ignored).
	// "simple" - use CFT deliverer
	// "cluster" - use BFT deliverer
	Policy string
}

type AddressOverride struct {
	From        string
	To          string
	CACertsFile string
}

// GlobalConfig obtains a set of configuration from viper, build and returns the config struct.
func GlobalConfig() *DeliverServiceConfig {
	c := &DeliverServiceConfig{}
	c.loadDeliverServiceConfig()
	return c
}

func LoadOverridesMap() (map[string]*orderers.Endpoint, error) {
	var overrides []AddressOverride
	err := viper.UnmarshalKey("peer.deliveryclient.addressOverrides", &overrides)
	if err != nil {
		return nil, errors.WithMessage(err, "could not unmarshal peer.deliveryclient.addressOverrides")
	}

	if len(overrides) == 0 {
		return nil, nil
	}

	overrideMap := map[string]*orderers.Endpoint{}
	for _, override := range overrides {
		var rootCerts [][]byte
		if override.CACertsFile != "" {
			pem, err := os.ReadFile(override.CACertsFile)
			if err != nil {
				logger.Warningf("could not read file '%s' specified for caCertsFile of orderer endpoint override from '%s' to '%s': %s", override.CACertsFile, override.From, override.To, err)
				continue
			}
			rootCerts = extractCerts(pem)
			if len(rootCerts) == 0 {
				logger.Warningf("Attempted to create a cert pool for override of orderer address '%s' to '%s' but did not find any valid certs in '%s'", override.From, override.To, override.CACertsFile)
				continue
			}
		}
		overrideMap[override.From] = &orderers.Endpoint{
			Address:   override.To,
			RootCerts: rootCerts,
		}
	}

	return overrideMap, nil
}

// extractCerts is a hacky way of breaking apart a collection of PEM encoded
// certificates. This is used to preserve the semantics of
// x509.CertPool#AppendCertsFromPEM after removing the CertPool from the
// orderers.Endpoint.
func extractCerts(pemCerts []byte) [][]byte {
	var certs [][]byte
	for len(pemCerts) > 0 {
		var block *pem.Block
		block, pemCerts = pem.Decode(pemCerts)
		if block == nil {
			break
		}
		if block.Type != "CERTIFICATE" || len(block.Headers) != 0 {
			continue
		}
		certs = append(certs, pem.EncodeToMemory(block))
	}
	return certs
}

func (c *DeliverServiceConfig) loadDeliverServiceConfig() {
	enabledKey := "peer.deliveryclient.blockGossipEnabled"
	enabledConfigOptionMissing := !viper.IsSet(enabledKey)
	if enabledConfigOptionMissing {
		logger.Infof("peer.deliveryclient.blockGossipEnabled is not set, defaulting to true.")
	}
	c.BlockGossipEnabled = enabledConfigOptionMissing || viper.GetBool(enabledKey)

	blockCensorshipTimeoutKey := "peer.deliveryclient.blockCensorshipTimeoutKey"
	blockCensorshipTimeoutOptionMissing := !viper.IsSet(blockCensorshipTimeoutKey)
	if blockCensorshipTimeoutOptionMissing {
		c.BlockCensorshipTimeoutKey = DefaultBlockCensorshipTimeoutKey
		logger.Infof("peer.deliveryclient.blockCensorshipTimeoutKey is not set, defaulting to %d s.", DefaultBlockCensorshipTimeoutKey)
	} else {
		c.BlockCensorshipTimeoutKey = viper.GetDuration(blockCensorshipTimeoutKey)
	}

	minimalReconnectInterval := "peer.deliveryclient.minimalReconnectInterval"
	MinimalReconnectIntervalOptionMissing := !viper.IsSet(minimalReconnectInterval)
	if MinimalReconnectIntervalOptionMissing {
		c.MinimalReconnectInterval = DefaultMinimalReconnectInterval
		logger.Infof("peer.deliveryclient.minimalReconnectInterval is not set, defaulting to %d ms.", DefaultMinimalReconnectInterval)
	} else {
		c.MinimalReconnectInterval = viper.GetDuration(minimalReconnectInterval)
	}

	c.PeerTLSEnabled = viper.GetBool("peer.tls.enabled")

	c.ReConnectBackoffThreshold = viper.GetDuration("peer.deliveryclient.reConnectBackoffThreshold")
	if c.ReConnectBackoffThreshold == 0 {
		c.ReConnectBackoffThreshold = DefaultReConnectBackoffThreshold
	}

	c.ReconnectTotalTimeThreshold = viper.GetDuration("peer.deliveryclient.reconnectTotalTimeThreshold")
	if c.ReconnectTotalTimeThreshold == 0 {
		c.ReconnectTotalTimeThreshold = DefaultReConnectTotalTimeThreshold
	}

	c.ConnectionTimeout = viper.GetDuration("peer.deliveryclient.connTimeout")
	if c.ConnectionTimeout == 0 {
		c.ConnectionTimeout = DefaultConnectionTimeout
	}

	c.KeepaliveOptions = comm.DefaultKeepaliveOptions
	if viper.IsSet("peer.keepalive.deliveryClient.interval") {
		c.KeepaliveOptions.ClientInterval = viper.GetDuration("peer.keepalive.deliveryClient.interval")
	}
	if viper.IsSet("peer.keepalive.deliveryClient.timeout") {
		c.KeepaliveOptions.ClientTimeout = viper.GetDuration("peer.keepalive.deliveryClient.timeout")
	}

	c.SecOpts = comm.SecureOptions{
		UseTLS:            viper.GetBool("peer.tls.enabled"),
		RequireClientCert: viper.GetBool("peer.tls.clientAuthRequired"),
	}

	if c.SecOpts.RequireClientCert {
		certFile := config.GetPath("peer.tls.clientCert.file")
		if certFile == "" {
			certFile = config.GetPath("peer.tls.cert.file")
		}

		keyFile := config.GetPath("peer.tls.clientKey.file")
		if keyFile == "" {
			keyFile = config.GetPath("peer.tls.key.file")
		}

		keyPEM, err := os.ReadFile(keyFile)
		if err != nil {
			panic(errors.WithMessagef(err, "unable to load key at '%s'", keyFile))
		}
		c.SecOpts.Key = keyPEM
		certPEM, err := os.ReadFile(certFile)
		if err != nil {
			panic(errors.WithMessagef(err, "unable to load cert at '%s'", certFile))
		}
		c.SecOpts.Certificate = certPEM
	}

	overridesMap, err := LoadOverridesMap()
	if err != nil {
		panic(err)
	}

	c.OrdererEndpointOverrides = overridesMap

	policyKey := "peer.deliveryclient.policy"
	policyMissing := !viper.IsSet(policyKey)
	policy := DefaultPolicy
	if policyMissing {
		logger.Infof("%s is not set, defaulting to %s.", policyKey, policy)
	} else {
		policy = viper.GetString(policyKey)
		validPolicies := []string{"simple", "cluster"}
		if !slices.Contains(validPolicies, policy) {
			logger.Warnf("%s is set to \"%s\" which is not valid policy, defaulting to \"%s\"", policyKey, policy, DefaultPolicy)
			policy = DefaultPolicy
		}
	}

	c.Policy = policy
}
