// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package cli

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strconv"
	"strings"

	"github.com/aws/aws-sdk-go/service/ssm"
	"github.com/aws/copilot-cli/internal/pkg/aws/identity"
	"github.com/aws/copilot-cli/internal/pkg/describe"
	"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
	"github.com/aws/copilot-cli/internal/pkg/version"
	"github.com/dustin/go-humanize/english"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/copilot-cli/internal/pkg/docker/dockerfile"

	"github.com/aws/copilot-cli/internal/pkg/docker/dockerengine"

	"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
	"github.com/aws/copilot-cli/internal/pkg/config"
	"github.com/aws/copilot-cli/internal/pkg/deploy"
	"github.com/aws/copilot-cli/internal/pkg/deploy/cloudformation"
	"github.com/aws/copilot-cli/internal/pkg/exec"
	"github.com/aws/copilot-cli/internal/pkg/initialize"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/aws/copilot-cli/internal/pkg/term/color"
	"github.com/aws/copilot-cli/internal/pkg/term/log"
	termprogress "github.com/aws/copilot-cli/internal/pkg/term/progress"
	"github.com/aws/copilot-cli/internal/pkg/term/prompt"
	"github.com/aws/copilot-cli/internal/pkg/term/selector"
	"github.com/aws/copilot-cli/internal/pkg/workspace"
	"github.com/spf13/afero"
	"github.com/spf13/cobra"
)

const (
	wkldInitImagePromptHelp = `The name of an existing Docker image. Images in the Docker Hub registry are available by default.
Other repositories are specified with either repository-url/image:tag or repository-url/image@digest`
	wkldInitAppRunnerImagePromptHelp = `The name of an existing Docker image. App Runner supports images hosted in ECR or ECR Public registries.`
)

const (
	defaultSvcPortString = "80"
	service              = "service"
	job                  = "job"
)

var (
	fmtSvcInitSvcTypePrompt  = "Which %s best represents your service's architecture?"
	svcInitSvcTypeHelpPrompt = fmt.Sprintf(`A %s is an internet-facing or private HTTP server managed by AWS App Runner that scales based on incoming requests.
To learn more see: https://git.io/JEEfb

A %s is an internet-facing HTTP server managed by Amazon ECS on AWS Fargate behind a load balancer.
To learn more see: https://git.io/JEEJe

A %s is a private, non internet-facing service accessible from other services in your VPC.
To learn more see: https://git.io/JEEJt

A %s is a private service that can consume messages published to topics in your application.
To learn more see: https://git.io/JEEJY`,
		manifestinfo.RequestDrivenWebServiceType,
		manifestinfo.LoadBalancedWebServiceType,
		manifestinfo.BackendServiceType,
		manifestinfo.WorkerServiceType,
	)

	fmtWkldInitNamePrompt     = "What do you want to %s this %s?"
	fmtWkldInitNameHelpPrompt = `The name will uniquely identify this %s within your app %s.
Deployed resources (such as your ECR repository, logs) will contain this %[1]s's name and be tagged with it.`

	fmtWkldInitDockerfilePrompt      = "Which " + color.Emphasize("Dockerfile") + " would you like to use for %s?"
	wkldInitDockerfileHelpPrompt     = "Dockerfile to use for building your container image."
	fmtWkldInitDockerfilePathPrompt  = "What is the path to the " + color.Emphasize("Dockerfile") + " for %s?"
	wkldInitDockerfilePathHelpPrompt = "Path to Dockerfile to use for building your container image."

	svcInitSvcPortPrompt     = "Which %s do you want customer traffic sent to?"
	svcInitSvcPortHelpPrompt = `The port will be used by the load balancer to route incoming traffic to this service.
You should set this to the port which your Dockerfile uses to communicate with the internet.`

	svcInitPublisherPrompt     = "Which topics do you want to subscribe to?"
	svcInitPublisherHelpPrompt = `A publisher is an existing SNS Topic to which a service publishes messages. 
These messages can be consumed by the Worker Service.`

	svcInitIngressTypePrompt     = "Would you like to accept traffic from your environment or the internet?"
	svcInitIngressTypeHelpPrompt = `"Environment" will configure your service as private.
"Internet" will configure your service as public.`

	wkldInitImagePrompt = fmt.Sprintf("What's the %s ([registry/]repository[:tag|@digest]) of the image to use?", color.Emphasize("location"))

	fmtStaticSiteInitDirFilePrompt      = "Which " + color.Emphasize("directories or files") + " would you like to upload for %s?"
	staticSiteInitDirFileHelpPrompt     = "Directories or files to use for building your static site."
	fmtStaticSiteInitDirFilePathPrompt  = "What is the path to the " + color.Emphasize("directory or file") + " for %s?"
	staticSiteInitDirFilePathHelpPrompt = "Path to directory or file to use for building your static site."
)

