package rules

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"iwarma.ru/console/correlator/events"
	"net/http"
	"strconv"
	"text/template"

	"iwarma.ru/console/correlator/config"

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

const (
	IncidentActionType = "incident"
)

type IncidentAction struct {
	Title                string   `json:"title"`   // template
	Comment              string   `json:"comment"` // template
	Effects              []string `json:"effects"`
	Category             string   `json:"category"`
	Importance           string   `json:"importance"`
	Assigned             string   `json:"assigned_to"`
	Description          string   `json:"description"` // template
	CloseRecommendations []string `json:"close_recommendations"`

	// Private
	token  string
	client *http.Client

	template struct {
		title       *template.Template
		comment     *template.Template
		description *template.Template
	}
}

func (action IncidentAction) GetType() string {
	return IncidentActionType
}

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

	result["title"] = action.Title

	if action.Comment != "" {
		result["comment"] = action.Comment
	}

	// Effects
	if len(action.Effects) > 0 {
		effects := make([]interface{}, 0)
		for _, cur := range action.Effects {
			tmp, err := strconv.Atoi(cur)
			if err != nil {
				return nil, err
			}
			effects = append(effects, tmp)
		}
		result["effects"] = effects
	}

	// Category
	if action.Category != "" {
		catPk, err := strconv.Atoi(action.Category)
		result["category"] = catPk

		if err != nil {
			return nil, err
		}
	}

	// Importance
	imp, err := strconv.Atoi(action.Importance)
	if err != nil {
		return nil, err
	}
	result["importance"] = imp

	// Assigned
	if action.Assigned != "" {
		ass, err := strconv.Atoi(action.Assigned)
		if err != nil {
			return nil, err
		}
		result["assigned_to"] = ass
	}

	// Description
	if action.Description != "" {
		result["description"] = action.Description
	}

	// Close recommendations
	if len(action.CloseRecommendations) > 0 {
		closeRec := make([]interface{}, 0)
		for _, cur := range action.CloseRecommendations {
			tmp, err := strconv.Atoi(cur)
			if err != nil {
				return nil, err
			}

			closeRec = append(closeRec, tmp)
		}
		result["close_recommendations"] = closeRec
	}

	return result, nil
}

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

func (action *IncidentAction) UnmarshalJSON(b []byte) error {
	var data interface{}
	err := json.Unmarshal(b, &data)
	if err != nil {
		return err
	}

	return action.ParseInterface(data)
}

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

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

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

	action.Title, ok = m["title"].(string)
	if !ok {
		return errors.New("no title")
	}

	// We can process without comment and description
	action.Comment, _ = m["comment"].(string)
	action.Description, _ = m["description"].(string)

	if w, ok := m["effects"]; ok {
		switch v := w.(type) {
		case []interface{}:
			{
				for _, cur := range v {
					switch w := cur.(type) {
					case string:
						action.Effects = append(action.Effects, w)
					case float64:
						action.Effects = append(action.Effects, fmt.Sprintf("%v", w))
					default:
						return fmt.Errorf("bad effect type: %T with value %v in interface %v", cur, cur, m)
					}
				}
			}
		case string:
			action.Effects = append(action.Effects, v)
		case float64:
			action.Effects = append(action.Effects, fmt.Sprintf("%v", v))
		default:
			return fmt.Errorf("bad effect type: %T with value %v in interface %v", v, v, m)
		}
	}

	action.Category, _ = m["category"].(string)
	action.Importance, ok = m["importance"].(string)
	if !ok {
		return errors.New("no importance")
	}

	action.Assigned, _ = m["assigned_to"].(string)

	if w, ok := m["close_recommendations"]; ok {
		switch v := w.(type) {
		case []interface{}:
			{
				for _, cur := range v {
					switch w := cur.(type) {
					case string:
						action.CloseRecommendations = append(action.CloseRecommendations, w)
					case float64:
						action.CloseRecommendations = append(action.CloseRecommendations, fmt.Sprintf("%v", w))
					case nil:
						break
					default:
						return fmt.Errorf("bad recommendations type: %T with value %v in interface %v", cur, cur, m)
					}
				}
			}
		case string:
			action.CloseRecommendations = append(action.CloseRecommendations, v)
		case float64:
			action.CloseRecommendations = append(action.CloseRecommendations, fmt.Sprintf("%v", v))
		default:
			return fmt.Errorf("bad close recommendations type: %T with value %v in interface %v", v, v, m)
		}
	}

	return nil
}

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

	if events == nil || len(*events) == 0 {
		cl.Error("No events")
		return nil
	}

	var err error

	// Check if we need to prepare templates
	if action.template.title == nil {
		action.template.title, err = template.New("Title").Parse(action.Title)
		if err != nil {
			cl.Errorf("Can't create title template: %v", err)
			return err
		}

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

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

	}

	// Check if we need to prepare http client
	if action.client == nil {
		if viper.GetBool(config.ConsoleIgnoreSSLErrors) {
			transport := &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			}
			action.client = &http.Client{Transport: transport}
		} else {
			action.client = &http.Client{}
		}
	}

	// Get auth token
	if action.token == "" {
		action.token, err = ObtainAuthToken()
		if err != nil {
			cl.Errorf("Can't get auth token: %v", err)
			return err
		}
	}

	// For this type of action, multi means that we need to add all events
	// to created incident

	// Prepare body
	ibody, err := action.ToInterface()
	if err != nil {
		return err
	}

	cur := *events
	event := cur[0]

	// Render templates
	if _, ok := ibody["title"]; ok {
		s, err := renderTemplate(action.template.title, event)
		if err != nil {
			return err
		}

		// Need to truncate title, so serializer will accept it
		if len(s) > 256 {
			s = s[:255]
		}

		ibody["title"] = s
	}

	if _, ok := ibody["comment"]; ok {
		ibody["comment"], err = renderTemplate(action.template.comment, event)
		if err != nil {
			return err
		}
	}

	if _, ok := ibody["description"]; ok {
		ibody["description"], err = renderTemplate(action.template.description, event)
		if err != nil {
			return err
		}
	}

	// Add type to connect incident with sensor
	ibody["sensor"] = event.GetString("type")

	// Insert incidents info
	// If we have a single-event rule, len of events will be 1
	ibody["event_count"] = len(*events)
	ibody["events"] = events

	jsonBody, err := json.Marshal(ibody)
	if err != nil {
		cl.Errorf("Can't serialize body: %v", err)
		return err
	}

	request, err := http.NewRequest("POST", viper.GetString(config.ConsoleUrlIncident), bytes.NewBuffer(jsonBody))
	if err != nil {
		cl.Errorf("Can't create request: %v", err)
		return err
	}

	// Set headers
	request.Header.Set("Authorization", fmt.Sprintf("Token %v", action.token))
	request.Header.Set("Content-type", "application/json")

	// Do request
	response, err := action.client.Do(request)
	if err != nil {
		cl.Errorf("Can't send request: %v", err)
		return err
	}
	defer response.Body.Close()

	// Check result
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		cl.Errorf("Can't read response body: %v", err)
		return err
	}

	// Dump request and response
	if viper.GetBool(config.DebugDumpRequest) {
		go DumpNetwork("incident", *request.URL, ibody, body, response.StatusCode)
	}

	if response.StatusCode != http.StatusCreated {
		cl.Errorf("Bad status code, expect %v, got %v", http.StatusCreated, response.Status)
		return fmt.Errorf("bad status code, expect %v, got %v", http.StatusCreated, response.Status)
	}

	return nil
}
