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

package cli

import (
	"context"
	"encoding"
	"io"

	sdkcloudformation "github.com/aws/aws-sdk-go/service/cloudformation"

	"github.com/aws/aws-sdk-go/aws/session"
	awscloudformation "github.com/aws/copilot-cli/internal/pkg/aws/cloudformation"
	"github.com/aws/copilot-cli/internal/pkg/aws/codepipeline"
	"github.com/aws/copilot-cli/internal/pkg/aws/ec2"
	awsecs "github.com/aws/copilot-cli/internal/pkg/aws/ecs"
	"github.com/aws/copilot-cli/internal/pkg/aws/secretsmanager"
	"github.com/aws/copilot-cli/internal/pkg/aws/ssm"
	clideploy "github.com/aws/copilot-cli/internal/pkg/cli/deploy"
	"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/deploy/cloudformation/stack"
	"github.com/aws/copilot-cli/internal/pkg/describe"
	stackdescr "github.com/aws/copilot-cli/internal/pkg/describe/stack"
	"github.com/aws/copilot-cli/internal/pkg/docker/dockerengine"
	"github.com/aws/copilot-cli/internal/pkg/docker/dockerfile"
	"github.com/aws/copilot-cli/internal/pkg/ecs"
	"github.com/aws/copilot-cli/internal/pkg/exec"
	"github.com/aws/copilot-cli/internal/pkg/initialize"
	"github.com/aws/copilot-cli/internal/pkg/logging"
	"github.com/aws/copilot-cli/internal/pkg/manifest"
	"github.com/aws/copilot-cli/internal/pkg/task"
	"github.com/aws/copilot-cli/internal/pkg/template"
	"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"
)

type cmd interface {
	// Validate returns an error if a flag's value is invalid.
	Validate() error

	// Ask prompts for flag values that are required but not passed in.
	Ask() error

	// Execute runs the command after collecting all required options.
	Execute() error
}

// actionCommand is the interface that every command that creates a resource implements.
type actionCommand interface {
	cmd
	// RecommendActions logs a list of follow-up suggestions users can run once the command executes successfully.
	RecommendActions() error
}

// SSM store interfaces.

type serviceStore interface {
	CreateService(svc *config.Workload) error
	GetService(appName, svcName string) (*config.Workload, error)
	ListServices(appName string) ([]*config.Workload, error)
	DeleteService(appName, svcName string) error
}

type jobStore interface {
	CreateJob(job *config.Workload) error
	GetJob(appName, jobName string) (*config.Workload, error)
	ListJobs(appName string) ([]*config.Workload, error)
	DeleteJob(appName, jobName string) error
}

type wlStore interface {
	ListWorkloads(appName string) ([]*config.Workload, error)
	GetWorkload(appName, name string) (*config.Workload, error)
}

type workloadListWriter interface {
	Write(appName string) error
}

type applicationStore interface {
	applicationCreator
	applicationUpdater
	applicationGetter
	applicationLister
	applicationDeleter
}

type applicationCreator interface {
	CreateApplication(app *config.Application) error
}

type applicationUpdater interface {
	UpdateApplication(app *config.Application) error
}

type applicationGetter interface {
	GetApplication(appName string) (*config.Application, error)
}

type applicationLister interface {
	ListApplications() ([]*config.Application, error)
}

type applicationDeleter interface {
	DeleteApplication(name string) error
}

type environmentStore interface {
	environmentCreator
	environmentGetter
	environmentLister
	environmentDeleter
	applicationGetter
}

type environmentCreator interface {
	CreateEnvironment(env *config.Environment) error
}

type environmentGetter interface {
	GetEnvironment(appName string, environmentName string) (*config.Environment, error)
}

type environmentLister interface {
	ListEnvironments(appName string) ([]*config.Environment, error)
}

type wsEnvironmentsLister interface {
	ListEnvironments() ([]string, error)
}

type environmentDeleter interface {
	DeleteEnvironment(appName, environmentName string) error
}

type store interface {
	applicationStore
	environmentStore
	serviceStore
	jobStore
	wlStore
}

type deployedEnvironmentLister interface {
	ListEnvironmentsDeployedTo(appName, svcName string) ([]string, error)
	ListDeployedServices(appName, envName string) ([]string, error)
	ListDeployedJobs(appName string, envName string) ([]string, error)
	IsServiceDeployed(appName, envName string, svcName string) (bool, error)
	ListSNSTopics(appName string, envName string) ([]deploy.Topic, error)
}

