package rules

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"iwarma.ru/console/correlator/events"
	"os"
	"os/exec"
	"regexp"
	"strings"
	"text/template"

	"iwarma.ru/console/correlator/config"

	log "github.com/sirupsen/logrus"
	"github.com/spf13/viper"
)

const (
	BashActionType = "bash"
)

type BashAction struct {
	Body string `json:"body"`

	bodyTemplate *template.Template
}

func (action *BashAction) GetType() string {
	return BashActionType
}

func (action BashAction) ToInterface() (map[string]interface{}, error) {
	result := make(map[string]interface{})

	result["body"] = action.Body
	result["type"] = BashActionType

	return result, nil
}

func (action BashAction) MarshalJSON() ([]byte, error) {
	data, err := action.ToInterface()
	if err != nil {
		return nil, err
	}

	return json.Marshal(data)
}

func (action *BashAction) UnmarshalJSON(b []byte) error {
	cur := struct {
		CurType string `json:"type"`
		Body    string `json:"body"`
	}{}

	err := json.Unmarshal(b, &cur)
	if err != nil {
		return err
	}

	if cur.CurType != BashActionType {
		return fmt.Errorf("bad action type, Expect %v, got %v", BashActionType, cur.CurType)
	}

	action.Body = cur.Body

	return nil
}

func (action *BashAction) ParseInterface(v interface{}) error {
	m, ok := v.(map[string]interface{})
	if !ok {
		return fmt.Errorf("can't parse %v from %T", v, v)
	}

	body, ok := m["body"].(string)
	if !ok {
		return errors.New("no body")
	}

	t, ok := m["type"].(string)
	if !ok {
		return errors.New("no type")
	}

	if t != BashActionType {
		return fmt.Errorf("bad type, expect %v got %v", BashActionType, t)
	}

	action.Body = body

	return nil
}

// See https://stackoverflow.com/a/52594719
func removeLBR(text string) string {
	re := regexp.MustCompile(`\x{000D}\x{000A}|[\x{000A}\x{000B}\x{000C}\x{000D}\x{0085}\x{2028}\x{2029}]`)
	return re.ReplaceAllString(text, "\n")
}

func (action *BashAction) Perform(events *[]*events.Event) error {
	cl := log.WithFields(log.Fields{"type": BashActionType, "event_count": len(*events)})
	cl.Debug("Start action")
	defer cl.Debug("End action")

	var err error

	// Check if we need to prepare template
	if action.bodyTemplate == nil {
		text := strings.Replace(action.Body, `\r\n`, "\n", -1)
		text = removeLBR(text)

		action.bodyTemplate, err = template.New("Body").Parse(text)
		if err != nil {
			cl.Errorf("Can't create body template: %v", err)
			return err
		}
	}

	// Actual action
	for _, event := range *events {

		// Render script body
		body, err := renderTemplate(action.bodyTemplate, event)
		if err != nil {
			cl.Errorf("Can't render script body: %v", err)
			return err
		}

		// Create temp file
		file, err := ioutil.TempFile("", "action.*.sh")
		if err != nil {
			cl.Errorf("Can't create script file: %v", err)
			return err
		}

		// Remove temp script
		defer func() {
			err = os.Remove(file.Name())
			if err != nil {
				cl.Errorf("Can't remove temp script: %v", err)
			}
		}()

		count, err := file.Write([]byte(body))
		if err != nil {
			cl.Errorf("Can't write script body: %v", err)
			return err
		}

		if count != len([]byte(body)) {
			cl.Errorf("Write bad bytes count, expec %v, got %v", len([]byte(body)), count)
		}

		err = file.Close()
		if err != nil {
			cl.Errorf("Can't close temp script: %v", err)
		}

		err = os.Chmod(file.Name(), 0775)
		if err != nil {
			cl.Errorf("Can't set script permissions: %v", err)
			return err
		}

		if viper.GetBool(config.DebugDumpRequest) {
			info, err := os.Stat(file.Name())
			if os.IsNotExist(err) {
				cl.Errorf("Script %v does not exist #0", file.Name())
			}
			cl.Debugf("File mode: %v", info.Mode())
			cl.Debugf("File size: %v", info.Size())
		}

		var buf bytes.Buffer

		cmd := exec.Command(file.Name())
		cmd.Stdout = &buf
		cmd.Stderr = &buf

		// TODO: Maybe set to goroutine
		err = cmd.Run()
		if err != nil {
			cl.Errorf("Script execution error: %v", err)
			return err
		}

		if viper.GetBool(config.Verbose) {
			cl.Infof("Script output:\n%v", buf.String())
		}
	}

	return nil
}
