package app

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

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/app"
	appPkg "coopcloud.tech/abra/pkg/app"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/log"
	"github.com/urfave/cli/v3"
)

var appCmdCommand = cli.Command{
	Name:      "command",
	Aliases:   []string{"cmd"},
	Usage:     "Run app commands",
	UsageText: "abra app cmd <domain> [<service>] <cmd> [<cmd-args>] [options]",
	Description: `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".`,
	Flags: []cli.Flag{
		internal.LocalCmdFlag,
		internal.RemoteUserFlag,
		internal.TtyFlag,
		internal.ChaosFlag,
	},
	Before: internal.SubCommandBefore,
	Commands: []*cli.Command{
		&appCmdListCommand,
	},
	ShellComplete: func(ctx context.Context, cmd *cli.Command) {
		args := cmd.Args()
		switch args.Len() {
		case 0:
			autocomplete.AppNameComplete(ctx, cmd)
		case 1:
			autocomplete.ServiceNameComplete(args.Get(0))
		case 2:
			cmdNameComplete(args.Get(0))
		}
	},
	Action: func(ctx context.Context, cmd *cli.Command) error {
		app := internal.ValidateApp(cmd)

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

		if internal.LocalCmd && internal.RemoteUser != "" {
			internal.ShowSubcommandHelpAndError(cmd, errors.New("cannot use --local & --user together"))
		}

		hasCmdArgs, parsedCmdArgs := parseCmdArgs(cmd.Args().Slice(), internal.LocalCmd)

		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 internal.LocalCmd {
			if !(cmd.Args().Len() >= 2) {
				internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
			}

			cmdName := cmd.Args().Get(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)
			}
		} else {
			if !(cmd.Args().Len() >= 3) {
				internal.ShowSubcommandHelpAndError(cmd, errors.New("missing arguments"))
			}

			targetServiceName := cmd.Args().Get(1)

			cmdName := cmd.Args().Get(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
			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, app.Recipe.AbraShPath, targetServiceName, cmdName, parsedCmdArgs); err != nil {
				log.Fatal(err)
			}
		}

		return nil
	},
}

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
}

func cmdNameComplete(appName string) {
	app, err := app.Get(appName)
	if err != nil {
		return
	}
	cmdNames, _ := getShCmdNames(app)
	if err != nil {
		return
	}
	for _, n := range cmdNames {
		fmt.Println(n)
	}
}

var appCmdListCommand = cli.Command{
	Name:      "list",
	Aliases:   []string{"ls"},
	Usage:     "List all available commands",
	UsageText: "abra app cmd ls <domain> [options]",
	Flags: []cli.Flag{
		internal.ChaosFlag,
	},
	ShellComplete: autocomplete.AppNameComplete,
	Before:        internal.SubCommandBefore,
	HideHelp:      true,
	Action: func(ctx context.Context, cmd *cli.Command) error {
		app := internal.ValidateApp(cmd)

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

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

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

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

		cmdNames, err := getShCmdNames(app)
		if err != nil {
			log.Fatal(err)
		}

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

		return nil
	},
}

func getShCmdNames(app appPkg.App) ([]string, error) {
	cmdNames, err := appPkg.ReadAbraShCmdNames(app.Recipe.AbraShPath)
	if err != nil {
		return nil, err
	}

	sort.Strings(cmdNames)
	return cmdNames, nil
}
