package parse

import (
	"fmt"
	"github.com/hashicorp/hcl/v2"
	filehelpers "github.com/turbot/go-kit/files"
	"github.com/turbot/go-kit/helpers"
	"github.com/turbot/steampipe/pkg/steampipeconfig/modconfig"
	"github.com/turbot/steampipe/pkg/steampipeconfig/versionmap"
	"github.com/zclconf/go-cty/cty"
	"runtime/debug"
)

const rootDependencyNode = "rootDependencyNode"

type ParseModFlag uint32

const (
	CreateDefaultMod ParseModFlag = 1 << iota
	CreatePseudoResources
)

/*
	ReferenceTypeValueMap is the raw data used to build the evaluation context

When resolving hcl references like :
- query.q1
- var.v1
- mod1.query.my_query.sql

ReferenceTypeValueMap is keyed  by resource type, then by resource name
*/
type ReferenceTypeValueMap map[string]map[string]cty.Value

type ModParseContext struct {
	ParseContext
	// the mod which is currently being parsed
	CurrentMod *modconfig.Mod
	// the workspace lock data
	WorkspaceLock *versionmap.WorkspaceLock

	Flags                ParseModFlag
	ListOptions          *filehelpers.ListOptions
	LoadedDependencyMods modconfig.ModMap

	// Variables are populated in an initial parse pass top we store them on the run context
	// so we can set them on the mod when we do the main parse

	// Variables is a map of the variables in the current mod
	// it is used to populate the variables property on the mod
	Variables map[string]*modconfig.Variable

	// DependencyVariables is a map of the variables in the dependency mods of the current mod
	// it is used to populate the variables property on the dependency
	DependencyVariables map[string]map[string]*modconfig.Variable
	ParentParseCtx      *ModParseContext

	// stack of parent resources for the currently parsed block
	// (unqualified name)
	parents []string

	// map of resource children, keyed by parent unqualified name
	blockChildMap map[string][]string

	// map of top  level blocks, for easy checking
	topLevelBlocks map[*hcl.Block]struct{}
	// map of block names, keyed by a hash of the blopck
	blockNameMap map[string]string
	// map of ReferenceTypeValueMaps keyed by mod
	// NOTE: all values from root mod are keyed with "local"
	referenceValues map[string]ReferenceTypeValueMap
}

func NewModParseContext(workspaceLock *versionmap.WorkspaceLock, rootEvalPath string, flags ParseModFlag, listOptions *filehelpers.ListOptions) *ModParseContext {
	parseContext := NewParseContext(rootEvalPath)
	c := &ModParseContext{
		ParseContext:         parseContext,
		Flags:                flags,
		WorkspaceLock:        workspaceLock,
		ListOptions:          listOptions,
		LoadedDependencyMods: make(modconfig.ModMap),

		blockChildMap: make(map[string][]string),
		blockNameMap:  make(map[string]string),
		// initialise variable maps - even though we later overwrite them
		Variables: make(map[string]*modconfig.Variable),
		referenceValues: map[string]ReferenceTypeValueMap{
			"local": make(ReferenceTypeValueMap),
		},
	}
	// add root node - this will depend on all other nodes
	c.dependencyGraph = c.newDependencyGraph()
	c.buildEvalContext()

	return c
}

func (r *ModParseContext) EnsureWorkspaceLock(mod *modconfig.Mod) error {
	// if the mod has dependencies, there must a workspace lock object in the run context
	// (mod MUST be the workspace mod, not a dependency, as we would hit this error as soon as we parse it)
	if mod.HasDependentMods() && (r.WorkspaceLock.Empty() || r.WorkspaceLock.Incomplete()) {
		return fmt.Errorf("not all dependencies are installed - run 'steampipe mod install'")
	}

	return nil
}

func (r *ModParseContext) PushParent(parent modconfig.ModTreeItem) {
	r.parents = append(r.parents, parent.GetUnqualifiedName())
}

func (r *ModParseContext) PopParent() string {
	n := len(r.parents) - 1
	res := r.parents[n]
	r.parents = r.parents[:n]
	return res
}