const (
	ingressTypeEnvironment = "Environment"
	ingressTypeInternet    = "Internet"

	rdwsTypeHint = "App Runner"
	lbwsTypeHint = "Public. ALB by default. Internet to ECS on Fargate"
	besTypeHint  = "Private. ALB optional. ECS on Fargate"
	wsTypeHint   = "Events to SQS to ECS on Fargate"
	ssTypeHint   = "Internet to CDN to S3 bucket"
)

var rdwsIngressOptions = []string{
	ingressTypeEnvironment,
	ingressTypeInternet,
}

var serviceTypeHints = map[string]string{
	manifestinfo.RequestDrivenWebServiceType: rdwsTypeHint,
	manifestinfo.LoadBalancedWebServiceType:  lbwsTypeHint,
	manifestinfo.BackendServiceType:          besTypeHint,
	manifestinfo.WorkerServiceType:           wsTypeHint,
	manifestinfo.StaticSiteType:              ssTypeHint,
}

type initWkldVars struct {
	appName           string
	wkldType          string
	name              string
	dockerfilePath    string
	image             string
	subscriptions     []string
	noSubscribe       bool
	sourcePaths       []string
	allowAppDowngrade bool
}

type initSvcVars struct {
	initWkldVars

	port        uint16
	ingressType string
}

type initSvcOpts struct {
	initSvcVars

	// Interfaces to interact with dependencies.
	fs           afero.Fs
	init         svcInitializer
	prompt       prompter
	store        store
	dockerEngine dockerEngine
	sel          dockerfileSelector
	sourceSel    staticSourceSelector
	topicSel     topicSelector
	mftReader    manifestReader
	svcLister    wlLister

	// Outputs stored on successful actions.
	manifestPath string
	platform     *manifest.PlatformString
	topics       []manifest.TopicSubscription
	staticAssets []manifest.FileUpload

	// For workspace validation.
	wsAppName         string
	wsPendingCreation bool

	// Cache variables
	df                  dockerfileParser
	manifestExists      bool
	additionalFoundPort uint16 // For logging a personalized recommended action with multiple ports.
	wsRoot              string

	dockerfile          func(path string) dockerfileParser
	initEnvDescriber    func(appName, envName string) (envDescriber, error)
	newAppVersionGetter func(appName string) (versionGetter, error)

	// Overridden in tests.
	templateVersion string
}

func newInitSvcOpts(vars initSvcVars) (*initSvcOpts, error) {
	fs := afero.NewOsFs()
	ws, err := workspace.Use(fs)
	if err != nil {
		return nil, err
	}
	sessProvider := sessions.ImmutableProvider(sessions.UserAgentExtras("svc init"))
	sess, err := sessProvider.Default()
	if err != nil {
		return nil, err
	}
	store := config.NewSSMStore(identity.New(sess), ssm.New(sess), aws.StringValue(sess.Config.Region))
	prompter := prompt.New()
	deployStore, err := deploy.NewStore(sessProvider, store)
	if err != nil {
		return nil, err
	}
	snsSel := selector.NewDeploySelect(prompter, store, deployStore)

	initSvc := &initialize.WorkloadInitializer{
		Store:    store,
		Ws:       ws,
		Prog:     termprogress.NewSpinner(log.DiagnosticWriter),
		Deployer: cloudformation.New(sess, cloudformation.WithProgressTracker(os.Stderr)),
	}
	dfSel, err := selector.NewDockerfileSelector(prompter, fs)
	if err != nil {
		return nil, err
	}
	sourceSel, err := selector.NewLocalFileSelector(prompter, fs, ws)
	if err != nil {
		return nil, fmt.Errorf("init a new local file selector: %w", err)
	}
	opts := &initSvcOpts{
		initSvcVars: vars,
		store:       store,
		fs:          fs,
		init:        initSvc,
		prompt:      prompter,
		sel:         dfSel,
		topicSel:    snsSel,
		sourceSel:   sourceSel,
		mftReader:   ws,
		svcLister:   ws,
		newAppVersionGetter: func(appName string) (versionGetter, error) {
			return describe.NewAppDescriber(appName)
		},
		initEnvDescriber: func(appName string, envName string) (envDescriber, error) {
			envDescriber, err := describe.NewEnvDescriber(describe.NewEnvDescriberConfig{
				App:         appName,
				Env:         envName,
				ConfigStore: store,
			})
			if err != nil {
				return nil, err
			}
			return envDescriber, nil
		},
		dockerEngine:    dockerengine.New(exec.NewCmd()),
		wsAppName:       tryReadingAppName(),
		wsRoot:          ws.ProjectRoot(),
		templateVersion: version.LatestTemplateVersion(),
	}
	opts.dockerfile = func(path string) dockerfileParser {
		if opts.df != nil {
			return opts.df
		}
		opts.df = dockerfile.New(opts.fs, opts.dockerfilePath)
		return opts.df
	}
	return opts, nil
}

