package mutate

import (
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"time"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/controller-runtime/pkg/log"

	jsonpatch "github.com/evanphx/json-patch"
	"github.com/go-logr/logr"
	"github.com/nirmata/kyverno/pkg/engine/anchor"
	"github.com/nirmata/kyverno/pkg/engine/response"
	"github.com/nirmata/kyverno/pkg/engine/utils"
)

// ProcessOverlay processes mutation overlay on the resource
func ProcessOverlay(log logr.Logger, ruleName string, overlay interface{}, resource unstructured.Unstructured) (resp response.RuleResponse, patchedResource unstructured.Unstructured) {
	startTime := time.Now()
	logger := log.WithValues("rule", ruleName)
	logger.V(4).Info("started applying overlay rule", "startTime", startTime)
	resp.Name = ruleName
	resp.Type = utils.Mutation.String()
	defer func() {
		resp.RuleStats.ProcessingTime = time.Since(startTime)
		logger.V(4).Info("finished applying overlay rule", "processingTime", resp.RuleStats.ProcessingTime)
	}()

	patches, overlayerr := processOverlayPatches(logger, resource.UnstructuredContent(), overlay)
	if !reflect.DeepEqual(overlayerr, overlayError{}) {
		switch overlayerr.statusCode {

		case conditionNotPresent:
			logger.V(3).Info("skip applying rule", "reason", "conditionNotPresent")
			resp.Success = true
			return resp, resource

		case conditionFailure:
			logger.V(3).Info("skip applying rule", "reason", "conditionFailure")
			//TODO: send zero response and not consider this as applied?
			resp.Success = true
			resp.Message = overlayerr.ErrorMsg()
			return resp, resource

		case overlayFailure:
			logger.Info("failed to process overlay")
			resp.Success = false
			resp.Message = fmt.Sprintf("failed to process overlay: %v", overlayerr.ErrorMsg())
			return resp, resource

		default:
			logger.Info("failed to process overlay")
			resp.Success = false
			resp.Message = fmt.Sprintf("Unknown type of error: %v", overlayerr.Error())
			return resp, resource
		}
	}

	logger.V(4).Info("processing overlay rule", "patches", len(patches))
	if len(patches) == 0 {
		resp.Success = true
		return resp, resource
	}

	// convert to RAW
	resourceRaw, err := resource.MarshalJSON()
	if err != nil {
		resp.Success = false
		logger.Error(err, "failed to marshal resource")
		resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
		return resp, resource
	}

	var patchResource []byte
	logger.V(5).Info("applying overlay patches", "patches", string(utils.JoinPatches(patches)))
	patchResource, err = utils.ApplyPatches(resourceRaw, patches)
	if err != nil {
		msg := fmt.Sprintf("failed to apply JSON patches: %v", err)
		resp.Success = false
		resp.Message = msg
		return resp, resource
	}

	logger.V(5).Info("patched resource", "patches", string(patchResource))
	err = patchedResource.UnmarshalJSON(patchResource)
	if err != nil {
		logger.Error(err, "failed to unmarshal resource")
		resp.Success = false
		resp.Message = fmt.Sprintf("failed to process JSON patches: %v", err)
		return resp, resource
	}

	// rule application successfully
	resp.Success = true
	resp.Message = fmt.Sprintf("successfully processed overlay")
	resp.Patches = patches
	// apply the patches to the resource
	return resp, patchedResource
}

