/*
Copyright The Helm Authors.

Licensed 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 util

import (
	"fmt"
	"log"
	"maps"

	"helm.sh/helm/v4/internal/copystructure"
	chart "helm.sh/helm/v4/pkg/chart"
	"helm.sh/helm/v4/pkg/chart/common"
)

func concatPrefix(a, b string) string {
	if a == "" {
		return b
	}
	return fmt.Sprintf("%s.%s", a, b)
}

// CoalesceValues coalesces all of the values in a chart (and its subcharts).
//
// Values are coalesced together using the following rules:
//
//   - Values in a higher level chart always override values in a lower-level
//     dependency chart
//   - Scalar values and arrays are replaced, maps are merged
//   - A chart has access to all of the variables for it, as well as all of
//     the values destined for its dependencies.
func CoalesceValues(chrt chart.Charter, vals map[string]any) (common.Values, error) {
	valsCopy, err := copyValues(vals)
	if err != nil {
		return vals, err
	}
	return coalesce(log.Printf, chrt, valsCopy, "", false)
}

// MergeValues is used to merge the values in a chart and its subcharts. This
// is different from Coalescing as nil/null values are preserved.
//
// Values are coalesced together using the following rules:
//
//   - Values in a higher level chart always override values in a lower-level
//     dependency chart
//   - Scalar values and arrays are replaced, maps are merged
//   - A chart has access to all of the variables for it, as well as all of
//     the values destined for its dependencies.
//
// Retaining Nils is useful when processes early in a Helm action or business
// logic need to retain them for when Coalescing will happen again later in the
// business logic.
func MergeValues(chrt chart.Charter, vals map[string]any) (common.Values, error) {
	valsCopy, err := copyValues(vals)
	if err != nil {
		return vals, err
	}
	return coalesce(log.Printf, chrt, valsCopy, "", true)
}

func copyValues(vals map[string]any) (common.Values, error) {
	v, err := copystructure.Copy(vals)
	if err != nil {
		return vals, err
	}

	valsCopy := v.(map[string]any)
	// if we have an empty map, make sure it is initialized
	if valsCopy == nil {
		valsCopy = make(map[string]any)
	}

	return valsCopy, nil
}

type printFn func(format string, v ...any)

// coalesce coalesces the dest values and the chart values, giving priority to the dest values.
//
// This is a helper function for CoalesceValues and MergeValues.
//
// Note, the merge argument specifies whether this is being used by MergeValues
// or CoalesceValues. Coalescing removes null values and their keys in some
// situations while merging keeps the null values.
func coalesce(printf printFn, ch chart.Charter, dest map[string]any, prefix string, merge bool) (map[string]any, error) {
	coalesceValues(printf, ch, dest, prefix, merge)
	return coalesceDeps(printf, ch, dest, prefix, merge)
}

// coalesceDeps coalesces the dependencies of the given chart.
func coalesceDeps(printf printFn, chrt chart.Charter, dest map[string]any, prefix string, merge bool) (map[string]any, error) {
	ch, err := chart.NewAccessor(chrt)
	if err != nil {
		return dest, err
	}
	for _, subchart := range ch.Dependencies() {
		sub, err := chart.NewAccessor(subchart)
		if err != nil {
			return dest, err
		}
		if c, ok := dest[sub.Name()]; !ok {
			// If dest doesn't already have the key, create it.
			dest[sub.Name()] = make(map[string]any)
		} else if !istable(c) {
			return dest, fmt.Errorf("type mismatch on %s: %t", sub.Name(), c)
		}
		if dv, ok := dest[sub.Name()]; ok {
			dvmap := dv.(map[string]any)
			subPrefix := concatPrefix(prefix, ch.Name())
			// Get globals out of dest and merge them into dvmap.
			coalesceGlobals(printf, dvmap, dest, subPrefix, merge)
			// Now coalesce the rest of the values.
			var err error
			dest[sub.Name()], err = coalesce(printf, subchart, dvmap, subPrefix, merge)
			if err != nil {
				return dest, err
			}
		}
	}
	return dest, nil
}

// coalesceGlobals copies the globals out of src and merges them into dest.
//
// For convenience, returns dest.
func coalesceGlobals(printf printFn, dest, src map[string]any, prefix string, _ bool) {
	var dg, sg map[string]any

	if destglob, ok := dest[common.GlobalKey]; !ok {
		dg = make(map[string]any)
	} else if dg, ok = destglob.(map[string]any); !ok {
		printf("warning: skipping globals because destination %s is not a table.", common.GlobalKey)
		return
	}

	if srcglob, ok := src[common.GlobalKey]; !ok {
		sg = make(map[string]any)
	} else if sg, ok = srcglob.(map[string]any); !ok {
		printf("warning: skipping globals because source %s is not a table.", common.GlobalKey)
		return
	}

	// EXPERIMENTAL: In the past, we have disallowed globals to test tables. This
	// reverses that decision. It may somehow be possible to introduce a loop
	// here, but I haven't found a way. So for the time being, let's allow
	// tables in globals.
	for key, val := range sg {
		if istable(val) {
			vv := copyMap(val.(map[string]any))
			if destv, ok := dg[key]; !ok {
				// Here there is no merge. We're just adding.
				dg[key] = vv
			} else {
				if destvmap, ok := destv.(map[string]any); !ok {
					printf("Conflict: cannot merge map onto non-map for %q. Skipping.", key)
				} else {
					// Basically, we reverse order of coalesce here to merge
					// top-down.
					subPrefix := concatPrefix(prefix, key)
					// In this location coalesceTablesFullKey should always have
					// merge set to true. The output of coalesceGlobals is run
					// through coalesce where any nils will be removed.
					coalesceTablesFullKey(printf, vv, destvmap, subPrefix, true)
					dg[key] = vv
				}
			}
		} else if dv, ok := dg[key]; ok && istable(dv) {
			// It's not clear if this condition can actually ever trigger.
			printf("key %s is table. Skipping", key)
		} else {
			// TODO: Do we need to do any additional checking on the value?
			dg[key] = val
		}
	}
	dest[common.GlobalKey] = dg
}

func copyMap(src map[string]any) map[string]any {
	m := make(map[string]any, len(src))
	maps.Copy(m, src)
	return m
}

// coalesceValues builds up a values map for a particular chart.
//
// Values in v will override the values in the chart.
func coalesceValues(printf printFn, c chart.Charter, v map[string]any, prefix string, merge bool) {
	ch, err := chart.NewAccessor(c)
	if err != nil {
		return
	}

	subPrefix := concatPrefix(prefix, ch.Name())

	// Using c.Values directly when coalescing a table can cause problems where
	// the original c.Values is altered. Creating a deep copy stops the problem.
	// This section is fault-tolerant as there is no ability to return an error.
	valuesCopy, err := copystructure.Copy(ch.Values())
	var vc map[string]any
	var ok bool
	if err != nil {
		// If there is an error something is wrong with copying c.Values it
		// means there is a problem in the deep copying package or something
		// wrong with c.Values. In this case we will use c.Values and report
		// an error.
		printf("warning: unable to copy values, err: %s", err)
		vc = ch.Values()
	} else {
		vc, ok = valuesCopy.(map[string]any)
		if !ok {
			// c.Values has a map[string]interface{} structure. If the copy of
			// it cannot be treated as map[string]interface{} there is something
			// strangely wrong. Log it and use c.Values
			printf("warning: unable to convert values copy to values type")
			vc = ch.Values()
		}
	}

	for key, val := range vc {
		if value, ok := v[key]; ok {
			if value == nil && !merge {
				// When the YAML value is null and we are coalescing instead of
				// merging, we remove the value's key.
				// This allows Helm's various sources of values (value files or --set) to
				// remove incompatible keys from any previous chart, file, or set values.
				delete(v, key)
			} else if dest, ok := value.(map[string]any); ok {
				// if v[key] is a table, merge nv's val table into v[key].
				src, ok := val.(map[string]any)
				if !ok {
					// If the original value is nil, there is nothing to coalesce, so we don't print
					// the warning
					if val != nil {
						printf("warning: skipped value for %s.%s: Not a table.", subPrefix, key)
					}
				} else {
					// If the key is a child chart, coalesce tables with Merge set to true
					merge := childChartMergeTrue(c, key, merge)

					// When coalescing, clean nils from chart defaults before merging
					// so they don't leak into the result.
					if !merge {
						cleanNilValues(src)
					}

					// Because v has higher precedence than nv, dest values override src
					// values.
					coalesceTablesFullKey(printf, dest, src, concatPrefix(subPrefix, key), merge)
				}
			}
		} else {
			// If the key is not in v, copy it from nv.
			// When coalescing, skip chart default nils and clean nils from
			// nested maps so they don't shadow globals or produce %!s(<nil>).
			if !merge {
				if val == nil {
					continue
				}
				if sub, ok := val.(map[string]any); ok {
					cleanNilValues(sub)
				}
			}
			v[key] = val
		}
	}
}

func childChartMergeTrue(chrt chart.Charter, key string, merge bool) bool {
	ch, err := chart.NewAccessor(chrt)
	if err != nil {
		return merge
	}
	for _, subchart := range ch.Dependencies() {
		sub, err := chart.NewAccessor(subchart)
		if err != nil {
			return merge
		}
		if sub.Name() == key {
			return true
		}
	}
	return merge
}

// CoalesceTables merges a source map into a destination map.
//
// dest is considered authoritative.
func CoalesceTables(dst, src map[string]any) map[string]any {
	return coalesceTablesFullKey(log.Printf, dst, src, "", false)
}

func MergeTables(dst, src map[string]any) map[string]any {
	return coalesceTablesFullKey(log.Printf, dst, src, "", true)
}

// coalesceTablesFullKey merges a source map into a destination map.
//
// dest is considered authoritative.
func coalesceTablesFullKey(printf printFn, dst, src map[string]any, prefix string, merge bool) map[string]any {
	// When --reuse-values is set but there are no modifications yet, return new values
	if src == nil {
		return dst
	}
	if dst == nil {
		return src
	}
	// Track original non-nil src keys before modifying src
	// This lets us distinguish between user nullifying a chart default vs
	// user setting nil for a key not in chart defaults.
	srcOriginalNonNil := make(map[string]bool)
	for key, val := range src {
		if val != nil {
			srcOriginalNonNil[key] = true
		}
	}
	for key, val := range dst {
		if val == nil {
			src[key] = nil
		}
	}
	// Because dest has higher precedence than src, dest values override src
	// values.
	for key, val := range src {
		fullkey := concatPrefix(prefix, key)
		if dv, ok := dst[key]; ok && !merge && dv == nil && srcOriginalNonNil[key] {
			// When coalescing (not merging), if dst has nil and src has a non-nil
			// value, the user is nullifying a chart default - remove the key.
			// But if src also has nil (or key not in src), preserve the nil
			delete(dst, key)
		} else if !ok {
			dst[key] = val
		} else if istable(val) {
			if istable(dv) {
				coalesceTablesFullKey(printf, dv.(map[string]any), val.(map[string]any), fullkey, merge)
			} else {
				printf("warning: cannot overwrite table with non table for %s (%v)", fullkey, val)
			}
		} else if istable(dv) && val != nil {
			printf("warning: destination for %s is a table. Ignoring non-table value (%v)", fullkey, val)
		}
	}
	return dst
}

// cleanNilValues recursively removes nil entries in-place from a map so that chart
// default nils don't leak into the coalesced result.
func cleanNilValues(m map[string]any) {
	for key, val := range m {
		if val == nil {
			delete(m, key)
		} else if sub, ok := val.(map[string]any); ok {
			cleanNilValues(sub)
		}
	}
}

// istable is a special-purpose function to see if the present thing matches the definition of a YAML table.
func istable(v any) bool {
	_, ok := v.(map[string]any)
	return ok
}
