package parser

import (
	"fmt"
	"strings"

	"github.com/porter-dev/porter/api/types"
	"github.com/porter-dev/porter/internal/helm"
	"github.com/porter-dev/porter/internal/models"
	"github.com/porter-dev/porter/internal/templater"
	"github.com/porter-dev/porter/internal/templater/infra"
	"github.com/porter-dev/porter/internal/templater/utils"
	"github.com/stefanmcshane/helm/pkg/chart"
	"github.com/stefanmcshane/helm/pkg/release"
	"k8s.io/client-go/dynamic"
	"sigs.k8s.io/yaml"

	td "github.com/porter-dev/porter/internal/templater/dynamic"
	tm "github.com/porter-dev/porter/internal/templater/helm/manifests"
	tv "github.com/porter-dev/porter/internal/templater/helm/values"
)

// ClientConfigDefault is a set of default clients to be used if a context in
// form.yaml does not declare otherwise.
type ClientConfigDefault struct {
	DynamicClient dynamic.Interface

	HelmAgent   *helm.Agent
	HelmRelease *release.Release
	HelmChart   *chart.Chart

	InfraOperation *models.Operation
}

// ContextConfig can read/write from a specified context (data source)
type ContextConfig struct {
	FromType       string   // "live" or "declared"
	Capabilities   []string // "read", "write"
	TemplateReader templater.TemplateReader
	TemplateWriter templater.TemplateWriter
}

// GetFormFromRelease returns the form by parsing a release's files. Returns nil if
// the form is not found, throws an error if the form was found but there was a parsing
// error.
func GetFormFromRelease(def *ClientConfigDefault, rel *release.Release) (*types.FormYAML, error) {
	for _, file := range rel.Chart.Files {
		if strings.Contains(file.Name, "form.yaml") {
			return FormYAMLFromBytes(def, file.Data, "", "")
		}
	}

	return nil, nil
}

// FormYAMLFromBytes generates a usable form yaml from raw form config and a
// set of default clients.
//
// stateType refers to the types of state that should be read. The two state types
// are "live" and "declared" -- if stateType is "", this will read both live and
// declared states.
func FormYAMLFromBytes(def *ClientConfigDefault, bytes []byte, stateType, contextType string) (*types.FormYAML, error) {
	form, err := unqueriedFormYAMLFromBytes(bytes, contextType)
	if err != nil {
		return nil, err
	}

	lookup := formToLookupTable(def, form, stateType)

	// merge data from lookup
	data := make(map[string]interface{})

	for _, lookupVal := range lookup {
		if lookupVal != nil {
			queryRes, err := lookupVal.TemplateReader.Read()
			if err != nil {
				continue
			}

			for queryResKey, queryResVal := range queryRes {
				data[queryResKey] = queryResVal
			}
		}
	}

	for i, tab := range form.Tabs {
		for j, section := range tab.Sections {
			for k, content := range section.Contents {
				key := fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k)

				if val, ok := data[key]; ok {
					content.Value = val
				}

				if content.Value == nil && content.Settings.Default != nil {
					content.Value = []interface{}{content.Settings.Default}
				}
			}
		}
	}

	// if the client config defaults are Helm-related, this is a cluster-scoped resource
	if def.HelmAgent != nil || def.HelmChart != nil || def.HelmRelease != nil {
		form.IsClusterScoped = true
	}

	return form, nil
}

func FormStreamer(
	def *ClientConfigDefault,
	bytes []byte,
	stateType string,
	targetContext *types.FormContext,
	on templater.OnDataStream,
	stopCh <-chan struct{},
) error {
	form, err := unqueriedFormYAMLFromBytes(bytes, targetContext.Type)
	if err != nil {
		return err
	}

	lookup := formToLookupTable(def, form, stateType)

	for lookupContext, lookupVal := range lookup {
		if lookupVal != nil && areContextsEqual(targetContext, lookupContext) {
			err := lookupVal.TemplateReader.ReadStream(on, stopCh)
			if err != nil {
				continue
			}
		}
	}

	return nil
}

func areContextsEqual(context1, context2 *types.FormContext) bool {
	if context1.Type != context2.Type {
		return false
	}

	subset12 := isSubset(context1.Config, context2.Config)
	subset21 := isSubset(context2.Config, context1.Config)

	return subset12 && subset21
}

func isSubset(map1, map2 map[string]string) bool {
	subset12 := true

	for key1, val1 := range map1 {
		if val2, exists := map2[key1]; !exists || val2 != val1 {
			subset12 = false
			break
		}
	}

	return subset12
}

