// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package injector

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"strings"

	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	"github.com/apache/skywalking-swck/operator/apis/operator/v1alpha1"
)

// Injection is each step of the injection process
type Injection interface {
	execute(*InjectProcessData) admission.Response
	// set next step
	setNext(Injection)
}

// InjectProcessData defines data that needs to be processed in the injection process
// Divide the entire injection process into 5 steps
// 1.Get injection strategy
// 2.Overlay the sidecar info
// 3.Overlay the agent by setting jvm string
// 4.Overlay the plugins by setting jvm string and set the optional plugins
// 5.Inject fields into Pod
// After all steps are completed, return fully injected Pod, Or there is an error
// in a certain step, inject error info into annotations and return incompletely injected Pod
type InjectProcessData struct {
	injectFileds      *SidecarInjectField
	annotation        *Annotations
	annotationOverlay *AnnotationOverlay
	swAgentL          *v1alpha1.SwAgentList
	pod               *corev1.Pod
	req               admission.Request
	log               logr.Logger
	kubeclient        client.Client
	ctx               context.Context
}

// PatchReq is to fill the injected pod into the request and return the Response
func PatchReq(pod *corev1.Pod, req admission.Request) admission.Response {
	marshaledPod, err := json.Marshal(pod)
	if err != nil {
		return admission.Errored(http.StatusInternalServerError, err)
	}

	return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
}

// GetStrategy is the first step of injection process
type GetStrategy struct {
	next Injection
}

// get injection strategy from pod's labels and annotations
// if don't need to inject, then return the original pod, otherwise go to the next step
func (gs *GetStrategy) execute(ipd *InjectProcessData) admission.Response {
	log.Info("=============== GetStrategy ================")
	ipd.injectFileds.GetInjectStrategy(&ipd.pod.ObjectMeta.Labels, &ipd.pod.ObjectMeta.Annotations)
	if !ipd.injectFileds.NeedInject {
		log.Info("don't inject agent")
		return admission.Allowed("ok")
	}
	return gs.next.execute(ipd)
}

func (gs *GetStrategy) setNext(next Injection) {
	gs.next = next
}

// OverlaySwAgentCR is the cr config step of injection process
type OverlaySwAgentCR struct {
	next Injection
}

// get configs from SwAgent CR
func (gs *OverlaySwAgentCR) execute(ipd *InjectProcessData) admission.Response {
	log.Info(fmt.Sprintf("=============== OverlaySwAgentCR(%d) ================ ", len(ipd.swAgentL.Items)))
	if !ipd.injectFileds.OverlaySwAgentCR(ipd.swAgentL) {
		log.Info("overlay SwAgent cr config error.")
		return PatchReq(ipd.pod, ipd.req)
	}
	return gs.next.execute(ipd)
}

func (gs *OverlaySwAgentCR) setNext(next Injection) {
	gs.next = next
}

// OverlaySidecar is the second step of injection process
type OverlaySidecar struct {
	next Injection
}

// OverlaySidecar will inject sidecar information such as image, command, args etc.
// Since we did not set the validation function for these fields, it usually goes to the next step
func (os *OverlaySidecar) execute(ipd *InjectProcessData) admission.Response {
	log.Info("=============== OverlaySidecar ================")
	if !ipd.injectFileds.OverlaySidecar(*ipd.annotation, ipd.annotationOverlay, &ipd.pod.ObjectMeta.Annotations) {
		return PatchReq(ipd.pod, ipd.req)
	}
	return os.next.execute(ipd)
}

func (os *OverlaySidecar) setNext(next Injection) {
	os.next = next
}

// OverlayAgent is the third step of injection process
type OverlayAgent struct {
	next Injection
}

// OverlayAgent overlays the agent by getting the pod's annotations
// If the agent overlay option is not set, go directly to the next step
// If set the wrong value in the annotation , inject the error info and return
func (oa *OverlayAgent) execute(ipd *InjectProcessData) admission.Response {
	log.Info("=============== OverlayAgent ================")
	if !ipd.injectFileds.OverlayAgent(*ipd.annotation, ipd.annotationOverlay, &ipd.pod.ObjectMeta.Annotations) {
		ipd.log.Info("overlay agent config error!please look the error annotation!")
		return PatchReq(ipd.pod, ipd.req)
	}
	return oa.next.execute(ipd)
}

func (oa *OverlayAgent) setNext(next Injection) {
	oa.next = next
}

// OverlayPlugins is the fourth step of injection process
type OverlayPlugins struct {
	next Injection
}

// OverlayPlugins contains two step , the first is to set jvm string , the second is to set optional plugins
// during the step , we need to add jvm string to the Env of injected container
func (op *OverlayPlugins) execute(ipd *InjectProcessData) admission.Response {
	log.Info("=============== OverlayPlugins ================")
	ipd.injectFileds.OverlayPlugins(&ipd.pod.ObjectMeta.Annotations)
	if ipd.injectFileds.JvmAgentConfigStr != "" {
		ipd.injectFileds.Env.Value = strings.Join([]string{ipd.injectFileds.Env.Value, ipd.injectFileds.JvmAgentConfigStr}, "=")
	}
	ipd.injectFileds.OverlayOptional(ipd.swAgentL.Items, &ipd.pod.ObjectMeta.Annotations)
	return op.next.execute(ipd)
}
func (op *OverlayPlugins) setNext(next Injection) {
	op.next = next
}

// PodInject is the sixth step of injection process
type PodInject struct {
	next Injection
}

// PodInject will inject all fields to the pod
func (pi *PodInject) execute(ipd *InjectProcessData) admission.Response {
	log.Info("=============== PodInject ================")
	ipd.injectFileds.Inject(ipd.pod)
	ipd.injectFileds.injectSucceedAnnotation(&ipd.pod.Annotations)
	log.Info("inject successfully!")
	return PatchReq(ipd.pod, ipd.req)
}
func (pi *PodInject) setNext(next Injection) {
	pi.next = next
}

// NewInjectProcess create a new InjectProcess
func NewInjectProcess(ctx context.Context, injectFileds *SidecarInjectField, annotation *Annotations,
	annotationOverlay *AnnotationOverlay, swAgentL *v1alpha1.SwAgentList, pod *corev1.Pod, req admission.Request, log logr.Logger,
	kubeclient client.Client) *InjectProcessData {
	return &InjectProcessData{
		ctx:               ctx,
		injectFileds:      injectFileds,
		annotation:        annotation,
		annotationOverlay: annotationOverlay,
		swAgentL:          swAgentL,
		pod:               pod,
		req:               req,
		log:               log,
		kubeclient:        kubeclient,
	}
}

// Run will connect the above six steps into a chain and start to execute the first step
func (ipd *InjectProcessData) Run() admission.Response {
	// set final step
	podInject := &PodInject{}

	// set next step is PodInject
	overlayPlugins := &OverlayPlugins{}
	overlayPlugins.setNext(podInject)

	// set next step is OverlayPlugins
	overlayAgent := &OverlayAgent{}
	overlayAgent.setNext(overlayPlugins)

	// set next step is OverlayAgent
	overlaysidecar := &OverlaySidecar{}
	overlaysidecar.setNext(overlayAgent)

	// set next step is OverlaySwAgentCR
	overlaySwAgentCR := &OverlaySwAgentCR{}
	overlaySwAgentCR.setNext(overlaysidecar)

	// set next step is OverlaySidecar
	getStrategy := &GetStrategy{}
	getStrategy.setNext(overlaySwAgentCR)

	// this is first step and do real injection
	return getStrategy.execute(ipd)
}
