package recorder

import (
	"context"
	"sync"

	"github.com/pingcap-inc/alioth/pkg/util"
	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
	"github.com/pingcap-inc/alioth/storage"
	"github.com/pingcap/log"
	"go.uber.org/zap"
)

// UnknowLogRecord used to record the log that exits in log files
// but not captured by log extractor
// Now support printing unknow logs through Flush method
type UnknowLogRecord struct {
	sync.RWMutex
	logs  map[string]*logpattern_go_proto.UnknowLogPattern
	store *storage.LogPatternStore
}

func NewUnknowLogRecorder(store *storage.LogPatternStore) *UnknowLogRecord {
	return &UnknowLogRecord{
		logs:  make(map[string]*logpattern_go_proto.UnknowLogPattern),
		store: store,
	}
}

// Record add one unknow log record
func (r *UnknowLogRecord) Record(logfile string, codefile string, codeline int32, message string, level string) {
	position := &logpattern_go_proto.Position{
		FilePath: codefile,
		Pos: &logpattern_go_proto.CodePosition{
			Line: codeline,
		},
	}
	pos := util.PosToStr(position)

	r.Lock()
	log := r.logs[pos]
	if log == nil {
		log = &logpattern_go_proto.UnknowLogPattern{
			Pos:           position,
			Level:         level,
			CovCountByLog: make(map[string]int32),
			Message:       message,
		}
		r.logs[pos] = log
	}

	log.CovCount = log.CovCount + 1
	if count, ok := log.CovCountByLog[logfile]; !ok {
		log.CovCountByLog[logfile] = 1
	} else {
		log.CovCountByLog[logfile] = count + 1
	}
	r.Unlock()
}

// Flush
// - flushs unknown log data from memory to store
// - output unknown log in log files
func (r *UnknowLogRecord) Flush(ctx context.Context) error {
	r.Lock()
	defer r.Unlock()

	if len(r.logs) == 0 {
		return nil
	}

	log.Warn("scan meets some unknow logs.", zap.Int("total count", len(r.logs)))
	index := 0
	for _, lg := range r.logs {
		err := r.store.WriteUnknownLog(ctx, lg)
		if err != nil {
			return err
		}
		index++
		if index < 32 {
			log.Warn("unknow logs", zap.String("log", lg.Message), zap.Int("count", int(lg.CovCount)), zap.Any("log files", lg.CovCountByLog))
		}
	}

	return nil
}

// Merge merges unknown log data into self
func (r *UnknowLogRecord) Merge(ctx context.Context, record *logpattern_go_proto.UnknowLogPattern) error {
	pos := util.PosToStr(record.Pos)
	r.Lock()
	if unknownLog, ok := r.logs[pos]; ok {
		for file, count := range record.CovCountByLog {
			// one file only be scaned by one `logcov merge` cmd, and only merged once
			if _, ok = unknownLog.CovCountByLog[file]; !ok {
				unknownLog.CovCountByLog[file] = count
				unknownLog.CovCount += count
			}
		}
	} else {
		unknownLog = &logpattern_go_proto.UnknowLogPattern{
			Pos:           record.Pos,
			CovCount:      record.CovCount,
			CovCountByLog: make(map[string]int32),
			Message:       record.Message,
			Level:         record.Level,
		}

		for file, count := range record.CovCountByLog {
			unknownLog.CovCountByLog[file] = count
		}
		r.logs[pos] = unknownLog
	}
	r.Unlock()
	return nil
}

// Forach supports visit all unknown log dara and run process logic of caller
// Limit: caller makes sure doesn't change this unknow log data
func (r *UnknowLogRecord) Forach(ctx context.Context, fn func(ctx context.Context, record *logpattern_go_proto.UnknowLogPattern) error) error {
	r.RLock()
	defer r.RUnlock()
	for _, record := range r.logs {
		if err := fn(ctx, record); err != nil {
			return err
		}
	}
	return nil
}

// LoadUnknowLogs loads unknow log data from store into memory
func (r *UnknowLogRecord) LoadUnknowLogs(ctx context.Context) error {
	r.Lock()
	defer r.Unlock()
	r.logs = make(map[string]*logpattern_go_proto.UnknowLogPattern)

	err := r.store.ScanUnknownLog(ctx, func(key, value []byte) error {
		lp := &logpattern_go_proto.UnknowLogPattern{}
		err := lp.Unmarshal(value)
		if err != nil {
			return err
		}
		pos := util.PosToStr(lp.Pos)
		r.logs[pos] = lp
		return nil
	})
	if err != nil {
		return err
	}

	return nil
}
