/*
Copyright 2023 The KubeSphere 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 variable

import (
	"context"
	"maps"
	"path/filepath"
	"reflect"
	"sync"

	"github.com/cockroachdb/errors"
	kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
	"k8s.io/apimachinery/pkg/types"
	ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"

	_const "github.com/kubesphere/kubekey/v4/pkg/const"
	"github.com/kubesphere/kubekey/v4/pkg/variable/source"
)

var (
	emptyGetFunc GetFunc = func(Variable) (any, error) {
		return nil, errors.New("nil value returned")
	}
	emptyMergeFunc MergeFunc = func(Variable) error {
		return nil
	}
)

// GetFunc get data from variable
type GetFunc func(Variable) (any, error)

// MergeFunc merge data to variable
type MergeFunc func(Variable) error

// Variable store all vars which playbook used.
type Variable interface {
	Get(getFunc GetFunc) (any, error)
	Merge(mergeFunc MergeFunc) error
}

// New variable. generate value from config args. and render to source.
func New(ctx context.Context, client ctrlclient.Client, playbook kkcorev1.Playbook, st source.SourceType) (Variable, error) {
	var err error
	// new source
	var s source.Source

	switch st {
	case source.MemorySource:
		s = source.NewMemorySource()
	case source.FileSource:
		path := filepath.Join(_const.GetWorkdirFromConfig(playbook.Spec.Config), _const.RuntimeDir, kkcorev1.SchemeGroupVersion.String(), _const.RuntimePlaybookDir, playbook.Namespace, playbook.Name, _const.RuntimePlaybookVariableDir)
		s, err = source.NewFileSource(path)
		if err != nil {
			return nil, err
		}
	default:
		return nil, errors.Errorf("unsupported source type: %v", st)
	}

	// get inventory
	var inventory = &kkcorev1.Inventory{}
	if playbook.Spec.InventoryRef != nil {
		if err := client.Get(ctx, types.NamespacedName{Namespace: playbook.Spec.InventoryRef.Namespace, Name: playbook.Spec.InventoryRef.Name}, inventory); err != nil {
			return nil, errors.Wrapf(err, "failed to get inventory %q from playbook %q", playbook.Spec.InventoryRef, ctrlclient.ObjectKeyFromObject(&playbook))
		}
	}

	v := &variable{
		key:    string(playbook.UID),
		source: s,
		value: &value{
			Config:    playbook.Spec.Config,
			Inventory: *inventory,
			Hosts:     make(map[string]host),
		},
	}

	if gd, ok := ConvertGroup(*inventory)["all"]; ok {
		for _, hostname := range gd {
			v.value.Hosts[hostname] = host{
				RemoteVars:  make(map[string]any),
				RuntimeVars: make(map[string]any),
			}
		}
	}

	// read data from source
	data, err := v.source.Read()
	if err != nil {
		return nil, errors.Wrapf(err, "failed to read data from source. playbook: %q", ctrlclient.ObjectKeyFromObject(&playbook))
	}

	for k, d := range data {
		// set hosts
		h := host{}
		if val, ok := d["remote"]; ok {
			remoteVars, ok := val.(map[string]any)
			if !ok {
				return nil, errors.New("type assertion failed. expected map[string]any for remote vars")
			}
			h.RemoteVars = remoteVars
		}
		if val, ok := d["runtime"]; ok {
			runtimeVars, ok := val.(map[string]any)
			if !ok {
				return nil, errors.New("type assertion failed. expected map[string]any for runtime vars")
			}
			h.RuntimeVars = runtimeVars
		}
		v.value.Hosts[k] = h
	}

	return v, nil
}

type variable struct {
	// key is the unique Identifier of the variable. usually the UID of the playbook.
	key string
	// source is where the variable is stored
	source source.Source
	// value is the data of the variable, which store in memory
	value *value
	// lock is the lock for value
	sync.RWMutex
}

// resultKey is the key used to store global result variables in the value struct.
// It is used as the only key in the Result map to hold result data set by result tasks.
const resultKey = "result"

// value holds all variable data for a playbook execution.
// It contains configuration, inventory, per-host variables, and global result variables.
type value struct {
	Config    kkcorev1.Config    // Playbook configuration
	Inventory kkcorev1.Inventory // Playbook inventory
	// Hosts stores variables for each host, including remote and runtime variables.
	Hosts map[string]host
	// Result stores global result variables set by result tasks.
	// The map always contains a single key (resultKey) for easy cloning and merging.
	Result map[string]any
}

type host struct {
	// RemoteVars sources from remote node config. as gather_fact.scope all tasks. it should not be changed.
	RemoteVars map[string]any `json:"remote"`
	// RuntimeVars sources from runtime. store which defined in each appeared block vars.
	RuntimeVars map[string]any `json:"runtime"`
}

// DeepCopy creates a deep copy of the variable struct, including all nested fields.
// It copies the Config and Inventory objects, and creates new maps for Hosts with cloned
// RemoteVars and RuntimeVars. The key and source fields are copied by reference since
// they don't need deep copying.
func (v *variable) DeepCopy() *variable {
	copyVal := &value{
		Config:    *v.value.Config.DeepCopy(),
		Inventory: *v.value.Inventory.DeepCopy(),
		Hosts:     make(map[string]host, len(v.value.Hosts)),
		Result:    maps.Clone(v.value.Result),
	}
	for k, h := range v.value.Hosts {
		copyVal.Hosts[k] = host{
			RemoteVars:  maps.Clone(h.RemoteVars),
			RuntimeVars: maps.Clone(h.RuntimeVars),
		}
	}

	return &variable{
		key:    v.key,
		source: v.source,
		value:  copyVal,
	}
}

// Get vars
func (v *variable) Get(f GetFunc) (any, error) {
	v.RLock()
	defer v.RUnlock()

	return f(v)
}

// Merge hosts vars to variable and sync to resource
func (v *variable) Merge(f MergeFunc) error {
	v.Lock()
	defer v.Unlock()

	// new variable to avoid get lock in mergeFunc
	nv := v.DeepCopy()
	if err := f(nv); err != nil {
		return err
	}
	return v.syncSource(*nv.value)
}

// syncSource sync hosts vars to source.
func (v *variable) syncSource(newVal value) error {
	v.value.Result = newVal.Result
	for hn, hv := range v.value.Hosts {
		if reflect.DeepEqual(newVal.Hosts[hn], hv) {
			// nothing change skip.
			continue
		}
		if err := v.source.Write(map[string]any{
			"remote":  newVal.Hosts[hn].RemoteVars,
			"runtime": newVal.Hosts[hn].RuntimeVars,
		}, hn); err != nil {
			return errors.Wrapf(err, "failed to write host %s variable to source", hn)
		}
		// update new value to variable
		v.value.Hosts[hn] = newVal.Hosts[hn]
	}

	return nil
}
