package variant

import (
	"fmt"
	"github.com/pkg/errors"
	log "github.com/sirupsen/logrus"

	"github.com/mumoshu/variant/pkg/get"
	"github.com/mumoshu/variant/pkg/util/maputil"
	"strings"
)

type TaskDef struct {
	Name              string       `yaml:"name,omitempty"`
	Description       string       `yaml:"description,omitempty"`
	Inputs            InputConfigs `yaml:"inputs,omitempty"`
	TaskDefs          TaskDefs     `yaml:"tasks,omitempty"`
	Script            string       `yaml:"script,omitempty"`
	Steps             []Step       `yaml:"steps,omitempty"`
	Autoenv           bool         `yaml:"autoenv,omitempty"`
	Autodir           bool         `yaml:"autodir,omitempty"`
	BindParamsFromEnv bool         `yaml:"bindParamsFromEnv,omitempty"`
	Interactive       bool         `yaml:"interactive,omitempty"`
	Private           bool         `yaml:"private,omitempty"`

	fun func(ctx ExecutionContext) (string, error)
}

type TaskDefs []*TaskDef

func (d TaskDefs) GoString() string {
	taskDefs := []string{}
	for _, t := range d {
		taskDefs = append(taskDefs, fmt.Sprintf("%#v", t))
	}
	return fmt.Sprintf("variant.TaskDefs{%s}", strings.Join(taskDefs, ", "))
}

type InputConfigs []*InputConfig

func (c InputConfigs) GoString() string {
	inputConfs := []string{}
	for _, t := range c {
		inputConfs = append(inputConfs, fmt.Sprintf("%#v", t))
	}
	return fmt.Sprintf("variant.InputConfigs{%s}", strings.Join(inputConfs, ", "))
}

type TaskDefV1 struct {
	Name        string                        `yaml:"name,omitempty"`
	Description string                        `yaml:"description,omitempty"`
	Inputs      []*InputConfig                `yaml:"inputs,omitempty"`
	Parameters  []*ParameterConfig            `yaml:"parameters,omitempty"`
	Options     []*OptionConfig               `yaml:"options,omitempty"`
	TaskDefs    []*TaskDef                    `yaml:"tasks,omitempty"`
	Runner      map[string]interface{}        `yaml:"runner,omitempty"`
	Script      string                        `yaml:"script,omitempty"`
	StepDefs    []map[interface{}]interface{} `yaml:"steps,omitempty"`
	Autoenv     bool                          `yaml:"autoenv,omitempty"`
	Autodir     bool                          `yaml:"autodir,omitempty"`
	BindEnvVar  bool                          `yaml:"bindParamsFromEnv,omitempty"`
	Interactive bool                          `yaml:"interactive,omitempty"`
	Private     bool                          `yaml:"private,omitempty"`
}

type TaskDefV2 struct {
	Description string                        `yaml:"description,omitempty"`
	Inputs      []*InputConfig                `yaml:"inputs,omitempty"`
	Parameters  []*ParameterConfig            `yaml:"parameters,omitempty"`
	Options     []*OptionConfig               `yaml:"options,omitempty"`
	Import      string                        `yaml:"import,omitempty"`
	TaskDefs    map[string]*TaskDef           `yaml:"tasks,omitempty"`
	Runner      map[string]interface{}        `yaml:"runner,omitempty"`
	Script      interface{}                   `yaml:"script,omitempty"`
	StepDefs    []map[interface{}]interface{} `yaml:"steps,omitempty"`
	Autoenv     bool                          `yaml:"autoenv,omitempty"`
	Autodir     bool                          `yaml:"autodir,omitempty"`
	BindEnvVar  bool                          `yaml:"bindParamsFromEnv,omitempty"`
	Interactive bool                          `yaml:"interactive,omitempty"`
	Private     bool                          `yaml:"private,omitempty"`
}

func (t *TaskDef) UnmarshalYAML(unmarshal func(interface{}) error) error {
	v3 := map[string]interface{}{}
	if err := unmarshal(&v3); err != nil {
		return err
	}

	log.Tracef("Unmarshalling: %v", v3)

	var v2 *TaskDefV2

	log.Tracef("Trying to parse v2 format")
	v2 = &TaskDefV2{
		Autoenv:     false,
		Autodir:     false,
		BindEnvVar:  false,
		Interactive: false,
		Inputs:      []*InputConfig{},
		TaskDefs:    map[string]*TaskDef{},
		StepDefs:    []map[interface{}]interface{}{},
	}

	if err := unmarshal(&v2); err != nil {
		return err
	}

	if v2.Import != "" {
		log.Debugf("Importing %s", v2.Import)

		err := get.Unmarshal(v2.Import, &v2)
		if err != nil {
			return err
		}
	}

	var script string
	switch s := v2.Script.(type) {
	case string:
		script = s
	case []interface{}:
		ss := make([]string, len(s))
		for i := range s {
			ss[i] = s[i].(string)
		}
		script = strings.Join(ss, "\n")
	}

	if len(v2.TaskDefs) == 0 && script == "" && len(v2.StepDefs) == 0 {
		return fmt.Errorf("Not v2 format: `tasks`, `script`, `steps` are missing.")
	}

	t.Description = v2.Description
	if len(v2.Inputs) > 0 {
		t.Inputs = v2.Inputs
	} else {
		for i, p := range v2.Parameters {
			c := i
			input := &InputConfig{
				Name:          p.Name,
				Description:   p.Description,
				ArgumentIndex: &c,
				Type:          p.Type,
				Default:       p.Default,
				Remainings:    p.Remainings,
				Properties:    p.Properties,
			}
			t.Inputs = append(t.Inputs, input)
		}
		for _, o := range v2.Options {
			input := &InputConfig{
				Name:        o.Name,
				Description: o.Description,
				Type:        o.Type,
				Default:     o.Default,
				Remainings:  o.Remainings,
				Properties:  o.Properties,
			}
			t.Inputs = append(t.Inputs, input)
		}
	}
	t.TaskDefs = TransformV2FlowConfigMapToArray(v2.TaskDefs)
	steps, err := readStepsFromStepDefs(script, v2.Runner, v2.StepDefs)
	if err != nil {
		return errors.Wrapf(err, "Error while reading v2 config")
	}
	t.Steps = steps
	t.Script = script
	t.Autoenv = v2.Autoenv
	t.Autodir = v2.Autodir
	t.BindParamsFromEnv = v2.BindEnvVar
	t.Interactive = v2.Interactive
	t.Private = v2.Private

	return nil
}

