// 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"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/blang/semver"

	"gopkg.in/k8snetworkplumbingwg/multus-cni.v4/pkg/logging"
)

const (
	configListCapabilityKey   = "plugins"
	multusPluginName          = "multus-shim"
	singleConfigCapabilityKey = "capabilities"
)

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

// MultusConf holds the multus configuration
type MultusConf struct {
	BinDir                   string              `json:"binDir,omitempty"`
	Capabilities             map[string]bool     `json:"capabilities,omitempty"`
	CNIVersion               string              `json:"cniVersion"`
	LogFile                  string              `json:"logFile,omitempty"`
	LogLevel                 string              `json:"logLevel,omitempty"`
	LogToStderr              bool                `json:"logToStderr,omitempty"`
	LogOptions               *logging.LogOptions `json:"logOptions,omitempty"`
	Name                     string              `json:"name"`
	ClusterNetwork           string              `json:"clusterNetwork,omitempty"`
	NamespaceIsolation       bool                `json:"namespaceIsolation,omitempty"`
	RawNonIsolatedNamespaces string              `json:"globalNamespaces,omitempty"`
	ReadinessIndicatorFile   string              `json:"readinessindicatorfile,omitempty"`
	Type                     string              `json:"type"`
	CniDir                   string              `json:"cniDir,omitempty"`
	CniConfigDir             string              `json:"cniConfigDir,omitempty"`
	AuxiliaryCNIChainName    string              `json:"auxiliaryCNIChainName,omitempty"`
	DaemonSocketDir          string              `json:"daemonSocketDir,omitempty"`
	MultusConfigFile         string              `json:"multusConfigFile,omitempty"`
	MultusMasterCni          string              `json:"multusMasterCNI,omitempty"`
	MultusAutoconfigDir      string              `json:"multusAutoconfigDir,omitempty"`
	ForceCNIVersion          bool                `json:"forceCNIVersion,omitempty"`
	OverrideNetworkName      bool                `json:"overrideNetworkName,omitempty"`
}

// ParseMultusConfig parses multus config from configPath and create MultusConf.
func ParseMultusConfig(configPath string) (*MultusConf, error) {
	config, err := os.ReadFile(configPath)
	if err != nil {
		return nil, fmt.Errorf("ParseMultusConfig failed to read the config file's contents: %w", err)
	}

	multusconf := MultusConf{
		MultusConfigFile: "auto",
		Type:             multusPluginName,
		Capabilities:     map[string]bool{},
		CniConfigDir:     "/etc/cni/net.d",
	}

	if err := json.Unmarshal(config, &multusconf); err != nil {
		return nil, fmt.Errorf("failed to unmarshall the daemon configuration: %w", err)
	}
	multusconf.Name = MultusDefaultNetworkName // change name

	return &multusconf, nil
}

// 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, delegate interface{}) 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) {
		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) {
	// before marshal, flush variables which is not required for multus-shim config
	mc.CniConfigDir = ""
	mc.MultusConfigFile = ""
	mc.MultusAutoconfigDir = ""
	mc.MultusMasterCni = ""
	mc.ForceCNIVersion = false
	// Readiness indicator file existence is already handled by the
	// ConfigManager via an fsnotify watch, so CmdAdd/CmdDel don't need to.
	mc.ReadinessIndicatorFile = ""

	data, err := json.Marshal(mc)
	return string(data), err
}

func (mc *MultusConf) setCapabilities(cniData interface{}) error {
	var enabledCapabilities []string
	var pluginsList []interface{}
	cniDataMap, ok := cniData.(map[string]interface{})
	if ok {
		if pluginsListEntry, ok := cniDataMap[configListCapabilityKey]; ok {
			pluginsList = pluginsListEntry.([]interface{})
		}
	} else {
		return errors.New("couldn't get cni config from delegate")
	}

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

	for _, capability := range enabledCapabilities {
		mc.Capabilities[capability] = true
	}
	return nil
}

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 := os.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
}