// Validate returns an error for any invalid optional flags.
func (o *initSvcOpts) Validate() error {
	// If this app is pending creation, we'll skip validation.
	if !o.wsPendingCreation {
		if err := validateWorkspaceApp(o.wsAppName, o.appName, o.store); err != nil {
			return err
		}
		o.appName = o.wsAppName
	}
	if o.dockerfilePath != "" && o.image != "" {
		return fmt.Errorf("--%s and --%s cannot be specified together", dockerFileFlag, imageFlag)
	}
	if o.dockerfilePath != "" {
		if _, err := o.fs.Stat(o.dockerfilePath); err != nil {
			return err
		}
	}
	if o.port != 0 {
		if err := validateSvcPort(o.port); err != nil {
			return err
		}
	}

	if o.image != "" && o.wkldType == manifestinfo.RequestDrivenWebServiceType {
		if err := validateAppRunnerImage(o.image); err != nil {
			return err
		}
	}
	if len(o.sourcePaths) != 0 {
		if o.wkldType != manifestinfo.StaticSiteType {
			return fmt.Errorf("'--%s' must be specified with '--%s %q'", sourcesFlag, typeFlag, manifestinfo.StaticSiteType)
		}
		if err := o.validateSourcePaths(o.sourcePaths); err != nil {
			return err
		}
		assets, err := o.convertStringsToAssets(o.sourcePaths)
		if err != nil {
			return fmt.Errorf("convert source strings to objects: %w", err)
		}
		o.staticAssets = assets
	}
	if err := validateSubscribe(o.noSubscribe, o.subscriptions); err != nil {
		return err
	}
	if err := o.validateIngressType(); err != nil {
		return err
	}
	return nil
}

func (o *initSvcOpts) validateSourcePaths(sources []string) error {
	if o.wsPendingCreation {
		// This can happen during `copilot init`, that we have to `Validate` before there is a workspace.
		// In this case, we skip the validation for path, and let `svc deploy` handle the validation.
		return nil
	}
	for _, source := range sources {
		_, err := o.fs.Stat(filepath.Join(o.wsRoot, source))
		if err != nil {
			return fmt.Errorf("source %q must be a valid path relative to the workspace %q: %w", source, o.wsRoot, err)
		}
	}
	return nil
}

// Ask prompts for and validates any required flags.
func (o *initSvcOpts) Ask() error {
	// NOTE: we optimize the case where `name` is given as a flag while `wkldType` is not.
	// In this case, we can try reading the manifest, and set `wkldType` to the value found in the manifest
	// without having to validate it. We can then short circuit the rest of the prompts for an optimal UX.
	if o.name != "" && o.wkldType == "" {
		// Best effort to validate the service name without type.
		if err := o.validateSvc(); err != nil {
			return err
		}
		shouldSkipAsking, err := o.manifestAlreadyExists()
		if err != nil {
			return err
		}
		if shouldSkipAsking {
			return nil
		}
	}
	if o.wkldType != "" {
		if err := validateSvcType(o.wkldType); err != nil {
			return err
		}
	} else {
		if err := o.askSvcType(); err != nil {
			return err
		}
	}
	if o.name == "" {
		if err := o.askSvcName(); err != nil {
			return err
		}
	}
	if err := o.validateSvc(); err != nil {
		return err
	}
	if err := o.askIngressType(); err != nil {
		return err
	}
	shouldSkipAsking, err := o.manifestAlreadyExists()
	if err != nil {
		return err
	}
	if shouldSkipAsking {
		return nil
	}
	return o.askSvcDetails()
}

