package task

import (
	"os"
	"path/filepath"
	"strings"

	"github.com/joho/godotenv"

	"github.com/go-task/task/v3/errors"
	"github.com/go-task/task/v3/internal/execext"
	"github.com/go-task/task/v3/internal/filepathext"
	"github.com/go-task/task/v3/internal/fingerprint"
	"github.com/go-task/task/v3/internal/omap"
	"github.com/go-task/task/v3/internal/templater"
	"github.com/go-task/task/v3/taskfile/ast"
)

// CompiledTask returns a copy of a task, but replacing variables in almost all
// properties using the Go template package.
func (e *Executor) CompiledTask(call *ast.Call) (*ast.Task, error) {
	return e.compiledTask(call, true)
}

// FastCompiledTask is like CompiledTask, but it skippes dynamic variables.
func (e *Executor) FastCompiledTask(call *ast.Call) (*ast.Task, error) {
	return e.compiledTask(call, false)
}

func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, error) {
	origTask, err := e.GetTask(call)
	if err != nil {
		return nil, err
	}

	var vars *ast.Vars
	if evaluateShVars {
		vars, err = e.Compiler.GetVariables(origTask, call)
	} else {
		vars, err = e.Compiler.FastGetVariables(origTask, call)
	}
	if err != nil {
		return nil, err
	}

	cache := &templater.Cache{Vars: vars}

	new := ast.Task{
		Task:                 origTask.Task,
		Label:                templater.Replace(origTask.Label, cache),
		Desc:                 templater.Replace(origTask.Desc, cache),
		Prompt:               templater.Replace(origTask.Prompt, cache),
		Summary:              templater.Replace(origTask.Summary, cache),
		Aliases:              origTask.Aliases,
		Sources:              templater.ReplaceGlobs(origTask.Sources, cache),
		Generates:            templater.ReplaceGlobs(origTask.Generates, cache),
		Dir:                  templater.Replace(origTask.Dir, cache),
		Set:                  origTask.Set,
		Shopt:                origTask.Shopt,
		Vars:                 nil,
		Env:                  nil,
		Dotenv:               templater.Replace(origTask.Dotenv, cache),
		Silent:               origTask.Silent,
		Interactive:          origTask.Interactive,
		Internal:             origTask.Internal,
		Method:               templater.Replace(origTask.Method, cache),
		Prefix:               templater.Replace(origTask.Prefix, cache),
		IgnoreError:          origTask.IgnoreError,
		Run:                  templater.Replace(origTask.Run, cache),
		IncludeVars:          origTask.IncludeVars,
		IncludedTaskfileVars: origTask.IncludedTaskfileVars,
		Platforms:            origTask.Platforms,
		Location:             origTask.Location,
		Requires:             origTask.Requires,
		Watch:                origTask.Watch,
		Namespace:            origTask.Namespace,
	}
	new.Dir, err = execext.Expand(new.Dir)
	if err != nil {
		return nil, err
	}
	if e.Dir != "" {
		new.Dir = filepathext.SmartJoin(e.Dir, new.Dir)
	}
	if new.Prefix == "" {
		new.Prefix = new.Task
	}

	dotenvEnvs := &ast.Vars{}
	if len(new.Dotenv) > 0 {
		for _, dotEnvPath := range new.Dotenv {
			dotEnvPath = filepathext.SmartJoin(new.Dir, dotEnvPath)
			if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) {
				continue
			}
			envs, err := godotenv.Read(dotEnvPath)
			if err != nil {
				return nil, err
			}
			for key, value := range envs {
				if ok := dotenvEnvs.Exists(key); !ok {
					dotenvEnvs.Set(key, ast.Var{Value: value})
				}
			}
		}
	}

	new.Env = &ast.Vars{}
	new.Env.Merge(templater.ReplaceVars(e.Taskfile.Env, cache), nil)
	new.Env.Merge(templater.ReplaceVars(dotenvEnvs, cache), nil)
	new.Env.Merge(templater.ReplaceVars(origTask.Env, cache), nil)
	if evaluateShVars {
		err = new.Env.Range(func(k string, v ast.Var) error {
			// If the variable is not dynamic, we can set it and return
			if v.Value != nil || v.Sh == "" {
				new.Env.Set(k, ast.Var{Value: v.Value})
				return nil
			}
			static, err := e.Compiler.HandleDynamicVar(v, new.Dir)
			if err != nil {
				return err
			}
			new.Env.Set(k, ast.Var{Value: static})
			return nil
		})
		if err != nil {
			return nil, err
		}
	}

	if len(origTask.Cmds) > 0 {
		new.Cmds = make([]*ast.Cmd, 0, len(origTask.Cmds))
		for _, cmd := range origTask.Cmds {
			if cmd == nil {
				continue
			}
			if cmd.For != nil {
				list, keys, err := itemsFromFor(cmd.For, new.Dir, new.Sources, vars, origTask.Location)
				if err != nil {
					return nil, err
				}
				// Name the iterator variable
				var as string
				if cmd.For.As != "" {
					as = cmd.For.As
				} else {
					as = "ITEM"
				}
				// Create a new command for each item in the list
				for i, loopValue := range list {
					extra := map[string]any{
						as: loopValue,
					}
					if len(keys) > 0 {
						extra["KEY"] = keys[i]
					}
					newCmd := cmd.DeepCopy()
					newCmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
					newCmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
					newCmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
					new.Cmds = append(new.Cmds, newCmd)
				}
				continue
			}
			// Defer commands are replaced in a lazy manner because
			// we need to include EXIT_CODE.
			if cmd.Defer {
				new.Cmds = append(new.Cmds, cmd.DeepCopy())
				continue
			}
			newCmd := cmd.DeepCopy()
			newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
			newCmd.Task = templater.Replace(cmd.Task, cache)
			newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)
			new.Cmds = append(new.Cmds, newCmd)
		}
	}
	if len(origTask.Deps) > 0 {
		new.Deps = make([]*ast.Dep, 0, len(origTask.Deps))
		for _, dep := range origTask.Deps {
			if dep == nil {
				continue
			}
			if dep.For != nil {
				list, keys, err := itemsFromFor(dep.For, new.Dir, new.Sources, vars, origTask.Location)
				if err != nil {
					return nil, err
				}
				// Name the iterator variable
				var as string
				if dep.For.As != "" {
					as = dep.For.As
				} else {
					as = "ITEM"
				}
				// Create a new command for each item in the list
				for i, loopValue := range list {
					extra := map[string]any{
						as: loopValue,
					}
					if len(keys) > 0 {
						extra["KEY"] = keys[i]
					}
					newDep := dep.DeepCopy()
					newDep.Task = templater.ReplaceWithExtra(dep.Task, cache, extra)
					newDep.Vars = templater.ReplaceVarsWithExtra(dep.Vars, cache, extra)
					new.Deps = append(new.Deps, newDep)
				}
				continue
			}
			newDep := dep.DeepCopy()
			newDep.Task = templater.Replace(dep.Task, cache)
			newDep.Vars = templater.ReplaceVars(dep.Vars, cache)
			new.Deps = append(new.Deps, newDep)
		}
	}

	if len(origTask.Preconditions) > 0 {
		new.Preconditions = make([]*ast.Precondition, 0, len(origTask.Preconditions))
		for _, precondition := range origTask.Preconditions {
			if precondition == nil {
				continue
			}
			newPrecondition := precondition.DeepCopy()
			newPrecondition.Sh = templater.Replace(precondition.Sh, cache)
			newPrecondition.Msg = templater.Replace(precondition.Msg, cache)
			new.Preconditions = append(new.Preconditions, newPrecondition)
		}
	}

	if len(origTask.Status) > 0 {
		timestampChecker := fingerprint.NewTimestampChecker(e.TempDir.Fingerprint, e.Dry)
		checksumChecker := fingerprint.NewChecksumChecker(e.TempDir.Fingerprint, e.Dry)

		for _, checker := range []fingerprint.SourcesCheckable{timestampChecker, checksumChecker} {
			value, err := checker.Value(&new)
			if err != nil {
				return nil, err
			}
			vars.Set(strings.ToUpper(checker.Kind()), ast.Var{Live: value})
		}

		// Adding new variables, requires us to refresh the templaters
		// cache of the the values manually
		cache.ResetCache()

		new.Status = templater.Replace(origTask.Status, cache)
	}

	// We only care about templater errors if we are evaluating shell variables
	if evaluateShVars && cache.Err() != nil {
		return &new, cache.Err()
	}

	return &new, nil
}