func processOverlayPatches(log logr.Logger, resource, overlay interface{}) ([][]byte, overlayError) {
	if path, overlayerr := meetConditions(log, resource, overlay); !reflect.DeepEqual(overlayerr, overlayError{}) {
		switch overlayerr.statusCode {
		// anchor key does not exist in the resource, skip applying policy
		case conditionNotPresent:
			log.V(4).Info("skip applying policy", "path", path, "error", overlayerr)
			return nil, newOverlayError(overlayerr.statusCode, fmt.Sprintf("Policy not applied, condition tag not present: %v at %s", overlayerr.ErrorMsg(), path))
		// anchor key is not satisfied in the resource, skip applying policy
		case conditionFailure:
			// anchor key is not satisfied in the resource, skip applying policy
			log.V(4).Info("failed to validate condition", "path", path, "error", overlayerr)
			return nil, newOverlayError(overlayerr.statusCode, fmt.Sprintf("Policy not applied, conditions are not met at %s, %v", path, overlayerr))
		}
	}

	patchBytes, err := MutateResourceWithOverlay(resource, overlay)
	if err != nil {
		return patchBytes, newOverlayError(overlayFailure, err.Error())
	}

	return patchBytes, overlayError{}
}

// MutateResourceWithOverlay is a start of overlaying process
func MutateResourceWithOverlay(resource, pattern interface{}) ([][]byte, error) {
	// It assumes that mutation is started from root, so "/" is passed
	return applyOverlay(resource, pattern, "/")
}

// applyOverlay detects type of current item and goes down through overlay and resource trees applying overlay
func applyOverlay(resource, overlay interface{}, path string) ([][]byte, error) {

	// resource item exists but has different type - replace
	// all subtree within this path by overlay
	if reflect.TypeOf(resource) != reflect.TypeOf(overlay) {
		patch, err := replaceSubtree(overlay, path)
		if err != nil {
			return nil, err
		}

		return [][]byte{patch}, nil
	}
	return applyOverlayForSameTypes(resource, overlay, path)
}

// applyOverlayForSameTypes is applyOverlay for cases when TypeOf(resource) == TypeOf(overlay)
func applyOverlayForSameTypes(resource, overlay interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	// detect the type of resource and overlay and select corresponding handler
	switch typedOverlay := overlay.(type) {
	// map
	case map[string]interface{}:
		typedResource := resource.(map[string]interface{})
		patches, err := applyOverlayToMap(typedResource, typedOverlay, path)
		if err != nil {
			return nil, err
		}
		appliedPatches = append(appliedPatches, patches...)
	// array
	case []interface{}:
		typedResource := resource.([]interface{})
		patches, err := applyOverlayToArray(typedResource, typedOverlay, path)
		if err != nil {
			return nil, err
		}
		appliedPatches = append(appliedPatches, patches...)
	// elementary types
	case string, float64, int64, bool:
		patch, err := replaceSubtree(overlay, path)
		if err != nil {
			return nil, err
		}
		appliedPatches = append(appliedPatches, patch)
	default:
		return nil, fmt.Errorf("Overlay has unsupported type: %T", overlay)
	}

	return appliedPatches, nil
}

// for each overlay and resource map elements applies overlay
func applyOverlayToMap(resourceMap, overlayMap map[string]interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	for key, value := range overlayMap {
		// skip anchor element because it has condition, not
		// the value that must replace resource value
		if anchor.IsConditionAnchor(key) {
			continue
		}

		noAnchorKey := removeAnchor(key)
		currentPath := path + noAnchorKey + "/"
		resourcePart, ok := resourceMap[noAnchorKey]

		if ok && !anchor.IsAddingAnchor(key) {
			// Key exists - go down through the overlay and resource trees
			patches, err := applyOverlay(resourcePart, value, currentPath)
			if err != nil {
				return nil, err
			}
			appliedPatches = append(appliedPatches, patches...)
		}

		if !ok {
			// Key does not exist - insert entire overlay subtree
			patch, err := insertSubtree(value, currentPath)
			if err != nil {
				return nil, err
			}
			appliedPatches = append(appliedPatches, patch)
		}
	}

	return appliedPatches, nil
}