// unqueriedFormYAMLFromBytes returns a FormYAML without values queries populated
func unqueriedFormYAMLFromBytes(bytes []byte, contextType string) (*types.FormYAML, error) {
	// parse bytes into object
	form := &types.FormYAML{}

	err := yaml.Unmarshal(bytes, form)
	if err != nil {
		return nil, err
	}

	// populate all context fields, with default set to helm/values with no config
	parent := &types.FormContext{}

	if contextType == "" {
		parent.Type = "helm/values"
	} else {
		parent.Type = contextType
	}

	for _, tab := range form.Tabs {
		if tab.Context == nil {
			tab.Context = parent
		}

		for _, section := range tab.Sections {
			if section.Context == nil {
				section.Context = tab.Context
			}

			for _, content := range section.Contents {
				if content.Context == nil {
					content.Context = section.Context
				}
			}
		}
	}

	return form, nil
}

// create map[*FormContext]*ContextConfig
// assumes all contexts populated
func formToLookupTable(def *ClientConfigDefault, form *types.FormYAML, stateType string) map[*types.FormContext]*ContextConfig {
	lookup := make(map[*types.FormContext]*ContextConfig)

	for i, tab := range form.Tabs {
		for j, section := range tab.Sections {
			for k, content := range section.Contents {
				if content.Context == nil {
					continue
				}

				if _, ok := lookup[content.Context]; !ok {
					lookup[content.Context] = formContextToContextConfig(def, content.Context, stateType)
				}

				if lookup[content.Context] == nil {
					continue
				}

				if content.Value != nil && fmt.Sprintf("%v", content.Value) != "" {
					// TODO -- case on whether value is proper query string, if not resolve it to a
					// proper query string
					query, err := utils.NewQuery(
						fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
						fmt.Sprintf("%v", content.Value),
						content.Settings.Default,
					)
					if err != nil {
						continue
					}

					if stateType == "" || stateType == lookup[content.Context].FromType {
						lookup[content.Context].TemplateReader.RegisterQuery(query)
					}
				} else if content.Variable != "" {
					// if variable field set without value field set, make variable field into jsonpath
					// query
					query, err := utils.NewQuery(
						fmt.Sprintf("tabs[%d].sections[%d].contents[%d]", i, j, k),
						fmt.Sprintf(".%v", content.Variable),
						content.Settings.Default,
					)
					if err != nil {
						continue
					}

					if stateType == "" || stateType == lookup[content.Context].FromType {
						lookup[content.Context].TemplateReader.RegisterQuery(query)
					}
				}
			}
		}
	}

	return lookup
}

// TODO -- this needs to be able to construct new context configs based on
// configuration for each context, but right now just uses the default config
// for everything
func formContextToContextConfig(def *ClientConfigDefault, context *types.FormContext, stateType string) *ContextConfig {
	res := &ContextConfig{}

	if context.Type == "helm/values" && (stateType == "" || stateType == "declared") {
		res.FromType = "declared"

		res.Capabilities = []string{"read", "write"}

		res.TemplateReader = &tv.TemplateReader{
			Release: def.HelmRelease,
			Chart:   def.HelmChart,
		}

		relName := ""

		if def.HelmRelease != nil {
			relName = def.HelmRelease.Name
		}

		res.TemplateWriter = &tv.TemplateWriter{
			Agent:       def.HelmAgent,
			Chart:       def.HelmChart,
			ReleaseName: relName,
		}
	} else if context.Type == "helm/manifests" && (stateType == "" || stateType == "live") {
		res.FromType = "live"

		res.Capabilities = []string{"read"}

		res.TemplateReader = &tm.TemplateReader{
			Release: def.HelmRelease,
		}
	} else if context.Type == "cluster" && (stateType == "" || stateType == "live") {
		res.FromType = "live"

		res.Capabilities = []string{"read"}

		// identify object based on passed config
		obj := &td.Object{
			Group:     context.Config["group"],
			Version:   context.Config["version"],
			Resource:  context.Config["resource"],
			Namespace: context.Config["namespace"],
			Name:      context.Config["name"],
		}

		res.TemplateReader = td.NewDynamicTemplateReader(def.DynamicClient, obj)
	} else if context.Type == "infra" && def.InfraOperation != nil {
		res.FromType = "declared"

		res.TemplateReader = &infra.OperationReader{
			Operation: def.InfraOperation,
		}
	} else {
		return nil
	}

	return res
}
