package cli

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	apiv1 "github.com/acorn-io/runtime/pkg/apis/api.acorn.io/v1"
	v1 "github.com/acorn-io/runtime/pkg/apis/internal.acorn.io/v1"
	"github.com/acorn-io/runtime/pkg/autoupgrade"
	cli "github.com/acorn-io/runtime/pkg/cli/builder"
	"github.com/acorn-io/runtime/pkg/client"
	"github.com/acorn-io/runtime/pkg/dev"
	"github.com/acorn-io/runtime/pkg/imagerules"
	"github.com/acorn-io/runtime/pkg/imagesource"
	"github.com/acorn-io/runtime/pkg/rulerequest"
	"github.com/acorn-io/runtime/pkg/wait"
	"github.com/acorn-io/z"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"github.com/spf13/pflag"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/yaml"
)

func NewRun(c CommandContext) *cobra.Command {
	cmd := cli.Command(&Run{out: c.StdOut, client: c.ClientFactory}, cobra.Command{
		Use:               "run [flags] IMAGE|DIRECTORY [acorn args]",
		SilenceUsage:      true,
		Short:             "Run an app from an image or Acornfile",
		ValidArgsFunction: newCompletion(c.ClientFactory, imagesCompletion(true)).withSuccessDirective(cobra.ShellCompDirectiveDefault).withShouldCompleteOptions(onlyNumArgs(1)).complete,
		Example: `
Running an app
 - Build and run from a directory
	acorn run --name my-app .

 - Run from an image
	acorn run --name my-app ghcr.io/acorn-io/hello-world

Updating an app
 - Rebuild an app from the current directory (see 'acorn dev' to do this dynamically)
	acorn run --update --name my-app .

 - Change an app's image
	acorn run --update --image ghcr.io/acorn-io/hello-world:v1.0.2 --name my-app

Automatic upgrades
 - Automatic upgrade for an app will be enabled if '#', '*', or '**' appears in the image's tag. Tags will be sorted according to the rules for these special characters described below. The newest tag will be selected for upgrade.
	'#' denotes a segment of the image tag that should be sorted numerically when finding the newest tag.
	'*' denotes a segment of the image tag that should sorted alphabetically when finding the latest tag.
	'**' denotes a wildcard. This segment of the image tag won't be considered when sorting. This is useful if your tags have a segment that is unpredictable.

 - Deploy the hello-world app with auto-upgrade enabled and matching all major, minor, and patch versions:
	acorn run ghcr.io/acorn-io/hello-world:v#.#.#

Publish Port Syntax
 - Publish port 80 for any containers that define it as a port
	acorn run -p 80 .

 - Publish container "myapp" using the hostname app.example.com
	acorn run --publish app.example.com:myapp .

Link Syntax
 - Link the running acorn application named "mydatabase" into the current app, replacing the container named "db"
	acorn run --link mydatabase:db .

Secret Syntax
- Bind the acorn secret named "mycredentials" into the current app, replacing the secret named "creds"
	acorn run --secret mycredentials:creds .

Volume Syntax
 - Create the volume named "mydata" with a size of 5 gigabyes and using the "fast" storage class
	acorn run --volume mydata,size=5G,class=fast .
 
- Bind the acorn volume named "mydata" into the current app, replacing the volume named "data"
	acorn run --volume mydata:data .`,
	})

	// These will produce an error if the flag doesn't exist or a completion function has already been registered for the
	// flag. Not returning the error since neither of these is likely occur.
	if err := cmd.RegisterFlagCompletionFunc("volume", newCompletion(c.ClientFactory, volumeFlagClassCompletion).complete); err != nil {
		cmd.Printf("Error registering completion function for -v flag: %v\n", err)
	}
	if err := cmd.RegisterFlagCompletionFunc("compute-class", newCompletion(c.ClientFactory, computeClassFlagCompletion).complete); err != nil {
		cmd.Printf("Error registering completion function for --compute-class flag: %v\n", err)
	}
	if err := cmd.RegisterFlagCompletionFunc("region", newCompletion(c.ClientFactory, regionsCompletion).complete); err != nil {
		cmd.Printf("Error registering completion function for --region flag: %v\n", err)
	}
	cmd.Flags().SetInterspersed(false)

	cmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
		fmt.Println(cmd.Short + "\n")
		fmt.Println(cmd.UsageString())
	})
	return cmd
}

