package cmd

// filesCmdGroup represents the instances command
import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"text/tabwriter"

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

var (
	flagAllDomains      bool
	flagAppsDeactivated bool
	flagSafeUpdate      bool
)

var (
	flagKonnectorAccountID   string
	flagKonnectorsParameters string
)

var (
	flagKonnectorContext             string
	flagKonnectorsShortMaintenance   bool
	flagKonnectorsDisallowManualExec bool
)

var webappsCmdGroup = &cobra.Command{
	Use:   "apps <command>",
	Short: "Interact with the applications",
	Long: `
cozy-stack apps allows to interact with the cozy applications.

It provides commands to install or update applications on
a cozy.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return cmd.Usage()
	},
}

var triggersCmdGroup = &cobra.Command{
	Use:   "triggers <command>",
	Short: "Interact with the triggers",
	Long: `
cozy-stack apps allows to interact with the cozy triggers.

It provides command to run a specific trigger.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return cmd.Usage()
	},
}

var installWebappCmd = &cobra.Command{
	Use: "install <slug> [sourceurl]",
	Short: `Install an application with the specified slug name
from the given source URL.`,
	Example: `
$ cozy-stack apps install --domain cozy.localhost:8080 drive registry://drive/stable
$ cozy-stack apps install banks 'git://github.com/cozy/cozy-banks.git#build'
$ cozy-stack apps install myapp 'git+ssh://git@gitlab.example.net/team/myapp.git#build'
`,
	Long: "[Some schemes](https://docs.cozy.io/en/cozy-stack/apps/#sources) are allowed as `[sourceurl]`.",
	RunE: func(cmd *cobra.Command, args []string) error {
		return installApp(cmd, args, consts.Apps)
	},
}

var updateWebappCmd = &cobra.Command{
	Use:     "update <slug> [sourceurl]",
	Short:   "Update the application with the specified slug name.",
	Aliases: []string{"upgrade"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateApp(cmd, args, consts.Apps)
	},
}

var uninstallWebappCmd = &cobra.Command{
	Use:     "uninstall <slug>",
	Short:   "Uninstall the application with the specified slug name.",
	Aliases: []string{"rm"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return uninstallApp(cmd, args, consts.Apps)
	},
}

var lsWebappsCmd = &cobra.Command{
	Use:   "ls",
	Short: "List the installed applications.",
	RunE: func(cmd *cobra.Command, args []string) error {
		return lsApps(cmd, args, consts.Apps)
	},
}

var showWebappCmd = &cobra.Command{
	Use:   "show <slug>",
	Short: "Show the application attributes",
	RunE: func(cmd *cobra.Command, args []string) error {
		return showApp(cmd, args, consts.Apps)
	},
}

var showWebappTriggersCmd = &cobra.Command{
	Use:   "show-from-app <slug>",
	Short: "Show the application triggers",
	RunE: func(cmd *cobra.Command, args []string) error {
		return showWebAppTriggers(cmd, args, consts.Apps)
	},
}

var showKonnectorCmd = &cobra.Command{
	Use:   "show <slug>",
	Short: "Show the application attributes",
	RunE: func(cmd *cobra.Command, args []string) error {
		return showApp(cmd, args, consts.Konnectors)
	},
}

var konnectorsCmdGroup = &cobra.Command{
	Use:   "konnectors <command>",
	Short: "Interact with the konnectors",
	Long: `
cozy-stack konnectors allows to interact with the cozy konnectors.

It provides commands to install or update applications on
a cozy.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return cmd.Usage()
	},
}

var installKonnectorCmd = &cobra.Command{
	Use: "install <slug> [sourceurl]",
	Short: `Install a konnector with the specified slug name
from the given source URL.`,
	Long: `
Install a konnector with the specified slug name. You can also provide the
version number to install a specific release if you use the registry:// scheme.
Following formats are accepted:
	registry://<konnector>/<channel>/<version>
	registry://<konnector>/<channel>
	registry://<konnector>/<version>
	registry://<konnector>

If you provide a channel and a version, the channel is ignored.
Default channel is stable.
Default version is latest.
`,
	Example: `
$ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable/1.0.1
$ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/stable
$ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline/1.2.0
$ cozy-stack konnectors install --domain cozy.localhost:8080 trainline registry://trainline
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		return installApp(cmd, args, consts.Konnectors)
	},
}

