package log

import (
	"io"
	"os"

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

var (
	hostName               string
	eventLogger, appLogger Logger
	fileLoggers            FileLoggers
)

func init() {
	hostName, _ = os.Hostname()

	ResetLogger()
}

type Config struct {
	EventLogFile string `json:"event-file"`
	AppLogFile   string `json:"app-file"`
	SentryDSN    string `json:"sentry-dsn"`
}

type LoggersConfig struct {
	EventLogWriter io.Writer
	AppLogWriter   io.Writer
	SentryDSN      string
}

type Logger struct {
	*zap.Logger
}

func CreateLoggersConfig(debug bool, cfg Config) *LoggersConfig {
	if debug {
		return &LoggersConfig{
			EventLogWriter: os.Stderr,
			AppLogWriter:   os.Stderr,
			SentryDSN:      cfg.SentryDSN,
		}
	} else {
		eLog := &FileLogger{Filename: cfg.EventLogFile}
		aLog := &FileLogger{Filename: cfg.AppLogFile}
		fileLoggers = FileLoggers{eLog, aLog}
		return &LoggersConfig{
			eLog,
			aLog,
			cfg.SentryDSN,
		}
	}
}

// For test, reset loggers from InitLogger
func ResetLogger() {
	eventLogger = Logger{zap.NewNop()}
	appLogger = Logger{zap.NewNop()}
}

func InitLogger(cfg *LoggersConfig) (err error) {
	eventLogger, err = initEventLogger(cfg.EventLogWriter)
	if err != nil {
		return
	}

	appLogger, err = initAppLogger(cfg.AppLogWriter, cfg.SentryDSN)
	return err
}

func initEventLogger(writer io.Writer) (Logger, error) {
	eventOutput := createWriterSync(writer)

	_zapCfg := zap.NewProductionConfig()
	_zapCfg.Sampling = nil

	_zapLogger, err := _zapCfg.Build()

	if err != nil {
		return Logger{}, err
	}

	productionEncoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "",
		NameKey:        "logger",
		CallerKey:      "",
		MessageKey:     "",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.LowercaseLevelEncoder,
		EncodeTime:     zapcore.EpochTimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	})

	_zapLogger = _zapLogger.WithOptions(
		zap.WrapCore(func(zapcore.Core) zapcore.Core {
			return zapcore.NewCore(productionEncoder, eventOutput, zapcore.InfoLevel)
		}),
	)

	_logger := Logger{_zapLogger}.With(zap.String("host", hostName))
	return _logger, nil
}

// Discard low level log
func initAppLogger(writer io.Writer, sentryDsn string) (Logger, error) {
	appOutput := createWriterSync(writer)

	_zapCfg := zap.NewProductionConfig()
	_zapCfg.Sampling = nil

	_zapLogger, err := _zapCfg.Build()

	if err != nil {
		return Logger{}, err
	}

	productionEncoder := zapcore.NewJSONEncoder(zap.NewDevelopmentEncoderConfig())

	sentryCore, err := NewSentryCore(
		sentryDsn,
		zapcore.WarnLevel,
	)

	if err != nil {
		return Logger{}, err
	}

	_zapLogger = _zapLogger.WithOptions(
		zap.WrapCore(func(zapcore.Core) zapcore.Core {
			return zapcore.NewTee(
				zapcore.NewCore(productionEncoder, appOutput, zapcore.InfoLevel),
				sentryCore,
			)
		}),
	)

	_logger := Logger{_zapLogger}.With(zap.String("host", hostName))
	return _logger, nil
}

func createWriterSync(writer io.Writer) zapcore.WriteSyncer {
	if writer == nil {
		return zapcore.AddSync(os.Stderr)
	} else {
		return zapcore.AddSync(writer)
	}
}

func L() Logger {
	return appLogger
}

func E() Logger {
	return eventLogger
}

func ReopenAllLoggers() {
	if fileLoggers == nil {
		L().Warn("No log file open")
	} else if err := fileLoggers.Reopen(); err != nil {
		L().Error("Reopen log file failed", zap.Error(err))
	} else {
		L().Info("Reopen log file success")
	}
}

func SyncAllLogs() {
	eventLogger.Sync()
	appLogger.Sync()
}

// With creates a child logger and adds structured context to it.
func (logger Logger) With(fields ...zap.Field) Logger {
	return Logger{logger.Logger.With(fields...)}
}

// Named adds a new path segment to the logger's name.
func (logger Logger) Named(name string) Logger {
	return Logger{logger.Logger.Named(name)}
}
