// Copyright (c) 2021 Multus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package config

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/blang/semver"
)

const (
	configListCapabilityKey   = "plugins"
	singleConfigCapabilityKey = "capabilities"
)

// LogOptionFunc mutates the `LoggingOptions` object
type LogOptionFunc func(logOptions *LogOptions)

// Option mutates the `conf` object
type Option func(conf *MultusConf)

// MultusConf holds the multus configuration, and persists it to disk
type MultusConf struct {
	BinDir                   string          `json:"binDir,omitempty"`
	Capabilities             map[string]bool `json:"capabilities,omitempty"`
	CNIVersion               string          `json:"cniVersion"`
	Delegates                []interface{}   `json:"delegates"`
	LogFile                  string          `json:"logFile,omitempty"`
	LogLevel                 string          `json:"logLevel,omitempty"`
	LogToStderr              bool            `json:"logToStderr,omitempty"`
	LogOptions               *LogOptions     `json:"logOptions,omitempty"`
	Kubeconfig               string          `json:"kubeconfig"`
	Name                     string          `json:"name"`
	NamespaceIsolation       bool            `json:"namespaceIsolation,omitempty"`
	RawNonIsolatedNamespaces string          `json:"globalNamespaces,omitempty"`
	ReadinessIndicatorFile   string          `json:"readinessindicatorfile,omitempty"`
	Type                     string          `json:"type"`
}

// LogOptions specifies the configuration of the log
type LogOptions struct {
	MaxAge     *int  `json:"maxAge,omitempty"`
	MaxSize    *int  `json:"maxSize,omitempty"`
	MaxBackups *int  `json:"maxBackups,omitempty"`
	Compress   *bool `json:"compress,omitempty"`
}

// NewMultusConfig creates a basic configuration generator. It can be mutated
// via the `With...` methods.
func NewMultusConfig(pluginName string, cniVersion string, kubeconfig string, configurationOptions ...Option) (*MultusConf, error) {
	multusConfig := &MultusConf{
		Name:         MultusDefaultNetworkName,
		CNIVersion:   cniVersion,
		Type:         pluginName,
		Capabilities: map[string]bool{},
		Kubeconfig:   kubeconfig,
		Delegates:    []interface{}{},
	}

	err := multusConfig.Mutate(configurationOptions...)
	return multusConfig, err
}

// CheckVersionCompatibility checks compatibilty of the
// top level cni version with the delegate cni version.
// Since version 0.4.0, CHECK was introduced, which
// causes incompatibility.
func CheckVersionCompatibility(mc *MultusConf) error {
	const versionFmt = "delegate cni version is %s while top level cni version is %s"
	v040, _ := semver.Make("0.4.0")
	multusCNIVersion, err := semver.Make(mc.CNIVersion)

	if err != nil {
		return errors.New("couldn't get top level cni version")
	}

	if multusCNIVersion.GTE(v040) {
		for _, delegate := range mc.Delegates {
			delegatesMap, ok := delegate.(map[string]interface{})
			if !ok {
				return errors.New("couldn't get cni version of delegate")
			}
			delegateVersion, ok := delegatesMap["cniVersion"].(string)
			if !ok {
				return errors.New("couldn't get cni version of delegate")
			}
			v, err := semver.Make(delegateVersion)
			if err != nil {
				return err
			}
			if v.LT(v040) {
				return fmt.Errorf(versionFmt, delegateVersion, mc.CNIVersion)
			}
		}
	}

	return nil
}

// Generate generates the multus configuration from whatever state is currently
// held
func (mc *MultusConf) Generate() (string, error) {
	data, err := json.Marshal(mc)
	return string(data), err
}

// Mutate updates the MultusConf attributes according to the provided
// configuration `Option`s
func (mc *MultusConf) Mutate(configurationOptions ...Option) error {
	for _, configOption := range configurationOptions {
		configOption(mc)
	}

	return CheckVersionCompatibility(mc)
}

// WithNamespaceIsolation mutates the inner state to enable the
// NamespaceIsolation attribute
func WithNamespaceIsolation() Option {
	return func(conf *MultusConf) {
		conf.NamespaceIsolation = true
	}
}

// WithGlobalNamespaces mutates the inner state to set the
// RawNonIsolatedNamespaces attribute
func WithGlobalNamespaces(globalNamespaces string) Option {
	return func(conf *MultusConf) {
		conf.RawNonIsolatedNamespaces = globalNamespaces
	}
}

// WithLogToStdErr mutates the inner state to enable the
// WithLogToStdErr attribute
func WithLogToStdErr() Option {
	return func(conf *MultusConf) {
		conf.LogToStderr = true
	}
}

// WithLogLevel mutates the inner state to set the
// LogLevel attribute
func WithLogLevel(logLevel string) Option {
	return func(conf *MultusConf) {
		conf.LogLevel = logLevel
	}
}

// WithLogFile mutates the inner state to set the
// logFile attribute
func WithLogFile(logFile string) Option {
	return func(conf *MultusConf) {
		conf.LogFile = logFile
	}
}

