package main

import (
	// External
	ws "git.gibonuddevalla.se/go/webservice"
	"git.gibonuddevalla.se/go/webservice/session"
	werr "git.gibonuddevalla.se/go/wrappederror"

	// Internal
	"smon/notification"

	// Standard
	"embed"
	"encoding/json"
	"flag"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"log/slog"
	"net/http"
	"os"
	"path"
	"slices"
	"sort"
	"strconv"
	"time"
)

const VERSION = "v21"

var (
	logger              *slog.Logger
	flagConfigFile      string
	flagDev             bool
	flagVersion         bool
	service             *ws.Service
	logFile             *os.File
	parsedTemplates     map[string]*template.Template
	componentFilenames  []string
	notificationManager notification.Manager
	smonConf            SmonConfiguration

	//go:embed sql
	sqlFS embed.FS

	//go:embed views
	viewFS embed.FS

	//go:embed static
	staticFS embed.FS
)

func init() { // {{{
	opts := slog.HandlerOptions{}
	logger = slog.New(slog.NewJSONHandler(os.Stdout, &opts))

	confDir, err := os.UserConfigDir()
	if err != nil {
		logger.Error("application", "error", werr.Wrap(err))
	}
	cfgPath := path.Join(confDir, "smon.yaml")
	flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the YAML configuration file")
	flag.BoolVar(&flagDev, "dev", false, "reload templates on each request")
	flag.BoolVar(&flagVersion, "version", false, "Display version and exit")
	flag.Parse()

	componentFilenames, err = getComponentFilenames(viewFS)
	if err != nil {
		logger.Error("application", "error", err)
		os.Exit(1)
	}
	parsedTemplates = make(map[string]*template.Template)
} // }}}