// Execute writes the service's manifest file and stores the service in SSM.
func (o *initSvcOpts) Execute() error {
	if !o.allowAppDowngrade {
		appVersionGetter, err := o.newAppVersionGetter(o.appName)
		if err != nil {
			return err
		}
		if err := validateAppVersion(appVersionGetter, o.appName, o.templateVersion); err != nil {
			return err
		}
	}
	// Check for a valid healthcheck and add it to the opts.
	var hc manifest.ContainerHealthCheck
	var err error
	if o.dockerfilePath != "" {
		hc, err = parseHealthCheck(o.dockerfile(o.dockerfilePath))
		if err != nil {
			log.Warningf("Cannot parse the HEALTHCHECK instruction from the Dockerfile: %v\n", err)
		}
	}
	// If the user passes in an image, their docker engine isn't necessarily running, and we can't do anything with the platform because we're not building the Docker image.
	if o.image == "" && !o.manifestExists {
		platform, err := legitimizePlatform(o.dockerEngine, o.wkldType)
		if err != nil {
			return err
		}
		if platform != "" {
			o.platform = &platform
		}
	}
	// Environments that are deployed and have​ only private subnets.
	envs, err := envsWithPrivateSubnetsOnly(o.store, o.initEnvDescriber, o.appName)
	if err != nil {
		return err
	}

	o.manifestPath, err = o.init.Service(&initialize.ServiceProps{
		WorkloadProps: initialize.WorkloadProps{
			App:            o.appName,
			Name:           o.name,
			Type:           o.wkldType,
			DockerfilePath: o.dockerfilePath,
			Image:          o.image,
			Platform: manifest.PlatformArgsOrString{
				PlatformString: o.platform,
			},
			Topics:                  o.topics,
			PrivateOnlyEnvironments: envs,
		},
		Port:        o.port,
		HealthCheck: hc,
		Private:     strings.EqualFold(o.ingressType, ingressTypeEnvironment),
		FileUploads: o.staticAssets,
	})
	if err != nil {
		return err
	}
	return nil
}

// RecommendActions returns follow-up actions the user can take after successfully executing the command.
func (o *initSvcOpts) RecommendActions() error {
	actions := []string{fmt.Sprintf("Update your manifest %s to change the defaults.", color.HighlightResource(o.manifestPath))}

	// If the Dockerfile exposes multiple ports, log a code block suggesting adding additional rules.
	if o.additionalFoundPort != 0 {
		multiplePortsAdditionalPathsAction := fmt.Sprintf(`It looks like your Dockerfile exposes multiple ports. 
You can specify multiple paths where your service will receive traffic by setting http.additional_rules:
%s`, color.HighlightCodeBlock(fmt.Sprintf(`http:
  path: /
  additional_rules:
  - path: /admin
    target_port: %d`, o.additionalFoundPort)))
		actions = append(actions, multiplePortsAdditionalPathsAction)
	}
	actions = append(actions, fmt.Sprintf("Run %s to deploy your service to a %s environment.",
		color.HighlightCode(fmt.Sprintf("copilot svc deploy --name %s --env %s", o.name, defaultEnvironmentName)),
		defaultEnvironmentName))

	logRecommendedActions(actions)
	return nil
}

func (o *initSvcOpts) askSvcDetails() error {
	if o.wkldType == manifestinfo.StaticSiteType {
		return o.askStaticSite()
	}
	err := o.askDockerfile()
	if err != nil {
		return err
	}
	if o.dockerfilePath == "" {
		if err := o.askImage(); err != nil {
			return err
		}
	}
	if err := o.askSvcPort(); err != nil {
		return err
	}
	return o.askSvcPublishers()
}

func (o *initSvcOpts) askSvcType() error {
	if o.wkldType != "" {
		return nil
	}

	msg := fmt.Sprintf(fmtSvcInitSvcTypePrompt, color.Emphasize("service type"))
	t, err := o.prompt.SelectOption(msg, svcInitSvcTypeHelpPrompt, svcTypePromptOpts(), prompt.WithFinalMessage("Service type:"))
	if err != nil {
		return fmt.Errorf("select service type: %w", err)
	}
	o.wkldType = t
	return nil
}

func (o *initSvcOpts) validateSvc() error {
	if err := validateSvcName(o.name, o.wkldType); err != nil {
		return err
	}
	return o.validateDuplicateSvc()
}

