// Copyright 2017 TiKV Project Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package logutil

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"sync/atomic"

	"github.com/pingcap/log"
	"github.com/tikv/pd/pkg/errs"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// FileLogConfig serializes file log related config in toml/json.
type FileLogConfig struct {
	// Log filename, leave empty to disable file log.
	Filename string `toml:"filename" json:"filename"`
	// Max size for a single file, in MB.
	MaxSize int `toml:"max-size" json:"max-size"`
	// Max log keep days, default is never deleting.
	MaxDays int `toml:"max-days" json:"max-days"`
	// Maximum number of old log files to retain.
	MaxBackups int `toml:"max-backups" json:"max-backups"`
}

// LogConfig serializes log related config in toml/json.
type LogConfig struct {
	// Log level.
	Level string `toml:"level" json:"level"`
	// Log format. one of json, text, or console.
	Format string `toml:"format" json:"format"`
	// Disable automatic timestamps in output.
	DisableTimestamp bool `toml:"disable-timestamp" json:"disable-timestamp"`
	// File log config.
	File FileLogConfig `toml:"file" json:"file"`
}

// StringToZapLogLevel translates log level string to log level.
func StringToZapLogLevel(level string) zapcore.Level {
	switch strings.ToLower(level) {
	case "fatal":
		return zapcore.FatalLevel
	case "error":
		return zapcore.ErrorLevel
	case "warn", "warning":
		return zapcore.WarnLevel
	case "debug":
		return zapcore.DebugLevel
	case "info":
		return zapcore.InfoLevel
	}
	return zapcore.InfoLevel
}

// SetupLogger setup the logger.
func SetupLogger(
	logConfig *log.Config,
	logger **zap.Logger,
	logProps **log.ZapProperties,
	redactInfoLogType RedactInfoLogType,
) error {
	lg, p, err := log.InitLogger(logConfig, zap.AddStacktrace(zapcore.FatalLevel))
	if err != nil {
		return errs.ErrInitLogger.Wrap(err)
	}
	*logger = lg
	*logProps = p
	setRedactType(redactInfoLogType)
	return nil
}

// LogPanic logs the panic reason and stack, then exit the process.
// Commonly used with a `defer`.
func LogPanic() {
	if e := recover(); e != nil {
		log.Fatal("panic", zap.Reflect("recover", e))
	}
}

// RedactInfoLogType is the behavior of redacting sensitive information in logs.
type RedactInfoLogType int

const (
	// RedactInfoLogOFF means log redaction is disabled.
	RedactInfoLogOFF RedactInfoLogType = iota
	// RedactInfoLogON means log redaction is enabled, and will replace the sensitive information with "?".
	RedactInfoLogON
	// RedactInfoLogMarker means log redaction is enabled, and will use single guillemets ‹› to enclose the sensitive information.
	RedactInfoLogMarker
)

// MarshalJSON implements the `json.Marshaler` interface to ensure the compatibility.
func (t RedactInfoLogType) MarshalJSON() ([]byte, error) {
	switch t {
	case RedactInfoLogON:
		return json.Marshal(true)
	case RedactInfoLogMarker:
		return json.Marshal("MARKER")
	default:
	}
	return json.Marshal(false)
}

const invalidRedactInfoLogTypeErrMsg = `the "redact-info-log" value is invalid; it should be either false, true, or "MARKER"`

// UnmarshalJSON implements the `json.Marshaler` interface to ensure the compatibility.
func (t *RedactInfoLogType) UnmarshalJSON(data []byte) error {
	var s string
	err := json.Unmarshal(data, &s)
	if err == nil && strings.ToUpper(s) == "MARKER" {
		*t = RedactInfoLogMarker
		return nil
	}
	var b bool
	err = json.Unmarshal(data, &b)
	if err != nil {
		return errors.New(invalidRedactInfoLogTypeErrMsg)
	}
	if b {
		*t = RedactInfoLogON
	} else {
		*t = RedactInfoLogOFF
	}
	return nil
}

