// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tfsdklog

import (
	"context"
	"regexp"

	"github.com/hashicorp/go-hclog"
	"github.com/hashicorp/terraform-plugin-log/internal/hclogutils"
	"github.com/hashicorp/terraform-plugin-log/internal/logging"
)

// NewRootSDKLogger returns a new context.Context that contains an SDK logger
// configured with the passed options.
func NewRootSDKLogger(ctx context.Context, options ...logging.Option) context.Context {
	opts := logging.ApplyLoggerOpts(options...)
	if opts.Name == "" {
		opts.Name = logging.DefaultSDKRootLoggerName
	}
	if sink := logging.GetSink(ctx); sink != nil {
		logger := sink.Named(opts.Name)
		sinkLoggerOptions := logging.GetSinkOptions(ctx)
		sdkLoggerOptions := hclogutils.LoggerOptionsCopy(sinkLoggerOptions)
		sdkLoggerOptions.Name = opts.Name

		if opts.Level != hclog.NoLevel {
			logger.SetLevel(opts.Level)
			sdkLoggerOptions.Level = opts.Level
		}

		ctx = logging.SetSDKRootLogger(ctx, logger)
		ctx = logging.SetSDKRootLoggerOptions(ctx, sdkLoggerOptions)

		return ctx
	}
	if opts.Level == hclog.NoLevel {
		opts.Level = hclog.Trace
	}

	// Cache root logger level outside context for performance reasons.
	rootLevelMutex.Lock()

	rootLevel = opts.Level

	rootLevelMutex.Unlock()

	loggerOptions := &hclog.LoggerOptions{
		Name:                     opts.Name,
		Level:                    opts.Level,
		JSONFormat:               true,
		IndependentLevels:        true,
		IncludeLocation:          opts.IncludeLocation,
		DisableTime:              !opts.IncludeTime,
		Output:                   opts.Output,
		AdditionalLocationOffset: opts.AdditionalLocationOffset,
	}

	ctx = logging.SetSDKRootLogger(ctx, hclog.New(loggerOptions))
	ctx = logging.SetSDKRootLoggerOptions(ctx, loggerOptions)

	return ctx
}

// NewRootProviderLogger returns a new context.Context that contains a provider
// logger configured with the passed options.
func NewRootProviderLogger(ctx context.Context, options ...logging.Option) context.Context {
	opts := logging.ApplyLoggerOpts(options...)
	if opts.Name == "" {
		opts.Name = logging.DefaultProviderRootLoggerName
	}
	if sink := logging.GetSink(ctx); sink != nil {
		logger := sink.Named(opts.Name)
		sinkLoggerOptions := logging.GetSinkOptions(ctx)
		providerLoggerOptions := hclogutils.LoggerOptionsCopy(sinkLoggerOptions)
		providerLoggerOptions.Name = opts.Name

		if opts.Level != hclog.NoLevel {
			logger.SetLevel(opts.Level)
			providerLoggerOptions.Level = opts.Level
		}

		ctx = logging.SetProviderRootLogger(ctx, logger)
		ctx = logging.SetProviderRootLoggerOptions(ctx, providerLoggerOptions)

		return ctx
	}
	if opts.Level == hclog.NoLevel {
		opts.Level = hclog.Trace
	}
	loggerOptions := &hclog.LoggerOptions{
		Name:                     opts.Name,
		Level:                    opts.Level,
		JSONFormat:               true,
		IndependentLevels:        true,
		IncludeLocation:          opts.IncludeLocation,
		DisableTime:              !opts.IncludeTime,
		Output:                   opts.Output,
		AdditionalLocationOffset: opts.AdditionalLocationOffset,
	}

	ctx = logging.SetProviderRootLogger(ctx, hclog.New(loggerOptions))
	ctx = logging.SetProviderRootLoggerOptions(ctx, loggerOptions)

	return ctx
}

// SetField returns a new context.Context that has a modified logger in it which
// will include key and value as fields in all its log output.
//
// In case of the same key is used multiple times (i.e. key collision),
// the last one set is the one that gets persisted and then outputted with the logs.
func SetField(ctx context.Context, key string, value interface{}) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithField(key, value)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// Trace logs `msg` at the trace level to the logger in `ctx`, with optional
// `additionalFields` structured key-value fields in the log output. Fields are
// shallow merged with any defined on the logger, e.g. by the `SetField()` function,
// and across multiple maps.
func Trace(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
	logger := logging.GetSDKRootLogger(ctx)
	if logger == nil {
		// this essentially should never happen in production the root
		// logger for  code should be injected by the  in
		// question, so really this is only likely in unit tests, at
		// most so just making this a no-op is fine
		return
	}

	additionalArgs, shouldOmit := logging.OmitOrMask(logging.GetSDKRootTFLoggerOpts(ctx), &msg, additionalFields)
	if shouldOmit {
		return
	}

	logger.Trace(msg, additionalArgs...)
}

