package store

import (
	"cmp"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/pingcap/log"
	dto "github.com/prometheus/client_model/go"
	"github.com/tidbcloud/serverless-metering/pkg/config"
	"go.uber.org/zap"
	"go.uber.org/zap/buffer"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

const logFileTimeFormat = "2006-01-02T15-04-05.000" // depends on lumberjack.go#backupTimeFormat

var metricEncoderPool = buffer.NewPool()

type LocalJsonLogger struct {
	zapLogger         *zap.Logger
	config            *config.LocalConfig
	lastRotateAt      int64
	maxRotateInterval int64
	rotate            func() error
}

func NewLocalJsonLogger() (LocalStorage, error) {
	cfg := config.Global().Store.Local
	zapLogger, rotate, err := newZapLogger(&log.Config{
		Format: "json",
		File: log.FileLogConfig{
			Filename:   cfg.Filename,
			MaxSize:    cfg.MaxSize,
			MaxDays:    cfg.MaxDays,
			MaxBackups: cfg.MaxBackups,
		},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to init zap logger: %s", err)
	}
	l := &LocalJsonLogger{
		zapLogger: zapLogger,
		rotate:    rotate,
		config:    &cfg,
		// If the process restart, reset the rotate timer.
		lastRotateAt:      time.Now().Unix(),
		maxRotateInterval: int64(cfg.MaxRotateSeconds),
	}
	go func() {
		if cfg.MaxRotateSeconds <= 0 {
			log.Info("disable local storage ticker", zap.Int("maxRotateSeconds", cfg.MaxRotateSeconds))
			return
		}
		ticker := time.NewTicker(time.Second * time.Duration(cfg.TickSeconds))
		for {
			<-ticker.C
			l.tick()
		}
	}()
	return l, nil
}

func newZapLogger(cfg *log.Config) (*zap.Logger, func() error, error) {
	if st, err := os.Stat(cfg.File.Filename); err == nil {
		if st.IsDir() {
			return nil, nil, errors.New("can't use directory as log file name")
		}
	}

	writer := &lumberjack.Logger{
		Filename:   cfg.File.Filename,
		MaxSize:    cfg.File.MaxSize,
		MaxBackups: cfg.File.MaxBackups,
		MaxAge:     cfg.File.MaxDays,
		LocalTime:  true,
	}

	logger, prop, err := log.InitLoggerWithWriteSyncer(
		cfg,
		zapcore.AddSync(writer),
		zapcore.AddSync(&errLogWriter{}),
	)
	if err != nil {
		return nil, nil, err
	}

	newCore := log.NewTextCore(&metricEncoder{}, prop.Syncer, prop.Level)
	logger = logger.WithOptions(zap.WrapCore(func(core zapcore.Core) zapcore.Core {
		return newCore
	}))

	return logger, writer.Rotate, nil
}

func (l *LocalJsonLogger) Log(labels map[string]string, metricFamilies map[string]*dto.MetricFamily) error {
	now := time.Now()
	for _, mf := range metricFamilies {
		name := *mf.Name
		metricType := *mf.Type
		for _, metric := range mf.Metric {
			var value float64
			switch metricType {
			case dto.MetricType_GAUGE:
				value = *metric.Gauge.Value
			case dto.MetricType_COUNTER:
				value = *metric.Counter.Value
			default:
				// only support gauge and counter for now
				log.Warn("unsupported metric type", zap.Any("metric", metric))
				continue
			}
			if value <= 0 {
				// tikv storage has a lot of 0 values, skip them
				continue
			}

			lbl := make(map[string]string)
			for k, v := range labels {
				lbl[k] = v
			}
			for _, l := range metric.Label {
				lbl[*l.Name] = *l.Value
			}

			metric := Metric{
				Timestamp: now.Unix(),
				Name:      name,
				Type:      int32(metricType),
				Value:     value,
				Labels:    lbl,
			}
			b, err := json.Marshal(metric)
			if err != nil {
				log.Warn("failed to marshal metric", zap.Any("metric", metric))
				continue
			}

			l.zapLogger.Info(string(b))
		}
	}
	return nil
}

func (l *LocalJsonLogger) LoadFiles(cursor int64) ([]*LocalFile, error) {
	filename := l.config.Filename
	ext := filepath.Ext(filename)
	prefix := filename[:len(filename)-len(ext)]
	var files []*LocalFile
	walkFn := func(path string, info os.DirEntry) {
		if info.IsDir() {
			return
		}
		if !strings.HasPrefix(path, prefix) {
			return
		}
		end, err := l.parseEndTs(path)
		if err != nil {
			log.Warn("failed to parse file ts", zap.Error(err), zap.String("path", path))
			return
		}
		if end <= cursor {
			return
		}
		files = append(files, &LocalFile{Path: path, End: end})
	}

	dir := filepath.Dir(filename)
	entries, err := os.ReadDir(dir)
	if err != nil {
		return nil, err
	}
	for _, entry := range entries {
		walkFn(filepath.Join(dir, entry.Name()), entry)
	}
	slices.SortFunc(files, func(i, j *LocalFile) int {
		return cmp.Compare(i.End, j.End)
	})
	return files, nil
}

func (l *LocalJsonLogger) parseEndTs(path string) (int64, error) {
	// metris.log
	filename := filepath.Base(l.config.Filename)
	// .log
	ext := filepath.Ext(filename)
	// metris
	prefix := filename[:len(filename)-len(ext)]

	// metris-2022-12-27T16-21-20.245.log
	filename = filepath.Base(path)
	// .log
	ext = filepath.Ext(path)
	// metris-2022-12-27T16-21-20.245
	filename = filename[:len(filename)-len(ext)]

	if strings.HasPrefix(filename, prefix+"-") {
		// 2022-12-27T16-21-20.245
		timeStr := strings.TrimPrefix(filename, prefix+"-")
		end, err := time.ParseInLocation(logFileTimeFormat, timeStr, time.Local)
		if err != nil {
			return 0, err
		}
		return end.Unix(), nil
	}
	return 0, nil
}

func (l *LocalJsonLogger) tick() {
	log.Debug("local json logger tick")
	files, err := l.LoadFiles(l.lastRotateAt)
	if err != nil {
		log.Error("failed to load files when tick", zap.Error(err))
		return
	}
	if len(files) == 0 {
		nowTs := time.Now().Unix()
		if nowTs-l.lastRotateAt > l.maxRotateInterval {
			l.doRotate(nowTs)
		}
		return
	}

	lastFile := files[len(files)-1]
	l.lastRotateAt = lastFile.End
}

func (l *LocalJsonLogger) doRotate(nowTs int64) {
	log.Info("local json logger rotate")
	err := l.rotate()
	if err != nil {
		log.Error("local json logger rotate failed", zap.Error(err))
		return
	}
	l.lastRotateAt = nowTs
}

type errLogWriter struct{}

func (*errLogWriter) Write(err []byte) (int, error) {
	log.Error("local json logger error", zap.ByteString("err", err))
	return len(err), nil
}

type metricEncoder struct{}

func (*metricEncoder) EncodeEntry(entry zapcore.Entry, _ []zapcore.Field) (*buffer.Buffer, error) {
	b := metricEncoderPool.Get()
	fmt.Fprintf(b, "%s\n", entry.Message)
	return b, nil
}

func (e *metricEncoder) Clone() zapcore.Encoder                        { return e }
func (*metricEncoder) AddArray(string, zapcore.ArrayMarshaler) error   { return nil }
func (*metricEncoder) AddObject(string, zapcore.ObjectMarshaler) error { return nil }
func (*metricEncoder) AddBinary(string, []byte)                        {}
func (*metricEncoder) AddByteString(string, []byte)                    {}
func (*metricEncoder) AddBool(string, bool)                            {}
func (*metricEncoder) AddComplex128(string, complex128)                {}
func (*metricEncoder) AddComplex64(string, complex64)                  {}
func (*metricEncoder) AddDuration(string, time.Duration)               {}
func (*metricEncoder) AddFloat64(string, float64)                      {}
func (*metricEncoder) AddFloat32(string, float32)                      {}
func (*metricEncoder) AddInt(string, int)                              {}
func (*metricEncoder) AddInt64(string, int64)                          {}
func (*metricEncoder) AddInt32(string, int32)                          {}
func (*metricEncoder) AddInt16(string, int16)                          {}
func (*metricEncoder) AddInt8(string, int8)                            {}
func (*metricEncoder) AddString(string, string)                        {}
func (*metricEncoder) AddTime(string, time.Time)                       {}
func (*metricEncoder) AddUint(string, uint)                            {}
func (*metricEncoder) AddUint64(string, uint64)                        {}
func (*metricEncoder) AddUint32(string, uint32)                        {}
func (*metricEncoder) AddUint16(string, uint16)                        {}
func (*metricEncoder) AddUint8(string, uint8)                          {}
func (*metricEncoder) AddUintptr(string, uintptr)                      {}
func (*metricEncoder) AddReflected(string, interface{}) error          { return nil }
func (*metricEncoder) OpenNamespace(string)                            {}