func (r *ModParseContext) PeekParent() string {
	if len(r.parents) == 0 {
		return r.CurrentMod.Name()
	}
	return r.parents[len(r.parents)-1]
}

// VariableValueMap converts a map of variables to a map of the underlying cty value
func VariableValueMap(variables map[string]*modconfig.Variable) map[string]cty.Value {
	ret := make(map[string]cty.Value, len(variables))
	for k, v := range variables {
		ret[k] = v.Value
	}
	return ret
}

// AddInputVariables adds variables to the run context.
// This function is called for the root run context after loading all input variables
func (r *ModParseContext) AddInputVariables(inputVariables *modconfig.ModVariableMap) {
	r.setRootVariables(inputVariables.RootVariables)
	r.setDependencyVariables(inputVariables.DependencyVariables)
}

// SetVariablesForDependencyMod adds variables to the run context.
// This function is called for dependent mod run context
func (r *ModParseContext) SetVariablesForDependencyMod(mod *modconfig.Mod, dependencyVariablesMap map[string]map[string]*modconfig.Variable) {
	r.setRootVariables(dependencyVariablesMap[mod.ShortName])
	r.setDependencyVariables(dependencyVariablesMap)
}

// setRootVariables sets the Variables property
// and adds the variables to the referenceValues map (used to build the eval context)
func (r *ModParseContext) setRootVariables(variables map[string]*modconfig.Variable) {
	r.Variables = variables
	// NOTE: we add with the name "var" not "variable" as that is how variables are referenced
	r.referenceValues["local"]["var"] = VariableValueMap(variables)
}

// setDependencyVariables sets the DependencyVariables property
// and adds the dependency variables to the referenceValues map (used to build the eval context
func (r *ModParseContext) setDependencyVariables(dependencyVariables map[string]map[string]*modconfig.Variable) {
	r.DependencyVariables = dependencyVariables
	// NOTE: we add with the name "var" not "variable" as that is how variables are referenced
	// add top level variables
	// add dependency mod variables, scoped by mod name
	for depModName, depVars := range r.DependencyVariables {
		// create map for this dependency if needed
		if r.referenceValues[depModName] == nil {
			r.referenceValues[depModName] = make(ReferenceTypeValueMap)
		}
		r.referenceValues[depModName]["var"] = VariableValueMap(depVars)
	}
}

// AddMod is used to add a mod with any pseudo resources to the eval context
// - in practice this will be a shell mod with just pseudo resources - other resources will be added as they are parsed
func (r *ModParseContext) AddMod(mod *modconfig.Mod) hcl.Diagnostics {
	if len(r.UnresolvedBlocks) > 0 {
		// should never happen
		panic("calling SetContent on runContext but there are unresolved blocks from a previous parse")
	}

	var diags hcl.Diagnostics

	moreDiags := r.storeResourceInCtyMap(mod)
	diags = append(diags, moreDiags...)

	resourceFunc := func(item modconfig.HclResource) (bool, error) {
		// add all mod resources except variables into cty map
		if _, ok := item.(*modconfig.Variable); !ok {
			moreDiags := r.storeResourceInCtyMap(item)
			diags = append(diags, moreDiags...)
		}
		// continue walking
		return true, nil
	}
	mod.WalkResources(resourceFunc)

	// rebuild the eval context
	r.buildEvalContext()
	return diags
}

func (r *ModParseContext) SetDecodeContent(content *hcl.BodyContent, fileData map[string][]byte) {
	// put blocks into map as well
	r.topLevelBlocks = make(map[*hcl.Block]struct{}, len(r.blocks))
	for _, b := range content.Blocks {
		r.topLevelBlocks[b] = struct{}{}
	}
	r.ParseContext.SetDecodeContent(content, fileData)
}

// AddDependencies :: the block could not be resolved as it has dependencies
// 1) store block as unresolved
// 2) add dependencies to our tree of dependencies
func (r *ModParseContext) AddDependencies(block *hcl.Block, name string, dependencies map[string]*modconfig.ResourceDependency) hcl.Diagnostics {
	// TACTICAL if this is NOT a top level block, add a suffix to the block name
	// this is needed to avoid circular dependency errors if a nested block references
	// a top level block with the same name
	if !r.IsTopLevelBlock(block) {
		name = "nested." + name
	}
	return r.ParseContext.AddDependencies(block, name, dependencies)
}

