package main

import (
	// External
	"git.ahall.se/go/html_template"
	werr "git.ahall.se/go/wrappederror"

	// Internal
	"smon/notification"

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

const VERSION = "v41"

var (
	logger              *slog.Logger
	flagConfigFile      string
	flagDev             bool
	flagVersion         bool
	parsedTemplates     map[string]*template.Template
	componentFilenames  []string
	notificationManager notification.Manager
	config              Config
	htmlEngine          HTMLTemplate.Engine

	//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.json")
	flag.StringVar(&flagConfigFile, "config", cfgPath, "Path and filename of the JSON 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)
	}

	logger.Info("application", "version", VERSION)
	var err error

	werr.Init()

	logger.Info("application", "config", flagConfigFile)
	config, err = initConfig(flagConfigFile)
	if err != nil {
		logger.Error("application", "op", "read_config", "error", err)
		os.Exit(1)
	}
	if config.NodataInterval < 10 {
		logger.Error("application → nodata_interval has to be larger or equal to 10.")
		os.Exit(1)
	}

	err = initDb(
		config.Database.Host,
		config.Database.Port,
		config.Database.Name,
		config.Database.Username,
		config.Database.Password,
	)
	if err != nil {
		logger.Error("database", "error", err)
		os.Exit(1)
	}

	htmlEngine, err = initHTMLTemplate()
	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)
	}

	http.HandleFunc("/", staticHandler)

	http.HandleFunc("/area/new/{name}", actionAreaNew)
	http.HandleFunc("/area/rename/{id}/{name}", actionAreaRename)
	http.HandleFunc("/area/delete/{id}", actionAreaDelete)

	http.HandleFunc("/section/new/{areaID}/{name}", actionSectionNew)
	http.HandleFunc("/section/rename/{id}/{name}", actionSectionRename)
	http.HandleFunc("/section/delete/{id}", actionSectionDelete)

	http.HandleFunc("/problems", pageProblems)
	http.HandleFunc("/problem/acknowledge/{id}", actionProblemAcknowledge)
	http.HandleFunc("/problem/unacknowledge/{id}", actionProblemUnacknowledge)

	http.HandleFunc("/datapoints", pageDatapoints)
	http.HandleFunc("/datapoint/edit/{id}", pageDatapointEdit)
	http.HandleFunc("/datapoint/update/{id}", actionDatapointUpdate)
	http.HandleFunc("/datapoint/delete/{id}", actionDatapointDelete)
	http.HandleFunc("/datapoint/values/{id}", pageDatapointValues)
	http.HandleFunc("/datapoint/json/{id}", actionDatapointJson)

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

	http.HandleFunc("/notifications", pageNotifications)

	http.HandleFunc("/configuration", pageConfiguration)
	http.HandleFunc("/configuration/theme", actionConfigurationTheme)
	http.HandleFunc("/configuration/timezone", actionConfigurationTimezone)
	http.HandleFunc("/configuration/notification", pageConfigurationNotification)
	http.HandleFunc("/configuration/notification/update/{prio}", actionConfigurationNotificationUpdate)
	http.HandleFunc("/configuration/notification/delete/{prio}", actionConfigurationNotificationDelete)
	http.HandleFunc("/entry/{datapoint}", actionEntryDatapoint)

	http.HandleFunc("/dashboard/values", actionDashboardValues)

	go nodataLoop()

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

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

	listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port)
	logger.Info("webserver", "listen", listen)
	err = http.ListenAndServe(listen, nil)
	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 initHTMLTemplate() (HTMLTemplate.Engine, error) { // {{{
	return HTMLTemplate.NewEngine(viewFS, staticFS, flagDev)
} // }}}

