package log_scanner

import (
	"context"
	"fmt"
	"sort"

	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
	recorder "github.com/pingcap-inc/alioth/recorder"
	"github.com/pingcap-inc/alioth/storage"
	"github.com/pingcap/log"
	"go.uber.org/zap"
	"golang.org/x/tools/cover"
)

// CoverageProfileProcessor is responsible for scanning the coverage profile,
// load the coverage profile,
// and then recording the coverage
// TODO: implement it into log processort
type CoverageProfileProcessor struct {
	store *storage.LogPatternStore

	file string

	profiles []*cover.Profile

	fileToProfile map[string]ProfileBlockArry

	coverager *recorder.Coverage
}

func NewCPProcessor(store *storage.LogPatternStore, file string) (*CoverageProfileProcessor, error) {
	coverager := recorder.NewCoverager(store)

	profiles, err := cover.ParseProfiles(file)
	if err != nil {
		return nil, err
	}

	fileToProfile := make(map[string]ProfileBlockArry)
	for _, profile := range profiles {
		if _, ok := fileToProfile[profile.FileName]; ok {
			return nil, fmt.Errorf("profile %s already exists", profile.FileName)
		}

		var pbArray ProfileBlockArry
		for _, block := range profile.Blocks {
			pbArray = append(pbArray, block)
		}
		sort.Sort(pbArray)
		fileToProfile[profile.FileName] = pbArray
	}

	return &CoverageProfileProcessor{
		file:          file,
		store:         store,
		profiles:      profiles,
		fileToProfile: fileToProfile,
		coverager:     coverager,
	}, nil
}

// Run starts the log processing
func (s *CoverageProfileProcessor) Run(ctx context.Context) error {
	if err := s.coverager.LoadCoverage(ctx); err != nil {
		return err
	}

	if err := s.store.ScanLogPattern(ctx, func(key, value []byte) error {
		lg := &logpattern_go_proto.LogPattern{}
		if err := lg.Unmarshal(value); err != nil {
			return nil
		}

		fileName := fmt.Sprintf("%s/%s", lg.Pos.PackageInfo.Repo, lg.Pos.FilePath)
		startLine, startColumn := int(lg.Pos.Pos.Line), int(lg.Pos.Pos.Column)
		endLine, endColumn := int(lg.Pos.EndPos.Line), int(lg.Pos.EndPos.Column)
		if profile, ok := s.fileToProfile[fileName]; ok {
			log.Info("compute coverage", zap.String("file", fileName), zap.Any("start position", lg.Pos.Pos), zap.Any("end position", lg.Pos.EndPos))
			sortFunc := func(i int) bool {
				return profile[i].StartLine > endLine
			}

			index := sort.Search(len(profile), sortFunc)

			if index > 0 {
				lc := &logpattern_go_proto.Coverage{
					Pos: lg.Pos,
				}
				for i := index - 1; i >= 0; i-- {
					leftCover := profile[i].StartLine < startLine || (profile[i].StartLine == startLine && profile[i].StartCol <= startColumn)
					rightCover := profile[i].EndLine > endLine || (profile[i].EndLine == endLine && profile[i].EndCol >= endColumn)
					if leftCover && rightCover && profile[i].Count > 0 {
						lc.CovCount += int32(profile[i].Count)
					}

					if profile[i].EndLine < startLine {
						break
					}
				}
				if lc.CovCount > 0 {
					if len(lg.Signature) > 0 {
						lc.Message = lg.Signature[0]
					}
					lc.CovCountByLog = map[string]int32{
						s.file: lc.CovCount,
					}
					log.Info("compute coverage", zap.String("file", fileName), zap.Any("start position", lg.Pos.Pos), zap.Any("end position", lg.Pos.EndPos))
					s.coverager.Merge(ctx, lc)
				}
			}

		} else {
			log.Warn("not found coverage profile", zap.String("file", fileName))
		}

		return nil
	}); err != nil {
		return err
	}

	if err := s.coverager.Flush(ctx); err != nil {
		log.Error("flush coverge data failed", zap.Error(err))
		return err
	}

	return nil
}

// ProfileBlockArry implement sort.Interface to sort cover.ProfileBlock
type ProfileBlockArry []cover.ProfileBlock

func (s ProfileBlockArry) Len() int { return len(s) }
func (s ProfileBlockArry) Less(i, j int) bool {
	return s[i].StartLine < s[j].StartLine || (s[i].StartLine == s[j].StartLine && s[i].StartCol < s[j].StartCol)
}
func (s ProfileBlockArry) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
