// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package mlog

import (
	"encoding/json"
	"fmt"
	"io"
	"os"

	"github.com/hashicorp/go-multierror"
	"github.com/mattermost/logr"
	logrFmt "github.com/mattermost/logr/format"
	"github.com/mattermost/logr/target"
	"go.uber.org/zap/zapcore"
)

const (
	DefaultMaxTargetQueue = 1000
	DefaultSysLogPort     = 514
)

type LogLevel struct {
	ID         logr.LevelID
	Name       string
	Stacktrace bool
}

type LogTarget struct {
	Type         string // one of "console", "file", "tcp", "syslog", "none".
	Format       string // one of "json", "plain"
	Levels       []LogLevel
	Options      json.RawMessage
	MaxQueueSize int
}

type LogTargetCfg map[string]*LogTarget
type LogrCleanup func() error

func newLogr() *logr.Logger {
	lgr := &logr.Logr{}
	lgr.OnExit = func(int) {}
	lgr.OnPanic = func(interface{}) {}
	lgr.OnLoggerError = onLoggerError
	lgr.OnQueueFull = onQueueFull
	lgr.OnTargetQueueFull = onTargetQueueFull

	logger := lgr.NewLogger()
	return &logger
}

func logrAddTargets(logger *logr.Logger, targets LogTargetCfg) error {
	lgr := logger.Logr()
	var errs error
	for name, t := range targets {
		target, err := NewLogrTarget(name, t)
		if err != nil {
			errs = multierror.Append(err)
			continue
		}
		if target != nil {
			target.SetName(name)
			lgr.AddTarget(target)
		}
	}
	return errs
}

// NewLogrTarget creates a `logr.Target` based on a target config.
// Can be used when parsing custom config files, or when programmatically adding
// built-in targets. Use `mlog.AddTarget` to add custom targets.
func NewLogrTarget(name string, t *LogTarget) (logr.Target, error) {
	formatter, err := newFormatter(name, t.Format)
	if err != nil {
		return nil, err
	}
	filter, err := newFilter(name, t.Levels)
	if err != nil {
		return nil, err
	}

	if t.MaxQueueSize == 0 {
		t.MaxQueueSize = DefaultMaxTargetQueue
	}

	switch t.Type {
	case "console":
		return newConsoleTarget(name, t, filter, formatter)
	case "file":
		return newFileTarget(name, t, filter, formatter)
	case "syslog":
		return newSyslogTarget(name, t, filter, formatter)
	case "tcp":
		return newTCPTarget(name, t, filter, formatter)
	case "none":
		return nil, nil
	}
	return nil, fmt.Errorf("invalid type '%s' for target %s", t.Type, name)
}

func newFilter(name string, levels []LogLevel) (logr.Filter, error) {
	filter := &logr.CustomFilter{}
	for _, lvl := range levels {
		filter.Add(logr.Level(lvl))
	}
	return filter, nil
}

func newFormatter(name string, format string) (logr.Formatter, error) {
	switch format {
	case "json", "":
		return &logrFmt.JSON{}, nil
	case "plain":
		return &logrFmt.Plain{Delim: " | "}, nil
	default:
		return nil, fmt.Errorf("invalid format '%s' for target %s", format, name)
	}
}

func newConsoleTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
	type consoleOptions struct {
		Out string `json:"Out"`
	}
	options := &consoleOptions{}
	if err := json.Unmarshal(t.Options, options); err != nil {
		return nil, err
	}

	var w io.Writer
	switch options.Out {
	case "stdout", "":
		w = os.Stdout
	case "stderr":
		w = os.Stderr
	default:
		return nil, fmt.Errorf("invalid out '%s' for target %s", options.Out, name)
	}

	newTarget := target.NewWriterTarget(filter, formatter, w, t.MaxQueueSize)
	return newTarget, nil
}

func newFileTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
	type fileOptions struct {
		Filename   string `json:"Filename"`
		MaxSize    int    `json:"MaxSizeMB"`
		MaxAge     int    `json:"MaxAgeDays"`
		MaxBackups int    `json:"MaxBackups"`
		Compress   bool   `json:"Compress"`
	}
	options := &fileOptions{}
	if err := json.Unmarshal(t.Options, options); err != nil {
		return nil, err
	}
	return newFileTargetWithOpts(name, t, target.FileOptions(*options), filter, formatter)
}

func newFileTargetWithOpts(name string, t *LogTarget, opts target.FileOptions, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
	if opts.Filename == "" {
		return nil, fmt.Errorf("missing 'Filename' option for target %s", name)
	}
	if err := checkFileWritable(opts.Filename); err != nil {
		return nil, fmt.Errorf("error writing to 'Filename' for target %s: %w", name, err)
	}

	newTarget := target.NewFileTarget(filter, formatter, opts, t.MaxQueueSize)
	return newTarget, nil
}

func newSyslogTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
	options := &SyslogParams{}
	if err := json.Unmarshal(t.Options, options); err != nil {
		return nil, err
	}

	if options.IP == "" {
		return nil, fmt.Errorf("missing 'IP' option for target %s", name)
	}
	if options.Port == 0 {
		options.Port = DefaultSysLogPort
	}
	return NewSyslogTarget(filter, formatter, options, t.MaxQueueSize)
}

func newTCPTarget(name string, t *LogTarget, filter logr.Filter, formatter logr.Formatter) (logr.Target, error) {
	options := &TcpParams{}
	if err := json.Unmarshal(t.Options, options); err != nil {
		return nil, err
	}

	if options.IP == "" {
		return nil, fmt.Errorf("missing 'IP' option for target %s", name)
	}
	if options.Port == 0 {
		return nil, fmt.Errorf("missing 'Port' option for target %s", name)
	}
	return NewTcpTarget(filter, formatter, options, t.MaxQueueSize)
}

func checkFileWritable(filename string) error {
	// try opening/creating the file for writing
	file, err := os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0600)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

func isLevelEnabled(logger *logr.Logger, level logr.Level) bool {
	if logger == nil || logger.Logr() == nil {
		return false
	}

	status := logger.Logr().IsLevelEnabled(level)
	return status.Enabled
}

// zapToLogr converts Zap fields to Logr fields.
// This will not be needed once Logr is used for all logging.
func zapToLogr(zapFields []Field) logr.Fields {
	encoder := zapcore.NewMapObjectEncoder()
	for _, zapField := range zapFields {
		zapField.AddTo(encoder)
	}
	return logr.Fields(encoder.Fields)
}

// mlogLevelToLogrLevel converts a mlog logger level to
// an array of discrete Logr levels.
func mlogLevelToLogrLevels(level string) []LogLevel {
	levels := make([]LogLevel, 0)
	levels = append(levels, LvlError, LvlPanic, LvlFatal, LvlStdLog)

	switch level {
	case LevelDebug:
		levels = append(levels, LvlDebug)
		fallthrough
	case LevelInfo:
		levels = append(levels, LvlInfo)
		fallthrough
	case LevelWarn:
		levels = append(levels, LvlWarn)
	}
	return levels
}