func httpError(w http.ResponseWriter, err error) { // {{{
	resp := struct {
		OK    bool
		Error string
	}{
		false,
		err.Error(),
	}
	j, _ := json.Marshal(resp)
	w.Write(j)
} // }}}
func pageError(w http.ResponseWriter, redirectURL string, pageErr error) { // {{{
	u, err := url.Parse(redirectURL)
	if err != nil {
		httpError(w, err)
		return
	}

	values := u.Query()
	values.Add("_err", pageErr.Error())
	u.RawQuery = values.Encode()

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

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

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

	htmlEngine.StaticResource(w, r)
} // }}}
func actionEntryDatapoint(w http.ResponseWriter, r *http.Request) { // {{{
	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 = db.Exec(
					context.Background(),
					`
					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.In(smonConfig.Timezone()).Format(`<span class="date">2006-01-02</span> <span class="time">15:04:05<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(layout+".gotmpl").Funcs(funcMap).ParseFS(os.DirFS("."), filenames...)
	} else {
		tmpl, err = template.New(layout+".gotmpl").Funcs(funcMap).ParseFS(viewFS, filenames...)
	}
	if err != nil {
		err = werr.Wrap(err).Log()
		return
	}

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

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


	dashboard, err := DashboardGet("start")
	if err != nil {
		logger.Error("dashboard", "error", err)
	}

	data := make(map[string]any)
	data["Dashboard"] = dashboard
	page.Data = data
	page.Render(w, r)
} // }}}
func actionDashboardValues(w http.ResponseWriter, r *http.Request) { // {{{
	values, err := DatapointsValue([]int{13, 15})
	if err != nil {
		logger.Error("dashboard", "error", err)
		httpError(w, err)
		return
	}

	j, _ := json.Marshal(struct {
		OK     bool
		Values []DatapointTiny
	}{
		true,
		values,
	})
	w.Write(j)
} // }}}

func actionAreaNew(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionAreaRename(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionAreaDelete(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}

func actionSectionNew(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionSectionRename(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionSectionDelete(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}

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

	// Manage the values from the timefilter component
	var err error
	var timeFrom, timeTo time.Time
	timeFrom, timeTo, err = timefilterParse(
		r.URL.Query().Get("time-f"),
		r.URL.Query().Get("time-t"),
		r.URL.Query().Get("time-offset"),
		r.URL.Query().Get("time-preset"),
	)
	if err != nil {
		httpError(w, err)
		return
	}

	// GET parameters for this page
	var selection string
	if r.URL.Query().Get("selection") == "all" {
		selection = "ALL"
	} else {
		selection = "CURRENT"
	}

	problems, err := ProblemsRetrieve(selection == "ALL", timeFrom, timeTo)
	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,
		"Selection":       selection,

		"TimeHidden": selection == "CURRENT",
		"TimeSubmit": "/problems",
		"TimeFrom":   timeFrom.Format("2006-01-02T15:04:05"),
		"TimeTo":     timeTo.Format("2006-01-02T15:04:05"),
	}
	page.Render(w, r)
} // }}}
func actionProblemAcknowledge(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionProblemUnacknowledge(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}

func pageDatapoints(w http.ResponseWriter, r *http.Request) { // {{{
	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, r)
} // }}}
func pageDatapointEdit(w http.ResponseWriter, r *http.Request) { // {{{
	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
		}
	}

	/* Triggers using this datapoint is provided as a list to update
	 * if changing the datapoint name. Parsing expr and automatically
	 * changing it to renamed datapoints would be nice in the future. */
	var triggers []Trigger
	triggers, err = TriggersRetrieveByDatapoint(datapoint.Name)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}
	slices.SortFunc(triggers, func(a, b Trigger) int {
		an := strings.ToUpper(a.Name)
		bn := strings.ToUpper(b.Name)
		if an < bn {
			return -1
		}
		if an > bn {
			return 1
		}
		return 0
	})

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

	page.Data = map[string]any{
		"Datapoint": datapoint,
		"Triggers":  triggers,
	}
	page.Render(w, r)
} // }}}
func actionDatapointUpdate(w http.ResponseWriter, r *http.Request) { // {{{
	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"))

	// Datapoint needs to be retrieved from database for the name.
	// If name has changed, trigger expressions needs to be updated.
	var dp Datapoint
	dp, err = DatapointRetrieve(id, "")
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(id).Log())
		return
	}
	prevDatapointName := dp.Name
	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
	}

	// Update the trigger expressions using this
	// datapoint name if changed.
	if prevDatapointName != dp.Name {
		var triggers []Trigger
		triggers, err = TriggersRetrieveByDatapoint(dp.Name)
		if err != nil {
			httpError(w, werr.Wrap(err).WithData(dp.Name))
			return
		}
		for _, trigger := range triggers {
			err = trigger.RenameDatapoint(prevDatapointName, dp.Name)
			if err != nil {
				httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name}))
				return
			}
			err = trigger.Update()
			if err != nil {
				httpError(w, werr.Wrap(err).WithData([]string{prevDatapointName, dp.Name, trigger.Name}))
				return
			}
		}
	}

	w.Header().Add("Location", "/datapoints")
	w.WriteHeader(302)
} // }}}
func actionDatapointDelete(w http.ResponseWriter, r *http.Request) { // {{{
	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) { // {{{
	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
	}

	// Manage the values from the timefilter component
	var timeFrom, timeTo time.Time
	timeFrom, timeTo, err = timefilterParse(
		r.URL.Query().Get("time-f"),
		r.URL.Query().Get("time-t"),
		r.URL.Query().Get("time-offset"),
		r.URL.Query().Get("time-preset"),
	)

	// Fetch data point values according to the times.
	var values []DatapointValue
	values, err = DatapointValues(id, timeFrom, timeTo)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	// GET parameters.
	display := r.URL.Query().Get("display")
	if display == "" && datapoint.Datatype == INT {
		display = "graph"
	}

	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":   display,

		"TimeSubmit": "/datapoint/values/" + strconv.Itoa(datapoint.ID),
		"TimeFrom":   timeFrom.Format("2006-01-02T15:04:05"),
		"TimeTo":     timeTo.Format("2006-01-02T15:04:05"),
	}
	page.Render(w, r)
} // }}}
func actionDatapointJson(w http.ResponseWriter, r *http.Request) { // {{{
	idStr := r.PathValue("id")
	id, err := strconv.Atoi(idStr)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	fromStr := r.URL.Query().Get("time-f")
	from, err := time.ParseInLocation("2006-01-02 15:04:05", fromStr[0:min(19, len(fromStr))], smonConfig.Timezone())
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	toStr := r.URL.Query().Get("time-t")
	to, err := time.ParseInLocation("2006-01-02 15:04:05", toStr[0:min(19, len(toStr))], smonConfig.Timezone())
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	values, err := DatapointValues(id, from, to)
	if err != nil {
		httpError(w, werr.Wrap(err).Log())
		return
	}

	j, _ := json.Marshal(values)
	w.Write(j)
} // }}}