func main() { // {{{
	if flagVersion {
		fmt.Println(VERSION)
		os.Exit(0)
	}

	var err error

	werr.Init()
	werr.SetLogCallback(logHandler)

	service, err = ws.New(flagConfigFile, VERSION, logger)
	if err != nil {
		logger.Error("application", "error", err)
		os.Exit(1)
	}

	j, _ := json.Marshal(service.Config.Application)
	json.Unmarshal(j, &smonConf)
	logFile, err = os.OpenFile(smonConf.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
	if err != nil {
		logger.Error("application", "error", err)
		return
	}
	if smonConf.NodataInterval < 10 {
		logger.Error("application → nodata_interval has to be larger or equal to 10.")
		return
	}

	service.SetDatabase(sqlProvider)
	service.SetStaticFS(staticFS, "static")
	service.SetStaticDirectory("static", flagDev)
	service.InitDatabaseConnection()
	err = service.Db.Connect()
	if err != nil {
		logger.Error("application", "error", err)
		return
	}

	notificationManager, err = InitNotificationManager()
	if err != nil {
		err = werr.Wrap(err).Log()
		logger.Error("notification", "error", err)
	}

	service.Register("/", false, false, staticHandler)

	service.Register("/area/new/{name}", false, false, actionAreaNew)
	service.Register("/area/rename/{id}/{name}", false, false, actionAreaRename)
	service.Register("/area/delete/{id}", false, false, actionAreaDelete)

	service.Register("/section/new/{areaID}/{name}", false, false, actionSectionNew)
	service.Register("/section/rename/{id}/{name}", false, false, actionSectionRename)
	service.Register("/section/delete/{id}", false, false, actionSectionDelete)

	service.Register("/problems", false, false, pageProblems)
	service.Register("/problem/acknowledge/{id}", false, false, actionProblemAcknowledge)
	service.Register("/problem/unacknowledge/{id}", false, false, actionProblemUnacknowledge)

	service.Register("/datapoints", false, false, pageDatapoints)
	service.Register("/datapoint/edit/{id}", false, false, pageDatapointEdit)
	service.Register("/datapoint/update/{id}", false, false, actionDatapointUpdate)
	service.Register("/datapoint/delete/{id}", false, false, actionDatapointDelete)
	service.Register("/datapoint/values/{id}", false, false, pageDatapointValues)

	service.Register("/triggers", false, false, pageTriggers)
	service.Register("/trigger/create/{sectionID}/{name}", false, false, actionTriggerCreate)
	service.Register("/trigger/edit/{id}", false, false, pageTriggerEdit)
	service.Register("/trigger/edit/{id}/{sectionID}", false, false, pageTriggerEdit)
	service.Register("/trigger/addDatapoint/{id}/{datapointName}", false, false, actionTriggerDatapointAdd)
	service.Register("/trigger/update/{id}", false, false, actionTriggerUpdate)
	service.Register("/trigger/run/{id}", false, false, actionTriggerRun)
	service.Register("/trigger/delete/{id}", false, false, actionTriggerDelete)

	service.Register("/configuration", false, false, pageConfiguration)
	service.Register("/configuration/theme", false, false, actionConfigurationTheme)
	service.Register("/entry/{datapoint}", false, false, actionEntryDatapoint)

	go nodataLoop()

	err = SmonConfigInit()
	if err != nil {
		logger.Error("configuration", "error", werr.Wrap(err))
		os.Exit(1)
	}

	err = service.Start()
	if err != nil {
		logger.Error("webserver", "error", werr.Wrap(err))
		os.Exit(1)
	}

} // }}}

func sqlProvider(dbname string, version int) (sql []byte, found bool) { // {{{
	var err error
	sql, err = sqlFS.ReadFile(fmt.Sprintf("sql/%05d.sql", version))
	if err != nil {
		return
	}
	found = true
	return
} // }}}
func logHandler(err werr.Error) { // {{{
	j, _ := json.Marshal(err)
	logFile.Write(j)
	logFile.Write([]byte("\n"))
} // }}}

func httpError(w http.ResponseWriter, err error) { // {{{
	resp := struct {
		OK    bool
		Error string
	}{
		false,
		err.Error(),
	}
	j, _ := json.Marshal(resp)
	w.Write(j)
} // }}}

func staticHandler(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
	if flagDev && !reloadTemplates(w) {
		return
	}

	if r.URL.Path == "/" {
		pageIndex(w, r, sess)
		return
	}

	service.StaticHandler(w, r, sess)
} // }}}
func actionEntryDatapoint(w http.ResponseWriter, r *http.Request, sess *session.T) { // {{{
	dpoint := r.PathValue("datapoint")
	value, _ := io.ReadAll(r.Body)

	err := DatapointAdd(dpoint, value)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var triggers []Trigger
	triggers, err = TriggersRetrieveByDatapoint(dpoint)
	if err != nil {
		logger.Error("entry", "error", err)
	}

	// Multiple triggers can use the same datapoint.
	for _, trigger := range triggers {
		var out any
		out, err = trigger.Run()
		if err != nil {
			err = werr.Wrap(err).Log()
			logger.Error("entry", "error", err)

		}
		logger.Debug("entry", "datapoint", dpoint, "value", value, "trigger", trigger, "result", out)

		var problemID int
		var problemState string
		switch v := out.(type) {
		case bool:
			// Trigger returning true - a problem occurred
			if v {
				problemID, err = ProblemStart(trigger)
				if err != nil {
					err = werr.Wrap(err).Log()
					logger.Error("entry", "error", err)
				}
				problemState = "PROBLEM"
			} else {
				// A problem didn't occur.
				problemID, err = ProblemClose(trigger)
				if err != nil {
					err = werr.Wrap(err).Log()
					logger.Error("entry", "error", err)
				}
				problemState = "OK"
			}

			// Has a change in problem state happened?
			if problemID == 0 && err == nil {
				logger.Debug("notification", "trigger", trigger.ID, "state", "no change")
				continue
			} else {
				logger.Debug("notification", "trigger", trigger.ID, "state", "change")
			}

			err = notificationManager.Send(problemID, []byte(problemState+": "+trigger.Name), func(notificationService *notification.Service, err error) {
				logger.Info(
					"notification",
					"service", (*notificationService).GetType(),
					"problemID", problemID,
					"prio", (*notificationService).GetPrio(),
					"ok", true,
				)

				var errBody any
				if err != nil {
					errBody, _ = json.Marshal(err)
				} else {
					errBody = nil
				}
				_, err = service.Db.Conn.Exec(
					`
						INSERT INTO notification_send(notification_id, problem_id, uuid, ok, error)
						SELECT
							id, $3, '', $4, $5
						FROM notification
						WHERE
							service=$1 AND
							prio=$2
					`,
					(*notificationService).GetType(),
					(*notificationService).GetPrio(),
					problemID,
					err == nil,
					errBody,
				)
				if err != nil {
					err = werr.Wrap(err).Log()
					logger.Error("entry", "error", err)
				}
			})
			if err != nil {
				err = werr.Wrap(err).Log()
				logger.Error("notification", "error", err)
			}

		default:
			err := fmt.Errorf(`Expression for trigger %s not returning bool (%T)`, trigger.Name, v)
			logger.Info("entry", "error", err)
			werr.Wrap(err).WithData(v).Log()
		}
	}

	j, _ := json.Marshal(struct{ OK bool }{true})
	w.Write(j)
} // }}}

func reloadTemplates(w http.ResponseWriter) bool { // {{{
	parsedTemplates = make(map[string]*template.Template)
	return true
} // }}}
func getComponentFilenames(efs fs.FS) (files []string, err error) { // {{{
	files = []string{}
	if err := fs.WalkDir(efs, "views/components", func(path string, d fs.DirEntry, err error) error {
		if d == nil {
			return nil
		}
		if d.IsDir() {
			return nil
		}
		files = append(files, path)
		return nil
	}); err != nil {
		return nil, err
	}

	return files, nil
} // }}}
func getPage(layout, page string) (tmpl *template.Template, err error) { // {{{
	layoutFilename := fmt.Sprintf("views/layouts/%s.gotmpl", layout)
	pageFilename := fmt.Sprintf("views/pages/%s.gotmpl", page)

	if tmpl, found := parsedTemplates[page]; found {
		return tmpl, nil
	}

	funcMap := template.FuncMap{
		"format_time": func(t time.Time) template.HTML {
			return template.HTML(
				t.Local().Format(`<span class="date">2006-01-02</span> <span class="time">15:04<span class="seconds">:05</span></span>`),
			)
		},
	}

	filenames := []string{layoutFilename, pageFilename}
	filenames = append(filenames, componentFilenames...)
	logger.Info("template", "op", "parse", "layout", layout, "page", page, "filenames", filenames)
	if flagDev {
		tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
	} else {
		tmpl, err = template.New("main.gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
	}
	if err != nil {
		err = werr.Wrap(err).Log()
		return
	}

	parsedTemplates[page] = tmpl
	return
} // }}}

func pageIndex(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
	page := Page{
		LAYOUT: "main",
		PAGE:   "index",
		CONFIG: smonConfig.Settings,
	}
	page.Render(w)
} // }}}

func actionAreaNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	name := r.PathValue("name")
	err := AreaCreate(name)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}
func actionAreaRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	name := r.PathValue("name")
	err = AreaRename(id, name)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}
func actionAreaDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(idStr).Log())
		return
	}

	err = AreaDelete(id)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(id).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}

func actionSectionNew(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("areaID")
	areaID, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	name := r.PathValue("name")
	err = SectionCreate(areaID, name)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}
func actionSectionRename(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	name := r.PathValue("name")
	err = SectionRename(id, name)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}
func actionSectionDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(idStr).Log())
		return
	}

	err = SectionDelete(id)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(id).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
	return
} // }}}

func pageProblems(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
	page := Page{
		LAYOUT: "main",
		PAGE:   "problems",
		CONFIG: smonConfig.Settings,
	}

	problems, err := ProblemsRetrieve()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var found bool
	problemsGrouped := make(map[string]map[string][]Problem)
	for _, problem := range problems {
		if _, found = problemsGrouped[problem.AreaName]; !found {
			problemsGrouped[problem.AreaName] = make(map[string][]Problem)
		}
		if _, found = problemsGrouped[problem.AreaName][problem.SectionName]; !found {
			problemsGrouped[problem.AreaName][problem.SectionName] = []Problem{}
		}

		problemsGrouped[problem.AreaName][problem.SectionName] = append(
			problemsGrouped[problem.AreaName][problem.SectionName],
			problem,
		)
	}

	page.Data = map[string]any{
		"Problems":        problems,
		"ProblemsGrouped": problemsGrouped,
	}
	page.Render(w)
	return
} // }}}
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	err = ProblemAcknowledge(id, true)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/problems")
	w.WriteHeader(302)

	return
} // }}}
func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	err = ProblemAcknowledge(id, false)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/problems")
	w.WriteHeader(302)

	return
} // }}}