// Secretsmanager interface.

type secretsManager interface {
	secretCreator
	secretDeleter
}

type secretCreator interface {
	CreateSecret(secretName, secretString string) (string, error)
}

type secretDeleter interface {
	DescribeSecret(secretName string) (*secretsmanager.DescribeSecretOutput, error)
	DeleteSecret(secretName string) error
}

type imageBuilderPusher interface {
	BuildAndPush(ctx context.Context, args *dockerengine.BuildArguments, w io.Writer) (string, error)
	Build(ctx context.Context, args *dockerengine.BuildArguments, w io.Writer) (string, error)
}

type repositoryLogin interface {
	Login() (string, error)
}

type repositoryService interface {
	repositoryLogin
	imageBuilderPusher
}

type ecsClient interface {
	TaskDefinition(app, env, svc string) (*awsecs.TaskDefinition, error)
	ServiceConnectServices(app, env, svc string) ([]*awsecs.Service, error)
	DescribeService(app, env, svc string) (*ecs.ServiceDesc, error)
}

type logEventsWriter interface {
	WriteLogEvents(opts logging.WriteLogEventsOpts) error
}

type execRunner interface {
	Run(name string, args []string, options ...exec.CmdOption) error
}

type eventsWriter interface {
	WriteEventsUntilStopped() error
}

type defaultSessionProvider interface {
	Default() (*session.Session, error)
}

type regionalSessionProvider interface {
	DefaultWithRegion(region string) (*session.Session, error)
}

type sessionFromRoleProvider interface {
	FromRole(roleARN string, region string) (*session.Session, error)
}

type sessionFromStaticProvider interface {
	FromStaticCreds(accessKeyID, secretAccessKey, sessionToken string) (*session.Session, error)
}

type sessionFromProfileProvider interface {
	FromProfile(name string) (*session.Session, error)
}

type sessionProvider interface {
	defaultSessionProvider
	regionalSessionProvider
	sessionFromRoleProvider
	sessionFromProfileProvider
	sessionFromStaticProvider
}

type describer interface {
	Describe() (describe.HumanJSONStringer, error)
}

type workloadDescriber interface {
	describer
	Manifest(string) ([]byte, error)
}

type wsFileDeleter interface {
	DeleteWorkspaceFile() error
}

type manifestReader interface {
	ReadWorkloadManifest(name string) (workspace.WorkloadManifest, error)
}

type environmentManifestWriter interface {
	WriteEnvironmentManifest(encoding.BinaryMarshaler, string) (string, error)
}

type workspacePathGetter interface {
	Path() string
}

type wsPipelineManifestReader interface {
	ReadPipelineManifest(path string) (*manifest.Pipeline, error)
}

type relPath interface {
	// Rel returns the path relative from the object's root path to the target path.
	//
	// Unlike filepath.Rel, the input path is allowed to be either relative to the
	// current working directory or absolute.
	Rel(path string) (string, error)
}

type wsPipelineIniter interface {
	relPath
	WritePipelineBuildspec(marshaler encoding.BinaryMarshaler, name string) (string, error)
	WritePipelineManifest(marshaler encoding.BinaryMarshaler, name string) (string, error)
	ListPipelines() ([]workspace.PipelineManifest, error)
}

type serviceLister interface {
	ListServices() ([]string, error)
}

type wsSvcReader interface {
	serviceLister
	manifestReader
}

type jobLister interface {
	ListJobs() ([]string, error)
}

type wsJobReader interface {
	manifestReader
	jobLister
}

type wlLister interface {
	ListWorkloads() ([]string, error)
}

type wsWorkloadReader interface {
	manifestReader
	ReadFile(path string) ([]byte, error)
	WorkloadExists(name string) (bool, error)
	WorkloadAddonFilePath(wkldName, fName string) string
	WorkloadAddonFileAbsPath(wkldName, fName string) string
}

type wsWorkloadReadWriter interface {
	wsWorkloadReader
	wsWriter
}

type wsReadWriter interface {
	wsWorkloadReadWriter
	wsEnvironmentReader
}

type wsJobDirReader interface {
	wsJobReader
	workspacePathGetter
}

type wsWlDirReader interface {
	wsJobReader
	wsSvcReader
	workspacePathGetter
	wlLister
	wsEnvironmentsLister
	WorkloadOverridesPath(string) string
	Summary() (*workspace.Summary, error)
}