func (o *initSvcOpts) validateDuplicateSvc() error {
	_, err := o.store.GetService(o.appName, o.name)
	if err == nil {
		// Skip error if service already exists in workspace
		if !o.wsPendingCreation {
			svcs, err := o.svcLister.ListWorkloads()
			if err != nil {
				return err
			}
			if slices.Contains(svcs, o.name) {
				return nil
			}
		}
		log.Errorf(`It seems like you are trying to init a service that already exists.
To recreate the service, please run:
1. %s. Note: The manifest file will not be deleted and will be used in Step 2.
If you'd prefer a new default manifest, please manually delete the existing one.
2. And then %s
`,
			color.HighlightCode(fmt.Sprintf("copilot svc delete --name %s", o.name)),
			color.HighlightCode(fmt.Sprintf("copilot svc init --name %s", o.name)))
		return fmt.Errorf("service %s already exists", color.HighlightUserInput(o.name))
	}

	var errNoSuchSvc *config.ErrNoSuchService
	if !errors.As(err, &errNoSuchSvc) {
		return fmt.Errorf("validate if service exists: %w", err)
	}
	return nil
}

func (o *initSvcOpts) askStaticSite() error {
	if len(o.staticAssets) != 0 {
		return nil
	}
	var sources []string
	var err error
	if o.wsPendingCreation {
		sources, err = selector.AskCustomPaths(
			o.prompt,
			fmt.Sprintf(fmtStaticSiteInitDirFilePathPrompt, color.HighlightUserInput(o.name)),
			staticSiteInitDirFilePathHelpPrompt,
			validateNonEmptyString, // When the workspace is absent, we skip the validation and leave it to `svc deploy`.
		)
	} else {
		sources, err = o.sourceSel.StaticSources(
			fmt.Sprintf(fmtStaticSiteInitDirFilePrompt, color.HighlightUserInput(o.name)),
			staticSiteInitDirFileHelpPrompt,
			fmt.Sprintf(fmtStaticSiteInitDirFilePathPrompt, color.HighlightUserInput(o.name)),
			staticSiteInitDirFilePathHelpPrompt,
			func(val interface{}) error {
				path, ok := val.(string)
				if !ok {
					return errValueNotAString
				}
				_, err := o.fs.Stat(filepath.Join(o.wsRoot, path))
				if err != nil {
					return fmt.Errorf("source %q must be a valid path relative to the workspace %q: %w", path, o.wsRoot, err)
				}
				return nil
			},
		)
	}
	if err != nil {
		return err
	}
	if o.staticAssets, err = o.convertStringsToAssets(sources); err != nil {
		return fmt.Errorf("convert source paths to asset objects: %w", err)
	}
	return nil
}

func (o *initSvcOpts) askSvcName() error {
	name, err := o.prompt.Get(
		fmt.Sprintf(fmtWkldInitNamePrompt, color.Emphasize("name"), "service"),
		fmt.Sprintf(fmtWkldInitNameHelpPrompt, service, o.appName),
		func(val interface{}) error {
			return validateSvcName(val, o.wkldType)
		},
		prompt.WithFinalMessage("Service name:"))
	if err != nil {
		return fmt.Errorf("get service name: %w", err)
	}
	o.name = name
	return nil
}

func (o *initSvcOpts) askIngressType() error {
	if o.wkldType != manifestinfo.RequestDrivenWebServiceType || o.ingressType != "" {
		return nil
	}

	var opts []prompt.Option
	for _, typ := range rdwsIngressOptions {
		opts = append(opts, prompt.Option{Value: typ})
	}

	t, err := o.prompt.SelectOption(svcInitIngressTypePrompt, svcInitIngressTypeHelpPrompt, opts, prompt.WithFinalMessage("Reachable from:"))
	if err != nil {
		return fmt.Errorf("select ingress type: %w", err)
	}
	o.ingressType = t
	return nil
}

func (o *initSvcOpts) validateIngressType() error {
	if o.wkldType != manifestinfo.RequestDrivenWebServiceType {
		return nil
	}
	if strings.EqualFold(o.ingressType, "internet") || strings.EqualFold(o.ingressType, "environment") {
		return nil
	}
	return fmt.Errorf("invalid ingress type %q: must be one of %s", o.ingressType, english.OxfordWordSeries(rdwsIngressOptions, "or"))
}

func (o *initSvcOpts) askImage() error {
	if o.image != "" {
		return nil
	}

	validator := prompt.RequireNonEmpty
	promptHelp := wkldInitImagePromptHelp
	if o.wkldType == manifestinfo.RequestDrivenWebServiceType {
		promptHelp = wkldInitAppRunnerImagePromptHelp
		validator = validateAppRunnerImage
	}

	image, err := o.prompt.Get(
		wkldInitImagePrompt,
		promptHelp,
		validator,
		prompt.WithFinalMessage("Image:"),
	)
	if err != nil {
		return fmt.Errorf("get image location: %w", err)
	}
	o.image = image
	return nil
}

