/*
Copyright 2021 The Knative 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 plugin

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"text/template"

	"github.com/spf13/cobra"

	"sigs.k8s.io/yaml"
)

// DistroConfig represents yaml configuration struct
type DistroConfig struct {
	Plugins []Plugin `yaml:"plugins"`
}

// NewDistroGenerateCmd represents plugin distro command
func NewDistroGenerateCmd() *cobra.Command {
	var config string
	var generateCmd = &cobra.Command{
		Use:   "distro",
		Short: "Generate required files to build `kn` with inline plugins.",
		RunE: func(cmd *cobra.Command, args []string) error {
			if !fileExists(config) {
				return fmt.Errorf("kn distro configuration file '%s' doesn't exist", config)
			}
			if !fileExists("cmd/kn/main.go") {
				return fmt.Errorf("cmd/kn/main.go doesn't exist, make sure the command is executed in knative/client root directory")
			}

			fmt.Println("Generating customized kn distro:")
			registerFile := filepath.Join("pkg", "kn", "root", "plugin_register.go")
			if fileExists(registerFile) {
				fmt.Println("⚠️  plugin_register.go file already exists, trying to append imports")
			}

			rawConf, err := os.ReadFile(config)
			if err != nil {
				return err
			}
			conf := &DistroConfig{}
			if err := yaml.Unmarshal(rawConf, conf); err != nil {
				return err
			}
			fmt.Println("✔  config file '" + config + "' processed")

			for _, p := range conf.Plugins {
				if err := processPlugin(p, registerFile); err != nil {
					return err
				}
			}
			//nolint:gosec // Expected go cmd.
			if err := exec.Command("gofmt", "-s", "-w", registerFile).Run(); err != nil {
				return fmt.Errorf("gofmt failed: %w", err)
			}
			if fileExists("hack/update-dep.sh") {
				_, err := exec.Command("hack/update-deps.sh").Output()
				if err != nil {
					return fmt.Errorf("update-deps.sh failed failed: %w", err)
				}
			} else {
				fmt.Println("⚠️  hack/update-dep.sh script doesn't exist, make sure to update vendor/ dir")
			}
			return nil
		},
	}
	generateCmd.Flags().StringVarP(&config, "config", "c", ".kn.yaml", "Path to `kn.yaml` config file")
	return generateCmd
}

// fileExists util func to check if file exists
func fileExists(file string) bool {
	_, err := os.Stat(file)
	return err == nil
}

// processPlugin does required import changes in plugin_register.go and go.mod file
func processPlugin(p Plugin, registerFile string) error {
	importPath := p.PluginImportPath
	if importPath == "" {
		importPath = p.Module + "/plugin"
	}
	if err := appendImport(registerFile, importPath); err != nil {
		return err
	}
	if err := processModuleRequire(p); err != nil {
		return err
	}
	if err := processModuleReplace(p); err != nil {
		return err
	}
	// Exec `go mod tidy` per plugin added to resolve version
	if err := goModTidy(); err != nil {
		return err
	}
	return nil
}

// processModuleRequire adds provided plugin module to go.mod require section
func processModuleRequire(plugin Plugin) error {
	//nolint:gosec // Expected go cmd.
	_, err := exec.Command("go", "mod", "edit", "-require", plugin.Module+"@"+plugin.Version).Output()
	if err != nil {
		return fmt.Errorf("go mod edit -require failed: %w", err)
	}
	fmt.Println("✔  go.mod require updated")
	return nil
}

// processModuleReplace adds provided module overrides to go.mod replace section
func processModuleReplace(plugin Plugin) error {
	if len(plugin.Replace) > 0 {
		for _, r := range plugin.Replace {
			// replace source of module dep, e.g. custom git repository
			source := r.Module
			if r.ModuleSource != "" {
				source = r.ModuleSource
			}
			//nolint:gosec // Expected go cmd.
			_, err := exec.Command("go", "mod", "edit", "-replace", r.Module+"="+source+"@"+r.Version).Output()
			if err != nil {
				return fmt.Errorf("go mod edit -replace failed: %w", err)
			}
			fmt.Println("✔  go.mod replace updated")
		}
	}
	return nil
}

// goModTidy executes `go mod tidy` command
func goModTidy() error {
	//nolint:gosec // Expected go cmd.
	_, err := exec.Command("go", "mod", "tidy").Output()
	if err != nil {
		return fmt.Errorf("go mod tidy failed: %w", err)
	}
	fmt.Println("✔  go mod tidy")
	return nil
}

// appendImport adds specified importPath to plugin registration file.
// New file is initialized if it doesn't exist.
// Warning message is displayed if the plugin import is already present.
func appendImport(file, importPath string) error {
	if _, err := os.Stat(file); os.IsNotExist(err) {
		f, err := os.Create(file)
		if err != nil {
			return err
		}
		t, err := template.New("register").Parse(registerTemplate)
		if err != nil {
			return err
		}
		fmt.Println("✔  " + importPath + " added to plugin_register.go")
		return t.Execute(f, importPath)
	}

	content, err := os.ReadFile(file)
	if err != nil {
		return err
	}

	if strings.Contains(string(content), importPath) {
		fmt.Println("⚠️  " + importPath + " is already present, no changes made")
		return nil
	}
	hook := "// Add #plugins# import here. Don't remove this line, it triggers an automatic replacement."
	content = bytes.Replace(content, []byte(hook), []byte(fmt.Sprintf("%s\n    _ \"%s\"", hook, importPath)), 1)
	fmt.Println("✔  " + importPath + " added to plugin_register.go")
	//nolint:gosec // Generate file keeps the same permissions as rest of sources.
	return os.WriteFile(file, content, 0644)
}
