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/log"
	"github.com/spf13/cobra"
)

var AppCmdCommand = &cobra.Command{
	Use:     "command <domain> [service | --local] <cmd> [[args] [flags] | [flags] -- [args]]",
	Aliases: []string{"cmd"},
	Short:   "Run app commands",
	Long: `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: `  # 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("requires at least 2 arguments with --local/-l")
			}

			if slices.Contains(os.Args, "--") {
				if cmd.ArgsLenAtDash() > 2 {
					return errors.New("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("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("cannot use --local & --user together")
		}

		hasCmdArgs, parsedCmdArgs := parseCmdArgs(args, local)

		if _, err := os.Stat(app.Recipe.AbraShPath); err != nil {
			if os.IsNotExist(err) {
				log.Fatalf("%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.Debugf("--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.Debugf("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("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.Debugf("%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.Fatalf("no service %s for %s?", targetServiceName, app.Name)
		}

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

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

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

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

var AppCmdListCommand = &cobra.Command{
	Use:     "list <domain> [flags]",
	Aliases: []string{"ls"},
	Short:   "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
	requestTTY bool
)

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

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

	AppCmdCommand.Flags().BoolVarP(
		&requestTTY,
		"tty",
		"t",
		false,
		"request remote TTY",
	)

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