func (o *initSvcOpts) manifestAlreadyExists() (bool, error) {
	if o.wsPendingCreation {
		return false, nil
	}
	localMft, err := o.mftReader.ReadWorkloadManifest(o.name)
	if err != nil {
		var (
			errNotFound          *workspace.ErrFileNotExists
			errWorkspaceNotFound *workspace.ErrWorkspaceNotFound
		)
		if !errors.As(err, &errNotFound) && !errors.As(err, &errWorkspaceNotFound) {
			return false, fmt.Errorf("read manifest file for service %s: %w", o.name, err)
		}
		return false, nil
	}
	o.manifestExists = true

	svcType, err := localMft.WorkloadType()
	if err != nil {
		return false, fmt.Errorf(`read "type" field for service %s from local manifest: %w`, o.name, err)
	}
	if o.wkldType != "" {
		if o.wkldType != svcType {
			return false, fmt.Errorf("manifest file for service %s exists with a different type %s", o.name, svcType)
		}
	} else {
		o.wkldType = svcType
	}
	log.Infof("Manifest file for service %s already exists. Skipping configuration.\n", o.name)
	return true, nil
}

// isDfSelected indicates if any Dockerfile is in use.
func (o *initSvcOpts) askDockerfile() error {
	if o.dockerfilePath != "" || o.image != "" {
		return nil
	}
	if err := o.dockerEngine.CheckDockerEngineRunning(); err != nil {
		var errDaemon *dockerengine.ErrDockerDaemonNotResponsive
		switch {
		case errors.Is(err, dockerengine.ErrDockerCommandNotFound):
			log.Info("Docker command is not found; Copilot won't build from a Dockerfile.\n")
			return nil
		case errors.As(err, &errDaemon):
			log.Info("Docker daemon is not responsive; Copilot won't build from a Dockerfile.\n")
			return nil
		default:
			return fmt.Errorf("check if docker engine is running: %w", err)
		}
	}
	df, err := o.sel.Dockerfile(
		fmt.Sprintf(fmtWkldInitDockerfilePrompt, color.HighlightUserInput(o.name)),
		fmt.Sprintf(fmtWkldInitDockerfilePathPrompt, color.HighlightUserInput(o.name)),
		wkldInitDockerfileHelpPrompt,
		wkldInitDockerfilePathHelpPrompt,
		func(v interface{}) error {
			return validatePath(afero.NewOsFs(), v)
		},
	)
	if err != nil {
		return fmt.Errorf("select Dockerfile: %w", err)
	}
	if df == selector.DockerfilePromptUseImage {
		return nil
	}
	o.dockerfilePath = df
	return nil
}

func (o *initSvcOpts) askSvcPort() (err error) {
	// If the port flag was set, use that and don't ask.
	if o.port != 0 {
		return nil
	}

	var ports []dockerfile.Port
	if o.dockerfilePath != "" && o.image == "" {
		// Check for exposed ports.
		ports, err = o.dockerfile(o.dockerfilePath).GetExposedPorts()
		// Ignore any errors in dockerfile parsing--we'll use the default port instead.
		if err != nil {
			log.Debugln(err.Error())
		}
	}

	defaultPort := defaultSvcPortString
	if o.dockerfilePath != "" {
		switch len(ports) {
		case 0:
			// There were no ports detected, keep the default port prompt.
		case 1:
			o.port = ports[0].Port
			return nil
		default:
			defaultPort = strconv.Itoa(int(ports[0].Port))
		}
	}

	if o.wkldType == manifestinfo.BackendServiceType || o.wkldType == manifestinfo.WorkerServiceType {
		return nil
	}
	port, err := o.prompt.Get(
		fmt.Sprintf(svcInitSvcPortPrompt, color.Emphasize("port")),
		svcInitSvcPortHelpPrompt,
		validateSvcPort,
		prompt.WithDefaultInput(defaultPort),
		prompt.WithFinalMessage("Port:"),
	)
	if err != nil {
		return fmt.Errorf("get port: %w", err)
	}
	portUint, err := strconv.ParseUint(port, 10, 16)
	if err != nil {
		return fmt.Errorf("parse port string: %w", err)
	}
	o.port = uint16(portUint)

	// Log a recommended action to update additional rules if multiple ports exposed.
	for _, foundPort := range ports {
		// Use the first exposed port in the Dockerfile that wasn't selected for traffic.
		if foundPort.Port != o.port {
			o.additionalFoundPort = foundPort.Port
			break
		}
	}

	return nil
}

