package task

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"strings"
	"sync"
	"sync/atomic"

	"github.com/go-task/task/execext"

	"golang.org/x/sync/errgroup"
)

const (
	// TaskFilePath is the default Taskfile
	TaskFilePath = "Taskfile"
	// MaximumTaskCall is the max number of times a task can be called.
	// This exists to prevent infinite loops on cyclic dependencies
	MaximumTaskCall = 100
)

// Executor executes a Taskfile
type Executor struct {
	Tasks   Tasks
	Dir     string
	Force   bool
	Watch   bool
	Verbose bool
	Silent  bool

	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer

	taskvars      Vars
	watchingFiles map[string]struct{}

	taskCallCount map[string]*int32

	dynamicCache   map[string]string
	muDynamicCache sync.Mutex
}

// Tasks representas a group of tasks
type Tasks map[string]*Task

// Task represents a task
type Task struct {
	Cmds      []*Cmd
	Deps      []*Dep
	Desc      string
	Sources   []string
	Generates []string
	Status    []string
	Dir       string
	Vars      Vars
	Set       string
	Env       Vars
	Silent    bool
}

// Run runs Task
func (e *Executor) Run(args ...string) error {
	if e.Stdin == nil {
		e.Stdin = os.Stdin
	}
	if e.Stdout == nil {
		e.Stdout = os.Stdout
	}
	if e.Stderr == nil {
		e.Stderr = os.Stderr
	}

	e.taskCallCount = make(map[string]*int32, len(e.Tasks))
	for k := range e.Tasks {
		e.taskCallCount[k] = new(int32)
	}

	if e.dynamicCache == nil {
		e.dynamicCache = make(map[string]string, 10)
	}

	// check if given tasks exist
	for _, a := range args {
		if _, ok := e.Tasks[a]; !ok {
			// FIXME: move to the main package
			e.PrintTasksHelp()
			return &taskNotFoundError{taskName: a}
		}
	}

	if e.Watch {
		if err := e.watchTasks(args...); err != nil {
			return err
		}
		return nil
	}

	for _, a := range args {
		if err := e.RunTask(context.Background(), Call{Task: a, Vars: nil}); err != nil {
			return err
		}
	}
	return nil
}

// RunTask runs a task by its name
func (e *Executor) RunTask(ctx context.Context, call Call) error {
	origTask, ok := e.Tasks[call.Task]
	if !ok {
		return &taskNotFoundError{call.Task}
	}
	if atomic.AddInt32(e.taskCallCount[call.Task], 1) >= MaximumTaskCall {
		return &MaximumTaskCallExceededError{task: call.Task}
	}

	vars, err := e.getVariables(call)
	if err != nil {
		return err
	}

	t, err := origTask.CompiledTask(e.Dir, vars)
	if err != nil {
		return err
	}

	if err := e.runDeps(ctx, t); err != nil {
		return err
	}

	// FIXME: doing again, since a var may have been overriden
	// using the `set:` attribute of a dependecy.
	// Remove this when `set` (that is deprecated) be removed
	vars, err = e.getVariables(call)
	if err != nil {
		return err
	}
	t, err = origTask.CompiledTask(e.Dir, vars)
	if err != nil {
		return err
	}

	if !e.Force {
		upToDate, err := t.isUpToDate(ctx)
		if err != nil {
			return err
		}
		if upToDate {
			e.printfln(`task: Task "%s" is up to date`, call.Task)
			return nil
		}
	}

	for i := range t.Cmds {
		if err := e.runCommand(ctx, t, call, i); err != nil {
			return &taskRunError{call.Task, err}
		}
	}
	return nil
}

func (e *Executor) runDeps(ctx context.Context, t *Task) error {
	g, ctx := errgroup.WithContext(ctx)

	for _, d := range t.Deps {
		d := d

		g.Go(func() error {
			return e.RunTask(ctx, Call{Task: d.Task, Vars: d.Vars})
		})
	}

	return g.Wait()
}

func (e *Executor) runCommand(ctx context.Context, t *Task, call Call, i int) error {
	cmd := t.Cmds[i]

	if cmd.Cmd == "" {
		return e.RunTask(ctx, Call{Task: cmd.Task, Vars: cmd.Vars})
	}

	opts := &execext.RunCommandOptions{
		Context: ctx,
		Command: cmd.Cmd,
		Dir:     t.Dir,
		Env:     t.getEnviron(),
		Stdin:   e.Stdin,
		Stderr:  e.Stderr,
	}

	if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) {
		e.println(cmd.Cmd)
	}
	if t.Set != "" {
		var stdout bytes.Buffer
		opts.Stdout = &stdout
		if err := execext.RunCommand(opts); err != nil {
			return err
		}
		return os.Setenv(t.Set, strings.TrimSpace(stdout.String()))
	}

	opts.Stdout = e.Stdout
	return execext.RunCommand(opts)
}

func (t *Task) getEnviron() []string {
	if t.Env == nil {
		return nil
	}

	envs := os.Environ()
	for k, v := range t.Env {
		envs = append(envs, fmt.Sprintf("%s=%s", k, v))
	}
	return envs
}
