package webhooks

import (
	"fmt"
	"net/http"
	"reflect"
	"strings"
	"time"

	"github.com/go-logr/logr"
	kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
	"github.com/kyverno/kyverno/pkg/common"
	"github.com/kyverno/kyverno/pkg/engine"
	enginectx "github.com/kyverno/kyverno/pkg/engine/context"
	policyvalidate "github.com/kyverno/kyverno/pkg/policy"
	"github.com/kyverno/kyverno/pkg/policycache"
	"github.com/kyverno/kyverno/pkg/policymutation"
	"github.com/kyverno/kyverno/pkg/userinfo"
	"github.com/kyverno/kyverno/pkg/utils"
	admissionutils "github.com/kyverno/kyverno/pkg/utils/admission"
	"github.com/kyverno/kyverno/pkg/webhooks/handlers"
	admissionv1 "k8s.io/api/admission/v1"
)

// TODO: use admission review sub resource ?
func isStatusUpdate(old, new kyverno.PolicyInterface) bool {
	if !reflect.DeepEqual(old.GetAnnotations(), new.GetAnnotations()) {
		return false
	}
	if !reflect.DeepEqual(old.GetLabels(), new.GetLabels()) {
		return false
	}
	if !reflect.DeepEqual(old.GetSpec(), new.GetSpec()) {
		return false
	}
	return true
}

func errorResponse(logger logr.Logger, err error, message string) *admissionv1.AdmissionResponse {
	logger.Error(err, message)
	return admissionutils.ResponseFailure(false, message+": "+err.Error())
}

func setupLogger(logger logr.Logger, name string, request *admissionv1.AdmissionRequest) logr.Logger {
	return logger.WithName("MutateWebhook").WithValues(
		"uid", request.UID,
		"kind", request.Kind,
		"namespace", request.Namespace,
		"name", request.Name,
		"operation", request.Operation,
		"gvk", request.Kind.String(),
	)
}

func (ws *WebhookServer) admissionHandler(filter bool, inner handlers.AdmissionHandler) http.HandlerFunc {
	if filter {
		inner = handlers.Filter(ws.configHandler, inner)
	}
	return handlers.Monitor(ws.webhookMonitor, handlers.Admission(ws.log, inner))
}

func (ws *WebhookServer) policyMutation(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	logger := setupLogger(ws.log, "policy mutation", request)
	policy, oldPolicy, err := admissionutils.GetPolicies(request)
	if err != nil {
		logger.Error(err, "failed to unmarshal policies from admission request")
		return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to default value, check kyverno controller logs for details: %v", err))
	}
	if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
		logger.V(4).Info("skip policy mutation on status update")
		return admissionutils.Response(true)
	}
	startTime := time.Now()
	logger.V(3).Info("start policy change mutation")
	defer logger.V(3).Info("finished policy change mutation", "time", time.Since(startTime).String())
	// Generate JSON Patches for defaults
	if patches, updateMsgs := policymutation.GenerateJSONPatchesForDefaults(policy, logger); len(patches) != 0 {
		return admissionutils.ResponseWithMessageAndPatch(true, strings.Join(updateMsgs, "'"), patches)
	}
	return admissionutils.Response(true)
}

//policyValidation performs the validation check on policy resource
func (ws *WebhookServer) policyValidation(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	logger := setupLogger(ws.log, "policy validation", request)
	policy, oldPolicy, err := admissionutils.GetPolicies(request)
	if err != nil {
		logger.Error(err, "failed to unmarshal policies from admission request")
		return admissionutils.ResponseWithMessage(true, fmt.Sprintf("failed to validate policy, check kyverno controller logs for details: %v", err))
	}
	if oldPolicy != nil && isStatusUpdate(oldPolicy, policy) {
		logger.V(4).Info("skip policy validation on status update")
		return admissionutils.Response(true)
	}
	startTime := time.Now()
	logger.V(3).Info("start policy change validation")
	defer logger.V(3).Info("finished policy change validation", "time", time.Since(startTime).String())
	response, err := policyvalidate.Validate(policy, ws.client, false, ws.openAPIController)
	if err != nil {
		logger.Error(err, "policy validation errors")
		return admissionutils.ResponseWithMessage(true, err.Error())
	}
	if response != nil && len(response.Warnings) != 0 {
		return response
	}
	return admissionutils.Response(true)
}