type Run struct {
	RunArgs
	Dev               bool  `usage:"Enable interactive dev mode: build image, stream logs/status in the foreground and stop on exit" short:"i"`
	BidirectionalSync bool  `usage:"In interactive mode download changes in addition to uploading" short:"b"`
	Wait              *bool `usage:"Wait for app to become ready before command exiting (default: true)"`
	Quiet             bool  `usage:"Do not print status" short:"q"`
	Update            bool  `usage:"Update the app if it already exists" short:"u"`
	Replace           bool  `usage:"Replace the app with only defined values, resetting undefined fields to default values" json:"replace,omitempty"` // Replace sets patchMode to false, resulting in a full update, resetting all undefined fields to their defaults

	out    io.Writer
	client ClientFactory
}

type RunArgs struct {
	UpdateArgs
	EnvFile string `usage:"Default env vars to apply" default:".acorn.env"`
	Name    string `usage:"Name of app to create" short:"n"`
}

func (s RunArgs) ToOpts() (client.AppRunOptions, error) {
	var (
		opts client.AppRunOptions
		err  error
	)

	opts.Name = s.Name
	opts.Region = s.Region
	opts.AutoUpgrade = s.AutoUpgrade
	opts.NotifyUpgrade = s.NotifyUpgrade
	opts.AutoUpgradeInterval = s.Interval

	opts.Memory, err = v1.ParseMemory(s.Memory)
	if err != nil {
		return opts, err
	}

	opts.ComputeClasses, err = v1.ParseComputeClass(s.ComputeClass)
	if err != nil {
		return opts, err
	}

	opts.Volumes, err = v1.ParseVolumes(s.Volume, true)
	if err != nil {
		return opts, err
	}

	opts.Secrets, err = v1.ParseSecrets(s.Secret)
	if err != nil {
		return opts, err
	}

	opts.Links, err = v1.ParseLinks(s.Link)
	if err != nil {
		return opts, err
	}

	opts.Env = v1.ParseNameValues(true, s.Env...)

	if s.EnvFile != "" {
		envData, err := os.ReadFile(s.EnvFile)
		if err == nil {
			opts.Env = append(opts.Env, v1.ParseNameValues(false, strings.Split(string(envData), "\n")...)...)
		} else if err != nil && !os.IsNotExist(err) {
			return opts, err
		}
	}

	opts.Labels, err = v1.ParseScopedLabels(s.Label...)
	if err != nil {
		return opts, err
	}

	opts.Annotations, err = v1.ParseScopedLabels(s.Annotation...)
	if err != nil {
		return opts, err
	}

	opts.Publish, err = v1.ParsePortBindings(s.Publish)
	if err != nil {
		return opts, err
	}

	if s.PublishAll != nil && *s.PublishAll {
		opts.PublishMode = v1.PublishModeAll
	} else if s.PublishAll != nil && !*s.PublishAll {
		opts.PublishMode = v1.PublishModeNone
	}

	return opts, nil
}