func pageDatapoints(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	page := Page{
		LAYOUT: "main",
		PAGE:   "datapoints",
		CONFIG: smonConfig.Settings,
	}

	datapoints, err := DatapointsRetrieve()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	// The datapoint selector in trigger edit wants the raw data in JSON.
	if r.URL.Query().Get("format") == "json" {
		j, _ := json.Marshal(datapoints)
		w.Write(j)
		return
	}

	page.Data = map[string]any{
		"Datapoints": datapoints,
	}
	page.Render(w)
	return
} // }}}
func pageDatapointEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var datapoint Datapoint
	if id == 0 {
		datapoint.Name = "new_datapoint"
		datapoint.Datatype = "INT"
	} else {
		datapoint, err = DatapointRetrieve(id, "")
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
	}

	page := Page{
		LAYOUT: "main",
		PAGE:   "datapoint_edit",
		CONFIG: smonConfig.Settings,
		MENU:   "datapoints",
		Icon:   "datapoints",
		Label:  "Datapoint",
	}

	page.Data = map[string]any{
		"Datapoint": datapoint,
	}
	page.Render(w)
	return
} // }}}
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var nodataSeconds int
	nodataSeconds, _ = strconv.Atoi(r.FormValue("nodata_seconds"))

	var dp Datapoint
	dp.ID = id
	dp.Group = r.FormValue("group")
	dp.Name = r.FormValue("name")
	dp.Datatype = DatapointType(r.FormValue("datatype"))
	dp.Comment = r.FormValue("comment")
	dp.NodataProblemSeconds = nodataSeconds
	err = dp.Update()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/datapoints")
	w.WriteHeader(302)
} // }}}
func actionDatapointDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	err = DatapointDelete(id)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/datapoints")
	w.WriteHeader(302)
} // }}}
func pageDatapointValues(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var datapoint Datapoint
	datapoint, err = DatapointRetrieve(id, "")
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var values []DatapointValue
	values, err = DatapointValues(id)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	page := Page{
		LAYOUT: "main",
		PAGE:   "datapoint_values",
		CONFIG: smonConfig.Settings,
		MENU:   "datapoints",
		Icon:   "datapoints",
		Label:  "Values for " + datapoint.Name,
	}

	page.Data = map[string]any{
		"Datapoint": datapoint,
		"Values":    values,
		"Display":   r.URL.Query().Get("display"),
	}
	page.Render(w)
	return
} // }}}

