// This package is used to save Kong audit log to a file.

package logger

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"path/filepath"
	"sync"
	"time"

	"github.com/spf13/viper"
	"gopkg.in/natefinch/lumberjack.v2"
	"gopkg.in/yaml.v3"

	"github.com/tidbcloud/audit-kong/log"
)

const (
	loadConfigInterval       = time.Second * 5
	rotateByLifetimeInterval = time.Second * 5
)

var (
	logger     *TimeRotatingLogger
	loggerOnce sync.Once
)

func GetLogger() *TimeRotatingLogger {
	loggerOnce.Do(func() {
		logger = NewTimeRotatingLogger()
	})
	return logger
}

type Config struct {
	EnableLog        bool   `yaml:"enableLog"`        // enable or disable logging to file, do not affect push
	LogDir           string `yaml:"logDir"`           // directory to store log files, set to empty to disable
	AuditLogFileName string `yaml:"auditLogFileName"` // name of the audit log file
	MaxSize          int    `yaml:"maxSize"`          // megabytes
	MaxLifetime      int    `yaml:"maxLifetime"`      // seconds, rotate by time interval
	MaxBackups       int    `yaml:"maxBackups"`       // number of backup files
	MaxAge           int    `yaml:"maxAge"`           // days to keep for backup files

	// ManualRotateTime is used to record the last time of a manual rotation is triggered.
	// use RFC3339 `string` instead of `time.Time` as viper doesn't work with `time.Time`
	ManualRotateTime string `yaml:"manualRotateTime"` // RFC3339

	rotateInterval time.Duration // duration format of `maxLifetime`
}

func (cfg *Config) String() string {
	jsonData, _ := json.Marshal(cfg)
	return string(jsonData)
}

func (cfg *Config) ToYAML() ([]byte, error) {
	return yaml.Marshal(cfg)
}

// TimeRotatingLogger wraps lumberjack.Logger to add time-based rotation
type TimeRotatingLogger struct {
	cfg     Config
	vp      *viper.Viper
	handler LogHandler

	lastRotate time.Time
	mutex      sync.Mutex
}

// NewTimeRotatingLogger creates a new time rotating logger
func NewTimeRotatingLogger() *TimeRotatingLogger {
	vp := viper.GetViper()
	vp.SetDefault("logDir", "/var/log/auditlog")
	vp.SetDefault("auditLogFileName", "ops-audit.log")
	vp.SetDefault("maxSize", 10)
	vp.SetDefault("maxLifetime", 86400) // 24h
	vp.SetDefault("maxBackups", 10)
	vp.SetDefault("maxAge", 7)

	trl := &TimeRotatingLogger{
		cfg:        Config{},
		vp:         vp,
		handler:    &NoOpLogHandler{},
		lastRotate: time.Now(),
	}
	trl.loadConfig()
	return trl
}

// Run runs background tasks for the logger
func (trl *TimeRotatingLogger) Run(ctx context.Context) {
	go trl.startHTTPServer(ctx)

	loadConfigTicker := time.NewTicker(loadConfigInterval)
	rotateByLifetimeTicker := time.NewTicker(rotateByLifetimeInterval)

	go func() {
		for {
			select {
			case <-ctx.Done():
				// rotate one last time before exit
				trl.mutex.Lock()
				if err := trl.rotate(); err != nil {
					log.Errorf("Failed to rotate log file: %v", err)
				}
				trl.mutex.Unlock()
				return
			case <-loadConfigTicker.C:
				trl.loadConfig()
			case <-rotateByLifetimeTicker.C:
				// even no new log is written, we still need to rotate by lifetime
				trl.mutex.Lock()
				if trl.cfg.rotateInterval > 0 && time.Since(trl.lastRotate) >= trl.cfg.rotateInterval {
					if err := trl.rotate(); err != nil {
						log.Errorf("Failed to rotate log file: %v", err)
					}
				}
				trl.mutex.Unlock()
			}
		}
	}()
}

