package inject

import (
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	corev1 "k8s.io/api/core/v1"
	"strings"

	"github.com/aws/aws-sdk-go/aws/arn"
)

const xrayDaemonContainerName = "xray-daemon"
const xrayDaemonContainerTemplate = `
{
  "name": "xray-daemon",
  "image": "{{ .XRayImage }}",
  "securityContext": {
    "runAsUser": 1337
  },
  "ports": [
    {
      "containerPort": {{ .XrayDaemonPort }},
      "name": "xray",
      "protocol": "UDP"
    }
  ],
  "env": [
    {
      "name": "AWS_REGION",
      "value": "{{ .AWSRegion }}"
    }
  ]
}
`

type XrayTemplateVariables struct {
	AWSRegion      string
	XRayImage      string
	XrayDaemonPort int32
}

type xrayMutatorConfig struct {
	awsRegion             string
	sidecarCPURequests    string
	sidecarMemoryRequests string
	sidecarCPULimits      string
	sidecarMemoryLimits   string
	xRayImage             string
	xRayDaemonPort        int32
	xRayLogLevel          string
	xRayConfigRoleArn     string
}

func newXrayMutator(mutatorConfig xrayMutatorConfig, enabled bool) *xrayMutator {
	return &xrayMutator{
		mutatorConfig: mutatorConfig,
		enabled:       enabled,
	}
}

type xrayMutator struct {
	mutatorConfig xrayMutatorConfig
	enabled       bool
}

func (m *xrayMutator) mutate(pod *corev1.Pod) error {
	if !m.enabled {
		return nil
	}
	if containsXRAYDaemonContainer(pod) {
		return nil
	}

	err := m.checkConfig()
	if err != nil {
		return err
	}

	xrayDaemonConfigMount, err := m.getXrayDaemonConfigMount(pod)
	if err != nil {
		return err
	}

	variables := m.buildTemplateVariables(pod)
	xrayDaemonSidecar, err := renderTemplate("xray-daemon", xrayDaemonContainerTemplate, variables)
	if err != nil {
		return err
	}

	container := corev1.Container{}
	err = json.Unmarshal([]byte(xrayDaemonSidecar), &container)
	if err != nil {
		return err
	}

	// add resource requests and limits
	container.Resources, err = sidecarResources(getSidecarCPURequest(m.mutatorConfig.sidecarCPURequests, pod),
		getSidecarMemoryRequest(m.mutatorConfig.sidecarMemoryRequests, pod),
		getSidecarCPULimit(m.mutatorConfig.sidecarCPULimits, pod),
		getSidecarMemoryLimit(m.mutatorConfig.sidecarMemoryLimits, pod))
	if err != nil {
		return err
	}

	// Add xray daemon configurations as args. If config file is mounted then it will override all other configurations. See
	// https://docs.aws.amazon.com/xray/latest/devguide/xray-daemon-configuration.html#xray-daemon-configuration-commandline
	err = m.buildXrayDaemonArgs(pod, &container, xrayDaemonConfigMount)
	if err != nil {
		return err
	}

	pod.Spec.Containers = append(pod.Spec.Containers, container)
	return nil
}

func (m *xrayMutator) getXrayDaemonConfigMount(pod *corev1.Pod) (map[string]string, error) {
	xrayDaemonConfigMount := make(map[string]string)
	if v, ok := pod.ObjectMeta.Annotations[AppMeshXrayAgentConfigAnnotation]; ok {
		if strings.Contains(v, ",") {
			return nil, errors.Errorf("provide only one config mount for annotation \"%s: %v\"", AppMeshXrayAgentConfigAnnotation, v)
		}
		pair := strings.Split(v, ":")
		if len(pair) != 2 { // volumeName:mountPath
			return nil, errors.Errorf("malformed annotation \"%s\", expected format: %s", AppMeshXrayAgentConfigAnnotation, "volumeName:mountPath")
		}
		volumeName := strings.TrimSpace(pair[0])
		mountPath := strings.TrimSpace(pair[1])
		xrayDaemonConfigMount[volumeName] = mountPath
	}
	return xrayDaemonConfigMount, nil
}