func pageTriggers(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
	areas, err := TriggersRetrieve()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	sort.SliceStable(areas, func(i, j int) bool {
		return areas[i].Name < areas[j].Name
	})

	page := Page{
		LAYOUT: "main",
		PAGE:   "triggers",
		CONFIG: smonConfig.Settings,
		Data: map[string]any{
			"Areas": areas,
		},
	}

	page.Render(w)
} // }}}
func actionTriggerCreate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	name := r.PathValue("name")
	sectionIDStr := r.PathValue("sectionID")
	sectionID, err := strconv.Atoi(sectionIDStr)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(sectionIDStr).Log())
		return
	}

	t, err := TriggerCreate(sectionID, name)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(struct {
			SectionID int
			Name      string
		}{
			sectionID,
			name,
		}).Log())
		return
	}

	resp := struct {
		OK      bool
		Trigger Trigger
	}{
		true,
		t,
	}
	j, _ := json.Marshal(resp)
	w.Header().Add("Content-Type", "application/json")
	w.Write(j)
} // }}}
func pageTriggerEdit(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	// Creating a new trigger uses the edit function.
	// ID == 0 - create a new trigger.
	// ID  > 0 - edit existing trigger.
	var trigger Trigger
	if id > 0 {
		trigger, err = TriggerRetrieve(id)
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
	} else {
		// A new trigger needs to know which section it belongs to.
		sectionIDStr := r.PathValue("sectionID")
		if sectionIDStr != "" {
			trigger.SectionID, err = strconv.Atoi(sectionIDStr)
			if err != nil {
				httpError(w, werr.Wrap(err).Log())
				return
			}
		}
	}

	datapoints := make(map[string]Datapoint)
	for _, dpname := range trigger.Datapoints {
		dp, err := DatapointRetrieve(0, dpname)
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
		dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value()
		datapoints[dpname] = dp
	}

	page := Page{
		LAYOUT: "main",
		PAGE:   "trigger_edit",
		MENU:   "triggers",
		CONFIG: smonConfig.Settings,
		Label:  "Trigger",
		Icon:   "triggers",
		Data: map[string]any{
			"Trigger":    trigger,
			"Datapoints": datapoints,
		},
	}

	page.Render(w)
} // }}}
func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	triggerID := r.PathValue("id")
	dpName := r.PathValue("datapointName")

	id, err := strconv.Atoi(triggerID)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var trigger Trigger
	if id > 0 {
		trigger, err = TriggerRetrieve(id)
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
	}

	if !slices.Contains(trigger.Datapoints, dpName) {
		trigger.Datapoints = append(trigger.Datapoints, dpName)
	}
	err = trigger.Update()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	j, _ := json.Marshal(struct{ OK bool }{OK: true})
	w.Header().Add("Content-Type", "application/json")
	w.Write(j)
} // }}}
func actionTriggerUpdate(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var trigger Trigger
	if id > 0 {
		trigger, err = TriggerRetrieve(id)
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
	} else {
		trigger.SectionID, err = strconv.Atoi(r.FormValue("sectionID"))
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
	}

	trigger.Name = r.FormValue("name")
	trigger.Expression = r.FormValue("expression")
	trigger.Datapoints = r.Form["datapoints[]"]
	err = trigger.Update()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/triggers")
	w.WriteHeader(302)
} // }}}
func actionTriggerRun(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	var trigger Trigger
	trigger, err = TriggerRetrieve(id)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	expr, _ := io.ReadAll(r.Body)
	trigger.Expression = string(expr)

	resp := struct {
		OK     bool
		Output any
	}{
		OK: true,
	}
	resp.Output, err = trigger.Run()
	if err != nil {
		werr.Wrap(err).Log()
		httpError(w, err)
		return
	}

	j, _ := json.Marshal(resp)
	w.Header().Add("Content-Type", "application/json")
	w.Write(j)
} // }}}
func actionTriggerDelete(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	err = TriggerDelete(id)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/triggers")
	w.WriteHeader(302)
} // }}}

func pageConfiguration(w http.ResponseWriter, _ *http.Request, _ *session.T) { // {{{
	areas, err := AreaRetrieve()
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	sort.SliceStable(areas, func(i, j int) bool {
		return areas[i].Name < areas[j].Name
	})

	page := Page{
		LAYOUT: "main",
		PAGE:   "configuration",
		CONFIG: smonConfig.Settings,
		Data: map[string]any{
			"Areas": areas,
		},
	}

	page.Render(w)
} // }}}
func actionConfigurationTheme(w http.ResponseWriter, r *http.Request, _ *session.T) { // {{{
	theme := r.FormValue("theme")
	err := smonConfig.SetTheme(theme)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
} // }}}