// ShouldCreateDefaultMod returns whether the flag is set to create a default mod if no mod definition exists
func (r *ModParseContext) ShouldCreateDefaultMod() bool {
	return r.Flags&CreateDefaultMod == CreateDefaultMod
}

// CreatePseudoResources returns whether the flag is set to create pseudo resources
func (r *ModParseContext) CreatePseudoResources() bool {
	return r.Flags&CreatePseudoResources == CreatePseudoResources
}

// AddResource stores this resource as a variable to be added to the eval context. It alse
func (r *ModParseContext) AddResource(resource modconfig.HclResource) hcl.Diagnostics {
	diagnostics := r.storeResourceInCtyMap(resource)
	if diagnostics.HasErrors() {
		return diagnostics
	}

	// rebuild the eval context
	r.buildEvalContext()

	return nil
}

func (r *ModParseContext) GetMod(modShortName string) *modconfig.Mod {
	if modShortName == r.CurrentMod.ShortName {
		return r.CurrentMod
	}
	// we need to iterate through dependency mods - we cannot use modShortNameas key as it is short name
	for _, dep := range r.LoadedDependencyMods {
		if dep.ShortName == modShortName {
			return dep
		}
	}
	return nil
}

func (r *ModParseContext) GetResourceMaps() *modconfig.ResourceMaps {
	dependencyResourceMaps := make([]*modconfig.ResourceMaps, len(r.LoadedDependencyMods))
	idx := 0
	// use the current mod as the base resource map
	resourceMap := r.CurrentMod.GetResourceMaps()

	// merge in the dependency mods
	for _, m := range r.LoadedDependencyMods {
		dependencyResourceMaps[idx] = m.GetResourceMaps()
		idx++
	}

	resourceMap = resourceMap.Merge(dependencyResourceMaps)
	return resourceMap
}

func (m *ModParseContext) GetResource(parsedName *modconfig.ParsedResourceName) (resource modconfig.HclResource, found bool) {
	return m.GetResourceMaps().GetResource(parsedName)
}

// eval functions
func (r *ModParseContext) buildEvalContext() {
	// convert variables to cty values
	variables := make(map[string]cty.Value)

	// now for each mod add all the values
	for mod, modMap := range r.referenceValues {
		if mod == "local" {
			for k, v := range modMap {
				variables[k] = cty.ObjectVal(v)
			}
			continue
		}

		// mod map is map[string]map[string]cty.Value
		// for each element (i.e. map[string]cty.Value) convert to cty object
		refTypeMap := make(map[string]cty.Value)
		for refType, typeValueMap := range modMap {
			refTypeMap[refType] = cty.ObjectVal(typeValueMap)
		}
		// now convert the cty map to a cty object
		variables[mod] = cty.ObjectVal(refTypeMap)
	}

	r.ParseContext.buildEvalContext(variables)
}

// update the cached cty value for the given resource, as long as itr does not already exist
func (r *ModParseContext) storeResourceInCtyMap(resource modconfig.HclResource) hcl.Diagnostics {
	// add resource to variable map
	ctyValue, diags := r.getResourceCtyValue(resource)
	if diags.HasErrors() {
		return diags
	}

	// add into the reference value map
	if diags := r.addReferenceValue(resource, ctyValue); diags.HasErrors() {
		return diags
	}

	// remove this resource from unparsed blocks
	delete(r.UnresolvedBlocks, resource.Name())

	return nil
}

