// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package plugins

import (
	"fmt"
	"go/token"
	"reflect"
	"strings"
	"unicode"

	"github.com/apache/skywalking-go/plugins/core/instrument"
	"github.com/apache/skywalking-go/tools/go-agent/config"
	"github.com/apache/skywalking-go/tools/go-agent/instrument/consts"
	"github.com/apache/skywalking-go/tools/go-agent/tools"

	"github.com/dave/dst"
)

var (
	configFieldKeyTag = "config"
	toolsImports      = "github.com/apache/skywalking-go/plugins/core/tools"
)

type ConfigEnhance struct {
	VarSpec               *dst.ValueSpec
	ConfigPrefix          string
	VarName               string
	Fields                []*ConfigField
	GenerateConfigFunName string
}

func NewConfigEnhance(varSpec *dst.ValueSpec, node dst.Node, inst instrument.Instrument) (*ConfigEnhance, error) {
	configDirective := tools.FindDirective(node, consts.DirectiveConfig)
	if configDirective == "" {
		return nil, fmt.Errorf("cannot find the config directive")
	}
	enhance := &ConfigEnhance{VarSpec: varSpec}
	info := strings.SplitN(configDirective, " ", 2)
	if len(info) == 2 {
		enhance.ConfigPrefix = info[1]
	} else {
		// default using the plugin name as the prefix
		enhance.ConfigPrefix = inst.Name()
	}
	enhance.VarName = varSpec.Names[0].Name

	varType, ok := varSpec.Type.(*dst.StructType)
	if !ok {
		return nil, fmt.Errorf("the config type of %s under %s plugin must be a structure", varSpec.Names[0].Name, inst.Name())
	}
	fs, err := NewConfigFields(varType)
	if err != nil {
		return nil, fmt.Errorf("analyzing the config %s under %s plugin failed: %v", varSpec.Names[0].Name, inst.Name(), err)
	}
	enhance.Fields = fs

	enhance.GenerateConfigFunName = fmt.Sprintf("initConfig%s", enhance.VarName)
	return enhance, nil
}

func (e *ConfigEnhance) PackageName() string {
	return ""
}

func (e *ConfigEnhance) BuildImports(decl *dst.GenDecl) {
	for _, spec := range decl.Specs {
		imp, ok := spec.(*dst.ImportSpec)
		if !ok {
			continue
		}
		if imp.Path.Value == fmt.Sprintf("%q", toolsImports) {
			return
		}
	}
	decl.Specs = append(decl.Specs, &dst.ImportSpec{
		Path: &dst.BasicLit{
			Kind:  token.STRING,
			Value: fmt.Sprintf("%q", toolsImports),
		},
	})
}

func (e *ConfigEnhance) BuildForDelegator() []dst.Decl {
	fun := &dst.FuncDecl{
		Name: dst.NewIdent(e.GenerateConfigFunName),
		Type: &dst.FuncType{},
		Body: &dst.BlockStmt{},
	}
	for _, field := range e.Fields {
		fun.Body.List = append(fun.Body.List, field.GenerateAssignFieldValue(e.VarName, []string{}, []string{e.ConfigPrefix})...)
	}
	return []dst.Decl{fun}
}

func (e *ConfigEnhance) ReplaceFileContent(path, content string) string {
	return content
}

func (e *ConfigEnhance) InitFunctions() []*EnhanceInitFunction {
	return []*EnhanceInitFunction{NewEnhanceInitFunction(e.GenerateConfigFunName, true)}
}

type ConfigField struct {
	Name        string
	Type        string
	Key         string
	ChildFields []*ConfigField
}

func NewConfigFields(structType *dst.StructType) ([]*ConfigField, error) {
	if structType.Fields == nil || len(structType.Fields.List) == 0 {
		return nil, fmt.Errorf("the config structure must have at least one field")
	}
	fields := make([]*ConfigField, 0, len(structType.Fields.List))
	for _, field := range structType.Fields.List {
		configField, err := NewConfigField(field)
		if err != nil {
			return nil, err
		}
		fields = append(fields, configField)
	}

	return fields, nil
}