type wsEnvironmentReader interface {
	wsEnvironmentsLister
	HasEnvironments() (bool, error)
	EnvOverridesPath() string
	ReadEnvironmentManifest(mftDirName string) (workspace.EnvironmentManifest, error)
	EnvAddonFilePath(fName string) string
	EnvAddonFileAbsPath(fName string) string
}

type wsPipelineReader interface {
	wsPipelineGetter
	wsPipelineManifestReader
	relPath
	PipelineOverridesPath(string) string
}

type wsPipelineGetter interface {
	wsPipelineManifestReader
	wlLister
	ListPipelines() ([]workspace.PipelineManifest, error)
}

type wsAppManager interface {
	Summary() (*workspace.Summary, error)
}

type wsAppManagerDeleter interface {
	wsAppManager
	wsFileDeleter
}

type wsWriter interface {
	Write(content encoding.BinaryMarshaler, path string) (string, error)
}

type uploader interface {
	Upload(bucket, key string, data io.Reader) (string, error)
}

type bucketEmptier interface {
	EmptyBucket(bucket string) error
}

type stackDescriber interface {
	Resources() ([]*stackdescr.Resource, error)
}

// Interfaces for deploying resources through CloudFormation. Facilitates mocking.
type environmentDeployer interface {
	CreateAndRenderEnvironment(conf cloudformation.StackConfiguration, bucketARN string) error
	DeleteEnvironment(appName, envName, cfnExecRoleARN string) error
	GetEnvironment(appName, envName string) (*config.Environment, error)
	Template(stackName string) (string, error)
	UpdateEnvironmentTemplate(appName, envName, templateBody, cfnExecRoleARN string) error
}

type wlDeleter interface {
	DeleteWorkload(in deploy.DeleteWorkloadInput) error
}

type svcRemoverFromApp interface {
	RemoveServiceFromApp(app *config.Application, svcName string) error
}

type jobRemoverFromApp interface {
	RemoveJobFromApp(app *config.Application, jobName string) error
}

type imageRemover interface {
	ClearRepository(repoName string) error // implemented by ECR Service
}

type pipelineDeployer interface {
	CreatePipeline(bucketName string, stackConfig cloudformation.StackConfiguration) error
	UpdatePipeline(bucketName string, stackConfig cloudformation.StackConfiguration) error
	PipelineExists(stackConfig cloudformation.StackConfiguration) (bool, error)
	DeletePipeline(pipeline deploy.Pipeline) error
	AddPipelineResourcesToApp(app *config.Application, region string) error
	Template(stackName string) (string, error)
	appResourcesGetter
	// TODO: Add StreamPipelineCreation method
}

type appDeployer interface {
	DeployApp(in *deploy.CreateAppInput) error
	AddServiceToApp(app *config.Application, svcName string, opts ...cloudformation.AddWorkloadToAppOpt) error
	AddJobToApp(app *config.Application, jobName string, opts ...cloudformation.AddWorkloadToAppOpt) error
	AddEnvToApp(opts *cloudformation.AddEnvToAppOpts) error
	DelegateDNSPermissions(app *config.Application, accountID string) error
	DeleteApp(name string) error
}

type appResourcesGetter interface {
	GetAppResourcesByRegion(app *config.Application, region string) (*stack.AppRegionalResources, error)
	GetRegionalAppResources(app *config.Application) ([]*stack.AppRegionalResources, error)
}

type envDeleterFromApp interface {
	appResourcesGetter
	RemoveEnvFromApp(opts *cloudformation.RemoveEnvFromAppOpts) error
}

type taskDeployer interface {
	DeployTask(input *deploy.CreateTaskResourcesInput, opts ...awscloudformation.StackOption) error
	GetTaskStack(taskName string) (*deploy.TaskStackInfo, error)
}

type taskStackManager interface {
	DeleteTask(task deploy.TaskStackInfo) error
	GetTaskStack(taskName string) (*deploy.TaskStackInfo, error)
}

type taskRunner interface {
	Run() ([]*task.Task, error)
	CheckNonZeroExitCode([]*task.Task) error
}

type defaultClusterGetter interface {
	HasDefaultCluster() (bool, error)
}

type deployer interface {
	environmentDeployer
	appDeployer
	pipelineDeployer
	ListTaskStacks(appName, envName string) ([]deploy.TaskStackInfo, error)
}

type domainHostedZoneGetter interface {
	PublicDomainHostedZoneID(domainName string) (string, error)
	ValidateDomainOwnership(domainName string) error
}

