/**
# Copyright (c) 2022, NVIDIA CORPORATION.  All rights reserved.
#
# 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 configure

import (
	"encoding/json"
	"fmt"
	"os"

	"github.com/NVIDIA/nvidia-container-toolkit/cmd/nvidia-ctk/runtime/nvidia"
	"github.com/NVIDIA/nvidia-container-toolkit/internal/config/engine/crio"
	"github.com/NVIDIA/nvidia-container-toolkit/internal/config/engine/docker"
	"github.com/pelletier/go-toml"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli/v2"
)

const (
	defaultRuntime = "docker"

	defaultDockerConfigFilePath = "/etc/docker/daemon.json"
	defaultCrioConfigFilePath   = "/etc/crio/crio.conf"
)

type command struct {
	logger *logrus.Logger
}

// NewCommand constructs an configure command with the specified logger
func NewCommand(logger *logrus.Logger) *cli.Command {
	c := command{
		logger: logger,
	}
	return c.build()
}

// config defines the options that can be set for the CLI through config files,
// environment variables, or command line config
type config struct {
	dryRun         bool
	runtime        string
	configFilePath string
	nvidiaOptions  nvidia.Options
}

func (m command) build() *cli.Command {
	// Create a config struct to hold the parsed environment variables or command line flags
	config := config{}

	// Create the 'configure' command
	configure := cli.Command{
		Name:  "configure",
		Usage: "Add a runtime to the specified container engine",
		Action: func(c *cli.Context) error {
			return m.configureWrapper(c, &config)
		},
	}

	configure.Flags = []cli.Flag{
		&cli.BoolFlag{
			Name:        "dry-run",
			Usage:       "update the runtime configuration as required but don't write changes to disk",
			Destination: &config.dryRun,
		},
		&cli.StringFlag{
			Name:        "runtime",
			Usage:       "the target runtime engine. One of [crio, docker]",
			Value:       defaultRuntime,
			Destination: &config.runtime,
		},
		&cli.StringFlag{
			Name:        "config",
			Usage:       "path to the config file for the target runtime",
			Destination: &config.configFilePath,
		},
		&cli.StringFlag{
			Name:        "nvidia-runtime-name",
			Usage:       "specify the name of the NVIDIA runtime that will be added",
			Value:       nvidia.RuntimeName,
			Destination: &config.nvidiaOptions.RuntimeName,
		},
		&cli.StringFlag{
			Name:        "runtime-path",
			Usage:       "specify the path to the NVIDIA runtime executable",
			Value:       nvidia.RuntimeExecutable,
			Destination: &config.nvidiaOptions.RuntimePath,
		},
		&cli.BoolFlag{
			Name:        "set-as-default",
			Usage:       "set the specified runtime as the default runtime",
			Destination: &config.nvidiaOptions.SetAsDefault,
		},
	}

	return &configure
}

func (m command) configureWrapper(c *cli.Context, config *config) error {
	switch config.runtime {
	case "crio":
		return m.configureCrio(c, config)
	case "docker":
		return m.configureDocker(c, config)
	}

	return fmt.Errorf("unrecognized runtime '%v'", config.runtime)
}

// configureDocker updates the docker config to enable the NVIDIA Container Runtime
func (m command) configureDocker(c *cli.Context, config *config) error {
	configFilePath := config.configFilePath
	if configFilePath == "" {
		configFilePath = defaultDockerConfigFilePath
	}

	cfg, err := docker.New(
		docker.WithPath(configFilePath),
	)
	if err != nil {
		return fmt.Errorf("unable to load config: %v", err)
	}

	err = cfg.AddRuntime(
		config.nvidiaOptions.RuntimeName,
		config.nvidiaOptions.RuntimePath,
		config.nvidiaOptions.SetAsDefault,
	)
	if err != nil {
		return fmt.Errorf("unable to update config: %v", err)
	}

	if config.dryRun {
		output, err := json.MarshalIndent(cfg, "", "    ")
		if err != nil {
			return fmt.Errorf("unable to convert to JSON: %v", err)
		}
		os.Stdout.WriteString(fmt.Sprintf("%s\n", output))
		return nil
	}
	n, err := cfg.Save(configFilePath)
	if err != nil {
		return fmt.Errorf("unable to flush config: %v", err)
	}

	if n == 0 {
		m.logger.Infof("Removed empty config from %v", configFilePath)
	} else {
		m.logger.Infof("Wrote updated config to %v", configFilePath)
	}
	m.logger.Infof("It is recommended that the docker daemon be restarted.")

	return nil
}

// configureCrio updates the crio config to enable the NVIDIA Container Runtime
func (m command) configureCrio(c *cli.Context, config *config) error {
	configFilePath := config.configFilePath
	if configFilePath == "" {
		configFilePath = defaultCrioConfigFilePath
	}

	cfg, err := crio.New(
		crio.WithPath(configFilePath),
	)
	if err != nil {
		return fmt.Errorf("unable to load config: %v", err)
	}

	err = cfg.AddRuntime(
		config.nvidiaOptions.RuntimeName,
		config.nvidiaOptions.RuntimePath,
		config.nvidiaOptions.SetAsDefault,
	)
	if err != nil {
		return fmt.Errorf("unable to update config: %v", err)
	}

	if config.dryRun {
		output, err := toml.Marshal(cfg)
		if err != nil {
			return fmt.Errorf("unable to convert to TOML: %v", err)
		}
		os.Stdout.WriteString(fmt.Sprintf("%s\n", output))
		return nil
	}
	n, err := cfg.Save(configFilePath)
	if err != nil {
		return fmt.Errorf("unable to flush config: %v", err)
	}

	if n == 0 {
		m.logger.Infof("Removed empty config from %v", configFilePath)
	} else {
		m.logger.Infof("Wrote updated config to %v", configFilePath)
	}
	m.logger.Infof("It is recommended that the cri-o daemon be restarted.")

	return nil
}