var updateKonnectorCmd = &cobra.Command{
	Use:     "update <slug> [sourceurl]",
	Short:   "Update the konnector with the specified slug name.",
	Aliases: []string{"upgrade"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return updateApp(cmd, args, consts.Konnectors)
	},
}

var uninstallKonnectorCmd = &cobra.Command{
	Use:     "uninstall <slug>",
	Short:   "Uninstall the konnector with the specified slug name.",
	Aliases: []string{"rm"},
	RunE: func(cmd *cobra.Command, args []string) error {
		return uninstallApp(cmd, args, consts.Konnectors)
	},
}

var lsKonnectorsCmd = &cobra.Command{
	Use:   "ls",
	Short: "List the installed konnectors.",
	RunE: func(cmd *cobra.Command, args []string) error {
		return lsApps(cmd, args, consts.Konnectors)
	},
}

var runKonnectorsCmd = &cobra.Command{
	Use:   "run <slug>",
	Short: "Run a konnector.",
	Long:  "Run a konnector named with specified slug using the specified options.",
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagDomain == "" {
			errPrintfln("%s", errMissingDomain)
			return cmd.Usage()
		}
		if len(args) < 1 {
			return cmd.Usage()
		}

		slug := args[0]
		c := newClient(flagDomain,
			consts.Jobs+":POST:konnector:worker",
			consts.Triggers,
			consts.Files,
			consts.Accounts,
		)

		ts, err := c.GetTriggers("konnector")
		if err != nil {
			return err
		}

		type localTrigger struct {
			id        string
			accountID string
		}

		var triggers []*localTrigger
		for _, t := range ts {
			var msg struct {
				Slug    string `json:"konnector"`
				Account string `json:"account"`
			}
			if err = json.Unmarshal(t.Attrs.Message, &msg); err != nil {
				return err
			}
			if msg.Slug == slug {
				triggers = append(triggers, &localTrigger{t.ID, msg.Account})
			}
		}

		if len(triggers) == 0 {
			return fmt.Errorf("Could not find a konnector %q: "+
				"it may be installed but it is not activated (no related trigger)", slug)
		}

		var trigger *localTrigger
		if len(triggers) > 1 || flagKonnectorAccountID != "" {
			if flagKonnectorAccountID == "" {
				return errors.New("Found multiple konnectors with different accounts: use the --account-id flag")
			}
			for _, t := range triggers {
				if t.accountID == flagKonnectorAccountID {
					trigger = t
					break
				}
			}
			if trigger == nil {
				return fmt.Errorf("Could not find konnector linked to account with id %q",
					flagKonnectorAccountID)
			}
		} else {
			trigger = triggers[0]
		}

		j, err := c.TriggerLaunch(trigger.id)
		if err != nil {
			return err
		}

		json, err := json.MarshalIndent(j, "", "  ")
		if err != nil {
			return err
		}

		fmt.Println(string(json))
		return nil
	},
}

var listMaintenancesCmd = &cobra.Command{
	Use:   "ls-maintenances",
	Short: `List the konnectors in maintenance`,
	RunE: func(cmd *cobra.Command, args []string) (err error) {
		ac := newAdminClient()
		list, err := ac.ListMaintenances(flagKonnectorContext)
		if err != nil {
			return err
		}
		for _, item := range list {
			json, err := json.MarshalIndent(item, "", "  ")
			if err != nil {
				return err
			}
			fmt.Println(string(json))
		}
		return nil
	},
}