func asAnySlice[T any](slice []T) []any {
	ret := make([]any, len(slice))
	for i, v := range slice {
		ret[i] = v
	}
	return ret
}

func itemsFromFor(
	f *ast.For,
	dir string,
	sources []*ast.Glob,
	vars *ast.Vars,
	location *ast.Location,
) ([]any, []string, error) {
	var keys []string // The list of keys to loop over (only if looping over a map)
	var values []any  // The list of values to loop over
	// Get the list from a matrix
	if f.Matrix.Len() != 0 {
		return asAnySlice(product(f.Matrix)), nil, nil
	}
	// Get the list from the explicit for list
	if len(f.List) > 0 {
		return f.List, nil, nil
	}
	// Get the list from the task sources
	if f.From == "sources" {
		glist, err := fingerprint.Globs(dir, sources)
		if err != nil {
			return nil, nil, err
		}
		// Make the paths relative to the task dir
		for i, v := range glist {
			if glist[i], err = filepath.Rel(dir, v); err != nil {
				return nil, nil, err
			}
		}
		values = asAnySlice(glist)
	}
	// Get the list from a variable and split it up
	if f.Var != "" {
		if vars != nil {
			v := vars.Get(f.Var)
			// If the variable is dynamic, then it hasn't been resolved yet
			// and we can't use it as a list. This happens when fast compiling a task
			// for use in --list or --list-all etc.
			if v.Value != nil && v.Sh == "" {
				switch value := v.Value.(type) {
				case string:
					if f.Split != "" {
						values = asAnySlice(strings.Split(value, f.Split))
					} else {
						values = asAnySlice(strings.Fields(value))
					}
				case []any:
					values = value
				case map[string]any:
					for k, v := range value {
						keys = append(keys, k)
						values = append(values, v)
					}
				default:
					return nil, nil, errors.TaskfileInvalidError{
						URI: location.Taskfile,
						Err: errors.New("loop var must be a delimiter-separated string, list or a map"),
					}
				}
			}
		}
	}
	return values, keys, nil
}

// product generates the cartesian product of the input map of slices.
func product(inputMap omap.OrderedMap[string, []any]) []map[string]any {
	if inputMap.Len() == 0 {
		return nil
	}

	// Start with an empty product result
	result := []map[string]any{{}}

	// Iterate over each slice in the slices
	_ = inputMap.Range(func(key string, slice []any) error {
		var newResult []map[string]any

		// For each combination in the current result
		for _, combination := range result {
			// Append each element from the current slice to the combinations
			for _, item := range slice {
				newComb := make(map[string]any, len(combination))
				// Copy the existing combination
				for k, v := range combination {
					newComb[k] = v
				}
				// Add the current item with the corresponding key
				newComb[key] = item
				newResult = append(newResult, newComb)
			}
		}

		// Update result with the new combinations
		result = newResult
		return nil
	})

	return result
}