// The alerter package is responsible for receiving alert messages generated by
// monitors and dispatching them via the appropriate alerter type such as Slack,
// PagerDuty and so forth.
//
// While the alerter does not utilize etcd watch functionality, it loads the
// appropriate alerter config on each message it receives. Due to this, it is
// important that the alerter is managed by the overwatch package (ie. gets
// shutdown/restarted during backend problems).
package alerter

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	log "github.com/Sirupsen/logrus"
	gouuid "github.com/satori/go.uuid"

	"github.com/9corp/9volt/base"
	"github.com/9corp/9volt/config"
	"github.com/9corp/9volt/util"
)

type IAlerter interface {
	Send(*Message, *AlerterConfig) error
	Identify() string
	ValidateConfig(*AlerterConfig) error
}

type AlerterConfig struct {
	Type        string            `json:"type"`
	Description string            `json:"description"`
	Options     map[string]string `json:"options"`
}

type Alerter struct {
	MemberID       string
	Config         *config.Config
	Log            log.FieldLogger
	Alerters       map[string]IAlerter
	MessageChannel <-chan *Message

	base.Component
}

type Message struct {
	Type        string            // "resolve", "warning", "critical"
	Key         []string          // Keys coming from the monitor config for Critical or WarningAlerters
	Title       string            // Short description of the alert
	Text        string            // In-depth description of the alert state
	Source      string            // Origin of the alert
	Description string            // Original check description so we can be verbose about what we are alerting on
	Count       int               // How many check attempts were made
	Contents    map[string]string // Set checker-specific data (ensuring alerters know how to use the data)
	uuid        string            // For private use within the alerter
}

func New(cfg *config.Config, messageChannel <-chan *Message) *Alerter {
	return &Alerter{
		MemberID:       cfg.MemberID,
		Config:         cfg,
		Log:            log.WithField("pkg", "alerter"),
		MessageChannel: messageChannel,
		Component: base.Component{
			Identifier: "alerter",
		},
	}
}

func (a *Alerter) Start() error {
	a.Log.Info("Starting alerter components...")

	a.Component.Ctx, a.Component.Cancel = context.WithCancel(context.Background())

	// Instantiate our alerters
	pagerduty := NewPagerduty(a.Config)
	slack := NewSlack(a.Config)
	email := NewEmail(a.Config)

	a.Alerters = map[string]IAlerter{
		pagerduty.Identify(): pagerduty,
		slack.Identify():     slack,
		email.Identify():     email,
	}

	// Launch our alerter message handler
	go a.run()

	return nil
}

func (a *Alerter) Stop() error {
	if a.Component.Cancel == nil {
		a.Log.Warning("Looks like .Cancel is nil; is this expected?")
	} else {
		a.Component.Cancel()
	}

	return nil
}

func (a *Alerter) run() error {
	llog := a.Log.WithField("method", "run")

OUTER:
	for {
		select {
		case msg := <-a.MessageChannel:
			// tag message
			msg.uuid = gouuid.NewV4().String()

			llog.WithFields(log.Fields{"uuid": msg.uuid, "source": msg.Source, "key": msg.Key}).Debug("Received message from checker")

			go a.handleMessage(msg)
		case <-a.Component.Ctx.Done():
			llog.Debug("Asked to shutdown")
			break OUTER
		}
	}

	llog.Debug("Exiting...")
	return nil
}

func (a *Alerter) handleMessage(msg *Message) error {
	llog := a.Log.WithField("method", "handleMessage")

	// validate message contents
	if err := a.validateMessage(msg); err != nil {
		a.Config.EQClient.AddWithErrorLog("Unable to validate message", llog, log.Fields{"uuid": msg.uuid, "err": err})
		return err
	}

	// Nothing to do if the alerter config is empty
	if len(msg.Key) == 0 {
		llog.WithField("uuid", msg.uuid).Debug("Not sending any message - alerter not set in config")
		return nil
	}

	errorList := make([]string, 0)

	// fetch alert configuration for each individual key, send alert for each.
	// keep track of any encountered errors; report result in the end
	for _, alerterKey := range msg.Key {
		alerterConfig, err := a.loadAlerterConfig(alerterKey, msg)
		if err != nil {
			errorList = append(errorList, fmt.Sprintf("Unable to load alerter key for %v: %v", msg.uuid, err.Error()))
			llog.WithFields(log.Fields{"uuid": msg.uuid, "err": err}).Error("Unable to load alerter key")
			continue
		}

		// validate the alerter config
		if err := a.Alerters[alerterConfig.Type].ValidateConfig(alerterConfig); err != nil {
			errorList = append(errorList, fmt.Sprintf("Unable to validate alerter config for %v: %v", msg.uuid, err.Error()))
			llog.WithFields(log.Fields{"uuid": msg.uuid, "err": err}).Error("Unable to validate alerter config")
			continue
		}

		// send the actual alert
		llog.WithFields(log.Fields{"uuid": msg.uuid, "alerter": alerterConfig.Type}).Debug("Sending message to alerter")

		if err := a.Alerters[alerterConfig.Type].Send(msg, alerterConfig); err != nil {
			errorList = append(errorList, fmt.Sprintf("Unable to complete message send for %v: %v", msg.uuid, err.Error()))
			llog.WithFields(log.Fields{"uuid": msg.uuid, "err": err}).Error("Unable to complete message send")
			continue
		}
	}

	if len(errorList) != 0 {
		a.Config.EQClient.AddWithErrorLog("Ran into errors during alert send", llog,
			log.Fields{"numErrors": len(errorList), "source": msg.Source, "key": msg.Key, "errors": strings.Join(errorList, "; ")})
	} else {
		llog.WithFields(log.Fields{"uuid": msg.uuid, "key": msg.Key}).Debugf("Successfully sent %v alert messages", len(msg.Key))
	}

	return nil
}

// Fetch an alert config for a given alert key; ensure we can unmarshal it
func (a *Alerter) loadAlerterConfig(alerterKey string, msg *Message) (*AlerterConfig, error) {
	jsonAlerterConfig, err := a.Config.DalClient.FetchAlerterConfig(alerterKey)
	if err != nil {
		a.Log.WithFields(log.Fields{"uuid": msg.uuid, "err": err}).Error("Unable to fetch alerter config for message")
		return nil, err
	}

	// try to unmarshal the data
	var alerterConfig *AlerterConfig

	if err := json.Unmarshal([]byte(jsonAlerterConfig), &alerterConfig); err != nil {
		a.Log.WithFields(log.Fields{"uuid": msg.uuid, "err": err}).Error("Unable to unmarshal alerter config")
		return nil, err
	}

	// check if we have given alerter
	if _, ok := a.Alerters[alerterConfig.Type]; !ok {
		err := fmt.Errorf("Unable to find any alerter named %v", alerterConfig.Type)
		a.Log.Error(err)
		return nil, err
	}

	return alerterConfig, nil
}

// Perform message validation; return err if one of required fields is not filled out
func (a *Alerter) validateMessage(msg *Message) error {
	if msg.Source == "" {
		return errors.New("Message must have the 'Source' value filled out")
	}

	if msg.Contents == nil {
		return errors.New("Message 'Contents' must be filled out")
	}

	validTypes := []string{"resolve", "critical", "warning"}

	if !util.StringSliceContains(validTypes, msg.Type) {
		return fmt.Errorf("Message 'Type' must contain one of %v", validTypes)
	}

	return nil
}