// UnmarshalTOML implements the `toml.Unmarshaler` interface to ensure the compatibility.
func (t *RedactInfoLogType) UnmarshalTOML(data any) error {
	switch v := data.(type) {
	case bool:
		if v {
			*t = RedactInfoLogON
		} else {
			*t = RedactInfoLogOFF
		}
		return nil
	case string:
		if strings.ToUpper(v) == "MARKER" {
			*t = RedactInfoLogMarker
			return nil
		}
		return errors.New(invalidRedactInfoLogTypeErrMsg)
	default:
	}
	return errors.New(invalidRedactInfoLogTypeErrMsg)
}

var (
	curRedactType atomic.Value
)

func init() {
	setRedactType(RedactInfoLogOFF)
}

func getRedactType() RedactInfoLogType {
	return curRedactType.Load().(RedactInfoLogType)
}

func setRedactType(redactInfoLogType RedactInfoLogType) {
	curRedactType.Store(redactInfoLogType)
}

const (
	leftMark  = '‹'
	rightMark = '›'
)

func redactInfo(input string) string {
	res := &strings.Builder{}
	res.Grow(len(input) + 2)
	_, _ = res.WriteRune(leftMark)
	for _, c := range input {
		// Double the mark character if it is already in the input string.
		// to avoid the ambiguity of the redacted content.
		if c == leftMark || c == rightMark {
			_, _ = res.WriteRune(c)
			_, _ = res.WriteRune(c)
		} else {
			_, _ = res.WriteRune(c)
		}
	}
	_, _ = res.WriteRune(rightMark)
	return res.String()
}

// ZapRedactByteString receives []byte argument and return omitted information zap.Field if redact log enabled
func ZapRedactByteString(key string, arg []byte) zap.Field {
	return zap.ByteString(key, RedactBytes(arg))
}

// ZapRedactString receives string argument and return omitted information in zap.Field if redact log enabled
func ZapRedactString(key, arg string) zap.Field {
	return zap.String(key, RedactString(arg))
}

// ZapRedactStringer receives stringer argument and return omitted information in zap.Field  if redact log enabled
func ZapRedactStringer(key string, arg fmt.Stringer) zap.Field {
	return zap.Stringer(key, RedactStringer(arg))
}

// RedactBytes receives []byte argument and return omitted information if redact log enabled
func RedactBytes(arg []byte) []byte {
	switch getRedactType() {
	case RedactInfoLogON:
		return []byte("?")
	case RedactInfoLogMarker:
		// Use unsafe conversion to avoid copy.
		return []byte(redactInfo(string(arg)))
	default:
	}
	return arg
}

// RedactString receives string argument and return omitted information if redact log enabled
func RedactString(arg string) string {
	switch getRedactType() {
	case RedactInfoLogON:
		return "?"
	case RedactInfoLogMarker:
		return redactInfo(arg)
	default:
	}
	return arg
}

// RedactStringer receives stringer argument and return omitted information if redact log enabled
func RedactStringer(arg fmt.Stringer) fmt.Stringer {
	switch getRedactType() {
	case RedactInfoLogON:
		return &redactedStringer{"?"}
	case RedactInfoLogMarker:
		return &redactedStringer{redactInfo(arg.String())}
	default:
	}
	return arg
}

type redactedStringer struct {
	content string
}

// String implement fmt.Stringer
func (rs *redactedStringer) String() string {
	return rs.content
}

// CondUint32 constructs a field with the given key and value conditionally.
// If the condition is true, it constructs a field with uint32 type; otherwise,
// skip the field.
func CondUint32(key string, val uint32, condition bool) zap.Field {
	if condition {
		return zap.Uint32(key, val)
	}
	return zap.Skip()
}

// IsLevelLegal checks whether the level is legal.
func IsLevelLegal(level string) bool {
	switch strings.ToLower(level) {
	case "fatal", "error", "warn", "warning", "debug", "info":
		return true
	default:
		return false
	}
}