func pageTriggers(w http.ResponseWriter, r *http.Request) { // {{{
	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, r)
} // }}}
func actionTriggerCreate(w http.ResponseWriter, r *http.Request) { // {{{
	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) { // {{{
	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, r)
} // }}}
func actionTriggerDatapointAdd(w http.ResponseWriter, r *http.Request) { // {{{
	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
	}

	// Also retrieve the datapoint to get the latest value
	// for immediate presentation when added.
	dp, err := DatapointRetrieve(0, dpName)
	if err != nil {
		httpError(w, werr.Wrap(err).WithData(dpName).Log())
		return
	}
	dp.LastDatapointValue.TemplateValue = dp.LastDatapointValue.Value()
	j, _ := json.Marshal(
		struct {
			OK        bool
			Datapoint Datapoint
		}{
			true,
			dp,
		})
	w.Header().Add("Content-Type", "application/json")
	w.Write(j)
} // }}}
func actionTriggerUpdate(w http.ResponseWriter, r *http.Request) { // {{{
	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) { // {{{
	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) { // {{{
	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, r *http.Request) { // {{{
	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,
			"NotificationServices": notificationManager.Services(),
			"AvailableServices":    notification.AvailableServices(),
		},
	}

	page.Render(w, r)
} // }}}
func actionConfigurationTheme(w http.ResponseWriter, r *http.Request) { // {{{
	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)
} // }}}
func actionConfigurationTimezone(w http.ResponseWriter, r *http.Request) { // {{{
	timezone := r.FormValue("timezone")

	_, err := time.LoadLocation(timezone)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	err = smonConfig.SetTimezone(timezone)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
} // }}}
func pageConfigurationNotification(w http.ResponseWriter, r *http.Request) { // {{{
	// This function is either receiving a type when creating a new service,
	// or a prio when editing an existing.
	notificationType := r.URL.Query().Get("type")
	prioStr := r.URL.Query().Get("prio")

	var service notification.Service
	if notificationType != "" {
		// Create a new instance of the selected notification type.
		service = notification.NewInstance(notificationType)
		logger.Info("FOO", "service", fmt.Sprintf("%p %#v\n", service, service))
	} else {
		// Find the existing service for editing.
		prio, err := strconv.Atoi(prioStr)
		if err != nil {
			httpError(w, werr.Wrap(err).Log())
			return
		}
		service = *notificationManager.GetService(prio)
	}

	if service == nil {
		data := struct {
			typ  string
			prio string
		}{notificationType, prioStr}
		err := fmt.Errorf("Invalid service")
		pageError(w, "/configuration", werr.Wrap(err).WithData(data).Log())
		return
	}

	// Make it easier for the user by initiating the prio field
	// for new notifications to the highest prio + 1.
	if notificationType != "" {
		prio := 0
		for _, svc := range notificationManager.Services() {
			prio = max(prio, svc.GetPrio()+1)
		}
		service.SetPrio(prio)
	}

	page := Page{
		LAYOUT: "notification",
		PAGE:   "notification/" + strings.ToLower(service.GetType()),
		CONFIG: smonConfig.Settings,
		Data: map[string]any{
			"Service": service,
		},
	}

	page.Render(w, r)
} // }}}
func actionConfigurationNotificationUpdate(w http.ResponseWriter, r *http.Request) { // {{{
	prioStr := r.PathValue("prio")
	prio, err := strconv.Atoi(prioStr)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	// prio -1 means a new service, not existing in database.
	var svc *notification.Service
	if prio == -1 {
		emptyService := notification.NewInstance(r.PostFormValue("type"))
		svc = &emptyService
	} else {
		svc = notificationManager.GetService(prio)
		if svc == nil {
			pageError(w, "/configuration", werr.New("Service with prio %d not found", prio).Log())
			return
		}
	}

	// The service is given all data to give it a chance to
	// validate and throwing errors.
	err = r.ParseForm()
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	err = (*svc).Update(r.PostForm)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	var created bool
	created, err = UpdateNotificationService(*svc)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	(*svc).Commit()

	if created {
		notificationManager.AddService(*svc)
	}

	notificationManager.Reprioritize()

	w.Header().Add("Location", "/configuration")
	w.WriteHeader(302)
} // }}}
func actionConfigurationNotificationDelete(w http.ResponseWriter, r *http.Request) { // {{{
	prioStr := r.PathValue("prio")
	prio, err := strconv.Atoi(prioStr)
	if err != nil {
		pageError(w, "/configuration", werr.Wrap(err).Log())
		return
	}

	DeleteNotificationService(prio)
	notificationManager.RemoveService(prio)

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

func pageNotifications(w http.ResponseWriter, r *http.Request) { // {{{
	var err error

	// Manage the values from the timefilter component
	var timeFrom, timeTo time.Time
	timeFrom, timeTo, err = timefilterParse(
		r.URL.Query().Get("time-f"),
		r.URL.Query().Get("time-t"),
		r.URL.Query().Get("time-offset"),
		r.URL.Query().Get("time-preset"),
	)

	nss, err := notificationsSent(timeFrom, timeTo)
	if err != nil {
		pageError(w, "/", werr.Wrap(err).Log())
	}

	page := Page{
		LAYOUT: "main",
		PAGE:   "notifications",
		CONFIG: smonConfig.Settings,
		Data: map[string]any{
			"Notifications": nss,
			"TimeSubmit":    "/notifications",
			"TimeFrom":      timeFrom.Format("2006-01-02T15:04:05"),
			"TimeTo":        timeTo.Format("2006-01-02T15:04:05"),
		},
	}

	page.Render(w, r)
} // }}}