// WithLogOptions mutates the inner state to set the
// LogOptions attribute
func WithLogOptions(logOptions *LogOptions) Option {
	return func(conf *MultusConf) {
		conf.LogOptions = logOptions
	}
}

// WithReadinessFileIndicator mutates the inner state to set the
// ReadinessIndicatorFile attribute
func WithReadinessFileIndicator(path string) Option {
	return func(conf *MultusConf) {
		conf.ReadinessIndicatorFile = path
	}
}

// WithAdditionalBinaryFileDir mutates the inner state to set the
// BinDir attribute
func WithAdditionalBinaryFileDir(directoryPath string) Option {
	return func(conf *MultusConf) {
		conf.BinDir = directoryPath
	}
}

// WithOverriddenName mutates the inner state to set the
// Name attribute
func WithOverriddenName(networkName string) Option {
	return func(conf *MultusConf) {
		conf.Name = networkName
	}
}

func withCapabilities(cniData interface{}) Option {
	var enabledCapabilities []string
	var pluginsList []interface{}
	cniDataMap, ok := cniData.(map[string]interface{})
	if ok {
		if pluginsListEntry, ok := cniDataMap[configListCapabilityKey]; ok {
			pluginsList = pluginsListEntry.([]interface{})
		}
	}

	if len(pluginsList) > 0 {
		for _, pluginData := range pluginsList {
			enabledCapabilities = append(
				enabledCapabilities,
				extractCapabilities(pluginData)...)
		}
	} else {
		enabledCapabilities = extractCapabilities(cniData)
	}

	return func(conf *MultusConf) {
		for _, capability := range enabledCapabilities {
			conf.Capabilities[capability] = true
		}
	}
}

func withDelegates(primaryCNIConfigData map[string]interface{}) Option {
	return func(conf *MultusConf) {
		conf.Delegates = []interface{}{primaryCNIConfigData}
	}
}

// MutateLogOptions update the LoggingOptions of the MultusConf according
// to the provided configuration `loggingOptions`
func MutateLogOptions(logOption *LogOptions, logOptionFunc ...LogOptionFunc) {
	for _, loggingOption := range logOptionFunc {
		loggingOption(logOption)
	}
}

// WithLogMaxSize mutates the inner state to set the
// logMaxSize attribute
func WithLogMaxSize(maxSize *int) LogOptionFunc {
	return func(logOptions *LogOptions) {
		logOptions.MaxSize = maxSize
	}
}

// WithLogMaxAge mutates the inner state to set the
// logMaxAge attribute
func WithLogMaxAge(maxAge *int) LogOptionFunc {
	return func(logOptions *LogOptions) {
		logOptions.MaxAge = maxAge
	}
}

// WithLogMaxBackups mutates the inner state to set the
// logMaxBackups attribute
func WithLogMaxBackups(maxBackups *int) LogOptionFunc {
	return func(logOptions *LogOptions) {
		logOptions.MaxBackups = maxBackups
	}
}

// WithLogCompress mutates the inner state to set the
// logCompress attribute
func WithLogCompress(compress *bool) LogOptionFunc {
	return func(logOptions *LogOptions) {
		logOptions.Compress = compress
	}
}

func extractCapabilities(capabilitiesInterface interface{}) []string {
	capabilitiesMap, ok := capabilitiesInterface.(map[string]interface{})
	if !ok {
		return nil
	}
	capabilitiesMapEntry, ok := capabilitiesMap[singleConfigCapabilityKey]
	if !ok {
		return nil
	}
	capabilities, ok := capabilitiesMapEntry.(map[string]interface{})
	if !ok {
		return nil
	}

	var enabledCapabilities []string
	if len(capabilities) > 0 {
		for capName, isCapabilityEnabled := range capabilities {
			if isCapabilityEnabled.(bool) {
				enabledCapabilities = append(enabledCapabilities, capName)
			}
		}
	}
	return enabledCapabilities
}

func findMasterPlugin(cniConfigDirPath string, remainingTries int) (string, error) {
	if remainingTries == 0 {
		return "", fmt.Errorf("could not find a plugin configuration in %s", cniConfigDirPath)
	}
	var cniPluginConfigs []string
	files, err := ioutil.ReadDir(cniConfigDirPath)
	if err != nil {
		return "", fmt.Errorf("error when listing the CNI plugin configurations: %w", err)
	}

	for _, file := range files {
		if strings.HasPrefix(file.Name(), "00-multus") {
			continue
		}
		fileExtension := filepath.Ext(file.Name())
		if fileExtension == ".conf" || fileExtension == ".conflist" {
			cniPluginConfigs = append(cniPluginConfigs, file.Name())
		}
	}

	if len(cniPluginConfigs) == 0 {
		time.Sleep(time.Second)
		return findMasterPlugin(cniConfigDirPath, remainingTries-1)
	}
	sort.Strings(cniPluginConfigs)
	return cniPluginConfigs[0], nil
}
