package cmd

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/url"
	"os"
	"strings"

	"github.com/cozy/cozy-stack/client/request"
	"github.com/cozy/cozy-stack/pkg/consts"
	"github.com/spf13/cobra"
)

var flagWithSources bool

var featureCmdGroup = &cobra.Command{
	Use:     "features <command>",
	Aliases: []string{"feature"},
	Short:   "Manage the feature flags",
}

var featureShowCmd = &cobra.Command{
	Use:   "show",
	Short: `Display the computed feature flags for an instance`,
	Long: `
cozy-stack feature show displays the feature flags that are shown by apps.
`,
	Example: `$ cozy-stack feature show --domain cozy.localhost:8080`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagDomain == "" {
			errPrintfln("%s", errMissingDomain)
			return cmd.Usage()
		}
		c := newClient(flagDomain, consts.Settings)
		req := &request.Options{
			Method: "GET",
			Path:   "/settings/flags",
		}
		if flagWithSources {
			req.Queries = url.Values{"include": {"source"}}
		}
		res, err := c.Req(req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var obj struct {
			Data struct {
				Attributes map[string]json.RawMessage `json:"attributes"`
			} `json:"data"`
			Included []struct {
				ID         string                     `json:"id"`
				Attributes map[string]json.RawMessage `json:"attributes"`
			} `json:"included"`
		}
		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
			return err
		}
		for k, v := range obj.Data.Attributes {
			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
		}
		if len(obj.Included) > 0 {
			fmt.Fprintf(os.Stdout, "\nSources:\n")
			for _, source := range obj.Included {
				fmt.Fprintf(os.Stdout, "- %s\n", source.ID)
				for k, v := range source.Attributes {
					fmt.Fprintf(os.Stdout, "\t- %s: %s\n", k, string(v))
				}
			}
		}
		return nil
	},
}

var featureFlagCmd = &cobra.Command{
	Use:     "flags",
	Aliases: []string{"flag"},
	Short:   `Display and update the feature flags for an instance`,
	Long: `
cozy-stack feature flags displays the feature flags that are specific to an instance.

It can also take a list of flags to update.

If you give a null value, the flag will be removed.
`,
	Example: `$ cozy-stack feature flags --domain cozy.localhost:8080 '{"add_this_flag": true, "remove_this_flag": null}'`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagDomain == "" {
			errPrintfln("%s", errMissingDomain)
			return cmd.Usage()
		}
		ac := newAdminClient()
		req := request.Options{
			Method: "GET",
			Path:   fmt.Sprintf("/instances/%s/feature/flags", flagDomain),
		}
		if len(args) > 0 {
			req.Method = "PATCH"
			req.Body = strings.NewReader(args[0])
		}
		res, err := ac.Req(&req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var obj map[string]json.RawMessage
		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
			return err
		}
		for k, v := range obj {
			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
		}
		return nil
	},
}

var featureSetCmd = &cobra.Command{
	Use:   "sets",
	Short: `Display and update the feature sets for an instance`,
	Long: `
cozy-stack feature sets displays the feature sets coming from the manager.

It can also take a space-separated list of sets that will replace the previous
list (no merge).

All the sets can be removed by setting an empty list ('').
`,
	Example: `$ cozy-stack feature sets --domain cozy.localhost:8080 'set1 set2'`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagDomain == "" {
			errPrintfln("%s", errMissingDomain)
			return cmd.Usage()
		}
		ac := newAdminClient()
		req := request.Options{
			Method: "GET",
			Path:   fmt.Sprintf("/instances/%s/feature/sets", flagDomain),
		}
		if len(args) > 0 {
			list := args
			if len(args) == 1 {
				list = strings.Fields(args[0])
			}
			buf, err := json.Marshal(list)
			if err != nil {
				return err
			}
			req.Method = "PUT"
			req.Body = bytes.NewReader(buf)
		}
		res, err := ac.Req(&req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var sets []string
		if err = json.NewDecoder(res.Body).Decode(&sets); err != nil {
			return err
		}
		for _, set := range sets {
			fmt.Fprintf(os.Stdout, "- %v\n", set)
		}
		return nil
	},
}

