// Package baseapp provides application-level logging facilities.
//
// It supports dynamic log level and output target control,
// hot reloading without restarting the process,
// and safe lifecycle management for long-running applications.
//
// The logger is designed for industrial-grade applications,
// providing features like:
//   - Console and file output (with log rotation and compression)
//   - Atomic-level dynamic log level changes
//   - Safe multi-threaded usage via internal synchronization
//
// Author: NiuJiuRu
// Email: niujiuru@qq.com
// Last modified: 2026-06-03

package baseapp

import "C"

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/sirupsen/logrus"
	"gopkg.in/ini.v1"
	"gopkg.in/natefinch/lumberjack.v2"
)

// 日志级别
type level uint32

const (
	LEVEL_TRACE level = level(logrus.TraceLevel) // trace
	LEVEL_DEBUG       = level(logrus.DebugLevel) // debug
	LEVEL_INFO        = level(logrus.InfoLevel)  // info
	LEVEL_WARN        = level(logrus.WarnLevel)  // warning
	LEVEL_ERROR       = level(logrus.ErrorLevel) // error
	LEVEL_FATAL       = level(logrus.FatalLevel) // fatal, 会结束程序运行, 会自动调用os.Exit(1)
	LEVEL_PANIC       = level(logrus.PanicLevel) // panic, 会触发程序崩溃, 不处理的话, =程序退出
)

func (l *level) FromString(s string) {
	lvl, err := logrus.ParseLevel(strings.TrimSpace(s))
	if err == nil {
		*l = level(lvl)
	} else {
		*l = LEVEL_INFO
	}
}

func (l level) String() string {
	return logrus.Level(l).String()
}

// 输出目标
type target uint32

const (
	TARGET_NONE    target = iota      // 不输出日志
	TARGET_CONSOLE target = 1 << iota // 控制台输出
	TARGET_FILE                       // 输出到文件
)

func (t *target) FromString(s string) {
	*t = TARGET_NONE

	for part := range strings.SplitSeq(s, ",") {
		switch strings.ToLower(strings.TrimSpace(part)) {
		case "console":
			*t |= TARGET_CONSOLE
		case "file":
			*t |= TARGET_FILE
		case "all":
			*t |= TARGET_CONSOLE | TARGET_FILE
		}
	}
}

func (t target) String() string {
	if t == TARGET_NONE {
		return "none"
	}

	parts := make([]string, 0, 2)
	if (t & TARGET_CONSOLE) != 0 {
		parts = append(parts, "console")
	}
	if (t & TARGET_FILE) != 0 {
		parts = append(parts, "file")
	}
	return strings.Join(parts, ",")
}

// 输出格式
type formatter struct{}

func (f *formatter) Format(entry *logrus.Entry) ([]byte, error) {
	msg := ""
	pid := os.Getpid()

	if entry.Caller != nil {
		file := filepath.Base(entry.Caller.File)
		line := entry.Caller.Line
		msg = fmt.Sprintf("[%s, PID: %d] [%s] (%s:%d): %s\n", entry.Time.Format("2006-01-02 15:04:05"), pid, entry.Level.String(), file, line, entry.Message)
	} else {
		msg = fmt.Sprintf("[%s, PID: %d] [%s]: %s\n", entry.Time.Format("2006-01-02 15:04:05"), pid, entry.Level.String(), entry.Message)
	}

	return []byte(msg), nil
}

type systemdConsoleWriter struct{}

func (w *systemdConsoleWriter) Write(p []byte) (int, error) {
	msg := p

	if idx := bytes.Index(msg, []byte("] ")); idx != -1 {
		msg = msg[idx+2:]
	}

	_, _ = os.Stdout.Write(msg)

	return len(p), nil
}

// 日志配置
type config struct {
	// 日志输出级别
	Level level

	// 日志输出目标
	Target target

	// 每个日志文件的最大大下, 以"MB"为单位
	MaxSize int `ini:"MaxFileSize"`

	// 保留日志文件的最大天数, 以"天"为单位
	MaxAge int `ini:"MaxReserveDays"`

	// 保留的最大备份文件数量, 以"个"为单位
	MaxBackups int `ini:"MaxBackupFileCnts"`

	// 轮转文件是否压缩(gzip)减少占用的空间
	Compress bool `ini:"IsCompress"`
}