// Debug logs `msg` at the debug level to the logger in `ctx`, with optional
// `additionalFields` structured key-value fields in the log output. Fields are
// shallow merged with any defined on the logger, e.g. by the `SetField()` function,
// and across multiple maps.
func Debug(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
	logger := logging.GetSDKRootLogger(ctx)
	if logger == nil {
		// this essentially should never happen in production the root
		// logger for  code should be injected by the  in
		// question, so really this is only likely in unit tests, at
		// most so just making this a no-op is fine
		return
	}

	additionalArgs, shouldOmit := logging.OmitOrMask(logging.GetSDKRootTFLoggerOpts(ctx), &msg, additionalFields)
	if shouldOmit {
		return
	}

	logger.Debug(msg, additionalArgs...)
}

// Info logs `msg` at the info level to the logger in `ctx`, with optional
// `additionalFields` structured key-value fields in the log output. Fields are
// shallow merged with any defined on the logger, e.g. by the `SetField()` function,
// and across multiple maps.
func Info(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
	logger := logging.GetSDKRootLogger(ctx)
	if logger == nil {
		// this essentially should never happen in production the root
		// logger for  code should be injected by the  in
		// question, so really this is only likely in unit tests, at
		// most so just making this a no-op is fine
		return
	}

	additionalArgs, shouldOmit := logging.OmitOrMask(logging.GetSDKRootTFLoggerOpts(ctx), &msg, additionalFields)
	if shouldOmit {
		return
	}

	logger.Info(msg, additionalArgs...)
}

// Warn logs `msg` at the warn level to the logger in `ctx`, with optional
// `additionalFields` structured key-value fields in the log output. Fields are
// shallow merged with any defined on the logger, e.g. by the `SetField()` function,
// and across multiple maps.
func Warn(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
	logger := logging.GetSDKRootLogger(ctx)
	if logger == nil {
		// this essentially should never happen in production the root
		// logger for  code should be injected by the  in
		// question, so really this is only likely in unit tests, at
		// most so just making this a no-op is fine
		return
	}

	additionalArgs, shouldOmit := logging.OmitOrMask(logging.GetSDKRootTFLoggerOpts(ctx), &msg, additionalFields)
	if shouldOmit {
		return
	}

	logger.Warn(msg, additionalArgs...)
}

// Error logs `msg` at the error level to the logger in `ctx`, with optional
// `additionalFields` structured key-value fields in the log output. Fields are
// shallow merged with any defined on the logger, e.g. by the `SetField()` function,
// and across multiple maps.
func Error(ctx context.Context, msg string, additionalFields ...map[string]interface{}) {
	logger := logging.GetSDKRootLogger(ctx)
	if logger == nil {
		// this essentially should never happen in production the root
		// logger for  code should be injected by the  in
		// question, so really this is only likely in unit tests, at
		// most so just making this a no-op is fine
		return
	}

	additionalArgs, shouldOmit := logging.OmitOrMask(logging.GetSDKRootTFLoggerOpts(ctx), &msg, additionalFields)
	if shouldOmit {
		return
	}

	logger.Error(msg, additionalArgs...)
}

