package utils

import (
	"reflect"
	"strings"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

const (
	redactedValue  = "***redacted***"
	maxRedactDepth = 10
)

// redactingCore wraps a zapcore.Core and redacts sensitive fields (like passwords)
// before they are written to the underlying core.
type redactingCore struct {
	zapcore.Core
}

func (c redactingCore) With(fields []zap.Field) zapcore.Core {
	return redactingCore{Core: c.Core.With(redactFields(fields))}
}

//nolint:gocritic // ent must match zapcore.Core interface signature
func (c redactingCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
	if c.Enabled(ent.Level) {
		ce = ce.AddCore(ent, c)
	}
	return ce
}

//nolint:gocritic // ent must match zapcore.Core interface signature
func (c redactingCore) Write(ent zapcore.Entry, fields []zap.Field) error {
	return c.Core.Write(ent, redactFields(fields))
}

// WrapWithRedactor returns a zap.Option that wraps the core with redactingCore.
func WrapWithRedactor() zap.Option {
	return zap.WrapCore(func(core zapcore.Core) zapcore.Core {
		return redactingCore{Core: core}
	})
}

func redactFields(fields []zap.Field) []zap.Field {
	out := make([]zap.Field, len(fields))
	for i, f := range fields {
		keyIsSensitive := isSensitiveKey(f.Key)
		if f.String != "" || f.Integer != 0 {
			if keyIsSensitive {
				out[i] = zap.String(f.Key, redactedValue)
			} else {
				out[i] = f
			}
		} else if f.Interface != nil {
			redacted, changed := redactAnyValue(reflect.ValueOf(f.Interface), keyIsSensitive, 0)
			if !changed {
				out[i] = f
				continue
			}
			out[i] = zap.Any(f.Key, redacted)
		} else {
			out[i] = f
		}
	}
	return out
}

// redactPrimitiveValue handles redaction of primitive values.
// Returns (redactedValue, true) if the value is sensitive and not zero,
// otherwise returns (v.Interface(), false).
func redactPrimitiveValue(v reflect.Value, isSensitive bool) (any, bool) {
	if !v.CanInterface() {
		return nil, false
	}
	if isSensitive && !v.IsZero() {
		return redactedValue, true
	}
	return v.Interface(), false
}

// redactAnyValue recursively processes a reflect.Value and redacts sensitive values.
// It calls itself recursively for nested structures.
func redactAnyValue(v reflect.Value, isSensitive bool, depth int) (any, bool) {
	if !v.IsValid() {
		return nil, false
	}
	if depth >= maxRedactDepth {
		return redactPrimitiveValue(v, isSensitive)
	}
	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
		if v.IsNil() {
			return nil, false
		}
		v = v.Elem()
	}
	switch v.Kind() {
	case reflect.Map:
		if v.Type().Key().Kind() != reflect.String {
			return redactPrimitiveValue(v, isSensitive)
		}
		out := make(map[string]any, v.Len())
		changed := false
		iter := v.MapRange()
		for iter.Next() {
			kVal := iter.Key()
			val := iter.Value()
			keyStr := kVal.String()
			keyIsSensitive := isSensitiveKey(keyStr)
			redacted, nestedChanged := redactAnyValue(val, keyIsSensitive, depth+1)
			out[keyStr] = redacted
			if nestedChanged {
				changed = true
			}
		}
		return out, changed
	case reflect.Struct:
		out := make(map[string]any, v.NumField())
		changed := false
		t := v.Type()
		for i := 0; i < v.NumField(); i++ {
			field := t.Field(i)
			fieldVal := v.Field(i)
			if !fieldVal.CanInterface() {
				continue
			}
			name := field.Name
			fieldIsSensitive := isSensitiveField(&field)
			redacted, nestedChanged := redactAnyValue(fieldVal, fieldIsSensitive, depth+1)
			out[name] = redacted
			if nestedChanged {
				changed = true
			}
		}
		return out, changed
	case reflect.Slice, reflect.Array:
		n := v.Len()
		out := make([]any, n)
		changed := false
		for i := 0; i < n; i++ {
			redacted, nestedChanged := redactAnyValue(v.Index(i), false, depth+1)
			out[i] = redacted
			if nestedChanged {
				changed = true
			}
		}
		return out, changed
	default:
		return redactPrimitiveValue(v, isSensitive)
	}
}

// isSensitiveField checks if a struct field is sensitive by checking both the field name
// and common struct tags (json, mapstructure, yaml).
func isSensitiveField(field *reflect.StructField) bool {
	// Check field name
	if isSensitiveKey(field.Name) {
		return true
	}
	// Check struct tags
	tagKeys := []string{"json", "mapstructure", "yaml"}
	for _, tagKey := range tagKeys {
		tag := field.Tag.Get(tagKey)
		if tag == "" {
			continue
		}
		// Extract the tag value (before comma, if present)
		tagValue := strings.Split(tag, ",")[0]
		if tagValue != "" && isSensitiveKey(tagValue) {
			return true
		}
	}
	return false
}

// isSensitiveKey determines whether a log field key should be treated as sensitive.
// It only treats keys as sensitive if they are:
//   - any key whose name ends with "password" (case-insensitive), e.g.:
//     "password", "db_password", "user-password", "session.password"
//   - or a field named "sastoken" (case-insensitive).
//   - or a field named "ServiceAccountKey" (case-insensitive).
//
// This avoids over-matching generic words like "secret" or "token".
func isSensitiveKey(k string) bool {
	lk := strings.ToLower(k)

	// Special-case field names.
	if lk == "sastoken" || lk == "serviceaccountkey" {
		return true
	}

	// Any key that ends with "password" is considered sensitive.
	return strings.HasSuffix(lk, "password")
}
