package inject

import (
	"errors"
	"fmt"
	"strings"

	"github.com/linkerd/linkerd2/pkg/healthcheck"
	"github.com/linkerd/linkerd2/pkg/k8s"
	v1 "k8s.io/api/core/v1"
)

const (
	hostNetworkEnabled                   = "host_network_enabled"
	sidecarExists                        = "sidecar_already_exists"
	unsupportedResource                  = "unsupported_resource"
	injectEnableAnnotationAbsent         = "injection_enable_annotation_absent"
	injectDisableAnnotationPresent       = "injection_disable_annotation_present"
	annotationAtNamespace                = "namespace"
	annotationAtWorkload                 = "workload"
	invalidInjectAnnotationWorkload      = "invalid_inject_annotation_at_workload"
	invalidInjectAnnotationNamespace     = "invalid_inject_annotation_at_ns"
	disabledAutomountServiceAccountToken = "disabled_automount_service_account_token_account"
	udpPortsEnabled                      = "udp_ports_enabled"
)

var (
	// Reasons is a map of inject skip reasons with human readable sentences
	Reasons = map[string]string{
		hostNetworkEnabled:                   "hostNetwork is enabled",
		sidecarExists:                        "pod has a sidecar injected already",
		unsupportedResource:                  "this resource kind is unsupported",
		injectEnableAnnotationAbsent:         fmt.Sprintf("neither the namespace nor the pod have the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectEnabled),
		injectDisableAnnotationPresent:       fmt.Sprintf("pod has the annotation \"%s:%s\"", k8s.ProxyInjectAnnotation, k8s.ProxyInjectDisabled),
		invalidInjectAnnotationWorkload:      fmt.Sprintf("invalid value for annotation \"%s\" at workload", k8s.ProxyInjectAnnotation),
		invalidInjectAnnotationNamespace:     fmt.Sprintf("invalid value for annotation \"%s\" at namespace", k8s.ProxyInjectAnnotation),
		disabledAutomountServiceAccountToken: "automountServiceAccountToken set to \"false\", with Values.identity.serviceAccountTokenProjection set to \"false\"",
		udpPortsEnabled:                      "UDP port(s) configured on pod spec",
	}

	// Set of valid inject annotation values
	ValidInjectAnnotationValues = map[string]struct{}{
		k8s.ProxyInjectEnabled:  {},
		k8s.ProxyInjectDisabled: {},
		k8s.ProxyInjectIngress:  {},
	}

	// Set of inject annotation values that indicate that injection is enabled
	InjectEnabledValues = map[string]struct{}{
		k8s.ProxyInjectEnabled: {},
		k8s.ProxyInjectIngress: {},
	}
)

// Report contains the Kind and Name for a given workload along with booleans
// describing the result of the injection transformation
type Report struct {
	Kind                         string
	Name                         string
	HostNetwork                  bool
	Sidecar                      bool
	UDP                          bool // true if any port in any container has `protocol: UDP`
	UnsupportedResource          bool // unsupported to inject
	InjectDisabled               bool
	InjectDisabledReason         string
	InjectAnnotationAt           string
	InjectAnnotationValue        string
	Annotatable                  bool
	Annotated                    bool
	AutomountServiceAccountToken bool

	// Uninjected consists of two boolean flags to indicate if a proxy and
	// proxy-init containers have been uninjected in this report
	Uninjected struct {
		// Proxy is true if a proxy container has been uninjected
		Proxy bool

		// ProxyInit is true if a proxy-init container has been uninjected
		ProxyInit bool
	}
}

// newReport returns a new Report struct, initialized with the Kind and Name
// from conf
func newReport(conf *ResourceConfig) *Report {
	var name string
	if conf.IsPod() {
		name = conf.pod.meta.Name
		if name == "" {
			name = conf.pod.meta.GenerateName
		}
	} else if m := conf.workload.Meta; m != nil {
		name = m.Name
	}

	report := &Report{
		Kind:                         strings.ToLower(conf.workload.metaType.Kind),
		Name:                         name,
		AutomountServiceAccountToken: true,
	}

	if conf.HasPodTemplate() {
		report.InjectDisabled, report.InjectDisabledReason, report.InjectAnnotationAt, report.InjectAnnotationValue = report.disabledByAnnotation(conf)
		report.HostNetwork = conf.pod.spec.HostNetwork
		report.Sidecar = healthcheck.HasExistingSidecars(conf.pod.spec)
		report.UDP = checkUDPPorts(conf.pod.spec)
		if conf.pod.spec.AutomountServiceAccountToken != nil &&
			(conf.values != nil && !conf.values.Identity.ServiceAccountTokenProjection) {
			report.AutomountServiceAccountToken = *conf.pod.spec.AutomountServiceAccountToken
		}
		if conf.origin == OriginWebhook {
			if vm := conf.serviceAccountVolumeMount(); vm == nil {
				// set to false only if it is not using the new linkerd-token volume projection
				if conf.values != nil && !conf.values.Identity.ServiceAccountTokenProjection {
					report.AutomountServiceAccountToken = false
				}
			}
		}
	} else {
		report.UnsupportedResource = true
	}

	if conf.HasPodTemplate() || conf.IsService() || conf.IsNamespace() {
		report.Annotatable = true
	}

	return report
}

// ResName returns a string "Kind/Name" for the workload referred in the report r
func (r *Report) ResName() string {
	return fmt.Sprintf("%s/%s", r.Kind, r.Name)
}

// Injectable returns false if the report flags indicate that the workload is on a host network
// or there is already a sidecar or the resource is not supported or inject is explicitly disabled.
// If false, the second returned value describes the reason.
func (r *Report) Injectable() (bool, []string) {
	var reasons []string
	if r.HostNetwork {
		reasons = append(reasons, hostNetworkEnabled)
	}
	if r.Sidecar {
		reasons = append(reasons, sidecarExists)
	}
	if r.UnsupportedResource {
		reasons = append(reasons, unsupportedResource)
	}
	if r.InjectDisabled {
		reasons = append(reasons, r.InjectDisabledReason)
	}

	if !r.AutomountServiceAccountToken {
		reasons = append(reasons, disabledAutomountServiceAccountToken)
	}

	if len(reasons) > 0 {
		return false, reasons
	}
	return true, nil
}

// IsAnnotatable returns true if the resource for a report can be annotated.
func (r *Report) IsAnnotatable() bool {
	return r.Annotatable
}

func checkUDPPorts(t *v1.PodSpec) bool {
	// Check for ports with `protocol: UDP`, which will not be routed by Linkerd
	for _, container := range t.Containers {
		for _, port := range container.Ports {
			if port.Protocol == v1.ProtocolUDP {
				return true
			}
		}
	}
	return false
}

// disabledByAnnotation checks the workload and namespace for the annotation
// that disables injection. It returns if it is disabled, why it is disabled,
// and the location where the annotation was present.
func (r *Report) disabledByAnnotation(conf *ResourceConfig) (bool, string, string, string) {
	// truth table of the effects of the inject annotation:
	//
	// origin  | namespace | pod      | inject?  | return
	// ------- | --------- | -------- | -------- | ------
	// webhook | enabled   | enabled  | yes      | false
	// webhook | enabled   | ""       | yes      | false
	// webhook | enabled   | disabled | no       | true
	// webhook | disabled  | enabled  | yes      | false
	// webhook | ""        | enabled  | yes      | false
	// webhook | disabled  | disabled | no       | true
	// webhook | ""        | disabled | no       | true
	// webhook | disabled  | ""       | no       | true
	// webhook | ""        | ""       | no       | true
	// cli     | n/a       | enabled  | yes      | false
	// cli     | n/a       | ""       | yes      | false
	// cli     | n/a       | disabled | no       | true

	podAnnotation := conf.pod.meta.Annotations[k8s.ProxyInjectAnnotation]
	nsAnnotation := conf.nsAnnotations[k8s.ProxyInjectAnnotation]

	if conf.origin == OriginCLI {
		return podAnnotation == k8s.ProxyInjectDisabled, "", "", podAnnotation
	}

	if !isInjectAnnotationValid(nsAnnotation) {
		return true, invalidInjectAnnotationNamespace, "", nsAnnotation
	}

	if !isInjectAnnotationValid(podAnnotation) {
		return true, invalidInjectAnnotationWorkload, "", podAnnotation
	}

	if doesAnnotationEnableInject(nsAnnotation) {
		if podAnnotation == k8s.ProxyInjectDisabled {
			return true, injectDisableAnnotationPresent, annotationAtWorkload, podAnnotation
		}
		return false, "", annotationAtNamespace, nsAnnotation
	}

	if !doesAnnotationEnableInject(podAnnotation) {
		return true, injectEnableAnnotationAbsent, "", podAnnotation
	}

	return false, "", annotationAtWorkload, podAnnotation
}

func doesAnnotationEnableInject(annotation string) bool {
	_, isEnabledValue := InjectEnabledValues[annotation]
	return isEnabledValue
}

func isInjectAnnotationValid(annotation string) bool {
	_, validValue := ValidInjectAnnotationValues[annotation]
	if annotation != "" && !validValue {
		// If the annotation is not empty, it must be a valid value
		return false
	}

	return true
}

// ThrowInjectError errors out `inject` when the report contains errors
// related to automountServiceAccountToken, hostNetwork, existing sidecar,
// or udp ports
// See - https://github.com/linkerd/linkerd2/issues/4214
func (r *Report) ThrowInjectError() []error {

	errs := []error{}

	if !r.AutomountServiceAccountToken {
		errs = append(errs, errors.New(Reasons[disabledAutomountServiceAccountToken]))
	}

	if r.HostNetwork {
		errs = append(errs, errors.New(Reasons[hostNetworkEnabled]))
	}

	if r.Sidecar {
		errs = append(errs, errors.New(Reasons[sidecarExists]))
	}

	if r.UDP {
		errs = append(errs, errors.New(Reasons[udpPortsEnabled]))
	}

	return errs
}
