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

package manifest

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/copilot-cli/internal/pkg/manifest/manifestinfo"
	"github.com/aws/copilot-cli/internal/pkg/template"
	"github.com/imdario/mergo"
)

const (
	requestDrivenWebSvcManifestPath string = "workloads/services/rd-web/manifest.yml"
)

// RequestDrivenWebService holds the configuration to create a Request-Driven Web Service.
type RequestDrivenWebService struct {
	Workload                      `yaml:",inline"`
	RequestDrivenWebServiceConfig `yaml:",inline"`
	Environments                  map[string]*RequestDrivenWebServiceConfig `yaml:",flow"` // Fields to override per environment.

	parser template.Parser
}

func (s *RequestDrivenWebService) subnets() *SubnetListOrArgs {
	return &s.Network.VPC.Placement.Subnets
}

// RequestDrivenWebServiceConfig holds the configuration that can be overridden per environments.
type RequestDrivenWebServiceConfig struct {
	RequestDrivenWebServiceHttpConfig `yaml:"http,flow"`
	InstanceConfig                    AppRunnerInstanceConfig              `yaml:",inline"`
	ImageConfig                       ImageWithPort                        `yaml:"image"`
	Variables                         map[string]Variable                  `yaml:"variables"`
	Secrets                           map[string]Secret                    `yaml:"secrets"`
	StartCommand                      *string                              `yaml:"command"`
	Tags                              map[string]string                    `yaml:"tags"`
	PublishConfig                     PublishConfig                        `yaml:"publish"`
	Network                           RequestDrivenWebServiceNetworkConfig `yaml:"network"`
	Observability                     Observability                        `yaml:"observability"`
	Count                             *string                              `yaml:"count"`
}

// Observability holds configuration for observability to the service.
type Observability struct {
	Tracing *string `yaml:"tracing"`
}

func (o *Observability) isEmpty() bool {
	return o.Tracing == nil
}

// ImageWithPort represents a container image with an exposed port.
type ImageWithPort struct {
	Image Image   `yaml:",inline"`
	Port  *uint16 `yaml:"port"`
}

// RequestDrivenWebServiceNetworkConfig represents options for network connection to AWS resources for a Request-Driven Web Service.
type RequestDrivenWebServiceNetworkConfig struct {
	VPC rdwsVpcConfig `yaml:"vpc"`
}

// IsEmpty returns empty if the struct has all zero members.
func (c *RequestDrivenWebServiceNetworkConfig) IsEmpty() bool {
	return c.VPC.isEmpty()
}

func (c *RequestDrivenWebServiceNetworkConfig) requiredEnvFeatures() []string {
	if aws.StringValue((*string)(c.VPC.Placement.PlacementString)) == string(PrivateSubnetPlacement) {
		return []string{template.NATFeatureName}
	}
	return nil
}

type rdwsVpcConfig struct {
	Placement PlacementArgOrString `yaml:"placement"`
}

func (c *rdwsVpcConfig) isEmpty() bool {
	return c.Placement.IsEmpty()
}

// RequestDrivenWebServiceHttpConfig represents options for configuring http.
type RequestDrivenWebServiceHttpConfig struct {
	HealthCheckConfiguration HealthCheckArgsOrString   `yaml:"healthcheck"`
	Alias                    *string                   `yaml:"alias"`
	Private                  Union[*bool, VPCEndpoint] `yaml:"private"`
}

// VPCEndpoint is used to configure a pre-existing VPC endpoint.
type VPCEndpoint struct {
	Endpoint *string `yaml:"endpoint"`
}

// AppRunnerInstanceConfig contains the instance configuration properties for an App Runner service.
type AppRunnerInstanceConfig struct {
	CPU      *int                 `yaml:"cpu"`
	Memory   *int                 `yaml:"memory"`
	Platform PlatformArgsOrString `yaml:"platform,omitempty"`
}

// RequestDrivenWebServiceProps contains properties for creating a new request-driven web service manifest.
type RequestDrivenWebServiceProps struct {
	*WorkloadProps
	Port     uint16
	Platform PlatformArgsOrString
	Private  bool
}

