package app

import (
	"context"
	"errors"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path"
	"strings"

	"coopcloud.tech/abra/cli/internal"
	"coopcloud.tech/abra/pkg/autocomplete"
	"coopcloud.tech/abra/pkg/client"
	"coopcloud.tech/abra/pkg/config"
	containerPkg "coopcloud.tech/abra/pkg/container"
	"coopcloud.tech/abra/pkg/formatter"
	"coopcloud.tech/abra/pkg/upstream/container"
	"github.com/docker/cli/cli/command"
	"github.com/docker/docker/api/types"
	"github.com/docker/docker/api/types/filters"
	dockerClient "github.com/docker/docker/client"
	"github.com/docker/docker/pkg/archive"
	"github.com/sirupsen/logrus"
	"github.com/urfave/cli"
)

var appCmdCommand = cli.Command{
	Name:    "command",
	Aliases: []string{"cmd"},
	Usage:   "Run app commands",
	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". Arguments can be passed into these functions
using the "-- <args>" syntax.

Example:

  abra app cmd example.com app create_user -- me@example.com
`,
	ArgsUsage: "<domain> [<service>] <command> [-- <args>]",
	Flags: []cli.Flag{
		internal.DebugFlag,
		internal.LocalCmdFlag,
		internal.RemoteUserFlag,
	},
	BashComplete: autocomplete.AppNameComplete,
	Before:       internal.SubCommandBefore,
	Action: func(c *cli.Context) error {
		app := internal.ValidateApp(c)

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

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

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

		abraSh := path.Join(config.RECIPES_DIR, app.Recipe, "abra.sh")
		if _, err := os.Stat(abraSh); err != nil {
			if os.IsNotExist(err) {
				logrus.Fatalf("%s does not exist for %s?", abraSh, app.Name)
			}
			logrus.Fatal(err)
		}

		if internal.LocalCmd {
			cmdName := c.Args().Get(1)
			if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
				logrus.Fatal(err)
			}

			logrus.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 {
				logrus.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, abraSh, cmdName, parsedCmdArgs)
			} else {
				logrus.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, abraSh, cmdName)
			}

			shell := "/bin/bash"
			if _, err := os.Stat(shell); errors.Is(err, os.ErrNotExist) {
				logrus.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 {
				logrus.Fatal(err)
			}
		} else {
			targetServiceName := c.Args().Get(1)

			cmdName := c.Args().Get(2)
			if err := ensureCommand(abraSh, app.Recipe, cmdName); err != nil {
				logrus.Fatal(err)
			}

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

			matchingServiceName := false
			for _, serviceName := range serviceNames {
				if serviceName == targetServiceName {
					matchingServiceName = true
				}
			}

			if !matchingServiceName {
				logrus.Fatalf("no service %s for %s?", targetServiceName, app.Name)
			}

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

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

			if err := runCmdRemote(cl, app, abraSh, targetServiceName, cmdName, parsedCmdArgs); err != nil {
				logrus.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 ensureCommand(abraSh, recipeName, execCmd string) error {
	bytes, err := ioutil.ReadFile(abraSh)
	if err != nil {
		return err
	}

	if !strings.Contains(string(bytes), execCmd) {
		return fmt.Errorf("%s doesn't have a %s function", recipeName, execCmd)
	}

	return nil
}

func runCmdRemote(cl *dockerClient.Client, app config.App, abraSh, serviceName, cmdName, cmdArgs string) error {
	filters := filters.NewArgs()
	filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), serviceName))

	targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, true)
	if err != nil {
		return err
	}

	logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(targetContainer.ID), app.Server)

	toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
	content, err := archive.TarWithOptions(abraSh, toTarOpts)
	if err != nil {
		return err
	}

	copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
	if err := cl.CopyToContainer(context.Background(), targetContainer.ID, "/tmp", content, copyOpts); err != nil {
		return err
	}

	// FIXME: avoid instantiating a new CLI
	dcli, err := command.NewDockerCli()
	if err != nil {
		return err
	}

	shell := "/bin/bash"
	findShell := []string{"test", "-e", shell}
	execCreateOpts := types.ExecConfig{
		AttachStderr: true,
		AttachStdin:  true,
		AttachStdout: true,
		Cmd:          findShell,
		Detach:       false,
		Tty:          false,
	}

	if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
		logrus.Infof("%s does not exist for %s, use /bin/sh as fallback", shell, app.Name)
		shell = "/bin/sh"
	}

	var cmd []string
	if cmdArgs != "" {
		cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s %s", serviceName, app.Name, app.StackName(), cmdName, cmdArgs)}
	} else {
		cmd = []string{shell, "-c", fmt.Sprintf("TARGET=%s; APP_NAME=%s; STACK_NAME=%s; . /tmp/abra.sh; %s", serviceName, app.Name, app.StackName(), cmdName)}
	}

	logrus.Debugf("running command: %s", strings.Join(cmd, " "))

	if internal.RemoteUser != "" {
		logrus.Debugf("running command with user %s", internal.RemoteUser)
		execCreateOpts.User = internal.RemoteUser
	}

	execCreateOpts.Cmd = cmd
	execCreateOpts.Tty = true

	if err := container.RunExec(dcli, cl, targetContainer.ID, &execCreateOpts); err != nil {
		return err
	}

	return nil
}