// for each overlay and resource array elements applies overlay
func applyOverlayToArray(resource, overlay []interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	if 0 == len(overlay) {
		return nil, errors.New("Empty array detected in the overlay")
	}

	if 0 == len(resource) {
		// If array resource is empty, insert part from overlay
		patch, err := insertSubtree(overlay, path)
		if err != nil {
			return nil, err
		}
		appliedPatches = append(appliedPatches, patch)

		return appliedPatches, nil
	}

	if reflect.TypeOf(resource[0]) != reflect.TypeOf(overlay[0]) {
		return nil, fmt.Errorf("Overlay array and resource array have elements of different types: %T and %T", overlay[0], resource[0])
	}

	return applyOverlayToArrayOfSameTypes(resource, overlay, path)
}

// applyOverlayToArrayOfSameTypes applies overlay to array elements if they (resource and overlay elements) have same type
func applyOverlayToArrayOfSameTypes(resource, overlay []interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	switch overlay[0].(type) {
	case map[string]interface{}:
		return applyOverlayToArrayOfMaps(resource, overlay, path)
	default:
		lastElementIdx := len(resource)

		// Add elements to the end
		for i, value := range overlay {
			currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
			// currentPath example: /spec/template/spec/containers/3/
			patch, err := insertSubtree(value, currentPath)
			if err != nil {
				return nil, err
			}
			appliedPatches = append(appliedPatches, patch)
		}
	}

	return appliedPatches, nil
}

// Array of maps needs special handling as far as it can have anchors.
func applyOverlayToArrayOfMaps(resource, overlay []interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	lastElementIdx := len(resource)
	for i, overlayElement := range overlay {
		typedOverlay := overlayElement.(map[string]interface{})
		anchors := utils.GetAnchorsFromMap(typedOverlay)

		if len(anchors) > 0 {
			// If we have anchors - choose corresponding resource element and mutate it
			patches, err := applyOverlayWithAnchors(resource, overlayElement, path)
			if err != nil {
				return nil, err
			}
			appliedPatches = append(appliedPatches, patches...)
		} else if hasNestedAnchors(overlayElement) {
			// If we have anchors on the lower level - continue traversing overlay and resource trees
			for j, resourceElement := range resource {
				currentPath := path + strconv.Itoa(j) + "/"
				// currentPath example: /spec/template/spec/containers/3/
				patches, err := applyOverlay(resourceElement, overlayElement, currentPath)
				if err != nil {
					return nil, err
				}
				appliedPatches = append(appliedPatches, patches...)
			}
		} else {
			// Overlay subtree has no anchors - insert new element
			currentPath := path + strconv.Itoa(lastElementIdx+i) + "/"
			// currentPath example: /spec/template/spec/containers/3/
			patch, err := insertSubtree(overlayElement, currentPath)
			if err != nil {
				return nil, err
			}
			appliedPatches = append(appliedPatches, patch)
		}
	}

	return appliedPatches, nil
}

func applyOverlayWithAnchors(resource []interface{}, overlay interface{}, path string) ([][]byte, error) {
	var appliedPatches [][]byte

	for i, resourceElement := range resource {
		currentPath := path + strconv.Itoa(i) + "/"
		// currentPath example: /spec/template/spec/containers/3/
		patches, err := applyOverlay(resourceElement, overlay, currentPath)
		if err != nil {
			return nil, err
		}
		appliedPatches = append(appliedPatches, patches...)
	}

	return appliedPatches, nil
}

func insertSubtree(overlay interface{}, path string) ([]byte, error) {
	return processSubtree(overlay, path, "add")
}

func replaceSubtree(overlay interface{}, path string) ([]byte, error) {
	return processSubtree(overlay, path, "replace")
}

func processSubtree(overlay interface{}, path string, op string) ([]byte, error) {

	if len(path) > 1 && path[len(path)-1] == '/' {
		path = path[:len(path)-1]
	}

	path = preparePath(path)
	value := prepareJSONValue(overlay)
	patchStr := fmt.Sprintf(`{ "op": "%s", "path": "%s", "value":%s }`, op, path, value)

	// explicitly handle boolean type in annotation
	// keep the type boolean as it is in any other fields
	if strings.Contains(path, "/metadata/annotations") {
		patchStr = wrapBoolean(patchStr)
	}

	// check the patch
	_, err := jsonpatch.DecodePatch([]byte("[" + patchStr + "]"))
	if err != nil {
		return nil, fmt.Errorf("Failed to make '%s' patch from an overlay '%s' for path %s, err: %v", op, value, path, err)
	}

	return []byte(patchStr), nil
}

