package log

import (
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/thrasher-corp/gocryptotrader/common/convert"
)

var (
	errSubloggerConfigIsNil              = errors.New("sublogger config is nil")
	errUnhandledOutputWriter             = errors.New("unhandled output writer")
	errLogPathIsEmpty                    = errors.New("log path is empty")
	errConfigNil                         = errors.New("config is nil")
	errFileLoggingNotConfiguredCorrectly = errors.New("file logging not configured correctly")
)

// getWriters returns a new multi writer holder from sub logger configuration.
// Note: Calling function must have mutex lock in place.
func getWriters(s *SubLoggerConfig) (*multiWriterHolder, error) {
	if s == nil {
		return nil, errSubloggerConfigIsNil
	}

	outputWriters := strings.Split(s.Output, "|")
	writers := make([]io.Writer, 0, len(outputWriters))
	for x := range outputWriters {
		var writer io.Writer
		switch strings.ToLower(outputWriters[x]) {
		case "stdout", "console":
			writer = os.Stdout
		case "stderr":
			writer = os.Stderr
		case "file":
			if !fileLoggingConfiguredCorrectly {
				return nil, errFileLoggingNotConfiguredCorrectly
			}
			writer = globalLogFile
		default:
			// Note: Do not want to add an io.Discard here as this adds
			// additional write calls for no reason.
			return nil, fmt.Errorf("%w: %s", errUnhandledOutputWriter, outputWriters[x])
		}
		writers = append(writers, writer)
	}
	return multiWriter(writers...)
}

// GenDefaultSettings return struct with known sane/working logger settings
func GenDefaultSettings() *Config {
	return &Config{
		Enabled: convert.BoolPtr(true),
		SubLoggerConfig: SubLoggerConfig{
			Level:  "INFO|DEBUG|WARN|ERROR",
			Output: "console",
		},
		LoggerFileConfig: &loggerFileConfig{
			FileName: "log.txt",
			Rotate:   convert.BoolPtr(false),
		},
		AdvancedSettings: advancedSettings{
			ShowLogSystemName: convert.BoolPtr(false),
			Spacer:            spacer,
			TimeStampFormat:   timestampFormat,
			Headers: headers{
				Info:  "[INFO]",
				Warn:  "[WARN]",
				Debug: "[DEBUG]",
				Error: "[ERROR]",
			},
		},
	}
}

// SetGlobalLogConfig sets the global config with the supplied config
func SetGlobalLogConfig(incoming *Config) error {
	if incoming == nil {
		return errConfigNil
	}
	var fileConf loggerFileConfig
	if incoming.LoggerFileConfig != nil {
		fileConf = *incoming.LoggerFileConfig
	}
	subs := make([]SubLoggerConfig, len(incoming.SubLoggers))
	copy(subs, incoming.SubLoggers)
	mu.Lock()
	defer mu.Unlock()
	globalLogConfig.SubLoggerConfig = incoming.SubLoggerConfig
	globalLogConfig.Enabled = convert.BoolPtr(incoming.Enabled != nil && *incoming.Enabled)
	globalLogConfig.LoggerFileConfig = &fileConf
	globalLogConfig.AdvancedSettings = incoming.AdvancedSettings
	return nil
}

// SetLogPath sets the log path for writing to file
func SetLogPath(newLogPath string) error {
	if newLogPath == "" {
		return errLogPathIsEmpty
	}
	mu.Lock()
	defer mu.Unlock()
	logPath = newLogPath
	return nil
}

// GetLogPath returns path of log file
func GetLogPath() string {
	mu.RLock()
	defer mu.RUnlock()
	return logPath
}

// configureSubLogger configures a new sub logger. Note: Calling function must
// have mutex lock in place.
func configureSubLogger(subLogger, levels string, output *multiWriterHolder) error {
	logPtr, found := SubLoggers[subLogger]
	if !found {
		return fmt.Errorf("sub logger %v not found", subLogger)
	}

	err := logPtr.setOutput(output)
	if err != nil {
		return err
	}
	logPtr.setLevels(splitLevel(levels))
	SubLoggers[subLogger] = logPtr
	return nil
}

// SetupSubLoggers configure all sub loggers with provided configuration values
func SetupSubLoggers(s []SubLoggerConfig) error {
	mu.Lock()
	defer mu.Unlock()
	for x := range s {
		output, err := getWriters(&s[x])
		if err != nil {
			return err
		}
		err = configureSubLogger(strings.ToUpper(s[x].Name), s[x].Level, output)
		if err != nil {
			return err
		}
	}
	return nil
}