var (
	Logger        *logrus.Logger
	logFileWriter *lumberjack.Logger
	logMu         sync.Mutex
	initialized   atomic.Bool
	cfgFile       string
	cfgLog        = &config{
		Level:      LEVEL_TRACE,
		Target:     TARGET_CONSOLE,
		MaxSize:    0,
		MaxAge:     0,
		MaxBackups: 0,
		Compress:   true,
	}
	ErrLoggerNotInitialized = errors.New("logger is not initialized")
)

func InitLogger(logCfgFile string) {
	logMu.Lock()
	defer logMu.Unlock()

	if initialized.Load() {
		return
	}

	cfgFile = logCfgFile
	if cfgFile == "" {
		cfgFile = filepath.Join(CFG_DIR, "config.ini")
	}

	levelStr := "trace"    // ---初始化设置默认的日志级别---
	targetStr := "console" // ---初始化设置默认的输出目标---

	cfgIni, err := ini.Load(cfgFile) // 从配置文件中加载设置
	if err == nil && cfgIni.HasSection("Log") {
		tmpCfgLog := *cfgLog
		err = cfgIni.Section("Log").MapTo(&tmpCfgLog)
		if err != nil {
			fmt.Printf("Failed to load the \"[Log]\" section data from the \"%s\" file: %v\n", cfgFile, err)
		} else {
			*cfgLog = tmpCfgLog
		}
		if cfgIni.Section("Log").HasKey("Level") {
			levelStr = cfgIni.Section("Log").Key("Level").String()
		}
		if cfgIni.Section("Log").HasKey("Target") {
			targetStr = cfgIni.Section("Log").Key("Target").String()
		}
	}

	Logger = logrus.New()
	setLogLevelInternal(levelStr)       // 1, 设置日志级别
	Logger.SetFormatter(new(formatter)) // 2, 设置日志格式
	setLogTargetInternal(targetStr)     // 3, 设置输出目标

	initialized.Store(true) ////// 标记日志模块已初始化完成
}

func GetLogLevel() (string, error) {
	logMu.Lock()
	defer logMu.Unlock()

	if !initialized.Load() {
		return "", ErrLoggerNotInitialized
	}

	return cfgLog.Level.String(), nil
}

func setLogLevelInternal(levelStr string) {
	var lvl level
	lvl.FromString(levelStr)
	cfgLog.Level = lvl

	Logger.SetLevel(logrus.Level(cfgLog.Level))
	Logger.SetReportCaller(
		lvl == LEVEL_TRACE || lvl == LEVEL_DEBUG,
	) // 使得输出日志时能够获取到文件名和行号
}

func UpdateLogLevel(levelStr string) error {
	logMu.Lock()
	defer logMu.Unlock()

	if !initialized.Load() {
		return ErrLoggerNotInitialized
	}

	setLogLevelInternal(levelStr)
	return nil
}

func GetLogTarget() (string, error) {
	logMu.Lock()
	defer logMu.Unlock()

	if !initialized.Load() {
		return "", ErrLoggerNotInitialized
	}

	return cfgLog.Target.String(), nil
}

func setLogTargetInternal(targetStr string) {
	var t target
	t.FromString(targetStr)
	cfgLog.Target = t

	writers := make([]io.Writer, 0)
	if (cfgLog.Target & TARGET_CONSOLE) != 0 {
		isSystemd := os.Getenv("INVOCATION_ID") != "" || os.Getenv("JOURNAL_STREAM") != ""

		if isSystemd {
			writers = append(writers, &systemdConsoleWriter{})
		} else {
			writers = append(writers, os.Stdout)
		}
	}
	if (cfgLog.Target & TARGET_FILE) != 0 {
		logDir := LOG_DIR
		if logDir == "" {
			logDir = "."
		} else if info, err := os.Stat(logDir); err != nil || !info.IsDir() {
			logDir = "."
		}
		logFile := filepath.Join(logDir, EXEC_FILENAME+".log")
		old := logFileWriter
		logFileWriter = &lumberjack.Logger{Filename: logFile, MaxSize: cfgLog.MaxSize, MaxAge: cfgLog.MaxAge,
			MaxBackups: cfgLog.MaxBackups, Compress: cfgLog.Compress, LocalTime: true}
		if old != nil {
			old.Close()
		}
		writers = append(writers, logFileWriter)
	}

	if len(writers) > 0 {
		multiWriter := io.MultiWriter(writers...)
		Logger.SetOutput(multiWriter)
	} else {
		Logger.SetOutput(io.Discard) // 不输出日志
	}
}