type dockerfileParser interface {
	GetExposedPorts() ([]dockerfile.Port, error)
	GetHealthCheck() (*dockerfile.HealthCheck, error)
}

type statusDescriber interface {
	Describe() (describe.HumanJSONStringer, error)
}

type envDescriber interface {
	Describe() (*describe.EnvDescription, error)
	Manifest() ([]byte, error)
	ValidateCFServiceDomainAliases() error
}

type versionCompatibilityChecker interface {
	versionGetter
	AvailableFeatures() ([]string, error)
}

type versionGetter interface {
	Version() (string, error)
}

type appUpgrader interface {
	UpgradeApplication(in *deploy.CreateAppInput) error
}

type pipelineGetter interface {
	GetPipeline(pipelineName string) (*codepipeline.Pipeline, error)
}

type deployedPipelineLister interface {
	ListDeployedPipelines(appName string) ([]deploy.Pipeline, error)
}

type executor interface {
	Execute() error
}

type executeAsker interface {
	Ask() error
	executor
}

type appSelector interface {
	Application(prompt, help string, additionalOpts ...string) (string, error)
}

type appEnvSelector interface {
	appSelector
	Environment(prompt, help, app string, additionalOpts ...prompt.Option) (string, error)
}

type cfnSelector interface {
	Resources(msg, finalMsg, help, body string) ([]template.CFNResource, error)
}

type configSelector interface {
	appEnvSelector
	Service(prompt, help, app string) (string, error)
	Job(prompt, help, app string) (string, error)
	Workload(prompt, help, app string) (string, error)
}

type deploySelector interface {
	appSelector
	DeployedService(prompt, help string, app string, opts ...selector.GetDeployedWorkloadOpts) (*selector.DeployedService, error)
	DeployedJob(prompt, help string, app string, opts ...selector.GetDeployedWorkloadOpts) (*selector.DeployedJob, error)
	DeployedWorkload(prompt, help string, app string, opts ...selector.GetDeployedWorkloadOpts) (*selector.DeployedWorkload, error)
}

type pipelineEnvSelector interface {
	Environments(prompt, help, app string, finalMsgFunc func(int) prompt.PromptConfig) ([]string, error)
}

type wsPipelineSelector interface {
	WsPipeline(prompt, help string) (*workspace.PipelineManifest, error)
}

type wsEnvironmentSelector interface {
	LocalEnvironment(msg, help string) (wl string, err error)
}

type codePipelineSelector interface {
	appSelector
	DeployedPipeline(prompt, help, app string) (deploy.Pipeline, error)
}

type wsSelector interface {
	appEnvSelector
	Service(prompt, help string) (string, error)
	Job(prompt, help string) (string, error)
	Workload(msg, help string) (string, error)
	Workloads(msg, help string) ([]string, error)
}

type staticSourceSelector interface {
	StaticSources(selPrompt, selHelp, anotherPathPrompt, anotherPathHelp string, pathValidator prompt.ValidatorFunc) ([]string, error)
}

type scheduleSelector interface {
	Schedule(scheduleTypePrompt, scheduleTypeHelp string, scheduleValidator, rateValidator prompt.ValidatorFunc) (string, error)
}

type cfTaskSelector interface {
	Task(prompt, help string, opts ...selector.GetDeployedTaskOpts) (string, error)
}

type dockerfileSelector interface {
	Dockerfile(selPrompt, notFoundPrompt, selHelp, notFoundHelp string, pv prompt.ValidatorFunc) (string, error)
}

type topicSelector interface {
	Topics(prompt, help, app string) ([]deploy.Topic, error)
}

type ec2Selector interface {
	VPC(prompt, help string) (string, error)
	Subnets(input selector.SubnetsInput) ([]string, error)
}

type credsSelector interface {
	Creds(prompt, help string) (*session.Session, error)
}

type ec2Client interface {
	HasDNSSupport(vpcID string) (bool, error)
	ListAZs() ([]ec2.AZ, error)
}

type serviceResumer interface {
	ResumeService(string) error
}

type jobInitializer interface {
	Job(props *initialize.JobProps) (string, error)
}

type svcInitializer interface {
	Service(props *initialize.ServiceProps) (string, error)
}

type wkldInitializerWithoutManifest interface {
	AddWorkloadToApp(appName, name, workloadType string) error
}

type roleDeleter interface {
	DeleteRole(string) error
}

type policyLister interface {
	ListPolicyNames() ([]string, error)
}

