package yaml

import (
	"bytes"
	"fmt"
	"github.com/SAP/jenkins-library/pkg/log"
	"github.com/SAP/jenkins-library/pkg/piperutils"
	"gopkg.in/yaml.v2"
	"io"
	"os"
	"reflect"
	"regexp"
	"strings"
)

type fUtils interface {
	FileRead(name string) ([]byte, error)
	FileWrite(name string, data []byte, mode os.FileMode) error
}

var _fileUtils fUtils

var _stat = os.Stat
var _traverse = traverse

// Substitute ...
func Substitute(ymlFile string, replacements map[string]interface{}, replacementsFiles []string) (bool, error) {

	if _fileUtils == nil {
		_fileUtils = piperutils.Files{}
	}

	bIn, err := _fileUtils.FileRead(ymlFile)
	if err != nil {
		return false, err
	}

	inDecoder := yaml.NewDecoder(bytes.NewReader(bIn))

	buf := new(bytes.Buffer)
	outEncoder := yaml.NewEncoder(buf)

	var updated bool

	mergedReplacements, err := getReplacements(replacements, replacementsFiles)
	if err != nil {
		return false, err
	}

	for {

		mIn := make(map[string]interface{})

		decodeErr := inDecoder.Decode(&mIn)

		if decodeErr != nil {
			if decodeErr == io.EOF {
				break
			}
			return false, decodeErr
		}

		if err != nil {
			return false, err
		}

		out, _updated, err := _traverse(mIn, mergedReplacements)
		if err != nil {
			return false, err
		}

		updated = _updated || updated

		err = outEncoder.Encode(out)
	}

	if updated {

		fInfo, err := _stat(ymlFile)
		if err != nil {
			return false, err
		}

		err = _fileUtils.FileWrite(ymlFile, buf.Bytes(), fInfo.Mode())
		if err != nil {
			return false, err
		}
	}

	return updated, nil
}

func traverse(node interface{}, replacements map[string]interface{}) (interface{}, bool, error) {
	switch t := node.(type) {
	case string:
		return handleString(t, replacements)
	case bool:
		return t, false, nil
	case int:
		return t, false, nil
	case map[string]interface{}:
		return handleMap(t, replacements)
	case map[interface{}]interface{}:
		m, err := keysToString(t)
		if err != nil {
			return nil, false, err
		}
		return handleMap(m, replacements)
	case []interface{}:
		return handleSlice(t, replacements)
	default:
		return nil, false, fmt.Errorf("Unknown type received: '%v' (%v)", reflect.TypeOf(node), node)
	}
}

func keysToString(m map[interface{}]interface{}) (map[string]interface{}, error) {
	result := map[string]interface{}{}
	for key, val := range m {
		if k, ok := key.(string); ok {
			result[k] = val
		} else {
			return nil, fmt.Errorf("Cannot downcast'%v' to string. Type: %v)", reflect.TypeOf(key), key)
		}
	}
	return result, nil
}

func handleString(value string, replacements map[string]interface{}) (interface{}, bool, error) {

	trimmed := strings.TrimSpace(value)
	re := regexp.MustCompile(`\(\(.*?\)\)`)
	matches := re.FindAllSubmatch([]byte(trimmed), -1)
	fullMatch := isFullMatch(trimmed, matches)
	if fullMatch {
		log.Entry().Infof("FullMatchFound: %v", value)
		parameterName := getParameterName(matches[0][0])
		parameterValue := getParameterValue(parameterName, replacements)
		if parameterValue == nil {
			return nil, false, fmt.Errorf("No value available for parameters '%s', replacements: %v", parameterName, replacements)
		}
		log.Entry().Infof("FullMatchFound: '%s', replacing with '%v'", parameterName, parameterValue)
		return parameterValue, true, nil
	}
	// we have to scan for multiple variables
	// we return always a string
	updated := false
	for i, match := range matches {
		parameterName := getParameterName(match[0])
		log.Entry().Infof("XPartial match found: (%d) %v, %v", i, parameterName, value)
		parameterValue := getParameterValue(parameterName, replacements)
		if parameterValue == nil {
			return nil, false, fmt.Errorf("No value available for parameter '%s', replacements: %v", parameterName, replacements)
		}

		var conversion string
		switch t := parameterValue.(type) {
		case string:
			conversion = "%s"
		case bool:
			conversion = "%t"
		case int:
			conversion = "%d"
		case float64:
			conversion = "%g" // exponent as need, only required digits
		default:
			return nil, false, fmt.Errorf("Unsupported datatype found during travseral of yaml file: '%v', type: '%v'", parameterValue, reflect.TypeOf(t))
		}
		valueAsString := fmt.Sprintf(conversion, parameterValue)
		log.Entry().Infof("Value as String: %v: '%v'", parameterName, valueAsString)
		value = strings.Replace(value, "(("+parameterName+"))", valueAsString, -1)
		updated = true
		log.Entry().Infof("PartialMatchFound (%d): '%v', replaced with : '%s'", i, parameterName, valueAsString)
	}

	return value, updated, nil
}

func getParameterName(b []byte) string {
	pName := string(b)
	log.Entry().Infof("ParameterName is: '%s'", pName)
	return strings.Replace(strings.Replace(string(b), "((", "", 1), "))", "", 1)
}

func getParameterValue(name string, replacements map[string]interface{}) interface{} {

	r := replacements[name]
	log.Entry().Infof("Value '%v' resolved for parameter '%s'", r, name)
	return r
}

func isFullMatch(value string, matches [][][]byte) bool {
	return strings.HasPrefix(value, "((") && strings.HasSuffix(value, "))") && len(matches) == 1 && len(matches[0]) == 1
}

func handleSlice(t []interface{}, replacements map[string]interface{}) ([]interface{}, bool, error) {
	tNode := make([]interface{}, 0)
	updated := false
	for _, e := range t {
		if val, _updated, err := traverse(e, replacements); err == nil {
			updated = updated || _updated
			tNode = append(tNode, val)

		} else {
			return nil, false, err
		}
	}
	return tNode, updated, nil
}

func handleMap(t map[string]interface{}, replacements map[string]interface{}) (map[string]interface{}, bool, error) {
	tNode := make(map[string]interface{})
	updated := false
	for key, value := range t {
		if val, _updated, err := traverse(value, replacements); err == nil {
			updated = updated || _updated
			tNode[key] = val
		} else {
			return nil, false, err
		}
	}
	return tNode, updated, nil
}

func getReplacements(replacements map[string]interface{}, replacementsFiles []string) (map[string]interface{}, error) {

	mReplacements := make(map[string]interface{})

	for _, replacementsFile := range replacementsFiles {
		bReplacements, err := _fileUtils.FileRead(replacementsFile)
		if err != nil {
			return nil, err
		}

		replacementsDecoder := yaml.NewDecoder(bytes.NewReader(bReplacements))

		for {
			decodeErr := replacementsDecoder.Decode(&mReplacements)

			if decodeErr != nil {
				if decodeErr == io.EOF {
					break
				}
				return nil, decodeErr
			}
		}
	}

	// the parameters from the map has a higher precedence,
	// hence we merge after resolving parameters from the files
	for k, v := range replacements {
		mReplacements[k] = v
	}
	return mReplacements, nil
}
