package app

import (
	"errors"
	"fmt"
	"os"
	"os/exec"
	"slices"
	"sort"
	"strings"

	"coopcloud.tech/abra/cli/internal"
	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/i18n"
	"coopcloud.tech/abra/pkg/log"
	"github.com/spf13/cobra"
)

// translators: `abra app cmd` aliases. use a comma separated list of aliases with
// no spaces in between
var appCmdAliases = i18n.G("cmd")

var AppCmdCommand = &cobra.Command{
	// translators: `app command` command
	Use:     i18n.G("command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]"),
	Aliases: strings.Split(appCmdAliases, ","),
	// translators: Short description for `app cmd` command
	Short: i18n.G("Run app commands"),
	Long: i18n.G(`Run an app specific command.

These commands are bash functions, defined in the abra.sh of the recipe itself.
They can be run within the context of a service (e.g. app) or locally on your
work station by passing "--local/-l".

N.B. If using the "--" style to pass arguments, flags (e.g. "--local/-l") must
be passed *before* the "--". It is possible to pass arguments without the "--"
as long as no dashes are present (i.e. "foo" works without "--", "-foo"
does not).`),
	Example: i18n.G(`  # pass <cmd> args/flags without "--"
  abra app cmd 1312.net app my_cmd_arg foo --user bar

  # pass <cmd> args/flags with "--"
  abra app cmd 1312.net app my_cmd_args --user bar -- foo -vvv

  # drop the [service] arg if using "--local/-l"
  abra app cmd 1312.net my_cmd --local`),
	Args: func(cmd *cobra.Command, args []string) error {
		if local {
			if !(len(args) >= 2) {
				return errors.New(i18n.G("requires at least 2 arguments with --local/-l"))
			}

			if slices.Contains(os.Args, "--") {
				if cmd.ArgsLenAtDash() > 2 {
					return errors.New(i18n.G("accepts at most 2 args with --local/-l"))
				}
			}

			// NOTE(d1): it is unclear how to correctly validate this case
			//
			// abra app cmd 1312.net app test_cmd_args foo --local
			// FATAL <recipe> doesn't have a app function
			//
			// "app" should not be there, but there is no reliable way to detect arg
			// count when the user can pass an arbitrary amount of recipe command
			// arguments
			return nil
		}

		if !(len(args) >= 3) {
			return errors.New(i18n.G("requires at least 3 arguments"))
		}

		return nil
	},
	ValidArgsFunction: func(
		cmd *cobra.Command,
		args []string,
		toComplete string) ([]string, cobra.ShellCompDirective) {
		switch l := len(args); l {
		case 0:
			return autocomplete.AppNameComplete()
		case 1:
			if !local {
				return autocomplete.ServiceNameComplete(args[0])
			}
			return autocomplete.CommandNameComplete(args[0])
		case 2:
			if !local {
				return autocomplete.CommandNameComplete(args[0])
			}
			return nil, cobra.ShellCompDirectiveDefault
		default:
			return nil, cobra.ShellCompDirectiveError
		}
	},
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		if local && remoteUser != "" {
			log.Fatal(i18n.G("cannot use --local & --user together"))
		}

		hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)

		if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
			if os.IsNotExist(err) {
				log.Fatal(i18n.G("%s does not exist for %s?", app.Recipe.AbraShPath, app.Name))
			}
			log.Fatal(err)
		}

		if local {
			cmdName := args[1]
			if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
				log.Fatal(err)
			}

			log.Debug(i18n.G("--local detected, running %s on local work station", cmdName))

			var exportEnv string
			for k, v := range app.Env {
				exportEnv = exportEnv + fmt.Sprintf("%s='%s'; ", k, v)
			}

			var sourceAndExec string
			if hasCmdArgs {
				log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
				sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName, parsedCmdArgs)
			} else {
				log.Debug(i18n.G("did not detect any command arguments"))
				sourceAndExec = fmt.Sprintf("TARGET=local; APP_NAME=%s; STACK_NAME=%s; %s . %s; %s", app.Name, app.StackName(), exportEnv, app.Recipe.AbraShPath, cmdName)
			}

			shell := "/bin/bash"
			if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
				log.Debug(i18n.G("%s does not exist locally, use /bin/sh as fallback", shell))
				shell = "/bin/sh"
			}
			cmd := exec.Command(shell, "-c", sourceAndExec)

			if err := internal.RunCmd(cmd); err != nil {
				log.Fatal(err)
			}

			return
		}

		cmdName := args[2]
		if err := internal.EnsureCommand(app.Recipe.AbraShPath, app.Recipe.Name, cmdName); err != nil {
			log.Fatal(err)
		}

		serviceNames, err := appPkg.GetAppServiceNames(app.Name)
		if err != nil {
			log.Fatal(err)
		}

		matchingServiceName := false
		targetServiceName := args[1]
		for _, serviceName := range serviceNames {
			if serviceName == targetServiceName {
				matchingServiceName = true
			}
		}

		if !matchingServiceName {
			log.Fatal(i18n.G("no service %s for %s?", targetServiceName, app.Name))
		}

		log.Debug(i18n.G("running command %s within the context of %s_%s", cmdName, app.StackName(), targetServiceName))

		if hasCmdArgs {
			log.Debug(i18n.G("parsed following command arguments: %s", parsedCmdArgs))
		} else {
			log.Debug(i18n.G("did not detect any command arguments"))
		}

		cl, err := client.New(app.Server)
		if err != nil {
			log.Fatal(err)
		}

		if err := internal.RunCmdRemote(
			cl,
			app,
			disableTTY,
			app.Recipe.AbraShPath,
			targetServiceName, cmdName, parsedCmdArgs, remoteUser); err != nil {
			log.Fatal(err)
		}
	},
}

