// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package conf

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/pkg/errors"
	"gopkg.in/ini.v1"
	log "unknwon.dev/clog/v2"
)

type loggerConf struct {
	Buffer int64
	Config any
}

type logConf struct {
	RootPath string
	Modes    []string
	Configs  []*loggerConf
}

// Log settings
var Log *logConf

// initLogConf returns parsed logging configuration from given INI file. When the
// argument "hookMode" is true, it only initializes the root path for log files.
// NOTE: Because we always create a console logger as the primary logger at init time,
// we need to remove it in case the user doesn't configure to use it after the logging
// service is initialized.
func initLogConf(cfg *ini.File, hookMode bool) (_ *logConf, hasConsole bool, _ error) {
	rootPath := cfg.Section("log").Key("ROOT_PATH").MustString(filepath.Join(WorkDir(), "log"))
	if hookMode {
		return &logConf{
			RootPath: ensureAbs(rootPath),
		}, false, nil
	}

	modes := strings.Split(cfg.Section("log").Key("MODE").MustString("console"), ",")
	lc := &logConf{
		RootPath: ensureAbs(rootPath),
		Modes:    make([]string, 0, len(modes)),
		Configs:  make([]*loggerConf, 0, len(modes)),
	}

	// Iterate over [log.*] sections to initialize individual logger.
	levelMappings := map[string]log.Level{
		"trace": log.LevelTrace,
		"info":  log.LevelInfo,
		"warn":  log.LevelWarn,
		"error": log.LevelError,
		"fatal": log.LevelFatal,
	}

	for i := range modes {
		modes[i] = strings.ToLower(strings.TrimSpace(modes[i]))
		secName := "log." + modes[i]
		sec, err := cfg.GetSection(secName)
		if err != nil {
			return nil, hasConsole, errors.Errorf("missing configuration section [%s] for %q logger", secName, modes[i])
		}

		level := levelMappings[strings.ToLower(sec.Key("LEVEL").MustString("trace"))]
		buffer := sec.Key("BUFFER_LEN").MustInt64(100)
		var c *loggerConf
		switch modes[i] {
		case log.DefaultConsoleName:
			hasConsole = true
			c = &loggerConf{
				Buffer: buffer,
				Config: log.ConsoleConfig{
					Level: level,
				},
			}

		case log.DefaultFileName:
			logPath := filepath.Join(lc.RootPath, "gogs.log")
			c = &loggerConf{
				Buffer: buffer,
				Config: log.FileConfig{
					Level:    level,
					Filename: logPath,
					FileRotationConfig: log.FileRotationConfig{
						Rotate:   sec.Key("LOG_ROTATE").MustBool(true),
						Daily:    sec.Key("DAILY_ROTATE").MustBool(true),
						MaxSize:  1 << uint(sec.Key("MAX_SIZE_SHIFT").MustInt(28)),
						MaxLines: sec.Key("MAX_LINES").MustInt64(1000000),
						MaxDays:  sec.Key("MAX_DAYS").MustInt64(7),
					},
				},
			}

		case log.DefaultSlackName:
			c = &loggerConf{
				Buffer: buffer,
				Config: log.SlackConfig{
					Level: level,
					URL:   sec.Key("URL").String(),
				},
			}

		case log.DefaultDiscordName:
			c = &loggerConf{
				Buffer: buffer,
				Config: log.DiscordConfig{
					Level:    level,
					URL:      sec.Key("URL").String(),
					Username: sec.Key("USERNAME").String(),
				},
			}

		default:
			continue
		}

		lc.Modes = append(lc.Modes, modes[i])
		lc.Configs = append(lc.Configs, c)
	}

	return lc, hasConsole, nil
}

// InitLogging initializes the logging service of the application. When the
// "hookMode" is true, it only initializes the root path for log files without
// creating any logger. It will also not remove the primary logger in "hookMode"
// and is up to the caller to decide when to remove it.
func InitLogging(hookMode bool) {
	logConf, hasConsole, err := initLogConf(File, hookMode)
	if err != nil {
		log.Fatal("Failed to init logging configuration: %v", err)
	}
	defer func() {
		Log = logConf
	}()

	if hookMode {
		return
	}

	err = os.MkdirAll(logConf.RootPath, os.ModePerm)
	if err != nil {
		log.Fatal("Failed to create log directory: %v", err)
	}

	for i, mode := range logConf.Modes {
		c := logConf.Configs[i]

		var err error
		var level log.Level
		switch mode {
		case log.DefaultConsoleName:
			level = c.Config.(log.ConsoleConfig).Level
			err = log.NewConsole(c.Buffer, c.Config)
		case log.DefaultFileName:
			level = c.Config.(log.FileConfig).Level
			err = log.NewFile(c.Buffer, c.Config)
		case log.DefaultSlackName:
			level = c.Config.(log.SlackConfig).Level
			err = log.NewSlack(c.Buffer, c.Config)
		case log.DefaultDiscordName:
			level = c.Config.(log.DiscordConfig).Level
			err = log.NewDiscord(c.Buffer, c.Config)
		default:
			panic("unreachable")
		}

		if err != nil {
			log.Fatal("Failed to init %s logger: %v", mode, err)
			return
		}
		log.Trace("Log mode: %s (%s)", strings.Title(mode), strings.Title(strings.ToLower(level.String())))
	}

	// ⚠️ WARNING: It is only safe to remove the primary logger until
	// there are other loggers that are initialized. Otherwise, the
	// application will print errors to nowhere.
	if !hasConsole {
		log.Remove(log.DefaultConsoleName)
	}
}