// SetupGlobalLogger setup the global loggers with the default global config
// values.
func SetupGlobalLogger(botName string, structuredOutput bool) error {
	mu.Lock()
	defer mu.Unlock()

	if fileLoggingConfiguredCorrectly {
		globalLogFile = &Rotate{
			FileName: globalLogConfig.LoggerFileConfig.FileName,
			MaxSize:  globalLogConfig.LoggerFileConfig.MaxSize,
			Rotate:   globalLogConfig.LoggerFileConfig.Rotate,
		}
	}

	writers, err := getWriters(&globalLogConfig.SubLoggerConfig)
	if err != nil {
		return err
	}

	for _, subLogger := range SubLoggers {
		subLogger.setLevels(splitLevel(globalLogConfig.Level))
		subLogger.structuredLogging = structuredOutput
		err = subLogger.setOutput(writers)
		if err != nil {
			return err
		}
		subLogger.botName = botName
	}
	logger = newLogger(globalLogConfig, botName)
	return nil
}

// SetFileLoggingState can set file logging state if it is correctly configured
// or not. This will bypass the ability to log to file if set as false.
func SetFileLoggingState(correctlyConfigured bool) {
	mu.Lock()
	fileLoggingConfiguredCorrectly = correctlyConfigured
	mu.Unlock()
}

func splitLevel(level string) (l Levels) {
	enabledLevels := strings.Split(level, "|")
	for x := range enabledLevels {
		switch enabledLevels[x] {
		case "DEBUG":
			l.Debug = true
		case "INFO":
			l.Info = true
		case "WARN":
			l.Warn = true
		case "ERROR":
			l.Error = true
		}
	}
	return
}

// registerNewSubLogger registers a new sub logger. Note: Calling function must
// have mutex lock in place.
func registerNewSubLogger(subLogger string) *SubLogger {
	tempHolder, err := getWriters(&SubLoggerConfig{
		Name:   strings.ToUpper(subLogger),
		Level:  "INFO|WARN|DEBUG|ERROR",
		Output: "stdout"})
	if err != nil {
		return nil
	}

	temp := &SubLogger{
		name:              strings.ToUpper(subLogger),
		output:            tempHolder,
		levels:            splitLevel("INFO|WARN|DEBUG|ERROR"),
		botName:           logger.botName,
		structuredLogging: globalLogConfig != nil && globalLogConfig.AdvancedSettings.StructuredLogging,
	}
	SubLoggers[subLogger] = temp
	return temp
}

// register all loggers at package init()
func init() {
	// Start persistent worker to handle logs
	go loggerWorker()

	mu.Lock()
	Global = registerNewSubLogger("LOG")

	ConnectionMgr = registerNewSubLogger("CONNECTION")
	CommunicationMgr = registerNewSubLogger("COMMS")
	APIServerMgr = registerNewSubLogger("API")
	ConfigMgr = registerNewSubLogger("CONFIG")
	DatabaseMgr = registerNewSubLogger("DATABASE")
	DataHistory = registerNewSubLogger("DATAHISTORY")
	OrderMgr = registerNewSubLogger("ORDER")
	PortfolioMgr = registerNewSubLogger("PORTFOLIO")
	SyncMgr = registerNewSubLogger("SYNC")
	TimeMgr = registerNewSubLogger("TIMEKEEPER")
	GCTScriptMgr = registerNewSubLogger("GCTSCRIPT")
	WebsocketMgr = registerNewSubLogger("WEBSOCKET")
	EventMgr = registerNewSubLogger("EVENT")
	DispatchMgr = registerNewSubLogger("DISPATCH")

	RequestSys = registerNewSubLogger("REQUESTER")
	ExchangeSys = registerNewSubLogger("EXCHANGE")
	GRPCSys = registerNewSubLogger("GRPC")
	RESTSys = registerNewSubLogger("REST")

	Ticker = registerNewSubLogger("TICKER")
	OrderBook = registerNewSubLogger("ORDERBOOK")
	Trade = registerNewSubLogger("TRADE")
	Fill = registerNewSubLogger("FILL")
	Currency = registerNewSubLogger("CURRENCY")
	mu.Unlock()
}