func preparePath(path string) string {
	if path == "" {
		path = "/"
	}

	annPath := "/metadata/annotations/"
	// escape slash in annotation patch
	if strings.Contains(path, annPath) {
		idx := strings.Index(path, annPath)
		p := path[idx+len(annPath):]
		path = path[:idx+len(annPath)] + strings.ReplaceAll(p, "/", "~1")
	}
	return path
}

// converts overlay to JSON string to be inserted into the JSON Patch
func prepareJSONValue(overlay interface{}) string {
	var err error
	// Need to remove anchors from the overlay struct
	overlayWithoutAnchors := removeAnchorFromSubTree(overlay)
	jsonOverlay, err := json.Marshal(overlayWithoutAnchors)
	if err != nil || hasOnlyAnchors(overlay) {
		log.Log.Error(err, "failed to marshall withoutanchors or has only anchors")
		return ""
	}

	return string(jsonOverlay)
}

func removeAnchorFromSubTree(overlay interface{}) interface{} {
	var result interface{}
	switch typed := overlay.(type) {
	case map[string]interface{}:
		// assuming only keys have anchors
		result = removeAnchroFromMap(typed)
	case []interface{}:
		arrayResult := make([]interface{}, 0)
		for _, value := range typed {
			arrayResult = append(arrayResult, removeAnchorFromSubTree(value))
		}
		result = arrayResult
	default:
		result = overlay
	}
	return result
}

func removeAnchroFromMap(overlay map[string]interface{}) map[string]interface{} {
	result := make(map[string]interface{})
	for k, v := range overlay {
		result[getRawKeyIfWrappedWithAttributes(k)] = removeAnchorFromSubTree(v)
	}
	return result
}

// Anchor has pattern value, so resource shouldn't be mutated with it
// If entire subtree has only anchor keys - we should skip inserting it
func hasOnlyAnchors(overlay interface{}) bool {
	switch typed := overlay.(type) {
	case map[string]interface{}:
		if anchors := utils.GetAnchorsFromMap(typed); len(anchors) == len(typed) {
			return true
		}

		for _, value := range typed {
			if !hasOnlyAnchors(value) {
				return false
			}
		}
	case []interface{}:
		for _, value := range typed {
			if !hasOnlyAnchors(value) {
				return false
			}
		}
	default:
		return false
	}

	return true
}

// Checks if subtree has anchors
func hasNestedAnchors(overlay interface{}) bool {
	switch typed := overlay.(type) {
	case map[string]interface{}:
		if anchors := utils.GetAnchorsFromMap(typed); len(anchors) > 0 {
			return true
		}

		for _, value := range typed {
			if hasNestedAnchors(value) {
				return true
			}
		}
		return false
	case []interface{}:
		for _, value := range typed {
			if hasNestedAnchors(value) {
				return true
			}
		}
		return false
	default:
		return false
	}
}

func wrapBoolean(patchStr string) string {
	reTrue := regexp.MustCompile(`:\s*true\s*`)
	if idx := reTrue.FindStringIndex(patchStr); len(idx) != 0 {
		return fmt.Sprintf("%s:\"true\"%s", patchStr[:idx[0]], patchStr[idx[1]:])
	}

	reFalse := regexp.MustCompile(`:\s*false\s*`)
	if idx := reFalse.FindStringIndex(patchStr); len(idx) != 0 {
		return fmt.Sprintf("%s:\"false\"%s", patchStr[:idx[0]], patchStr[idx[1]:])
	}
	return patchStr
}