// OmitLogWithFieldKeys returns a new context.Context that has a modified logger
// that will omit to write any log when any of the given keys is found
// within its fields.
//
// Each call to this function is additive:
// the keys to omit by are added to the existing configuration.
//
// Example:
//
//	configuration = `['foo', 'baz']`
//
//	log1 = `{ msg = "...", fields = { 'foo': '...', 'bar': '...' }`  -> omitted
//	log2 = `{ msg = "...", fields = { 'bar': '...' }`                -> printed
//	log3 = `{ msg = "...", fields = { 'baz': '...', 'boo': '...' }`  -> omitted
func OmitLogWithFieldKeys(ctx context.Context, keys ...string) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithOmitLogWithFieldKeys(keys...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// OmitLogWithMessageRegexes returns a new context.Context that has a modified logger
// that will omit to write any log that has a message matching any of the
// given *regexp.Regexp.
//
// Each call to this function is additive:
// the regexp to omit by are added to the existing configuration.
//
// Example:
//
//	configuration = `[regexp.MustCompile("(foo|bar)")]`
//
//	log1 = `{ msg = "banana apple foo", fields = {...}`     -> omitted
//	log2 = `{ msg = "pineapple mango", fields = {...}`      -> printed
//	log3 = `{ msg = "pineapple mango bar", fields = {...}`  -> omitted
func OmitLogWithMessageRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithOmitLogWithMessageRegexes(expressions...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// OmitLogWithMessageStrings  returns a new context.Context that has a modified logger
// that will omit to write any log that matches any of the given string.
//
// Each call to this function is additive:
// the string to omit by are added to the existing configuration.
//
// Example:
//
//	configuration = `['foo', 'bar']`
//
//	log1 = `{ msg = "banana apple foo", fields = {...}`     -> omitted
//	log2 = `{ msg = "pineapple mango", fields = {...}`      -> printed
//	log3 = `{ msg = "pineapple mango bar", fields = {...}`  -> omitted
func OmitLogWithMessageStrings(ctx context.Context, matchingStrings ...string) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithOmitLogWithMessageStrings(matchingStrings...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskFieldValuesWithFieldKeys returns a new context.Context that has a modified logger
// that masks (replaces) with asterisks (`***`) any field value where the
// key matches one of the given keys.
//
// Each call to this function is additive:
// the keys to mask by are added to the existing configuration.
//
// Example:
//
//	configuration = `['foo', 'baz']`
//
//	log1 = `{ msg = "...", fields = { 'foo': '***', 'bar': '...' }`  -> masked value
//	log2 = `{ msg = "...", fields = { 'bar': '...' }`                -> as-is value
//	log3 = `{ msg = "...", fields = { 'baz': '***', 'boo': '...' }`  -> masked value
func MaskFieldValuesWithFieldKeys(ctx context.Context, keys ...string) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithMaskFieldValuesWithFieldKeys(keys...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskAllFieldValuesRegexes returns a new context.Context that has a modified logger
// that masks (replaces) with asterisks (`***`) all field value substrings,
// matching one of the given *regexp.Regexp.
//
// Note that the replacement will happen, only for field values that are of type string.
//
// Each call to this function is additive:
// the regexp to mask by are added to the existing configuration.
//
// Example:
//
//	configuration = `[regexp.MustCompile("(foo|bar)")]`
//
//	log1 = `{ msg = "...", fields = { 'k1': '***', 'k2': '***', 'k3': 'baz' }`  -> masked value
//	log2 = `{ msg = "...", fields = { 'k1': 'boo', 'k2': 'far', 'k3': 'baz' }`  -> as-is value
//	log2 = `{ msg = "...", fields = { 'k1': '*** *** baz' }`                    -> masked value
func MaskAllFieldValuesRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithMaskAllFieldValuesRegexes(expressions...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskAllFieldValuesStrings returns a new context.Context that has a modified logger
// that masks (replaces) with asterisks (`***`) all field value substrings,
// equal to one of the given strings.
//
// Note that the replacement will happen, only for field values that are of type string.
//
// Each call to this function is additive:
// the regexp to mask by are added to the existing configuration.
//
// Example:
//
//	configuration = `[regexp.MustCompile("(foo|bar)")]`
//
//	log1 = `{ msg = "...", fields = { 'k1': '***', 'k2': '***', 'k3': 'baz' }`  -> masked value
//	log2 = `{ msg = "...", fields = { 'k1': 'boo', 'k2': 'far', 'k3': 'baz' }`  -> as-is value
//	log2 = `{ msg = "...", fields = { 'k1': '*** *** baz' }`                    -> masked value
func MaskAllFieldValuesStrings(ctx context.Context, matchingStrings ...string) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithMaskAllFieldValuesStrings(matchingStrings...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskMessageRegexes returns a new context.Context that has a modified logger
// that masks (replaces) with asterisks (`***`) all message substrings,
// matching one of the given *regexp.Regexp.
//
// Each call to this function is additive:
// the regexp to mask by are added to the existing configuration.
//
// Example:
//
//	configuration = `[regexp.MustCompile("(foo|bar)")]`
//
//	log1 = `{ msg = "banana apple ***", fields = {...}`     -> masked portion
//	log2 = `{ msg = "pineapple mango", fields = {...}`      -> as-is
//	log3 = `{ msg = "pineapple mango ***", fields = {...}`  -> masked portion
func MaskMessageRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithMaskMessageRegexes(expressions...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskMessageStrings returns a new context.Context that has a modified logger
// that masks (replace) with asterisks (`***`) all message substrings,
// equal to one of the given strings.
//
// Each call to this function is additive:
// the string to mask by are added to the existing configuration.
//
// Example:
//
//	configuration = `['foo', 'bar']`
//
//	log1 = `{ msg = "banana apple ***", fields = { 'k1': 'foo, bar, baz' }`  -> masked portion
//	log2 = `{ msg = "pineapple mango", fields = {...}`                       -> as-is
//	log3 = `{ msg = "pineapple mango ***", fields = {...}`                   -> masked portion
func MaskMessageStrings(ctx context.Context, matchingStrings ...string) context.Context {
	lOpts := logging.GetSDKRootTFLoggerOpts(ctx)

	// Copy to prevent slice/map aliasing issues.
	// Reference: https://github.com/hashicorp/terraform-plugin-log/issues/131
	lOpts = logging.WithMaskMessageStrings(matchingStrings...)(lOpts.Copy())

	return logging.SetSDKRootTFLoggerOpts(ctx, lOpts)
}

// MaskLogRegexes is a shortcut to invoke MaskMessageRegexes and MaskAllFieldValuesRegexes using the same input.
// Refer to those functions for details.
func MaskLogRegexes(ctx context.Context, expressions ...*regexp.Regexp) context.Context {
	return MaskMessageRegexes(MaskAllFieldValuesRegexes(ctx, expressions...), expressions...)
}

// MaskLogStrings is a shortcut to invoke MaskMessageStrings and MaskAllFieldValuesStrings using the same input.
// Refer to those functions for details.
func MaskLogStrings(ctx context.Context, matchingStrings ...string) context.Context {
	return MaskMessageStrings(MaskAllFieldValuesStrings(ctx, matchingStrings...), matchingStrings...)
}