// resourceMutation mutates resource
func (ws *WebhookServer) resourceMutation(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	logger := setupLogger(ws.log, "resource mutation", request)
	if excludeKyvernoResources(request.Kind.Kind) {
		return admissionutils.ResponseSuccess(true, "")
	}
	if request.Operation == admissionv1.Delete {
		resource, err := utils.ConvertResource(request.OldObject.Raw, request.Kind.Group, request.Kind.Version, request.Kind.Kind, request.Namespace)
		if err == nil {
			ws.prGenerator.Add(buildDeletionPrInfo(resource))
		} else {
			logger.Info(fmt.Sprintf("Converting oldObject failed: %v", err))
		}
		return admissionutils.ResponseSuccess(true, "")
	}
	logger.V(4).Info("received an admission request in mutating webhook")
	requestTime := time.Now().Unix()
	mutatePolicies := ws.pCache.GetPolicies(policycache.Mutate, request.Kind.Kind, request.Namespace)
	verifyImagesPolicies := ws.pCache.GetPolicies(policycache.VerifyImages, request.Kind.Kind, request.Namespace)
	if len(mutatePolicies) == 0 && len(verifyImagesPolicies) == 0 {
		logger.V(4).Info("no policies matched admission request")
		return admissionutils.ResponseSuccess(true, "")
	}
	addRoles := containsRBACInfo(mutatePolicies)
	policyContext, err := ws.buildPolicyContext(request, addRoles)
	if err != nil {
		logger.Error(err, "failed to build policy context")
		return admissionutils.ResponseFailure(false, err.Error())
	}
	// update container images to a canonical form
	if err := enginectx.MutateResourceWithImageInfo(request.Object.Raw, policyContext.JSONContext); err != nil {
		ws.log.Error(err, "failed to patch images info to resource, policies that mutate images may be impacted")
	}
	mutatePatches := ws.applyMutatePolicies(request, policyContext, mutatePolicies, requestTime, logger)
	newRequest := patchRequest(mutatePatches, request, logger)
	imagePatches, err := ws.applyImageVerifyPolicies(newRequest, policyContext, verifyImagesPolicies, logger)
	if err != nil {
		logger.Error(err, "image verification failed")
		return admissionutils.ResponseFailure(false, err.Error())
	}
	var patches = append(mutatePatches, imagePatches...)
	return admissionutils.ResponseSuccessWithPatch(true, "", patches)
}

func (ws *WebhookServer) resourceValidation(request *admissionv1.AdmissionRequest) *admissionv1.AdmissionResponse {
	logger := setupLogger(ws.log, "resource validation", request)
	if request.Operation == admissionv1.Delete {
		ws.handleDelete(request)
	}
	if excludeKyvernoResources(request.Kind.Kind) {
		return admissionutils.ResponseSuccess(true, "")
	}
	logger.V(6).Info("received an admission request in validating webhook")
	// timestamp at which this admission request got triggered
	requestTime := time.Now().Unix()
	policies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, "")
	// Get namespace policies from the cache for the requested resource namespace
	nsPolicies := ws.pCache.GetPolicies(policycache.ValidateEnforce, request.Kind.Kind, request.Namespace)
	policies = append(policies, nsPolicies...)
	generatePolicies := ws.pCache.GetPolicies(policycache.Generate, request.Kind.Kind, request.Namespace)
	if len(generatePolicies) == 0 && request.Operation == admissionv1.Update {
		// handle generate source resource updates
		go ws.handleUpdatesForGenerateRules(request, []kyverno.PolicyInterface{})
	}
	var roles, clusterRoles []string
	if containsRBACInfo(policies, generatePolicies) {
		var err error
		roles, clusterRoles, err = userinfo.GetRoleRef(ws.rbLister, ws.crbLister, request, ws.configHandler)
		if err != nil {
			return errorResponse(logger, err, "failed to fetch RBAC data")
		}
	}
	userRequestInfo := kyverno.RequestInfo{
		Roles:             roles,
		ClusterRoles:      clusterRoles,
		AdmissionUserInfo: *request.UserInfo.DeepCopy(),
	}
	ctx, err := newVariablesContext(request, &userRequestInfo)
	if err != nil {
		return errorResponse(logger, err, "failed create policy rule context")
	}
	namespaceLabels := make(map[string]string)
	if request.Kind.Kind != "Namespace" && request.Namespace != "" {
		namespaceLabels = common.GetNamespaceSelectorsFromNamespaceLister(request.Kind.Kind, request.Namespace, ws.nsLister, logger)
	}
	newResource, oldResource, err := utils.ExtractResources(nil, request)
	if err != nil {
		return errorResponse(logger, err, "failed create parse resource")
	}
	if err := ctx.AddImageInfo(&newResource); err != nil {
		return errorResponse(logger, err, "failed add image information to policy rule context")
	}
	policyContext := &engine.PolicyContext{
		NewResource:         newResource,
		OldResource:         oldResource,
		AdmissionInfo:       userRequestInfo,
		ExcludeGroupRole:    ws.configHandler.GetExcludeGroupRole(),
		ExcludeResourceFunc: ws.configHandler.ToFilter,
		JSONContext:         ctx,
		Client:              ws.client,
	}
	vh := &validationHandler{
		log:         ws.log,
		eventGen:    ws.eventGen,
		prGenerator: ws.prGenerator,
	}
	ok, msg := vh.handleValidation(ws.promConfig, request, policies, policyContext, namespaceLabels, requestTime)
	if !ok {
		logger.Info("admission request denied")
		return admissionutils.ResponseFailure(false, msg)
	}
	// push admission request to audit handler, this won't block the admission request
	ws.auditHandler.Add(request.DeepCopy())
	// process generate policies
	ws.applyGeneratePolicies(request, policyContext, generatePolicies, requestTime, logger)
	return admissionutils.ResponseSuccess(true, "")
}