var activateMaintenanceKonnectorsCmd = &cobra.Command{
	Use:   "maintenance [slug]",
	Short: `Activate the maintenance for the given konnector`,
	RunE: func(cmd *cobra.Command, args []string) (err error) {
		if len(args) != 1 {
			return cmd.Help()
		}

		messages := make(map[string]interface{})
		for {
			locale := prompt("Locale (empty to abort):")
			if locale == "" {
				break
			}
			if len(locale) > 5 {
				fmt.Fprintf(os.Stdout, "Invalid locale name: %q\n", locale)
				continue
			}
			shortMessage := prompt("Short message:")
			longMessage := prompt("Long message:")
			messages[locale] = map[string]string{
				"short_message": shortMessage,
				"long_message":  longMessage,
			}
		}
		opts := map[string]interface{}{
			"flag_short_maintenance":    flagKonnectorsShortMaintenance,
			"flag_disallow_manual_exec": flagKonnectorsDisallowManualExec,
			"messages":                  messages,
		}
		ac := newAdminClient()
		return ac.ActivateMaintenance(args[0], opts)
	},
}

var deactivateMaintenanceKonnectorsCmd = &cobra.Command{
	Use:   "deactivate-maintenance [slug]",
	Short: `Deactivate maintenance for the given konnector`,
	RunE: func(cmd *cobra.Command, args []string) (err error) {
		if len(args) != 1 {
			return cmd.Help()
		}
		ac := newAdminClient()
		return ac.DeactivateMaintenance(args[0])
	},
}

func installApp(cmd *cobra.Command, args []string, appType string) error {
	if len(args) < 1 {
		return cmd.Usage()
	}
	slug := args[0]
	source := "registry://" + slug + "/stable"
	if len(args) > 1 {
		source = args[1]
	}
	if flagAllDomains {
		return foreachDomains(func(in *client.Instance) error {
			c := newClient(in.Attrs.Domain, appType)
			_, err := c.InstallApp(&client.AppOptions{
				AppType:     appType,
				Slug:        slug,
				SourceURL:   source,
				Deactivated: flagAppsDeactivated,
			})
			if err != nil {
				if err.Error() == app.ErrAlreadyExists.Error() {
					return nil
				}
				return err
			}
			fmt.Fprintf(os.Stdout, "Application installed successfully on %s\n", in.Attrs.Domain)
			return nil
		})
	}
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}

	var overridenParameters *json.RawMessage
	if flagKonnectorsParameters != "" {
		tmp := json.RawMessage(flagKonnectorsParameters)
		overridenParameters = &tmp
	}

	c := newClient(flagDomain, appType)
	manifest, err := c.InstallApp(&client.AppOptions{
		AppType:     appType,
		Slug:        slug,
		SourceURL:   source,
		Deactivated: flagAppsDeactivated,

		OverridenParameters: overridenParameters,
	})
	if err != nil {
		return err
	}
	fmt.Fprintf(os.Stdout, "%s (%s) has been installed on %s\n", slug, manifest.Attrs.Version, flagDomain)

	return nil
}

func updateApp(cmd *cobra.Command, args []string, appType string) error {
	if len(args) == 0 || len(args) > 2 {
		return cmd.Usage()
	}
	var src string
	if len(args) > 1 {
		src = args[1]
	}
	if flagAllDomains {
		return foreachDomains(func(in *client.Instance) error {
			c := newClient(in.Attrs.Domain, appType)
			_, err := c.UpdateApp(&client.AppOptions{
				AppType:   appType,
				Slug:      args[0],
				SourceURL: src,
			}, flagSafeUpdate)
			if err != nil {
				if err.Error() == app.ErrNotFound.Error() {
					return nil
				}
				return err
			}
			fmt.Fprintf(os.Stdout, "Application updated successfully on %s\n", in.Attrs.Domain)
			return nil
		})
	}
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}

	var overridenParameters *json.RawMessage
	if flagKonnectorsParameters != "" {
		tmp := json.RawMessage(flagKonnectorsParameters)
		overridenParameters = &tmp
	}

	c := newClient(flagDomain, appType)
	opts := &client.AppOptions{
		AppType:   appType,
		Slug:      args[0],
		SourceURL: src,

		OverridenParameters: overridenParameters,
	}
	manifest, err := c.GetApp(opts)
	if err != nil {
		return err
	}
	newManifest, err := c.UpdateApp(opts, flagSafeUpdate)
	if err != nil {
		return err
	}
	msg := "%s is already up-to-date at %s\n"
	if app.IsMoreRecent(manifest.Attrs.Version, newManifest.Attrs.Version) {
		msg = "%s has been upgraded to %s\n"
	} else if app.IsMoreRecent(newManifest.Attrs.Version, manifest.Attrs.Version) {
		msg = "%s has been downgraded to %s\n"
	}
	fmt.Fprintf(os.Stdout, msg, args[0], newManifest.Attrs.Version)

	return nil
}

