package matcher

import (
	"context"
	"fmt"
	"path/filepath"

	"github.com/pingcap-inc/alioth/pkg/util"
	logpattern_go_proto "github.com/pingcap-inc/alioth/proto"
	scanner "github.com/pingcap-inc/alioth/scanner/log_scan"
	"github.com/pingcap-inc/alioth/storage"
)

// BriefPattern only contains brief data of log pattern
type BriefPattern struct {
	// unique id generate by *logpattern_go_proto Position
	id string

	// only contains file name and line number, e.g. db.go:181
	matchedPos   string
	matchedLevel string

	// the function body range that log pattern belongs to
	LPos string
	RPos string

	pattern *logpattern_go_proto.LogPattern
}

// NewBriefPattern generate a brief pattern using *logpattern_go_proto Position
func NewBriefPattern(pattern *logpattern_go_proto.LogPattern) *BriefPattern {
	patternPos := pattern.GetPos()

	bp := &BriefPattern{
		id:           util.PosToStr(patternPos),
		matchedPos:   fmt.Sprintf("%s:%d", filepath.Base(patternPos.FilePath), patternPos.Pos.Line),
		matchedLevel: pattern.GetLevel(),
		pattern:      pattern,
	}

	// compute the function body range that log pattern belongs to
	if pattern.IsDeferLog {
		bp.LPos = fmt.Sprintf("%s:%d", filepath.Base(patternPos.FilePath), pattern.Func.Pos.Pos.Line)
		bp.RPos = fmt.Sprintf("%s:%d", filepath.Base(patternPos.FilePath), pattern.Func.Pos.EndPos.Line)
	}

	return bp
}

func (bp *BriefPattern) ID() string {
	return bp.id
}

func (bp *BriefPattern) Pattern() *logpattern_go_proto.LogPattern {
	return bp.pattern
}

func (bp *BriefPattern) Range() []string {
	if bp.LPos != "" {
		return []string{bp.LPos, bp.RPos}
	}

	return nil
}

func (bp *BriefPattern) String() string {
	return fmt.Sprintf("id %s: position %s, level %s, signature %+v", bp.id, bp.matchedPos, bp.matchedLevel, bp.pattern.GetSignature())
}

// PatternMatcher helps to find the log pattern
type PatternMatcher struct {
	store *storage.LogPatternStore

	// find log pattern throught log message as index key
	trie *patternTrie
	// find log pattern throught log position as index key
	posFilter *positionFilter
}

// MockPatternMatcher mocks a PatternMatcher for testing
func MockPatternMatcher(logs []*logpattern_go_proto.LogPattern) (*PatternMatcher, error) {
	ps := &PatternMatcher{
		trie:      NewPatternTrie(),
		posFilter: NewPositionFilter(),
	}

	for _, lp := range logs {
		if len(lp.Signature) > 0 {
			logSingature := lp.Signature[0]
			err := ps.trie.Insert(logSingature, lp)
			if err != nil {
				return nil, err
			}
		}
	}
	return ps, nil
}

// NewPatternMatcher return a PatternMatcher,
// and the corresponding filter for matching is loaded from the *keyvalue.Store
// the matching algorithm
// 1. use log message to match log pattern
// 2. use log position to match log pattern
// match result
// - If all log message, log position, and log level are matched, it is a strong match
// - If both log message and log position are matched, it is a weak match
// - If both log position and log level are matched, it is a weak match
// - For defer log pattern, if log message is matched and log position iswithin the function range of defer log pattern, it is a weak match
func NewPatternMatcher(store *storage.LogPatternStore) (*PatternMatcher, error) {
	ps := &PatternMatcher{
		trie:      NewPatternTrie(),
		posFilter: NewPositionFilter(),
		store:     store,
	}

	err := ps.load(context.Background())
	return ps, err
}

// load reads all log patterns from the store to build a matching trie
// the current algorithm only uses the first log signature to construct the trie
// it can be extended to support multiple sinatures matching in the future
func (p *PatternMatcher) load(ctx context.Context) error {
	return p.store.ScanLogPattern(ctx, func(_, value []byte) error {
		lp := &logpattern_go_proto.LogPattern{}
		err := lp.Unmarshal(value)
		if err != nil {
			return err
		}

		if len(lp.Signature) > 0 && len(lp.Signature[0]) > 0 {
			// has log signature, use log signature as index key to match log pattern
			logSingature := lp.Signature[0]
			err := p.trie.Insert(logSingature, lp)
			if err != nil {
				return err
			}
		} else if !lp.IsDeferLog {
			// doesn't has log signature, use log position as index key to match log pattern
			// if log pattern is in defer statement(has function range), the matching algorithm cannot be used
			position := fmt.Sprintf("%s:%d", filepath.Base(lp.Pos.FilePath), lp.Pos.Pos.Line)
			p.posFilter.Insert(position, lp)
		}

		return nil
	})
}

/*
Match return the matched log pattern.
The matching algorithm is as follows
1. Use log sigatures to match log patterns
2. Use log position to match log patterns
*/
func (p *PatternMatcher) Match(lp *scanner.Log) *MatchedResult {
	if lp == nil {
		return nil
	}

	position := fmt.Sprintf("%s:%d", lp.File, lp.Line)
	result := newMatchedResult(&MatchedOptions{
		LogLevel: lp.Level,
		Position: position,
	})

	p.trie.Match(lp.Msg, result)
	p.posFilter.Match(position, result)
	return result
}