func (r *ModParseContext) getResourceCtyValue(resource modconfig.HclResource) (cty.Value, hcl.Diagnostics) {
	ctyValue, err := resource.(modconfig.CtyValueProvider).CtyValue()
	if err != nil {
		return cty.Zero, r.errToCtyValueDiags(resource, err)
	}
	// if this is a value map, merge in the values of base structs
	// if it is NOT a value map, the resource must have overridden CtyValue so do not merge base structs
	if ctyValue.Type().FriendlyName() != "object" {
		return ctyValue, nil
	}
	// TODO [node_reuse] fetch nested structs and serialise automatically https://github.com/turbot/steampipe/issues/2924
	valueMap := ctyValue.AsValueMap()
	if valueMap == nil {
		valueMap = make(map[string]cty.Value)
	}
	base := resource.GetHclResourceImpl()
	if err := r.mergeResourceCtyValue(base, valueMap); err != nil {
		return cty.Zero, r.errToCtyValueDiags(resource, err)
	}

	if qp, ok := resource.(modconfig.QueryProvider); ok {
		base := qp.GetQueryProviderImpl()
		if err := r.mergeResourceCtyValue(base, valueMap); err != nil {
			return cty.Zero, r.errToCtyValueDiags(resource, err)
		}
	}

	if treeItem, ok := resource.(modconfig.ModTreeItem); ok {
		base := treeItem.GetModTreeItemImpl()
		if err := r.mergeResourceCtyValue(base, valueMap); err != nil {
			return cty.Zero, r.errToCtyValueDiags(resource, err)
		}
	}
	return cty.ObjectVal(valueMap), nil
}

// merge the cty value of the given interface into valueMap
// (note: this mutates valueMap)
func (r *ModParseContext) mergeResourceCtyValue(resource modconfig.CtyValueProvider, valueMap map[string]cty.Value) (err error) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println(string(debug.Stack()))
			err = fmt.Errorf("panic in mergeResourceCtyValue: %s", helpers.ToError(r).Error())
		}
	}()
	ctyValue, err := resource.CtyValue()
	if err != nil {
		return err
	}
	if ctyValue == cty.Zero {
		return nil
	}
	// merge results
	for k, v := range ctyValue.AsValueMap() {
		valueMap[k] = v
	}
	return nil
}

func (r *ModParseContext) errToCtyValueDiags(resource modconfig.HclResource, err error) hcl.Diagnostics {
	return hcl.Diagnostics{&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  fmt.Sprintf("failed to convert resource '%s' to its cty value", resource.Name()),
		Detail:   err.Error(),
		Subject:  resource.GetDeclRange(),
	}}
}

func (r *ModParseContext) addReferenceValue(resource modconfig.HclResource, value cty.Value) hcl.Diagnostics {
	parsedName, err := modconfig.ParseResourceName(resource.Name())
	if err != nil {
		return hcl.Diagnostics{&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  fmt.Sprintf("failed to parse resource name %s", resource.Name()),
			Detail:   err.Error(),
			Subject:  resource.GetDeclRange(),
		}}
	}

	// TODO validate mod name clashes
	// TODO mod reserved names
	// TODO handle aliases

	key := parsedName.Name
	typeString := parsedName.ItemType

	// the resource name will not have a mod - but the run context knows which mod we are parsing
	mod := r.CurrentMod
	modName := mod.ShortName
	if mod.ModPath == r.RootEvalPath {
		modName = "local"
	}
	variablesForMod, ok := r.referenceValues[modName]
	// do we have a map of reference values for this dep mod?
	if !ok {
		// no - create one
		variablesForMod = make(ReferenceTypeValueMap)
		r.referenceValues[modName] = variablesForMod
	}
	// do we have a map of reference values for this type
	variablesForType, ok := variablesForMod[typeString]
	if !ok {
		// no - create one
		variablesForType = make(map[string]cty.Value)
	}

	// DO NOT update the cached cty values if the value already exists
	// this can happen in the case of variables where we initialise the context with values read from file
	// or passed on the command line,	// does this item exist in the map
	if _, ok := variablesForType[key]; !ok {
		variablesForType[key] = value
		variablesForMod[typeString] = variablesForType
		r.referenceValues[modName] = variablesForMod
	}

	return nil
}

func (r *ModParseContext) AddLoadedDependentMods(mods modconfig.ModMap) {
	for k, v := range mods {
		if _, alreadyLoaded := r.LoadedDependencyMods[k]; !alreadyLoaded {
			r.LoadedDependencyMods[k] = v
		}
	}
}

func (r *ModParseContext) IsTopLevelBlock(block *hcl.Block) bool {
	_, isTopLevel := r.topLevelBlocks[block]
	return isTopLevel
}