func legitimizePlatform(engine dockerEngine, wkldType string) (manifest.PlatformString, error) {
	if err := engine.CheckDockerEngineRunning(); err != nil {
		// This is a best-effort attempt to detect the platform for users.
		// If docker is not available, we skip this information.
		var errDaemon *dockerengine.ErrDockerDaemonNotResponsive
		switch {
		case errors.Is(err, dockerengine.ErrDockerCommandNotFound):
			log.Info("Docker command is not found; Copilot won't detect and populate the \"platform\" field in the manifest.\n")
			return "", nil
		case errors.As(err, &errDaemon):
			log.Info("Docker daemon is not responsive; Copilot won't detect and populate the \"platform\" field in the manifest.\n")
			return "", nil
		default:
			return "", fmt.Errorf("check if docker engine is running: %w", err)
		}
	}
	detectedOs, detectedArch, err := engine.GetPlatform()
	if err != nil {
		return "", fmt.Errorf("get docker engine platform: %w", err)
	}
	redirectedPlatform, err := manifest.RedirectPlatform(detectedOs, detectedArch, wkldType)
	if err != nil {
		return "", fmt.Errorf("redirect docker engine platform: %w", err)
	}
	if redirectedPlatform == "" {
		return "", nil
	}
	// Return an error if a platform cannot be redirected.
	if wkldType == manifestinfo.RequestDrivenWebServiceType && detectedOs == manifest.OSWindows {
		return "", manifest.ErrAppRunnerInvalidPlatformWindows
	}
	// Messages are logged only if the platform was redirected.
	msg := fmt.Sprintf("Architecture type %s has been detected. We will set platform '%s' instead. If you'd rather build and run as architecture type %s, please change the 'platform' field in your workload manifest to '%s'.\n", detectedArch, redirectedPlatform, manifest.ArchARM64, dockerengine.PlatformString(detectedOs, manifest.ArchARM64))
	if manifest.IsArmArch(detectedArch) && wkldType == manifestinfo.RequestDrivenWebServiceType {
		msg = fmt.Sprintf("Architecture type %s has been detected. At this time, %s architectures are not supported for App Runner workloads. We will set platform '%s' instead.\n", detectedArch, detectedArch, redirectedPlatform)
	}
	log.Warningf(msg)
	return manifest.PlatformString(redirectedPlatform), nil
}

func (o *initSvcOpts) askSvcPublishers() (err error) {
	if o.wkldType != manifestinfo.WorkerServiceType {
		return nil
	}
	// publishers already specified by flags
	if len(o.subscriptions) > 0 {
		var topicSubscriptions []manifest.TopicSubscription
		for _, sub := range o.subscriptions {
			sub, err := parseSerializedSubscription(sub)
			if err != nil {
				return err
			}
			topicSubscriptions = append(topicSubscriptions, sub)
		}
		o.topics = topicSubscriptions
		return nil
	}

	// if --no-subscriptions flag specified, no need to ask for publishers
	if o.noSubscribe {
		return nil
	}

	topics, err := o.topicSel.Topics(svcInitPublisherPrompt, svcInitPublisherHelpPrompt, o.appName)
	if err != nil {
		return fmt.Errorf("select publisher: %w", err)
	}

	subscriptions := make([]manifest.TopicSubscription, 0, len(topics))
	for _, t := range topics {
		subscriptions = append(subscriptions, manifest.TopicSubscription{
			Name:    aws.String(t.Name()),
			Service: aws.String(t.Workload()),
		})
	}
	o.topics = subscriptions

	return nil
}

func validateWorkspaceApp(wsApp, inputApp string, store store) error {
	if wsApp == "" {
		// NOTE: This command is required to be executed under a workspace. We don't prompt for it.
		return errNoAppInWorkspace
	}
	// This command must be run within the app's workspace.
	if inputApp != "" && inputApp != wsApp {
		return fmt.Errorf("cannot specify app %s because the workspace is already registered with app %s", inputApp, wsApp)
	}
	if _, err := store.GetApplication(wsApp); err != nil {
		return fmt.Errorf("get application %s configuration: %w", wsApp, err)
	}
	return nil
}

