// Copyright © 2016 Asteris, LLC
//
// 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 shell

import (
	"bytes"

	"fmt"
	"io"
	"io/ioutil"
	"os/exec"
	"time"

	"github.com/pkg/errors"

	log "github.com/Sirupsen/logrus"
	"github.com/asteris-llc/converge/helpers/transform"
	"github.com/asteris-llc/converge/load/registry"
	"github.com/asteris-llc/converge/resource"
	"golang.org/x/net/context"
)

var (
	defaultInterpreter = "/bin/sh"
	defaultCheckFlags  = []string{"-n"}
	defaultExecFlags   = []string{}
)

// Preparer for shell tasks
//
// Task allows you to run arbitrary shell commands on your system, first
// checking if the command should be run.
type Preparer struct {
	// the shell interpreter that will be used for your scripts. `/bin/sh` is
	// used by default.
	Interpreter string `hcl:"interpreter"`

	// flags to pass to the `interpreter` binary to check validity. For
	// `/bin/sh` this is `-n`
	CheckFlags []string `hcl:"check_flags"`

	// flags to pass to the interpreter at execution time
	ExecFlags []string `hcl:"exec_flags"`

	// the script to run to check if a resource needs to be changed. It should
	// exit with exit code 0 if the resource does not need to be changed, and
	// 1 (or above) otherwise.
	Check string `hcl:"check" nonempty:"true"`

	// the script to run to apply the resource. Normal shell exit code
	// expectations apply (that is, exit code 0 for success, 1 or above for
	// failure.)
	Apply string `hcl:"apply" nonempty:"true"`

	// the amount of time the command will wait before halting forcefully.
	Timeout *time.Duration `hcl:"timeout"`

	// the working directory this command should be run in
	Dir string `hcl:"dir"`

	// any environment variables that should be passed to the command
	Env map[string]string `hcl:"env"`
}

// Prepare a new shell task
func (p *Preparer) Prepare(ctx context.Context, render resource.Renderer) (resource.Task, error) {
	env := transform.StringsMapToStringSlice(
		p.Env,
		func(k, v string) string {
			return fmt.Sprintf("%s=%s", k, v)
		},
	)

	generator := &CommandGenerator{
		Interpreter: p.Interpreter,
		Flags:       p.ExecFlags,
		Dir:         p.Dir,
		Env:         env,
		Timeout:     p.Timeout,
	}

	shell := &Shell{
		CmdGenerator: generator,
		CheckStmt:    p.Check,
		ApplyStmt:    p.Apply,
		Dir:          p.Dir,
		Env:          env,
	}

	return shell, checkSyntax(p.Interpreter, p.CheckFlags, p.Check)
}

func checkSyntax(interpreter string, flags []string, script string) error {
	if interpreter == "" {
		interpreter = defaultInterpreter
		if len(flags) > 0 {
			return errors.New("custom syntax check_flags given without an interpreter")
		}
		flags = defaultCheckFlags
	} else {
		if len(flags) == 0 {
			// TODO: add ID in here somehow
			log.Debug("no check_flags specified for interpeter, skipping syntax validation")
			return nil
		}
	}
	command := exec.Command(interpreter, flags...)
	cmdStdin, cmdStdout, cmdStderr, err := cmdGetPipes(command)
	if err != nil {
		return errors.Wrap(err, "unable to communicate with subprocess")
	}
	if err := command.Start(); err != nil {
		return errors.Wrap(err, "unable to start subprocess")
	}
	if _, err := cmdStdin.Write([]byte(script)); err != nil {
		return errors.Wrap(err, "unable to write to interpreter")
	}

	if err := cmdStdin.Close(); err != nil {
		return errors.Wrap(err, "failed to close stdin")
	}

	var buffer bytes.Buffer
	if data, err := ioutil.ReadAll(cmdStdout); err == nil {
		if len(data) > 0 {
			buffer.WriteString("Command Stdout:\n")
			buffer.Write(data)
		}
	}
	if data, err := ioutil.ReadAll(cmdStderr); err == nil {
		if len(data) > 0 {
			buffer.WriteString("Command Stderr:\n")
			buffer.Write(data)
		}
	}

	if err := command.Wait(); err != nil {
		return errors.Wrap(err, "syntax error: "+buffer.String())
	}

	return nil
}

func cmdGetPipes(command *exec.Cmd) (io.WriteCloser, io.ReadCloser, io.ReadCloser, error) {
	var err error
	cmdStdin, err := command.StdinPipe()
	if err != nil {
		return nil, nil, nil, errors.Wrap(err, "failed to get stdin pipe")
	}
	cmdStderr, err := command.StderrPipe()
	if err != nil {
		return nil, nil, nil, errors.Wrap(err, "failed to get stderr pipe")
	}
	cmdStdout, err := command.StdoutPipe()
	if err != nil {
		return nil, nil, nil, errors.Wrap(err, "failed to get stdout pipe")
	}
	return cmdStdin, cmdStdout, cmdStderr, nil
}

func init() {
	registry.Register("task", (*Preparer)(nil), (*Shell)(nil))
	registry.Register("healthcheck.task", (*Preparer)(nil), (*Shell)(nil))
}