func (t *TaskDef) CopyTo(other *TaskDef) {
	other.Description = t.Description
	other.Inputs = t.Inputs
	other.TaskDefs = t.TaskDefs
	other.Steps = t.Steps
	other.Script = t.Script
	other.Autoenv = t.Autoenv
	other.Autodir = t.Autodir
	other.BindParamsFromEnv = t.BindParamsFromEnv
	other.Interactive = t.Interactive
	other.Private = t.Private
}

func (t *TaskDef) Add(args []string, taskDef *TaskDef, f func(ctx ExecutionContext) (string, error)) error {
	return t.add([]string{}, args, taskDef, f)
}

func (t *TaskDef) add(ctx []string, args []string, taskDef *TaskDef, f func(ctx ExecutionContext) (string, error)) error {
	if len(args) == 0 {
		return errors.New("invalid argument: args should not be empty")
	}
	taskName := args[0]
	ctx = append([]string{}, ctx...)
	ctx = append(ctx, taskName)
	rest := args[1:]

	for i := range t.TaskDefs {
		if t.TaskDefs[i].Name == taskName {
			return t.TaskDefs[i].add(ctx, rest, taskDef, f)
		}
	}
	if len(args) == 1 {
		td := *taskDef
		td.fun = f
		td.Name = taskName
		t.TaskDefs = append(t.TaskDefs, &td)
		return nil
	}

	return fmt.Errorf("parent task named \"%s\" does not exist: use TaskDef#Add([]string{%s}, ...) to add it", strings.Join(ctx, "."), strings.Join(ctx, ", "))
}

func TransformV2FlowConfigMapToArray(v2 map[string]*TaskDef) []*TaskDef {
	result := []*TaskDef{}
	for name, t2 := range v2 {
		t := &TaskDef{}

		t.Name = name
		t2.CopyTo(t)

		result = append(result, t)
	}
	return result
}

var stepLoaders []StepLoader

func Register(stepLoader StepLoader) {
	stepLoaders = append(stepLoaders, stepLoader)
}

func init() {
	stepLoaders = []StepLoader{}
}

type stepLoadingContextImpl struct{}

func (s stepLoadingContextImpl) LoadStep(config StepDef) (Step, error) {
	step, err := LoadStep(config)

	return step, err
}

func LoadStep(config StepDef) (Step, error) {
	var lastError error

	lastError = nil

	context := stepLoadingContextImpl{}
	for _, loader := range stepLoaders {
		var s Step
		s, lastError = loader.LoadStep(config, context)

		log.WithField("step", s).Debugf("step loaded")

		if lastError == nil {
			return s, nil
		}
	}
	return nil, errors.Wrapf(lastError, "all loader failed to load step")
}

func readStepsFromStepDefs(script string, runner map[string]interface{}, stepDefs []map[interface{}]interface{}) ([]Step, error) {
	result := []Step{}

	if script != "" {
		if len(stepDefs) > 0 {
			return nil, fmt.Errorf("both script and steps exist.")
		}

		raw := map[string]interface{}{
			"name":   "script",
			"script": script,
			"silent": false,
		}
		if runner != nil {
			raw["runner"] = runner
		}
		s, err := LoadStep(NewStepDef(raw))

		if err != nil {
			log.Panicf("step failed to load: %v", err)
		}

		result = []Step{s}
	} else {
		for i, stepDef := range stepDefs {
			defaultName := fmt.Sprintf("step-%d", i+1)

			if stepDef["name"] == "" || stepDef["name"] == nil {
				stepDef["name"] = defaultName
			}

			converted, castErr := maputil.CastKeysToStrings(stepDef)

			if castErr != nil {
				panic(castErr)
			}

			s, err := LoadStep(NewStepDef(converted))

			if err != nil {
				return nil, errors.Wrapf(err, "Error reading step[%d]", i)
			}

			result = append(result, s)
		}
	}

	return result, nil
}