func uninstallApp(cmd *cobra.Command, args []string, appType string) error {
	if len(args) != 1 {
		return cmd.Usage()
	}
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}
	c := newClient(flagDomain, appType)
	manifest, err := c.UninstallApp(&client.AppOptions{
		AppType: appType,
		Slug:    args[0],
	})
	if err != nil {
		return err
	}
	fmt.Fprintf(os.Stdout, "%s has been uninstalled\n", manifest.Attrs.Slug)
	return nil
}

func showApp(cmd *cobra.Command, args []string, appType string) error {
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}
	if len(args) < 1 {
		return cmd.Usage()
	}
	c := newClient(flagDomain, appType)
	manifest, err := c.GetApp(&client.AppOptions{
		Slug:    args[0],
		AppType: appType,
	})
	if err != nil {
		return err
	}
	json, err := json.MarshalIndent(manifest.Attrs, "", "  ")
	if err != nil {
		return err
	}
	fmt.Println(string(json))
	return nil
}

func showWebAppTriggers(cmd *cobra.Command, args []string, appType string) error {
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}
	if len(args) < 1 {
		return cmd.Usage()
	}
	c := newClient(flagDomain, appType, consts.Triggers)
	manifest, err := c.GetApp(&client.AppOptions{
		Slug:    args[0],
		AppType: appType,
	})
	if err != nil {
		return err
	}

	var triggerIDs []string
	if manifest.Attrs.Services == nil {
		fmt.Fprintf(os.Stdout, "No triggers\n")
		return nil
	}
	for _, service := range *manifest.Attrs.Services {
		if service.TriggerID == "" {
			continue
		}
		triggerIDs = append(triggerIDs, service.TriggerID)
	}
	var triggers []*client.Trigger
	var trigger *client.Trigger
	for _, triggerID := range triggerIDs {
		trigger, err = c.GetTrigger(triggerID)
		if err != nil {
			return err
		}

		triggers = append(triggers, trigger)
	}
	json, err := json.MarshalIndent(triggers, "", "  ")
	if err != nil {
		return err
	}
	fmt.Println(string(json))
	return nil
}

var listTriggerCmd = &cobra.Command{
	Use:     "ls",
	Short:   `List triggers`,
	Example: "$ cozy-stack triggers ls --domain cozy.localhost:8080",
	RunE: func(cmd *cobra.Command, args []string) error {
		if flagDomain == "" {
			errPrintfln("%s", errMissingDomain)
			return cmd.Usage()
		}
		c := newClient(flagDomain, consts.Triggers)
		list, err := c.ListTriggers()
		if err != nil {
			return err
		}
		w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
		for _, t := range list {
			fmt.Fprintf(w, "%s\t%s\t%s\t%v\t%s\n",
				t.ID,
				t.Attrs.WorkerType,
				t.Attrs.Type,
				t.Attrs.Arguments,
				t.Attrs.Debounce,
			)
		}
		return w.Flush()
	},
}

var launchTriggerCmd = &cobra.Command{
	Use:     "launch [triggerId]",
	Short:   `Creates a job from a specific trigger`,
	Example: "$ cozy-stack triggers launch --domain cozy.localhost:8080 748f42b65aca8c99ec2492eb660d1891",
	RunE:    launchTrigger,
}

func launchTrigger(cmd *cobra.Command, args []string) error {
	if len(args) < 1 {
		return cmd.Usage()
	}
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}

	// Creates client
	c := newClient(flagDomain, consts.Triggers)

	// Creates job
	j, err := c.TriggerLaunch(args[0])
	if err != nil {
		return err
	}

	// Print JSON
	json, err := json.MarshalIndent(j, "", "  ")
	if err != nil {
		return err
	}
	fmt.Println(string(json))
	return nil
}