func (s *Run) Run(cmd *cobra.Command, args []string) (err error) {
	defer func() {
		if errors.Is(err, pflag.ErrHelp) {
			err = nil
		}
	}()

	c, err := s.client.CreateDefault()
	if err != nil {
		return err
	}

	var (
		imageSource = imagesource.NewImageSource(s.client.AcornConfigFile(), s.File, s.ArgsFile, args, nil, z.Dereference(s.AutoUpgrade))
		app         *apiv1.App
		updated     bool
	)

	opts, err := s.ToOpts()
	if err != nil {
		return err
	}

	// If auto-upgrade is not set, set it to true if auto-upgrade is implied
	if !z.Dereference(opts.AutoUpgrade) && autoupgrade.Implied(imageSource.Image, s.Interval, z.Dereference(opts.NotifyUpgrade)) {
		opts.AutoUpgrade = z.Pointer(true)
	}

	// Force install prompt if needed
	_, err = c.Info(cmd.Context())
	if err != nil {
		return err
	}

	if s.Dev {
		return dev.Dev(cmd.Context(), c, &dev.Options{
			ImageSource:       imageSource,
			Run:               opts,
			Replace:           s.Replace,
			Dangerous:         s.Dangerous,
			BidirectionalSync: s.BidirectionalSync,
		})
	}

	defer func() {
		if err == nil && (s.Wait == nil || *s.Wait) && app != nil {
			app, getErr := c.AppGet(cmd.Context(), app.Name)
			if getErr == nil && (app.GetStopped() || !app.DeletionTimestamp.IsZero()) {
				return
			}
			_ = wait.App(cmd.Context(), c, app.Name, s.Quiet)
		}
	}()

	if s.Replace || s.Update {
		if s.Output != "" {
			return fmt.Errorf("--output can not be combined with --update or --replace")
		}
		app, updated, err = s.update(cmd.Context(), c, imageSource, opts)
		if err != nil {
			return err
		}
		if updated {
			fmt.Println(app.Name)
			return nil
		}
	}

	image, deployArgs, profiles, err := imageSource.GetImageAndDeployArgs(cmd.Context(), c)
	if err != nil {
		if apierrors.IsUnauthorized(err) {
			logrus.Debugf("Error pulling %s: %v", image, err)
			return fmt.Errorf("not authorized to pull %s - Use `acorn login <REGISTRY>` to login to the registry", image)
		}
		err = client.TranslateNotAllowed(err)
		if naErr := (*imagerules.ErrImageNotAllowed)(nil); errors.As(err, &naErr) {
			if _, isPattern := autoupgrade.Pattern(image); isPattern {
				err.(*imagerules.ErrImageNotAllowed).Image = image
				logrus.Debugf("Valid tags for pattern %s were not allowed to run: %v", image, naErr)
				if choice, promptErr := rulerequest.HandleNotAllowed(s.Dangerous, image); promptErr != nil {
					return promptErr
				} else if choice != "NO" {
					iarErr := rulerequest.CreateImageAllowRule(cmd.Context(), c, image, choice)
					if iarErr != nil {
						return iarErr
					}
					return s.Run(cmd, args)
				}
			}
		}
		return err
	}
	opts.DeployArgs = deployArgs
	opts.Profiles = profiles

	if s.Output != "" {
		app := client.ToApp(c.GetNamespace(), image, &opts)
		return outputApp(s.out, s.Output, app)
	}

	app, err = rulerequest.PromptRun(cmd.Context(), c, s.Dangerous, image, opts)
	if err != nil {
		return err
	}
	fmt.Println(app.Name)
	return nil
}

func (s *Run) update(ctx context.Context, c client.Client, imageSource imagesource.ImageSource, opts client.AppRunOptions) (*apiv1.App, bool, error) {
	if s.Name == "" {
		return nil, false, fmt.Errorf("--name is required for --update or --replace")
	}

	app, err := c.AppGet(ctx, s.Name)
	if apierrors.IsNotFound(err) {
		if !imageSource.IsImageSet() {
			return nil, false, fmt.Errorf("acorn \"%s\" is missing but can not be created without specifying an image to run or build", s.Name)
		}
		return nil, false, nil
	} else if err != nil {
		return nil, false, err
	}

	updateOpts := opts.ToUpdate()
	updateOpts.Replace = s.Replace

	if imageSource.IsImageSet() {
		image, deployArgs, profiles, err := imageSource.GetImageAndDeployArgs(ctx, c)
		if err != nil {
			return nil, false, err
		}
		updateOpts.Image = image
		updateOpts.DeployArgs = deployArgs
		updateOpts.Profiles = profiles
	} else if len(imageSource.Args) > 0 {
		imageSource.Image = app.Status.AppImage.Name
		if _, updateOpts.DeployArgs, updateOpts.Profiles, err = imageSource.GetImageAndDeployArgs(ctx, c); err != nil {
			return nil, false, err
		}
	}

	app, err = rulerequest.PromptUpdate(ctx, c, s.Dangerous, app.Name, updateOpts)
	if err != nil {
		return nil, false, err
	}

	return app, true, nil
}

func outputApp(out io.Writer, format string, app *apiv1.App) error {
	data, err := json.Marshal(app)
	if err != nil {
		return err
	}

	mapData := map[string]any{}
	if err := json.Unmarshal(data, &mapData); err != nil {
		return err
	}

	delete(mapData, "status")
	delete(mapData["metadata"].(map[string]any), "uid")
	delete(mapData["metadata"].(map[string]any), "resourceVersion")
	delete(mapData["metadata"].(map[string]any), "managedFields")
	delete(mapData["metadata"].(map[string]any), "creationTimestamp")

	if format == "json" {
		data, err = json.MarshalIndent(mapData, "", "  ")
	} else {
		data, err = yaml.Marshal(mapData)
	}
	if err != nil {
		return err
	}

	if out == nil {
		_, err = os.Stdout.Write(data)
	} else {
		_, err = out.Write(data)
	}
	return err
}
