// Package alertmanager implements a Service capable of processing webhooks from prometheus alertmanager.
package alertmanager

import (
	"bytes"
	"encoding/json"
	"fmt"
	html "html/template"
	"net/http"
	"strings"
	text "text/template"

	"github.com/matrix-org/go-neb/database"
	"github.com/matrix-org/go-neb/types"
	log "github.com/sirupsen/logrus"
	mevt "maunium.net/go/mautrix/event"
	"maunium.net/go/mautrix/id"
)

// ServiceType of the Alertmanager service.
const ServiceType = "alertmanager"

// Service contains the Config fields for the Alertmanager service.
//
// This service will send notifications into a Matrix room when Alertmanager sends
// webhook events to it. It requires a public domain which Alertmanager can reach.
// Notices will be sent as the service user ID.
//
// For the template strings, take a look at https://golang.org/pkg/text/template/
// and the html variant https://golang.org/pkg/html/template/.
// The data they get is a webhookNotification
//
// You can set msg_type to either m.text or m.notice
//
// Example JSON request:
//    {
//        rooms: {
//            "!ewfug483gsfe:localhost": {
//                "text_template": "your plain text template goes here",
//                "html_template": "your html template goes here",
//                "msg_type": "m.text"
//            },
//        }
//    }
type Service struct {
	types.DefaultService
	webhookEndpointURL string
	// The URL which should be added to alertmanagers config - Populated by Go-NEB after Service registration.
	WebhookURL string `json:"webhook_url"`
	// A map of matrix rooms to templates
	Rooms map[id.RoomID]struct {
		TextTemplate string           `json:"text_template"`
		HTMLTemplate string           `json:"html_template"`
		MsgType      mevt.MessageType `json:"msg_type"`
	} `json:"rooms"`
}

// WebhookNotification is the payload from Alertmanager
type WebhookNotification struct {
	Version           string            `json:"version"`
	GroupKey          string            `json:"groupKey"`
	Status            string            `json:"status"`
	Receiver          string            `json:"receiver"`
	GroupLabels       map[string]string `json:"groupLabels"`
	CommonLabels      map[string]string `json:"commonLabels"`
	CommonAnnotations map[string]string `json:"commonAnnotations"`
	ExternalURL       string            `json:"externalURL"`
	Alerts            []struct {
		Status       string            `json:"status"`
		Labels       map[string]string `json:"labels"`
		Annotations  map[string]string `json:"annotations"`
		StartsAt     string            `json:"startsAt"`
		EndsAt       string            `json:"endsAt"`
		GeneratorURL string            `json:"generatorURL"`
		SilenceURL   string
	} `json:"alerts"`
}

// OnReceiveWebhook receives requests from Alertmanager and sends requests to Matrix as a result.
func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli types.MatrixClient) {
	decoder := json.NewDecoder(req.Body)
	var notif WebhookNotification
	if err := decoder.Decode(&notif); err != nil {
		log.WithError(err).Error("Alertmanager webhook received an invalid JSON payload")
		w.WriteHeader(400)
		return
	}

	// add the silence link for each alert
	// see 'newSilenceFromAlertLabels' in
	// https://github.com/prometheus/alertmanager/blob/master/ui/app/src/Views/SilenceForm/Parsing.elm
	for i := range notif.Alerts {
		alert := &notif.Alerts[i]
		filters := []string{}
		for label, val := range alert.Labels {
			filters = append(filters, fmt.Sprintf("%s%%3D\"%s\"", label, val))
		}
		alert.SilenceURL = fmt.Sprintf("%s#silences/new?filter={%s}", notif.ExternalURL, strings.Join(filters, ","))
	}

	for roomID, templates := range s.Rooms {
		var msg interface{}
		// we don't check whether the templates parse because we already did when storing them in the db
		textTemplate, _ := text.New("textTemplate").Parse(templates.TextTemplate)
		var bodyBuffer bytes.Buffer
		if err := textTemplate.Execute(&bodyBuffer, notif); err != nil {
			log.WithError(err).Error("Alertmanager webhook failed to execute text template")
			w.WriteHeader(500)
			return
		}
		if templates.HTMLTemplate != "" {
			// we don't check whether the templates parse because we already did when storing them in the db
			htmlTemplate, _ := html.New("htmlTemplate").Parse(templates.HTMLTemplate)
			var formattedBodyBuffer bytes.Buffer
			if err := htmlTemplate.Execute(&formattedBodyBuffer, notif); err != nil {
				log.WithError(err).Error("Alertmanager webhook failed to execute HTML template")
				w.WriteHeader(500)
				return
			}
			msg = mevt.MessageEventContent{
				Body:          bodyBuffer.String(),
				MsgType:       templates.MsgType,
				Format:        mevt.FormatHTML,
				FormattedBody: formattedBodyBuffer.String(),
			}
		} else {
			msg = mevt.MessageEventContent{
				Body:    bodyBuffer.String(),
				MsgType: templates.MsgType,
			}
		}

		log.WithFields(log.Fields{
			"message": msg,
			"room_id": roomID,
		}).Print("Sending Alertmanager notification to room")
		if _, e := cli.SendMessageEvent(roomID, mevt.EventMessage, msg); e != nil {
			log.WithError(e).WithField("room_id", roomID).Print(
				"Failed to send Alertmanager notification to room.")
		}
	}
	w.WriteHeader(200)
}

// Register makes sure the Config information supplied is valid.
func (s *Service) Register(oldService types.Service, client types.MatrixClient) error {
	s.WebhookURL = s.webhookEndpointURL
	for _, templates := range s.Rooms {
		// validate that we have at least a plain text template
		if templates.TextTemplate == "" {
			return fmt.Errorf("plain text template missing")
		}

		// validate the plain text template is valid
		_, err := text.New("textTemplate").Parse(templates.TextTemplate)
		if err != nil {
			return fmt.Errorf("plain text template is invalid: %v", err)
		}

		if templates.HTMLTemplate != "" {
			// validate that the html template is valid
			_, err := html.New("htmlTemplate").Parse(templates.HTMLTemplate)
			if err != nil {
				return fmt.Errorf("html template is invalid: %v", err)
			}
		}
		// validate that the msgtype is either m.notice or m.text
		if templates.MsgType != "m.notice" && templates.MsgType != "m.text" {
			return fmt.Errorf("msg_type is neither 'm.notice' nor 'm.text'")
		}
	}
	s.joinRooms(client)
	return nil
}

// PostRegister deletes this service if there are no registered repos.
func (s *Service) PostRegister(oldService types.Service) {
	// At least one room still active
	if len(s.Rooms) > 0 {
		return
	}
	// Delete this service since no repos are configured
	logger := log.WithFields(log.Fields{
		"service_type": s.ServiceType(),
		"service_id":   s.ServiceID(),
	})
	logger.Info("Removing service as no repositories are registered.")
	if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil {
		logger.WithError(err).Error("Failed to delete service")
	}
}

func (s *Service) joinRooms(client types.MatrixClient) {
	for roomID := range s.Rooms {
		if _, err := client.JoinRoom(roomID.String(), "", nil); err != nil {
			log.WithFields(log.Fields{
				log.ErrorKey: err,
				"room_id":    roomID,
			}).Error("Failed to join room")
		}
	}
}

func init() {
	types.RegisterService(func(serviceID string, serviceUserID id.UserID, webhookEndpointURL string) types.Service {
		return &Service{
			DefaultService:     types.NewDefaultService(serviceID, serviceUserID, ServiceType),
			webhookEndpointURL: webhookEndpointURL,
		}
	})
}