func UpdateLogTarget(targetStr string) error {
	logMu.Lock()
	defer logMu.Unlock()

	if !initialized.Load() {
		return ErrLoggerNotInitialized
	}

	setLogTargetInternal(targetStr)
	return nil
}

func SaveLogConfig() error {
	logMu.Lock()
	defer logMu.Unlock()

	if !initialized.Load() {
		return ErrLoggerNotInitialized
	}

	dir := filepath.Dir(cfgFile)
	if err := os.MkdirAll(dir, 0755); err != nil {
		return err
	}

	cfg := ini.Empty()

	if _, err := os.Stat(cfgFile); err == nil {
		if c, err := ini.Load(cfgFile); err == nil {
			cfg = c
		}
	}

	sec := cfg.Section("Log")
	sec.Key("Level").SetValue(cfgLog.Level.String())
	sec.Key("Target").SetValue(cfgLog.Target.String())
	sec.Key("MaxFileSize").SetValue(strconv.Itoa(cfgLog.MaxSize))
	sec.Key("MaxReserveDays").SetValue(strconv.Itoa(cfgLog.MaxAge))
	sec.Key("MaxBackupFileCnts").SetValue(strconv.Itoa(cfgLog.MaxBackups))
	sec.Key("IsCompress").SetValue(strconv.FormatBool(cfgLog.Compress))

	tmp := cfgFile + ".tmp"

	if err := cfg.SaveTo(tmp); err != nil {
		return err
	}

	f, err := os.OpenFile(tmp, os.O_RDWR, 0644)
	if err != nil {
		return err
	}

	if err := f.Sync(); err != nil {
		f.Close()
		return err
	}
	f.Close()

	if err := os.Rename(tmp, cfgFile); err != nil {
		return err
	}

	if df, err := os.Open(dir); err == nil {
		_ = df.Sync()
		df.Close()
	}

	return nil
}

func ExitLogger() {
	logMu.Lock()
	defer logMu.Unlock()

	if Logger != nil {
		Logger.SetOutput(io.Discard) // 不输出日志
	}

	if logFileWriter != nil {
		logFileWriter.Close() // 关闭日志文件, 只是将日志提交到内核缓存区, 如果要强制落盘, 避免日志丢失,
		// 需要修改开源库代码, 在关闭函数中调用"l.file.Sync()"方法并等待一会儿, 20251021 by niujiuru.
		logFileWriter = nil
	}

	if initialized.Load() {
		initialized.Store(false)
	}
}

//export LogFromGo
func LogFromGo(level *C.char, file *C.char, line C.int, message *C.char) {
	if !initialized.Load() {
		return
	}

	goLevel := C.GoString(level)
	goMessage := C.GoString(message)

	if (cfgLog.Level == LEVEL_TRACE || cfgLog.Level == LEVEL_DEBUG) && line > 0 {
		gofile := filepath.Base(C.GoString(file))
		goline := int(line)
		goMessage = fmt.Sprintf("(from %s:%d): %s", gofile, goline, goMessage)
	}

	switch goLevel {
	case "trace":
		Logger.Tracef("%s", goMessage)
	case "debug":
		Logger.Debugf("%s", goMessage)
	case "info":
		Logger.Infof("%s", goMessage)
	case "warn":
		Logger.Warnf("%s", goMessage)
	case "error":
		Logger.Errorf("%s", goMessage)
	case "fatal":
		Logger.Fatalf("%s", goMessage)
	}
}
