/*
 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

     https://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 main

import (
	"fmt"
	"io"
	"math"
	"net/http"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/spf13/cobra"
)

const DefaultBaseUrl string = "https://opendev.org/openstack/openstack-helm/raw/branch/master/values_overrides"

func newRootCommand() *cobra.Command {
	var cwd string
	var download bool
	var baseUrl string
	var basePath string
	var subchart string
	var chart string

	cwd, _ = os.Getwd()
	rootCmd := &cobra.Command{
		Use: "get-values-overrides <feature1> <feature2> ...",
		Long: `Get Helm values overrides for a set of features.

If 3 features are passed, then the overrides will be looked
up in the following order:

<overrides_path>/<chart>/<feature-3>.yaml
<overrides_path>/<chart>/<feature-2>.yaml
<overrides_path>/<chart>/<feature-2>-<feature-3>.yaml
<overrides_path>/<chart>/<feature-1>.yaml
<overrides_path>/<chart>/<feature-1>-<feature-3>.yaml
<overrides_path>/<chart>/<feature-1>-<feature-2>.yaml
<overrides_path>/<chart>/<feature-1>-<feature-2>-<feature-3>.yaml

If you think of the features as bits of a binary number where <feature3> is
the least significant bit, then the order corresponds to all numbers from 001
to 111 in binary representation.`,
		Run: func(cmd *cobra.Command, args []string) {
			features := args[0:]
			if len(features) == 0 {
				fmt.Fprintln(os.Stderr, "No features provided")
				os.Exit(0)
			}
			fmt.Fprintf(os.Stderr, "Base URL: %s\nBase path: %s\n", baseUrl, basePath)
			fmt.Fprintf(os.Stderr, "Chart: %s\n", chart)
			if subchart != "" {
				fmt.Fprintf(os.Stderr, "Subchart: %s\n", subchart)
			}
			fmt.Fprintf(os.Stderr, "Features: %s\n", strings.Join(features, " "))
			overrideCandidates := generateOverrideCandidates(features)
			overrideArgs := getOverrideArgs(baseUrl, basePath, chart, subchart, overrideCandidates, download)
			fmt.Fprintf(os.Stderr, "Resulting override args: %s\n", strings.Join(overrideArgs, " "))
			fmt.Println(strings.Join(overrideArgs, " "))
		},
	}
	rootCmd.Flags().BoolVarP(&download, "download", "d", false, "Download the overrides from the internet if not found in the path (default: false)")
	rootCmd.Flags().StringVarP(&baseUrl, "url", "u", DefaultBaseUrl, "Base url to download overrides")
	rootCmd.Flags().StringVarP(&basePath, "path", "p", cwd, "Path to the overrides")
	rootCmd.Flags().StringVarP(&subchart, "subchart", "s", "", "Subchart to get the overrides for")
	rootCmd.Flags().StringVarP(&chart, "chart", "c", "", "Chart to get the overrides for")
	rootCmd.MarkFlagRequired("chart")
	return rootCmd
}

func sliceReverse[T comparable](s []T) {
	sort.SliceStable(s, func(i, j int) bool {
		return i > j
	})
}

func num2items(num uint32, power int) []int {
	featureNums := make([]int, 0)
	for i := 0; i < power; i++ {
		if uint32(math.Pow(2, float64(i)))&num != 0 {
			featureNums = append(featureNums, i)
		}
	}
	sliceReverse(featureNums)
	return featureNums
}

func overrideFile(basePath, chart, subchart, overrideName string) string {
	if subchart != "" {
		return filepath.Join(basePath, chart, subchart, overrideName)
	}
	return filepath.Join(basePath, chart, overrideName)
}

func overrideUrl(baseUrl, chart, subchart, overrideName string) string {
	if subchart != "" {
		return fmt.Sprintf("%s/%s/%s/%s", baseUrl, chart, subchart, overrideName)
	}
	return fmt.Sprintf("%s/%s/%s", baseUrl, chart, overrideName)
}

func downloadOverride(baseUrl, basePath, chart, subchart, overrideName string) error {
	fullUrl := overrideUrl(baseUrl, chart, subchart, overrideName)
	filename := overrideFile(basePath, chart, subchart, overrideName)
	fmt.Fprintf(os.Stderr, "Trying to download %s\n", fullUrl)
	resp, err := http.Get(fullUrl)
	if err != nil {
		return err
	}
	if resp.StatusCode != 200 {
		return fmt.Errorf("failed to download %s: %s", fullUrl, resp.Status)
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	err = os.MkdirAll(filepath.Dir(filename), 0755)
	if err != nil {
		return err
	}
	err = os.WriteFile(filename, body, 0644)
	if err != nil {
		return err
	}
	return nil
}

func generateOverrideCandidates(features []string) []string {
	sliceReverse(features)
	overrideCandidates := make([]string, 0)
	for num := uint32(1); num < uint32(math.Pow(2, float64(len(features)))); num++ {
		words := make([]string, 0)
		items := num2items(num, len(features))
		for _, item := range items {
			words = append(words, features[item])
		}
		overrideCandidates = append(overrideCandidates, fmt.Sprintf("%s.yaml", strings.Join(words, "-")))
	}
	return overrideCandidates
}

func getOverrideArgs(baseUrl, basePath, chart, subchart string, overrideCandidates []string, download bool) []string {
	overrideArgs := make([]string, 0)
	for _, overrideCandidate := range overrideCandidates {
		overrideCandidateFile := overrideFile(basePath, chart, subchart, overrideCandidate)
		fmt.Fprintf(os.Stderr, "Override candidate: %s\n", overrideCandidateFile)
		_, err := os.Stat(overrideCandidateFile)
		if err == nil {
			fmt.Fprintf(os.Stderr, "File found: %s\n", overrideCandidateFile)
			overrideArgs = append(overrideArgs, fmt.Sprintf("--values %s", overrideCandidateFile))
		} else {
			fmt.Fprintf(os.Stderr, "File not found: %s\n", overrideCandidateFile)
			if download {
				err = downloadOverride(baseUrl, basePath, chart, subchart, overrideCandidate)
				if err != nil {
					fmt.Fprintln(os.Stderr, err)
				} else {
					fmt.Fprintf(os.Stderr, "Successfully downloaded %s\n", overrideCandidate)
					overrideArgs = append(overrideArgs, fmt.Sprintf("--values %s", overrideCandidateFile))
				}
			}
		}
	}
	return overrideArgs
}

func main() {
	if err := newRootCommand().Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}