// Copyright (c) 2022 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 api

import (
	"encoding/json"
	stderrors "errors"
	"fmt"
	"os"
	"strings"

	"github.com/containernetworking/cni/pkg/skel"
	cnitypes "github.com/containernetworking/cni/pkg/types"

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

// ShimNetConf for the SHIM cni config file written in json
type ShimNetConf struct {
	// Note: This struct contains NetConf in pkg/types, but this struct is only used to parse
	// following fields, so we skip to include NetConf here. Other fields are directly send to
	// multus-daemon as a part of skel.CmdArgs, StdinData.
	// types.NetConf

	CNIVersion      string `json:"cniVersion,omitempty"`
	MultusSocketDir string `json:"daemonSocketDir"`
	LogFile         string `json:"logFile,omitempty"`
	LogLevel        string `json:"logLevel,omitempty"`
	LogToStderr     bool   `json:"logToStderr,omitempty"`
}

// readyCheckFunc defines a type for API readiness check functions
type readyCheckFunc func(string) error

// CmdAdd implements the CNI spec ADD command handler
func CmdAdd(args *skel.CmdArgs) error {
	response, cniVersion, err := postRequest(args, WaitUntilAPIReady)
	if err != nil {
		return logging.Errorf("CmdAdd (shim): %v", err)
	}

	logging.Verbosef("CmdAdd (shim): %v", *response.Result)
	return cnitypes.PrintResult(response.Result, cniVersion)
}

// CmdCheck implements the CNI spec CHECK command handler
func CmdCheck(args *skel.CmdArgs) error {
	_, _, err := postRequest(args, WaitUntilAPIReady)
	if err != nil {
		return logging.Errorf("CmdCheck (shim): %v", err)
	}

	return err
}

// CmdDel implements the CNI spec DEL command handler
func CmdDel(args *skel.CmdArgs) error {
	_, _, err := postRequest(args, CheckAPIReadyNow)
	if err != nil {
		// No error in DEL (as of CNI spec)
		logging.Errorf("CmdDel (shim): %v", err)
	}
	return nil
}

// CmdGC implements the CNI spec GC command handler
func CmdGC(args *skel.CmdArgs) error {
	_, _, err := postRequest(args, WaitUntilAPIReady)
	if err != nil {
		return logging.Errorf("CmdGC (shim): %v", err)
	}
	return nil
}

// CmdStatus implements the CNI spec STATUS command handler
func CmdStatus(args *skel.CmdArgs) error {
	_, _, err := postRequest(args, WaitUntilAPIReady)
	if err != nil {
		return logging.Errorf("CmdStatus (shim): %v", err)
	}
	return nil
}

func postRequest(args *skel.CmdArgs, readinessCheck readyCheckFunc) (*Response, string, error) {
	multusShimConfig, err := shimConfig(args.StdinData)
	if err != nil {
		return nil, "", fmt.Errorf("invalid CNI configuration passed to multus-shim: %w", err)
	}

	// Execute the readiness check as necessary (e.g. don't wait on CNI DEL)
	if err := readinessCheck(multusShimConfig.MultusSocketDir); err != nil {
		return nil, multusShimConfig.CNIVersion, err
	}

	cniRequest, err := newCNIRequest(args)
	if err != nil {
		return nil, multusShimConfig.CNIVersion, err
	}

	var body []byte
	body, err = DoCNI("http://dummy/cni", cniRequest, SocketPath(multusShimConfig.MultusSocketDir))
	if err != nil {
		var cniErr *cnitypes.Error
		if stderrors.As(err, &cniErr) {
			return nil, multusShimConfig.CNIVersion, err
		}
		return nil, multusShimConfig.CNIVersion, fmt.Errorf("%s: StdinData: %s", err.Error(), string(args.StdinData))
	}

	response := &Response{}
	if len(body) != 0 {
		if err = json.Unmarshal(body, response); err != nil {
			err = fmt.Errorf("failed to unmarshal response '%s': %v", string(body), err)
			return nil, multusShimConfig.CNIVersion, err
		}
	}
	return response, multusShimConfig.CNIVersion, nil
}

// Create and fill a Request with this Plugin's environment and stdin which
// contain the CNI variables and configuration
func newCNIRequest(args *skel.CmdArgs) (*Request, error) {
	envMap := make(map[string]string)
	for _, item := range os.Environ() {
		idx := strings.Index(item, "=")
		if idx > 0 {
			envMap[strings.TrimSpace(item[:idx])] = item[idx+1:]
		}
	}

	return &Request{
		Env:    envMap,
		Config: args.StdinData,
	}, nil
}

func shimConfig(cniConfig []byte) (*ShimNetConf, error) {
	multusConfig := &ShimNetConf{}
	if err := json.Unmarshal(cniConfig, multusConfig); err != nil {
		return nil, fmt.Errorf("failed to gather the multus configuration: %w", err)
	}
	if multusConfig.MultusSocketDir == "" {
		multusConfig.MultusSocketDir = defaultMultusRunDir
	}
	// Logging
	logging.SetLogStderr(multusConfig.LogToStderr)
	if multusConfig.LogFile != "" {
		logging.SetLogFile(multusConfig.LogFile)
	}
	if multusConfig.LogLevel != "" {
		logging.SetLogLevel(multusConfig.LogLevel)
	}
	return multusConfig, nil
}
