package matchers

import (
	"encoding/hex"
	"fmt"
	"regexp"
	"strings"

	"github.com/projectdiscovery/govaluate"
	"github.com/projectdiscovery/nuclei/v3/pkg/operators/cache"
	"github.com/projectdiscovery/nuclei/v3/pkg/operators/common/dsl"
)

// CompileMatchers performs the initial setup operation on a matcher
func (matcher *Matcher) CompileMatchers() error {
	var ok bool

	// Support hexadecimal encoding for matchers too.
	if matcher.Encoding == "hex" {
		for i, word := range matcher.Words {
			if decoded, err := hex.DecodeString(word); err == nil && len(decoded) > 0 {
				matcher.Words[i] = string(decoded)
			}
		}
	}

	// Set up the matcher type
	computedType, err := toMatcherTypes(matcher.GetType().String())
	if err != nil {
		return fmt.Errorf("unknown matcher type specified: %s", matcher.Type)
	}

	matcher.matcherType = computedType

	// Validate the matcher structure
	if err := matcher.Validate(); err != nil {
		return err
	}

	// By default, match on body if user hasn't provided any specific items
	if matcher.Part == "" && matcher.GetType() != DSLMatcher {
		matcher.Part = "body"
	}

	// Compile the regexes (with shared cache)
	for _, regex := range matcher.Regex {
		if cached, err := cache.Regex().GetIFPresent(regex); err == nil && cached != nil {
			matcher.regexCompiled = append(matcher.regexCompiled, cached)
			continue
		}
		compiled, err := regexp.Compile(regex)
		if err != nil {
			return fmt.Errorf("could not compile regex: %s", regex)
		}
		_ = cache.Regex().Set(regex, compiled)
		matcher.regexCompiled = append(matcher.regexCompiled, compiled)
	}

	// Compile and validate binary Values in matcher
	for _, value := range matcher.Binary {
		if decoded, err := hex.DecodeString(value); err != nil {
			return fmt.Errorf("could not hex decode binary: %s", value)
		} else {
			matcher.binaryDecoded = append(matcher.binaryDecoded, string(decoded))
		}
	}

	// Compile the dsl expressions (with shared cache)
	for _, dslExpression := range matcher.DSL {
		if cached, err := cache.DSL().GetIFPresent(dslExpression); err == nil && cached != nil {
			matcher.dslCompiled = append(matcher.dslCompiled, cached)
			continue
		}
		compiledExpression, err := govaluate.NewEvaluableExpressionWithFunctions(dslExpression, dsl.HelperFunctions)
		if err != nil {
			return &dsl.CompilationError{DslSignature: dslExpression, WrappedError: err}
		}
		_ = cache.DSL().Set(dslExpression, compiledExpression)
		matcher.dslCompiled = append(matcher.dslCompiled, compiledExpression)
	}

	// Set up the condition type, if any.
	if matcher.Condition != "" {
		matcher.condition, ok = ConditionTypes[matcher.Condition]
		if !ok {
			return fmt.Errorf("unknown condition specified: %s", matcher.Condition)
		}
	} else {
		matcher.condition = ORCondition
	}

	if matcher.CaseInsensitive {
		if matcher.GetType() != WordsMatcher {
			return fmt.Errorf("case-insensitive flag is supported only for 'word' matchers (not '%s')", matcher.Type)
		}
		for i := range matcher.Words {
			matcher.Words[i] = strings.ToLower(matcher.Words[i])
		}
	}
	return nil
}

// GetType returns the condition type of the matcher
// todo: the field should be exposed natively
func (matcher *Matcher) GetCondition() ConditionType {
	return matcher.condition
}