func (m *xrayMutator) buildXrayDaemonArgs(pod *corev1.Pod, xrayDaemonContainer *corev1.Container, xrayDaemonConfigMount map[string]string) error {
	// For-loop will loop only once as we expect only a single xrayDaemonConfigMount to be provided.
	// If more than 1 is supplied we error out in `getXrayDaemonConfigMount` func itself.
	for volumeName, mountPath := range xrayDaemonConfigMount {
		volumeMount := corev1.VolumeMount{
			Name:      volumeName,
			MountPath: mountPath,
			ReadOnly:  true,
		}
		xrayDaemonContainer.VolumeMounts = append(xrayDaemonContainer.VolumeMounts, volumeMount)
		// Override the xray daemon's entrypoint command do load config from xray-daemon.yaml
		xrayDaemonContainer.Command = []string{"/xray", "--config", fmt.Sprintf("%v/xray-daemon.yaml", strings.TrimRight(mountPath, " /"))}
		// We return here since file config will override all other cli arguments
		return nil
	}

	// Check for other arguments such as role-arn or log-level.
	//
	// Ignore if empty or `prod`, since xray agent's loglevel by default is set to `prod`
	if m.mutatorConfig.xRayLogLevel != "" && m.mutatorConfig.xRayLogLevel != "prod" {
		switch m.mutatorConfig.xRayLogLevel {
		case "dev":
			fallthrough
		case "debug":
			fallthrough
		case "info":
			fallthrough
		case "warn":
			fallthrough
		case "error":
			xrayDaemonContainer.Args = append(xrayDaemonContainer.Args, "--log-level", m.mutatorConfig.xRayLogLevel)
		default:
			return errors.Errorf("tracing.logLevel: \"%v\" is not valid."+
				" Set one of dev, debug, info, prod, warn, error", m.mutatorConfig.xRayLogLevel)
		}
	}
	if m.mutatorConfig.xRayConfigRoleArn != "" {
		if arn.IsARN(m.mutatorConfig.xRayConfigRoleArn) {
			xrayDaemonContainer.Args = append(xrayDaemonContainer.Args, "--role-arn", m.mutatorConfig.xRayConfigRoleArn)
		} else {
			return errors.Errorf("tracing.role: \"%v\" is not a valid `--role-arn`."+
				" Please refer to AWS X-Ray Documentation for more information", m.mutatorConfig.xRayConfigRoleArn)
		}
	}
	return nil
}

func (m *xrayMutator) buildTemplateVariables(pod *corev1.Pod) XrayTemplateVariables {
	return XrayTemplateVariables{
		AWSRegion:      m.mutatorConfig.awsRegion,
		XRayImage:      m.mutatorConfig.xRayImage,
		XrayDaemonPort: m.mutatorConfig.xRayDaemonPort,
	}
}

func (m *xrayMutator) checkConfig() error {
	var missingConfig []string

	if m.mutatorConfig.awsRegion == "" {
		missingConfig = append(missingConfig, "AWSRegion")
	}
	if m.mutatorConfig.xRayImage == "" {
		missingConfig = append(missingConfig, "xRayImage")
	}

	if m.mutatorConfig.xRayDaemonPort == 0 {
		missingConfig = append(missingConfig, "xRayDaemonPort")
	}

	if len(missingConfig) > 0 {
		return errors.Errorf("Missing configuration parameters: %s", strings.Join(missingConfig, ","))
	}

	return nil
}

func containsXRAYDaemonContainer(pod *corev1.Pod) bool {
	for _, container := range pod.Spec.Containers {
		if container.Name == xrayDaemonContainerName {
			return true
		}
	}
	return false
}
