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"
)

// cache coverage data in the memory
type Coverage struct {
	sync.RWMutex
	coverage map[string]*logpattern_go_proto.Coverage

	store *storage.LogPatternStore
}

func NewCoverager(store *storage.LogPatternStore) *Coverage {
	return &Coverage{
		store:    store,
		coverage: make(map[string]*logpattern_go_proto.Coverage),
	}
}

// Record stores new coverage record, used by logcov scan
func (c *Coverage) Record(logFile string, logMessage string, pattern *logpattern_go_proto.LogPattern) {
	pos := util.PosToStr(pattern.Pos)
	c.Lock()
	cov := c.coverage[pos]
	if cov == nil {
		cov = &logpattern_go_proto.Coverage{
			Pos:           pattern.GetPos(),
			CovCountByLog: make(map[string]int32),
			Message:       logMessage,
		}
		c.coverage[pos] = cov
	}

	cov.CovCount = cov.CovCount + 1
	if count, ok := cov.CovCountByLog[logFile]; !ok {
		cov.CovCountByLog[logFile] = 1
	} else {
		cov.CovCountByLog[logFile] = count + 1
	}
	c.Unlock()
}

// Flush flushs coverge data in the memory to store
func (c *Coverage) Flush(ctx context.Context) error {
	c.Lock()
	defer c.Unlock()

	if len(c.coverage) == 0 {
		return nil
	}

	log.Info("coverage summary", zap.Int("covered count", len(c.coverage)), zap.String("query coverage detail", "analyze --log-coverage=${log-pattern-store}"))
	for _, cov := range c.coverage {
		err := c.store.WriteLogCoverage(ctx, cov)
		if err != nil {
			return err
		}
	}

	return nil
}

// Merge merges coverage data into self, if there no same coverage, create new one
func (c *Coverage) Merge(ctx context.Context, record *logpattern_go_proto.Coverage) error {
	pos := util.PosToStr(record.Pos)
	c.Lock()
	if coverage, ok := c.coverage[pos]; ok {
		for file, count := range record.CovCountByLog {
			if _, ok := coverage.CovCountByLog[file]; !ok {
				coverage.CovCountByLog[file] = count
				coverage.CovCount += count
			}
		}
	} else {
		coverage = &logpattern_go_proto.Coverage{
			Pos:           record.Pos,
			CovCount:      record.CovCount,
			CovCountByLog: make(map[string]int32),
			Message:       record.Message,
		}

		for file, count := range record.CovCountByLog {
			coverage.CovCountByLog[file] = count
		}
		c.coverage[pos] = coverage
	}
	c.Unlock()

	return nil
}

// Forach supports visit all coverge data and run process logic for caller
// Limit: caller makes sure doesn't change this coverage data
func (c *Coverage) Forach(ctx context.Context, fn func(ctx context.Context, record *logpattern_go_proto.Coverage) error) error {
	c.RLock()
	defer c.RUnlock()
	for _, record := range c.coverage {
		if err := fn(ctx, record); err != nil {
			return err
		}
	}
	return nil
}

// LoadCoverage loads coverage data from store into memory
func (c *Coverage) LoadCoverage(ctx context.Context) error {
	c.Lock()
	defer c.Unlock()

	c.coverage = make(map[string]*logpattern_go_proto.Coverage)
	err := c.store.ScanLogCoverage(ctx, func(key, value []byte) error {
		lp := &logpattern_go_proto.Coverage{}
		err := lp.Unmarshal(value)
		if err != nil {
			return err
		}
		pos := util.PosToStr(lp.Pos)
		c.coverage[pos] = lp
		return nil
	})
	if err != nil {
		return err
	}

	return nil
}
