// Package logger add by blacknc <blacknc@163.com> at 2024/03/13 15:05:49
package logger

import (
	"context"
	"crypto/md5"
	"dataapps/commons/contextvar"
	"encoding/hex"
	"reflect"
	"regexp"
	"strings"

	"github.com/tidbcloud/tiinf/errs"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

const redactSalt = "7e8b7a3a7bd9c3e5f6a4b2c1d0e9f" // random salt to prevent hash collisions with real keys

func LogWithError(ctx context.Context, err error, msg string, fields ...zap.Field) {
	logLevel := zap.ErrorLevel
	if err == nil {
		logLevel = zap.InfoLevel
	} else if IsClientErr(err) ||
		IsTestUser(contextvar.AccountID(ctx)) ||
		strings.Contains(err.Error(), "context deadline exceeded") ||
		strings.Contains(err.Error(), "context canceled") {
		logLevel = zap.WarnLevel
	}
	fields = append(fields, zap.Error(err))
	contextvar.Logger(ctx).Log(logLevel, msg, fields...)
}

func Info(ctx context.Context, msg string, fields ...zap.Field) {
	contextvar.Logger(ctx).Info(msg, fields...)
}

func Warn(ctx context.Context, msg string, fields ...zap.Field) {
	contextvar.Logger(ctx).Warn(msg, fields...)
}

func Error(ctx context.Context, msg string, fields ...zap.Field) {
	contextvar.Logger(ctx).Error(msg, fields...)
}

func Log(ctx context.Context, level zapcore.Level, msg string, fields ...zap.Field) {
	contextvar.Logger(ctx).Log(level, msg, fields...)
}

func IsTestUser(email string) bool {
	switch email {
	case "neebiv@snapmail.cc":
		return true
	default:
		return false
	}
}

func IsClientErr(err error) bool {
	if err == nil {
		return false
	}
	if tierr, ok := err.(errs.Error); ok {
		return tierr.HTTPCode() >= 400 && tierr.HTTPCode() < 500
	}
	return false
}

// Redact logs value under key, redacting Credential.Key if present and any
// string field whose value contains "password". It copies value before
// redacting so the original is never mutated. Both value and its Credential
// field may be pointers or plain structs.
func Redact(key string, value interface{}) zap.Field {
	if value == nil {
		return zap.Any(key, nil)
	}

	v := reflect.ValueOf(value)
	isPtr := v.Kind() == reflect.Ptr
	if isPtr {
		if v.IsNil() {
			return zap.Any(key, value)
		}
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return zap.Any(key, value)
	}

	// Copy the outer struct so we never mutate the original.
	outerCopy := reflect.New(v.Type()).Elem()
	outerCopy.Set(v)
	redactPasswords(outerCopy)
	ret := func() interface{} {
		if isPtr {
			return outerCopy.Addr().Interface()
		}
		return outerCopy.Interface()
	}

	credField := v.FieldByName("Credential")
	if !credField.IsValid() {
		return zap.Any(key, ret())
	}

	// Resolve Credential to its underlying struct (deref pointer if needed).
	credIsPtr := credField.Kind() == reflect.Ptr
	credVal := credField
	if credIsPtr {
		if credField.IsNil() {
			return zap.Any(key, ret())
		}
		credVal = credField.Elem()
	}
	if credVal.Kind() != reflect.Struct {
		return zap.Any(key, ret())
	}

	keyField := credVal.FieldByName("Key")
	if !keyField.IsValid() || keyField.Kind() != reflect.String || keyField.String() == "" {
		return zap.Any(key, ret())
	}

	orig := keyField.String()
	sum := md5.Sum([]byte(orig + redactSalt))
	redactedValue := "[md5]" + hex.EncodeToString(sum[:])

	if credIsPtr {
		// The copy shares the original Credential pointer; make an independent copy.
		credCopy := reflect.New(credVal.Type()).Elem()
		credCopy.Set(credVal)
		credCopy.FieldByName("Key").SetString(redactedValue)
		outerCopy.FieldByName("Credential").Set(credCopy.Addr())
	} else {
		// Credential was copied by value; redact the Key directly in the copy.
		outerCopy.FieldByName("Credential").FieldByName("Key").SetString(redactedValue)
	}

	return zap.Any(key, ret())
}

// sqlSecretRe matches password-like assignments in raw SQL text (single- or
// double-quoted strings, with '' escapes, or bare unquoted values).
var sqlSecretRe = regexp.MustCompile(`(?i)(password|passwd|pwd)([a-z0-9_]*)(\s*=\s*)(N?'(?:[^']|'')+'|"(?:[^"]|"")+"|[^\s,;)]+)`)

// RedactSQL masks the literal value of password-like assignments in raw SQL
// text, e.g. `update t set password='2'` -> `password='[REDACTED]'`.
func RedactSQL(sql string) string {
	return sqlSecretRe.ReplaceAllString(sql, `$1$2$3'[REDACTED]'`)
}

// redactPasswords replaces, in place on the given struct copy, exported string
// fields whose value contains "password" with "[REDACTED]".
func redactPasswords(s reflect.Value) {
	t := s.Type()
	for i := 0; i < t.NumField(); i++ {
		if f := s.Field(i); f.Kind() == reflect.String && t.Field(i).PkgPath == "" &&
			strings.Contains(strings.ToLower(f.String()), "password") {
			f.SetString("[REDACTED]")
		}
	}
}