// NewRequestDrivenWebService creates a new Request-Driven Web Service manifest with default values.
func NewRequestDrivenWebService(props *RequestDrivenWebServiceProps) *RequestDrivenWebService {
	svc := newDefaultRequestDrivenWebService()
	svc.Name = aws.String(props.Name)
	svc.RequestDrivenWebServiceConfig.ImageConfig.Image.Location = stringP(props.Image)
	svc.RequestDrivenWebServiceConfig.ImageConfig.Image.Build.BuildArgs.Dockerfile = stringP(props.Dockerfile)
	svc.RequestDrivenWebServiceConfig.ImageConfig.Port = aws.Uint16(props.Port)
	svc.RequestDrivenWebServiceConfig.InstanceConfig.Platform = props.Platform
	if props.Private {
		svc.Private = BasicToUnion[*bool, VPCEndpoint](aws.Bool(true))
		svc.Network.VPC.Placement.PlacementString = (*PlacementString)(aws.String("private"))
	}
	svc.parser = template.New()
	return svc
}

// MarshalBinary serializes the manifest object into a binary YAML document.
// Implements the encoding.BinaryMarshaler interface.
func (s *RequestDrivenWebService) MarshalBinary() ([]byte, error) {
	content, err := s.parser.Parse(requestDrivenWebSvcManifestPath, *s)
	if err != nil {
		return nil, err
	}
	return content.Bytes(), nil
}

// Dockerfile returns the relative path of the Dockerfile in the manifest.
func (s *RequestDrivenWebService) Dockerfile() string {
	return s.ImageConfig.Image.dockerfilePath()
}

// Port returns the exposed the exposed port in the manifest.
// A RequestDrivenWebService always has a port exposed therefore the boolean is always true.
func (s *RequestDrivenWebService) Port() (port uint16, ok bool) {
	return aws.Uint16Value(s.ImageConfig.Port), true
}

// Publish returns the list of topics where notifications can be published.
func (s *RequestDrivenWebService) Publish() []Topic {
	return s.RequestDrivenWebServiceConfig.PublishConfig.publishedTopics()
}

// ContainerPlatform returns the platform for the service.
func (s *RequestDrivenWebService) ContainerPlatform() string {
	if s.InstanceConfig.Platform.IsEmpty() {
		return platformString(OSLinux, ArchAMD64)
	}
	return platformString(s.InstanceConfig.Platform.OS(), s.InstanceConfig.Platform.Arch())
}

// BuildArgs returns a docker.BuildArguments object given a context directory.
func (s *RequestDrivenWebService) BuildArgs(contextDir string) (map[string]*DockerBuildArgs, error) {
	required, err := requiresBuild(s.ImageConfig.Image)
	if err != nil {
		return nil, err
	}
	// Creating an map to store buildArgs of all sidecar images and main container image.
	buildArgsPerContainer := make(map[string]*DockerBuildArgs, 1)
	if required {
		buildArgsPerContainer[aws.StringValue(s.Name)] = s.ImageConfig.Image.BuildConfig(contextDir)
	}
	return buildArgsPerContainer, nil
}

func (s RequestDrivenWebService) applyEnv(envName string) (workloadManifest, error) {
	overrideConfig, ok := s.Environments[envName]
	if !ok {
		return &s, nil
	}
	// Apply overrides to the original service configuration.
	for _, t := range defaultTransformers {
		err := mergo.Merge(&s, RequestDrivenWebService{
			RequestDrivenWebServiceConfig: *overrideConfig,
		}, mergo.WithOverride, mergo.WithTransformers(t))
		if err != nil {
			return nil, err
		}
	}
	s.Environments = nil
	return &s, nil
}

func (s *RequestDrivenWebService) requiredEnvironmentFeatures() []string {
	var features []string
	features = append(features, s.Network.requiredEnvFeatures()...)
	return features
}

// newDefaultRequestDrivenWebService returns an empty RequestDrivenWebService with only the default values set.
func newDefaultRequestDrivenWebService() *RequestDrivenWebService {
	return &RequestDrivenWebService{
		Workload: Workload{
			Type: aws.String(manifestinfo.RequestDrivenWebServiceType),
		},
		RequestDrivenWebServiceConfig: RequestDrivenWebServiceConfig{
			ImageConfig: ImageWithPort{},
			InstanceConfig: AppRunnerInstanceConfig{
				CPU:    aws.Int(1024),
				Memory: aws.Int(2048),
			},
		},
	}
}