func lsApps(cmd *cobra.Command, args []string, appType string) error {
	if flagDomain == "" {
		errPrintfln("%s", errMissingDomain)
		return cmd.Usage()
	}
	c := newClient(flagDomain, appType)
	manifests, err := c.ListApps(appType)
	if err != nil {
		return err
	}
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	for _, m := range manifests {
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\n",
			m.Attrs.Slug,
			m.Attrs.Source,
			m.Attrs.Version,
			m.Attrs.State,
		)
	}
	return w.Flush()
}

func foreachDomains(predicate func(*client.Instance) error) error {
	ac := newAdminClient()
	list, err := ac.ListInstances()
	if err != nil {
		return nil
	}
	var hasErr bool
	for _, i := range list {
		if err = predicate(i); err != nil {
			errPrintfln("%s: %s", i.Attrs.Domain, err)
			hasErr = true
		}
	}
	if hasErr {
		return errors.New("At least one error occurred while executing this command")
	}
	return nil
}

func init() {
	webappsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance")
	webappsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively")

	installWebappCmd.PersistentFlags().BoolVar(&flagAppsDeactivated, "ask-permissions", false, "specify that the application should not be activated after installation")
	updateWebappCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes")
	updateKonnectorCmd.PersistentFlags().BoolVar(&flagSafeUpdate, "safe", false, "do not upgrade if there are blocking changes")

	runKonnectorsCmd.PersistentFlags().StringVar(&flagKonnectorAccountID, "account-id", "", "specify the account ID to use for running the konnector")

	listMaintenancesCmd.PersistentFlags().StringVar(&flagKonnectorContext, "context", "", "include konnectors in maintenance for apps registry of this context")
	activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsShortMaintenance, "short", false, "specify a short maintenance")
	activateMaintenanceKonnectorsCmd.PersistentFlags().BoolVar(&flagKonnectorsDisallowManualExec, "no-manual-exec", false, "specify a maintenance disallowing manual execution")

	triggersCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance")
	triggersCmdGroup.AddCommand(launchTriggerCmd)
	triggersCmdGroup.AddCommand(listTriggerCmd)
	triggersCmdGroup.AddCommand(showWebappTriggersCmd)

	webappsCmdGroup.AddCommand(lsWebappsCmd)
	webappsCmdGroup.AddCommand(showWebappCmd)
	webappsCmdGroup.AddCommand(installWebappCmd)
	webappsCmdGroup.AddCommand(updateWebappCmd)
	webappsCmdGroup.AddCommand(uninstallWebappCmd)

	konnectorsCmdGroup.PersistentFlags().StringVar(&flagDomain, "domain", cozyDomain(), "specify the domain name of the instance")
	konnectorsCmdGroup.PersistentFlags().StringVar(&flagKonnectorsParameters, "parameters", "", "override the parameters of the installed konnector")
	konnectorsCmdGroup.PersistentFlags().BoolVar(&flagAllDomains, "all-domains", false, "work on all domains iteratively")

	konnectorsCmdGroup.AddCommand(lsKonnectorsCmd)
	konnectorsCmdGroup.AddCommand(showKonnectorCmd)
	konnectorsCmdGroup.AddCommand(installKonnectorCmd)
	konnectorsCmdGroup.AddCommand(updateKonnectorCmd)
	konnectorsCmdGroup.AddCommand(uninstallKonnectorCmd)
	konnectorsCmdGroup.AddCommand(runKonnectorsCmd)
	konnectorsCmdGroup.AddCommand(listMaintenancesCmd)
	konnectorsCmdGroup.AddCommand(activateMaintenanceKonnectorsCmd)
	konnectorsCmdGroup.AddCommand(deactivateMaintenanceKonnectorsCmd)

	RootCmd.AddCommand(triggersCmdGroup)
	RootCmd.AddCommand(webappsCmdGroup)
	RootCmd.AddCommand(konnectorsCmdGroup)
}