type serviceDescriber interface {
	DescribeService(app, env, svc string) (*ecs.ServiceDesc, error)
}

type apprunnerServiceDescriber interface {
	ServiceARN(env string) (string, error)
}

type ecsCommandExecutor interface {
	ExecuteCommand(in awsecs.ExecuteCommandInput) error
}

type ssmPluginManager interface {
	ValidateBinary() error
	InstallLatestBinary() error
}

type taskStopper interface {
	StopOneOffTasks(app, env, family string) error
	StopDefaultClusterTasks(familyName string) error
	StopWorkloadTasks(app, env, workload string) error
}

type serviceLinkedRoleCreator interface {
	CreateECSServiceLinkedRole() error
}

type roleTagsLister interface {
	ListRoleTags(string) (map[string]string, error)
}

type roleManager interface {
	roleTagsLister
	roleDeleter
	serviceLinkedRoleCreator
}

type stackExistChecker interface {
	Exists(string) (bool, error)
}

type runningTaskSelector interface {
	RunningTask(prompt, help string, opts ...selector.TaskOpts) (*awsecs.Task, error)
}

type dockerEngine interface {
	CheckDockerEngineRunning() error
	GetPlatform() (string, string, error)
}

type codestar interface {
	GetConnectionARN(string) (string, error)
}

type publicIPGetter interface {
	PublicIP(ENI string) (string, error)
}

type cliStringer interface {
	CLIString() (string, error)
}

type secretPutter interface {
	PutSecret(in ssm.PutSecretInput) (*ssm.PutSecretOutput, error)
}

type servicePauser interface {
	PauseService(svcARN string) error
}

type interpolator interface {
	Interpolate(s string) (string, error)
}

type workloadDeployer interface {
	UploadArtifacts() (*clideploy.UploadArtifactsOutput, error)
	GenerateCloudFormationTemplate(in *clideploy.GenerateCloudFormationTemplateInput) (
		*clideploy.GenerateCloudFormationTemplateOutput, error)
	DeployWorkload(in *clideploy.DeployWorkloadInput) (clideploy.ActionRecommender, error)
	IsServiceAvailableInRegion(region string) (bool, error)
	templateDiffer
}

type templateDiffer interface {
	DeployDiff(inTmpl string) (string, error)
}

type dockerEngineRunner interface {
	CheckDockerEngineRunning() error
	Run(context.Context, *dockerengine.RunOptions) error
	IsContainerRunning(context.Context, string) (bool, error)
	Stop(context.Context, string) error
	Build(context.Context, *dockerengine.BuildArguments, io.Writer) error
	Exec(ctx context.Context, container string, out io.Writer, cmd string, args ...string) error
	ContainerExitCode(ctx context.Context, containerName string) (int, error)
	IsContainerHealthy(ctx context.Context, containerName string) (bool, error)
	Rm(context.Context, string) error
}

type workloadStackGenerator interface {
	UploadArtifacts() (*clideploy.UploadArtifactsOutput, error)
	GenerateCloudFormationTemplate(in *clideploy.GenerateCloudFormationTemplateInput) (
		*clideploy.GenerateCloudFormationTemplateOutput, error)
	AddonsTemplate() (string, error)
	templateDiffer
}

type runner interface {
	Run() error
}

type envDeployer interface {
	DeployEnvironment(in *clideploy.DeployEnvironmentInput) error
	Validate(*manifest.Environment) error
	UploadArtifacts() (*clideploy.UploadEnvArtifactsOutput, error)
	GenerateCloudFormationTemplate(in *clideploy.DeployEnvironmentInput) (
		*clideploy.GenerateCloudFormationTemplateOutput, error)
	templateDiffer
}

type envPackager interface {
	GenerateCloudFormationTemplate(in *clideploy.DeployEnvironmentInput) (*clideploy.GenerateCloudFormationTemplateOutput, error)
	Validate(*manifest.Environment) error
	UploadArtifacts() (*clideploy.UploadEnvArtifactsOutput, error)
	AddonsTemplate() (string, error)
	templateDiffer
}

type stackConfiguration interface {
	StackName() string
	Template() (string, error)
	Parameters() ([]*sdkcloudformation.Parameter, error)
	Tags() []*sdkcloudformation.Tag
	SerializedParameters() (string, error)
}

type secretGetter interface {
	GetSecretValue(context.Context, string) (string, error)
}

type dockerWorkload interface {
	Dockerfile() string
}
