package obfuscation

import (
	"fmt"
	"strconv"
	"strings"

	"golang.org/x/exp/slices"

	"github.com/rs/zerolog/log"
	"github.com/valyala/fastjson"
)

type Obfuscator struct {
	Hasher Hasher
}

func (obfuscator Obfuscator) ObfuscateString(raw string) string {
	return obfuscator.Hasher.HashBytes([]byte(raw))
}

var (
	parserPool fastjson.ParserPool
	arenaPool  fastjson.ArenaPool
)

func (obfuscator Obfuscator) ObfuscateJSON(
	raw string,
	excludedPaths []string,
) (string, error) {
	parser := parserPool.Get()
	defer parserPool.Put(parser)
	json, err := parser.Parse(raw)
	if err != nil {
		return "", err
	}

	arena := arenaPool.Get()
	defer arenaPool.Put(arena)
	obfuscatedJSON, err := obfuscator.obfuscateJSON(
		*json,
		arena,
		"",
		excludedPaths,
		false,
	)
	if err != nil {
		return "", err
	}

	bytes := obfuscatedJSON.MarshalTo([]byte{})
	return string(bytes), nil
}

func (obfuscator Obfuscator) obfuscateJSON(
	raw fastjson.Value,
	arena *fastjson.Arena,
	cursor string,
	excludedPaths []string,
	onExcludedPath bool,
) (*fastjson.Value, error) {
	var obfuscatedJSON *fastjson.Value

	onExcludedPath = onExcludedPath || isCursorInExcludedPath(cursor, excludedPaths)
	log.Trace().
		Msgf("cursor: %v, onExcludedPath: %v", cursor, onExcludedPath)

	if onExcludedPath {
		return &raw, nil
	}
	switch raw.Type() {
	// Handle complex JSON types which require recursive handling
	case fastjson.TypeArray:
		array, err := raw.Array()
		if err != nil {
			return nil, err
		}
		obfuscatedArray := arena.NewArray()
		for i, item := range array { //nolint:varnamelen
			obfuscatedItem, err := obfuscator.obfuscateJSON(
				*item,
				arena,
				fmt.Sprintf("%s[]", cursor),
				excludedPaths,
				onExcludedPath,
			)
			if err != nil {
				return nil, err
			}
			obfuscatedArray.SetArrayItem(i, obfuscatedItem)
		}
		obfuscatedJSON = obfuscatedArray
	case fastjson.TypeObject:
		object, err := raw.Object()
		if err != nil {
			return nil, err
		}
		keys := getKeys(object)
		obfuscatedObject := arena.NewObject()
		for _, key := range keys {
			value := object.Get(key)
			obfuscatedValue, err := obfuscator.obfuscateJSON(
				*value,
				arena,
				fmt.Sprintf("%s.%s", cursor, key),
				excludedPaths,
				onExcludedPath,
			)
			if err != nil {
				return nil, err
			}
			obfuscatedObject.Set(key, obfuscatedValue)
		}
		obfuscatedJSON = obfuscatedObject
	// Handle primitive JSON types
	case fastjson.TypeNumber:
		number, err := raw.Float64()
		if err != nil {
			return nil, err
		}
		// we are keeping two decimal points for all numbers before obfuscation -
		// 10 will become 10.00, 10.999 will become 10.99
		str := strconv.FormatFloat(number, 'f', 2, 64)
		obfuscatedString := arena.NewString(
			obfuscator.Hasher.HashBytes([]byte(str)),
		)
		obfuscatedJSON = obfuscatedString
	case fastjson.TypeString:
		stringBytes, err := raw.StringBytes()
		if err != nil {
			return nil, err
		}
		obfuscatedString := arena.NewString(
			obfuscator.Hasher.HashBytes(stringBytes),
		)
		obfuscatedJSON = obfuscatedString
	// Handle constant JSON types
	case fastjson.TypeTrue:
		obfuscatedString := arena.NewString(
			obfuscator.Hasher.HashBytes([]byte("true")),
		)
		obfuscatedJSON = obfuscatedString
	case fastjson.TypeFalse:
		obfuscatedString := arena.NewString(
			obfuscator.Hasher.HashBytes([]byte("false")),
		)
		obfuscatedJSON = obfuscatedString
	case fastjson.TypeNull:
		obfuscatedString := arena.NewString(
			obfuscator.Hasher.HashBytes([]byte("null")),
		)
		obfuscatedJSON = obfuscatedString
	}

	return obfuscatedJSON, nil
}

// isCursorInExcludedPath checks if the given path segment should be excluded from obfuscation
// usage only slices.Contains(excludedPaths, cursor) cannot work for JSONPath exclusions,
// since it compares the whole string and works only for simple strings exclusions
func isCursorInExcludedPath(cursor string, excludedPaths []string) bool {
	// simple string comparison
	if slices.Contains(excludedPaths, cursor) {
		return true
	}

	// json path support
	if cursor == "" {
		return false
	}
	for _, path := range excludedPaths {
		if strings.HasSuffix(path, cursor) {
			return true
		}
	}
	return false
}

func getKeys(object *fastjson.Object) []string {
	keys := []string{}
	collectKeys := func(key []byte, _ *fastjson.Value) {
		keys = append(keys, string(key))
	}
	object.Visit(collectKeys)

	return keys
}
