package consensus

import (
	"io"
	"path/filepath"
	"time"

	hraft "github.com/hashicorp/raft"
	"golang.org/x/xerrors"

	"github.com/filecoin-project/lotus/node/config"
	"github.com/filecoin-project/lotus/node/repo"
)

// Configuration defaults
var (
	DefaultDataSubFolder        = "raft-cluster"
	DefaultWaitForLeaderTimeout = 15 * time.Second
	DefaultCommitRetries        = 1
	DefaultNetworkTimeout       = 100 * time.Second
	DefaultCommitRetryDelay     = 200 * time.Millisecond
	DefaultBackupsRotate        = 6
)

// ClusterRaftConfig allows to configure the Raft Consensus component for the node cluster.
type ClusterRaftConfig struct {
	// config to enabled node cluster with raft consensus
	ClusterModeEnabled bool
	// A folder to store Raft's data.
	DataFolder string
	// InitPeerset provides the list of initial cluster peers for new Raft
	// peers (with no prior state). It is ignored when Raft was already
	// initialized or when starting in staging mode.
	InitPeerset []string
	// LeaderTimeout specifies how long to wait for a leader before
	// failing an operation.
	WaitForLeaderTimeout time.Duration
	// NetworkTimeout specifies how long before a Raft network
	// operation is timed out
	NetworkTimeout time.Duration
	// CommitRetries specifies how many times we retry a failed commit until
	// we give up.
	CommitRetries int
	// How long to wait between retries
	CommitRetryDelay time.Duration
	// BackupsRotate specifies the maximum number of Raft's DataFolder
	// copies that we keep as backups (renaming) after cleanup.
	BackupsRotate int
	// A Hashicorp Raft's configuration object.
	RaftConfig *hraft.Config

	// Tracing enables propagation of contexts across binary boundaries.
	Tracing bool
}

func DefaultClusterRaftConfig() *ClusterRaftConfig {
	var cfg ClusterRaftConfig
	cfg.DataFolder = "" // empty so it gets omitted
	cfg.InitPeerset = []string{}
	cfg.WaitForLeaderTimeout = DefaultWaitForLeaderTimeout
	cfg.NetworkTimeout = DefaultNetworkTimeout
	cfg.CommitRetries = DefaultCommitRetries
	cfg.CommitRetryDelay = DefaultCommitRetryDelay
	cfg.BackupsRotate = DefaultBackupsRotate
	cfg.RaftConfig = hraft.DefaultConfig()

	// These options are imposed over any Default Raft Config.
	cfg.RaftConfig.ShutdownOnRemove = false
	cfg.RaftConfig.LocalID = "will_be_set_automatically"

	// Set up logging
	cfg.RaftConfig.LogOutput = io.Discard
	return &cfg
}

func NewClusterRaftConfig(userRaftConfig *config.UserRaftConfig) *ClusterRaftConfig {
	var cfg ClusterRaftConfig
	cfg.DataFolder = userRaftConfig.DataFolder
	cfg.InitPeerset = userRaftConfig.InitPeersetMultiAddr
	cfg.WaitForLeaderTimeout = time.Duration(userRaftConfig.WaitForLeaderTimeout)
	cfg.NetworkTimeout = time.Duration(userRaftConfig.NetworkTimeout)
	cfg.CommitRetries = userRaftConfig.CommitRetries
	cfg.CommitRetryDelay = time.Duration(userRaftConfig.CommitRetryDelay)
	cfg.BackupsRotate = userRaftConfig.BackupsRotate

	// Keep this to be default hraft config for now
	cfg.RaftConfig = hraft.DefaultConfig()

	// These options are imposed over any Default Raft Config.
	cfg.RaftConfig.ShutdownOnRemove = false
	cfg.RaftConfig.LocalID = "will_be_set_automatically"

	// Set up logging
	cfg.RaftConfig.LogOutput = io.Discard

	return &cfg

}

// Validate checks that this configuration has working values,
// at least in appearance.
func ValidateConfig(cfg *ClusterRaftConfig) error {
	if cfg.RaftConfig == nil {
		return xerrors.Errorf("no hashicorp/raft.Config")
	}
	if cfg.WaitForLeaderTimeout <= 0 {
		return xerrors.Errorf("wait_for_leader_timeout <= 0")
	}

	if cfg.NetworkTimeout <= 0 {
		return xerrors.Errorf("network_timeout <= 0")
	}

	if cfg.CommitRetries < 0 {
		return xerrors.Errorf("commit_retries is invalid")
	}

	if cfg.CommitRetryDelay <= 0 {
		return xerrors.Errorf("commit_retry_delay is invalid")
	}

	if cfg.BackupsRotate <= 0 {
		return xerrors.Errorf("backups_rotate should be larger than 0")
	}

	return hraft.ValidateConfig(cfg.RaftConfig)
}

// GetDataFolder returns the Raft data folder that we are using.
func (cfg *ClusterRaftConfig) GetDataFolder(repo repo.LockedRepo) string {
	if cfg.DataFolder == "" {
		return filepath.Join(repo.Path(), DefaultDataSubFolder)
	}
	return filepath.Join(repo.Path(), cfg.DataFolder)
}