func (o initSvcOpts) convertStringsToAssets(sources []string) ([]manifest.FileUpload, error) {
	assets := make([]manifest.FileUpload, len(sources))
	var root string
	if !o.wsPendingCreation {
		root = o.wsRoot
	}
	for i, source := range sources {
		info, err := o.fs.Stat(filepath.Join(root, source))
		var recursive bool
		if err == nil && info.IsDir() {
			// Swallow the error at this point, because the path would have been validated during Validate or Ask, or will
			// be validated during deploy.
			recursive = true
		}
		assets[i] = manifest.FileUpload{
			Source:    source,
			Recursive: recursive,
		}
	}
	return assets, nil
}

// parseSerializedSubscription parses the service and topic name out of keys specified in the form "service:topicName"
func parseSerializedSubscription(input string) (manifest.TopicSubscription, error) {
	attrs := regexpMatchSubscription.FindStringSubmatch(input)
	if len(attrs) == 0 {
		return manifest.TopicSubscription{}, fmt.Errorf("parse subscription from key: %s", input)
	}
	return manifest.TopicSubscription{
		Name:    aws.String(attrs[2]),
		Service: aws.String(attrs[1]),
	}, nil
}

func parseHealthCheck(df dockerfileParser) (manifest.ContainerHealthCheck, error) {
	hc, err := df.GetHealthCheck()
	if err != nil {
		return manifest.ContainerHealthCheck{}, fmt.Errorf("get healthcheck: %w", err)
	}
	if hc == nil {
		return manifest.ContainerHealthCheck{}, nil
	}
	return manifest.ContainerHealthCheck{
		Interval:    &hc.Interval,
		Timeout:     &hc.Timeout,
		StartPeriod: &hc.StartPeriod,
		Retries:     &hc.Retries,
		Command:     hc.Cmd,
	}, nil
}

func svcTypePromptOpts() []prompt.Option {
	var options []prompt.Option
	for _, svcType := range manifestinfo.ServiceTypes() {
		options = append(options, prompt.Option{
			Value: svcType,
			Hint:  serviceTypeHints[svcType],
		})
	}
	return options
}

// buildSvcInitCmd build the command for creating a new service.
func buildSvcInitCmd() *cobra.Command {
	vars := initSvcVars{}
	cmd := &cobra.Command{
		Use:   "init",
		Short: "Creates a new service in an application.",
		Long: `Creates a new service in an application.
This command is also run as part of "copilot init".`,
		Example: `
  Create a "frontend" load balanced web service.
  /code $ copilot svc init --name frontend --svc-type "Load Balanced Web Service" --dockerfile ./frontend/Dockerfile

  Create a "subscribers" backend service.
  /code $ copilot svc init --name subscribers --svc-type "Backend Service"`,
		RunE: runCmdE(func(cmd *cobra.Command, args []string) error {
			opts, err := newInitSvcOpts(vars)
			if err != nil {
				return err
			}
			if err := opts.Validate(); err != nil { // validate flags
				return err
			}
			log.Warningln("It's best to run this command in the root of your workspace.")
			if err := opts.Ask(); err != nil {
				return err
			}
			if err := opts.Execute(); err != nil {
				return err
			}
			if err := opts.RecommendActions(); err != nil {
				return err
			}
			return nil
		}),
	}
	cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, "", appFlagDescription)
	cmd.Flags().StringVarP(&vars.name, nameFlag, nameFlagShort, "", svcFlagDescription)
	cmd.Flags().StringVarP(&vars.wkldType, svcTypeFlag, typeFlagShort, "", svcTypeFlagDescription)
	cmd.Flags().StringVarP(&vars.dockerfilePath, dockerFileFlag, dockerFileFlagShort, "", dockerFileFlagDescription)
	cmd.Flags().StringVarP(&vars.image, imageFlag, imageFlagShort, "", imageFlagDescription)
	cmd.Flags().Uint16Var(&vars.port, svcPortFlag, 0, svcPortFlagDescription)
	cmd.Flags().StringArrayVar(&vars.subscriptions, subscribeTopicsFlag, []string{}, subscribeTopicsFlagDescription)
	cmd.Flags().BoolVar(&vars.noSubscribe, noSubscriptionFlag, false, noSubscriptionFlagDescription)
	cmd.Flags().StringVar(&vars.ingressType, ingressTypeFlag, "", ingressTypeFlagDescription)
	cmd.Flags().StringArrayVar(&vars.sourcePaths, sourcesFlag, nil, sourcesFlagDescription)
	cmd.Flags().BoolVar(&vars.allowAppDowngrade, allowDowngradeFlag, false, allowDowngradeFlagDescription)

	return cmd
}
