package engine

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"

	"github.com/rs/zerolog"
	"github.com/rs/zerolog/log"

	dec "github.com/Checkmarx/kics/v2/pkg/detector"
	"github.com/Checkmarx/kics/v2/pkg/model"
	"github.com/Checkmarx/kics/v2/pkg/utils"
)

type searchLineCalculator struct {
	lineNr                   int
	vObj                     map[string]interface{}
	file                     model.FileMetadata
	detector                 *dec.DetectLine
	oldSearchLineOutput      string
	newSearchLineOutput      string
	usingComputeSimilarityID bool
	vulnerabilityLines       model.VulnerabilityLines
}

func (s *searchLineCalculator) calculate() {
	if searchLine, ok := s.vObj["searchLine"]; ok {
		line := make([]string, 0, len(searchLine.([]interface{})))
		for _, strElement := range searchLine.([]interface{}) {
			line = append(line, strElement.(string))
		}
		var err error
		var oldLine int
		oldLine, s.lineNr, err = dec.GetLineBySearchLine(line, &s.file, s.usingComputeSimilarityID)
		if err != nil {
			log.Error().Msgf("failed to get line information from searchLine, using searchKey")
		}
		if s.lineNr >= 0 {
			if oldLine >= 0 { // old may be different than new
				s.oldSearchLineOutput = fmt.Sprintf("%d", oldLine) // for backward compatibility and old capabilities
			}
			s.vulnerabilityLines = s.detector.GetAdjacent(&s.file, s.lineNr)
		}
		s.newSearchLineOutput = strings.Join(line, ".") // create a concatenated string for the new similarityID
	}
}

func (s *searchLineCalculator) generateSearchLineOutputWithoutGJson() {
	if searchLine, ok := s.vObj["searchLine"]; ok {
		line := make([]string, 0, len(searchLine.([]interface{})))
		for _, strElement := range searchLine.([]interface{}) {
			line = append(line, strElement.(string))
		}
		s.newSearchLineOutput = strings.Join(line, ".") // create a concatenated string for the new similarityID
	}
}

func mergeWithMetadata(base, additional map[string]interface{}) map[string]interface{} {
	for k, v := range additional {
		if _, ok := base[k]; ok {
			continue
		}
		base[k] = v
	}
	return base
}
func mustMapKeyToString(m map[string]interface{}, key string) *string {
	res, err := mapKeyToString(m, key, true)
	excludedFields := []string{"value", "resourceName", "resourceType", "remediation", "remediationType"}
	if err != nil && !utils.Contains(key, excludedFields) {
		log.Warn().
			Str("reason", err.Error()).
			Msgf("Failed to get key %s in map", key)
	}
	return res
}
func mapKeyToString(m map[string]interface{}, key string, allowNil bool) (*string, error) {
	v, ok := m[key]
	if !ok {
		return nil, fmt.Errorf("key '%s' not found in map", key)
	}
	switch vv := v.(type) {
	case json.Number:
		return stringToPtrString(vv.String()), nil
	case string:
		return stringToPtrString(vv), nil
	case int, int32, int64:
		return stringToPtrString(fmt.Sprintf("%d", vv)), nil
	case float32:
		return stringToPtrString(strconv.FormatFloat(float64(vv), 'f', -1, formatFloat64)), nil
	case float64:
		return stringToPtrString(strconv.FormatFloat(vv, 'f', -1, formatFloat64)), nil
	case nil:
		if allowNil {
			return nil, nil
		}
		return stringToPtrString("null"), nil
	case bool:
		return stringToPtrString(fmt.Sprintf("%v", vv)), nil
	}
	log.Debug().
		Msg("Detecting line. can't format item to string")
	if allowNil {
		return nil, nil
	}
	return stringToPtrString(""), nil
}
func stringToPtrString(v string) *string {
	return &v
}

// PtrStringToString - converts a pointer to string to a string
func PtrStringToString(v *string) string {
	if v == nil {
		return ""
	}
	return *v
}
func tryOverride(overrideKey, vulnParam string, vObj map[string]interface{}) *string {
	if overrideKey != "" {
		if override, ok := vObj["override"].(map[string]interface{}); ok {
			if overrideObject, ok := override[overrideKey].(map[string]interface{}); ok {
				if _, ok := overrideObject[vulnParam]; ok {
					overrideValue, err := mapKeyToString(overrideObject, vulnParam, true)
					if err != nil {
						return nil
					} else if overrideValue != nil {
						return overrideValue
					}
				}
			}
		}
	}
	return nil
}
func getStringFromMap(vulnParam, defaultParam, overrideKey string, vObj map[string]interface{}, logWithFields *zerolog.Logger) string {
	ts, err := mapKeyToString(vObj, vulnParam, false)
	if err != nil {
		logWithFields.Err(err).
			Msgf("Saving result. failed to detect %s", vulnParam)
		return defaultParam
	}
	overrideValue := tryOverride(overrideKey, vulnParam, vObj)
	if overrideValue != nil {
		ts = overrideValue
	}
	return *ts
}
func getBoolFromMap(
	vulnParam string,
	defaultParam bool,
	overrideKey string,
	vObj map[string]interface{},
	logWithFields *zerolog.Logger) bool {
	ts, err := mapKeyToString(vObj, vulnParam, false)
	if err != nil {
		return defaultParam
	}
	overrideValue := tryOverride(overrideKey, vulnParam, vObj)
	if overrideValue != nil {
		ts = overrideValue
	}
	res, err := strconv.ParseBool(*ts)
	if err != nil {
		logWithFields.Err(err).
			Msgf("Saving result. failed to detect %s", vulnParam)
		return defaultParam
	}
	return res
}
func getSeverity(severity string) model.Severity {
	for _, si := range model.AllSeverities {
		if severity == string(si) {
			return si
		}
	}
	return ""
}