func NewConfigField(f *dst.Field) (*ConfigField, error) {
	if len(f.Names) == 0 {
		return nil, fmt.Errorf("the config structure must have named field")
	}
	conf := &ConfigField{
		Name: f.Names[0].Name,
	}
	switch t := f.Type.(type) {
	case *dst.Ident:
		conf.Type = t.Name
	case *dst.ArrayType:
		conf.Type = tools.GenerateTypeNameByExp(t)
	case *dst.StructType:
		fs, err := NewConfigFields(t)
		if err != nil {
			return nil, err
		}
		conf.ChildFields = fs
	default:
		return nil, fmt.Errorf("the config structure field %s type %T is not supported", conf.Name, t)
	}
	conf.initFlags(f.Tag)
	return conf, nil
}

func (f *ConfigField) initFlags(flagLit *dst.BasicLit) {
	if flagLit == nil {
		f.Key = f.generateDefaultKey(f.Name)
		return
	}

	tag := reflect.StructTag(flagLit.Value)
	value, ok := tag.Lookup(configFieldKeyTag)
	if ok {
		f.Key = value
		return
	}

	f.Key = f.generateDefaultKey(f.Name)
}

func (f *ConfigField) generateDefaultKey(keyName string) string {
	var result []rune
	for i, r := range keyName {
		if unicode.IsUpper(r) {
			if i != 0 {
				result = append(result, '_')
			}
			result = append(result, []rune(strings.ToLower(string(r)))...)
		} else {
			result = append(result, r)
		}
	}
	return string(result)
}

func (f *ConfigField) GenerateAssignFieldValue(varName string, field, path []string) []dst.Stmt {
	field = append(field, f.Name)
	path = append(path, f.Key)
	if len(f.ChildFields) > 0 {
		result := make([]dst.Stmt, 0)
		for _, child := range f.ChildFields {
			result = append(result, child.GenerateAssignFieldValue(varName, field, path)...)
		}
		return result
	}
	fieldKeyPathStr := strings.Join(path, ".")
	fieldPathStr := strings.Join(field, ".")
	pluginConfig := config.GetConfig().Plugin.Config.ParseToStringValue(path...)
	if pluginConfig == nil {
		panic(fmt.Errorf("cannot find the config %s", fieldKeyPathStr))
	}
	resultType := f.Type
	getFromEnvStr := ""
	if pluginConfig.EnvKey != "" {
		getFromEnvStr = fmt.Sprintf("if v := tools.GetEnvValue(%q); v != \"\" { result = v };", pluginConfig.EnvKey)
	}
	parseResStr := ""
	parseErrorMessage := fmt.Sprintf(`"cannot parse the config %s: " + err.Error()`, fieldKeyPathStr)
	switch f.Type {
	case "string":
		parseResStr = "return result"
	case "bool":
		parseResStr = "return tools.ParseBool(result)"
	case "int":
		parseResStr = "if v, err := tools.Atoi(result); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "int16":
		parseResStr = "if v, err := tools.ParseInt(result, 10, 16); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "int32":
		parseResStr = "if v, err := tools.ParseInt(result, 10, 32); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "int64":
		parseResStr = "if v, err := tools.ParseInt(result, 10, 64); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "float32":
		parseResStr = "if v, err := tools.ParseFloat(result, 32); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "float64":
		parseResStr = "if v, err := tools.ParseFloat(result, 64); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "float":
		parseResStr = "if v, err := tools.ParseFloat(result, 64); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	case "[]string":
		parseResStr = "if v, err := tools.ParseStringArray(result); err != nil { panic(" + parseErrorMessage + ") } else { return v }"
	default:
		panic("unsupported config type " + f.Type)
	}
	stmtStr := fmt.Sprintf("%s.%s = func () %s { result := %q; %s%s }()",
		varName, fieldPathStr, resultType, pluginConfig.Default, getFromEnvStr, parseResStr)
	return tools.GoStringToStats(stmtStr)
}