func (trl *TimeRotatingLogger) startHTTPServer(ctx context.Context) {
	mux := http.NewServeMux()
	mux.HandleFunc("/rotate", func(w http.ResponseWriter, r *http.Request) {
		trl.mutex.Lock()
		defer trl.mutex.Unlock()

		if err := trl.rotate(); err != nil {
			http.Error(w, fmt.Sprintf("Failed to rotate log file: %v", err), http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte("OK"))
	})

	srv := &http.Server{
		Addr:              ":9080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
	}

	go func() {
		<-ctx.Done()
		//nolint:contextcheck //ignore context check
		_ = srv.Shutdown(context.Background())
	}()

	if err := srv.ListenAndServe(); err != nil {
		log.Errorf("HTTP server stopped: %v", err)
	}
}

func (trl *TimeRotatingLogger) loadConfig() {
	tmp := Config{}
	err := trl.vp.Unmarshal(&tmp)
	if err != nil {
		log.Errorf("Failed to unmarshal logger config: %v", err)
		return
	}

	tmp.rotateInterval = time.Duration(tmp.MaxLifetime) * time.Second

	trl.mutex.Lock()
	defer trl.mutex.Unlock()

	switch {
	case diffHandlerConfig(&trl.cfg, &tmp):
		log.Infof("Logger config updated: %s", &tmp)
		if trl.cfg.EnableLog != tmp.EnableLog ||
			trl.cfg.LogDir != tmp.LogDir || trl.cfg.AuditLogFileName != tmp.AuditLogFileName {
			// if the log dir/filename changed or enabling/disabling log,
			// we need to rotate the previous log file first
			if err := trl.rotate(); err != nil {
				log.Errorf("Failed to rotate log file: %v", err)
			}
		}
		// close the old LogHandler and recreate a new one
		trl.handler.Close()
		if !tmp.EnableLog || tmp.LogDir == "" || tmp.AuditLogFileName == "" {
			trl.handler = &NoOpLogHandler{}
		} else {
			trl.handler = &lumberjack.Logger{
				Filename:   filepath.Join(tmp.LogDir, tmp.AuditLogFileName),
				MaxSize:    tmp.MaxSize,
				MaxBackups: tmp.MaxBackups,
				MaxAge:     tmp.MaxAge,
			}
		}
	case trl.cfg.ManualRotateTime != tmp.ManualRotateTime:
		log.Infof("Logger config updated: %s", &tmp)
		// if no rotate after ManualRotateTime, we need to rotate now
		var manualRotateTime time.Time
		if err := manualRotateTime.UnmarshalText([]byte(tmp.ManualRotateTime)); err != nil {
			log.Errorf("Failed to unmarshal ManualRotateTime: %v", err)
		}
		if trl.lastRotate.Before(manualRotateTime) {
			if err := trl.rotate(); err != nil {
				log.Errorf("Failed to rotate log file: %v", err)
			}
			log.Infof("Log file rotated manually at %s", tmp.ManualRotateTime)
		}
	case trl.cfg.MaxLifetime != tmp.MaxLifetime:
		log.Infof("Logger config updated: %s", &tmp)
	}

	trl.cfg = tmp
}

func (trl *TimeRotatingLogger) Write(p []byte) (n int, err error) {
	trl.mutex.Lock()
	defer trl.mutex.Unlock()

	if trl.cfg.rotateInterval > 0 && time.Since(trl.lastRotate) >= trl.cfg.rotateInterval {
		if err := trl.rotate(); err != nil {
			log.Errorf("Failed to rotate log file: %v", err)
		}
	}

	return trl.handler.Write(p)
}

func (trl *TimeRotatingLogger) rotate() error {
	if err := trl.handler.Rotate(); err != nil {
		return err
	}
	log.Infof("Log file rotated")
	trl.lastRotate = time.Now()
	return nil
}

func diffHandlerConfig(oldConfig, newConfig *Config) bool {
	// `maxLifetime` is not a config in the handler, no need to recreate handler for it
	return oldConfig.EnableLog != newConfig.EnableLog ||
		oldConfig.LogDir != newConfig.LogDir ||
		oldConfig.AuditLogFileName != newConfig.AuditLogFileName ||
		oldConfig.MaxSize != newConfig.MaxSize ||
		oldConfig.MaxBackups != newConfig.MaxBackups ||
		oldConfig.MaxAge != newConfig.MaxAge
}