// translators: `abra app command list` aliases. use a comma separated list of
// aliases with no spaces in between
var appCmdListAliases = i18n.G("ls")

var AppCmdListCommand = &cobra.Command{
	// translators: `app cmd list` command
	Use:     i18n.G("list <domain> [flags]"),
	Aliases: strings.Split(appCmdListAliases, ","),
	// translators: Short description for `app cmd list` command
	Short: i18n.G("List all available commands"),
	Args:  cobra.MinimumNArgs(1),
	Run: func(cmd *cobra.Command, args []string) {
		app := internal.ValidateApp(args)

		if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
			log.Fatal(err)
		}

		cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
		if err != nil {
			log.Fatal(err)
		}

		sort.Strings(cmdNames)

		for _, cmdName := range cmdNames {
			fmt.Println(cmdName)
		}
	},
}

func parseCmdArgs(args []string, isLocal bool) (bool, string) {
	var (
		parsedCmdArgs string
		hasCmdArgs    bool
	)

	if isLocal {
		if len(args) > 2 {
			return true, fmt.Sprintf("%s ", strings.Join(args[2:], " "))
		}
	} else {
		if len(args) > 3 {
			return true, fmt.Sprintf("%s ", strings.Join(args[3:], " "))
		}
	}

	return hasCmdArgs, parsedCmdArgs
}

var (
	local      bool
	remoteUser string
	disableTTY bool
)

func init() {
	AppCmdCommand.Flags().BoolVarP(
		&local,
		i18n.G("local"),
		i18n.G("l"),
		false,
		i18n.G("run command locally"),
	)

	AppCmdCommand.Flags().StringVarP(
		&remoteUser,
		i18n.G("user"),
		i18n.G("u"),
		"",
		i18n.G("request remote user"),
	)

	AppCmdCommand.Flags().BoolVarP(
		&disableTTY,
		i18n.G("tty"),
		i18n.G("T"),
		false,
		i18n.G("disable remote TTY"),
	)

	AppCmdCommand.Flags().BoolVarP(
		&internal.Chaos,
		i18n.G("chaos"),
		i18n.G("C"),
		false,
		i18n.G("ignore uncommitted recipes changes"),
	)
}