var featureRatioCmd = &cobra.Command{
	Use:     "ratio <context-name>",
	Aliases: []string{"context"},
	Short:   `Display and update the feature flags for a context`,
	Long: `
cozy-stack feature ratio displays the feature flags for a context.

It can also create, update, or remove flags (with a ratio and value).

To remove a flag, set it to an empty array (or null).
`,
	Example: `$ cozy-stack feature ratio --context beta '{"set_this_flag": [{"ratio": 0.1, "value": 1}, {"ratio": 0.9, "value": 2}] }'`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagContext == "" {
			return cmd.Usage()
		}
		ac := newAdminClient()
		req := request.Options{
			Method: "GET",
			Path:   fmt.Sprintf("/instances/feature/contexts/%s", flagContext),
		}
		if len(args) > 0 {
			req.Method = "PATCH"
			req.Body = strings.NewReader(args[0])
		}
		res, err := ac.Req(&req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var obj map[string]json.RawMessage
		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
			return err
		}
		for k, v := range obj {
			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
		}
		return nil
	},
}

var featureConfigCmd = &cobra.Command{
	Use:   "config <context-name>",
	Short: `Display the feature flags from configuration for a context`,
	Long: `
cozy-stack feature config displays the feature flags from configuration for a context.

These flags are read only and can only be updated by changing configuration and restarting the stack.
`,
	Example: `$ cozy-stack feature config --context beta`,
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagContext == "" {
			return cmd.Usage()
		}
		ac := newAdminClient()
		req := request.Options{
			Method: "GET",
			Path:   fmt.Sprintf("/instances/feature/config/%s", flagContext),
		}
		res, err := ac.Req(&req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var obj map[string]json.RawMessage
		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
			return err
		}
		for k, v := range obj {
			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
		}
		return nil
	},
}

var featureDefaultCmd = &cobra.Command{
	Use:   "defaults",
	Short: `Display and update the default values for feature flags`,
	Long: `
cozy-stack feature defaults displays the default values for feature flags.

It can also take a list of flags to update.

If you give a null value, the flag will be removed.
`,
	Example: `$ cozy-stack feature defaults '{"add_this_flag": true, "remove_this_flag": null}'`,
	RunE: func(cmd *cobra.Command, args []string) error {
		ac := newAdminClient()
		req := request.Options{
			Method: "GET",
			Path:   "/instances/feature/defaults",
		}
		if len(args) > 0 {
			req.Method = "PATCH"
			req.Body = strings.NewReader(args[0])
		}
		res, err := ac.Req(&req)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		var obj map[string]json.RawMessage
		if err = json.NewDecoder(res.Body).Decode(&obj); err != nil {
			return err
		}
		for k, v := range obj {
			fmt.Fprintf(os.Stdout, "- %s: %s\n", k, string(v))
		}
		return nil
	},
}

func init() {
	featureShowCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
	featureShowCmd.Flags().BoolVar(&flagWithSources, "source", false, "Show the sources of the feature flags")
	featureFlagCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
	featureSetCmd.Flags().StringVar(&flagDomain, "domain", "", "Specify the domain name of the instance")
	featureRatioCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags")
	featureConfigCmd.Flags().StringVar(&flagContext, "context", "", "The context for the feature flags")

	featureCmdGroup.AddCommand(featureShowCmd)
	featureCmdGroup.AddCommand(featureFlagCmd)
	featureCmdGroup.AddCommand(featureSetCmd)
	featureCmdGroup.AddCommand(featureRatioCmd)
	featureCmdGroup.AddCommand(featureConfigCmd)
	featureCmdGroup.AddCommand(featureDefaultCmd)
	RootCmd.AddCommand(featureCmdGroup)
}
