package web

import (
	"bytes"
	"compress/gzip"
	"database/sql"
	"embed"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"io"
	"io/fs"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/gin-contrib/multitemplate"
	"github.com/gin-gonic/gin"
	"github.com/robfig/cron/v3"
	"github.com/spf13/viper"
	"gorm.io/driver/mysql"
	"gorm.io/driver/postgres"
	"gorm.io/driver/sqlite"
	"gorm.io/driver/sqlserver"
	"gorm.io/gorm"

	. "github.com/broodjeaap/go-watch/models"
	"github.com/broodjeaap/go-watch/notifiers"
)

//go:embed templates static watchTemplates config.tmpl
var EMBED_FS embed.FS

type Web struct {
	router          *gin.Engine                   // gin router instance
	templates       multitemplate.Renderer        // multitemplate instance
	cron            *cron.Cron                    // cron instance
	urlCache        map[string]string             // holds url -> http response
	cronWatch       map[FilterID]cron.EntryID     // holds cronFilter.ID -> EntryID
	db              *gorm.DB                      // gorm db instance
	notifiers       map[string]notifiers.Notifier // holds notifierName -> notifier
	startupWarnings []string                      // simple list of warnings/errors found during startup, displayed on / page
	urlPrefix       string                        // allows gowatch to run behind a reverse proxy on a subpath
}

// NewWeb creates a new web instance and calls .init() before returning it
func NewWeb() *Web {
	web := &Web{
		urlCache:        make(map[string]string, 5),
		startupWarnings: make([]string, 0, 10),
	}
	web.init()
	return web
}

// init initializes DB, routers, cron jobs and notifiers
func (web *Web) init() {
	web.urlCache = make(map[string]string, 5)
	if !viper.GetBool("gin.debug") {
		gin.SetMode(gin.ReleaseMode)
	}
	web.validateProxyURL()
	web.initDB()
	web.initRouter()
	web.initNotifiers()
	go web.initCronJobs()
}

// startupWarning is a helper function to add a message to web.startupWarnings and print it to stdout
func (web *Web) startupWarning(m ...any) {
	warning := fmt.Sprint(m...)
	log.Println(warning)
	web.startupWarnings = append(web.startupWarnings, warning)
}

func (web *Web) addCronJobIfCronFilter(filter *Filter, startup bool) {
	if filter.ID == 0 {
		return
	}
	if filter.Type != "cron" {
		return
	}
	if filter.Var2 == "no" {
		return
	}
	scheduleSplit := strings.Split(filter.Var1, "+")
	scheduleTrimmed := strings.TrimSpace(scheduleSplit[0])
	entryID, err := web.cron.AddFunc(scheduleTrimmed, func() { web.addCronJitter(filter.WatchID, filter) })
	if err != nil {
		if startup {
			web.startupWarning("Could not start job for Watch: ", filter.WatchID)
		} else {
			log.Println("Could not start job for Watch: ", filter.WatchID)
		}
		return
	}
	log.Println("Started CronJob for WatchID", filter.WatchID, "with schedule:", filter.Var1)
	web.cronWatch[filter.ID] = entryID
}

func (web *Web) addCronJitter(watchID WatchID, filter *Filter) {
	durations, err := getJittersFromScheduleString(filter.Var1)
	if err != nil {
		log.Println("Could not parse Schedule string:", filter.Var1)
		return
	}

	for _, duration := range durations {
		time.Sleep(duration)
	}
	TriggerSchedule(filter.WatchID, web, &filter.ID)
}

// validateProxyURL calls url.Parse with the proxy.url, if there is an error, it's added to startupWarnings
func (web *Web) validateProxyURL() {
	if viper.IsSet("proxy.url") {
		_, err := url.Parse(viper.GetString("proxy.url"))
		if err != nil {
			web.startupWarning("Could not parse proxy url, check config")
			return
		}
	}
}

// initDB initializes the database with the database.dsn value.
func (web *Web) initDB() {
	dsn := "./watch.db"
	if viper.IsSet("database.dsn") {
		dsn = viper.GetString("database.dsn")
	}

	conf := &gorm.Config{}
	conf.PrepareStmt = true
	var dialector gorm.Dialector
	if strings.HasPrefix(dsn, "sqlserver") {
		dialector = sqlserver.Open(dsn)
	} else if strings.HasPrefix(dsn, "postgres") {
		dialector = postgres.Open(dsn)
	} else if strings.HasPrefix(dsn, "mysql") {
		dialector = mysql.Open(dsn)
	} else {
		dialector = sqlite.Open(dsn)
	}

	// retry connection to the db a couple times with exp retry time
	var err error
	delay := time.Duration(1) * time.Second
	maxDelay := time.Duration(32) * time.Second
	for {
		time.Sleep(delay)
		delay *= 2
		if delay >= maxDelay {
			os.Exit(1)
		}

		web.db, err = gorm.Open(dialector, conf)
		if err != nil {
			log.Println("Could not open db connection, retry in:", delay.String(), err)
			continue
		}

		db, err := web.db.DB()
		if err != nil {
			log.Println("Could not get DB, retry in:", delay.String(), err)
			continue
		}

		err = db.Ping()
		if err != nil {
			log.Println("Could not ping db, retry in:", delay.String(), err)
			continue
		}
		break
	}
	web.db.AutoMigrate(&Watch{}, &Filter{}, &FilterConnection{}, &FilterOutput{}, ExpectFail{})
}

// initRouer initializes the GoWatch routes, binding web.func to a url path
func (web *Web) initRouter() {
	web.router = gin.Default()

	web.initTemplates()
	web.router.HTMLRender = web.templates

	if viper.IsSet("gin.urlprefix") {
		urlPrefix := viper.GetString("gin.urlprefix")
		if urlPrefix != "/" {
			urlPrefix = path.Join("/", urlPrefix) + "/"
		}
		web.urlPrefix = urlPrefix
		log.Println("Running under path: " + web.urlPrefix)
	} else {
		web.urlPrefix = "/"
	}

	staticFS, err := fs.Sub(EMBED_FS, "static")
	if err != nil {
		log.Fatalln("Could not load static embed fs")
	}
	web.router.StaticFS(web.urlPrefix+"static", http.FS(staticFS))
	web.router.StaticFileFS(web.urlPrefix+"favicon.ico", "favicon.ico", http.FS(staticFS))

	gowatch := web.router.Group(web.urlPrefix)

	gowatch.GET("", web.index)
	gowatch.GET("watch/view/:id", web.watchView)
	gowatch.GET("watch/edit/:id", web.watchEdit)
	gowatch.GET("watch/create", web.watchCreate)
	gowatch.POST("watch/create", web.watchCreatePost)
	gowatch.POST("watch/update", web.watchUpdate)
	gowatch.POST("watch/delete", web.deleteWatch)
	gowatch.GET("watch/export/:id", web.exportWatch)
	gowatch.POST("watch/import/:id", web.importWatch)

	gowatch.GET("cache/view", web.cacheView)
	gowatch.POST("cache/clear", web.cacheClear)

	gowatch.GET("notifiers/view", web.notifiersView)
	gowatch.POST("notifiers/test", web.notifiersTest)

	gowatch.GET("schedules/view", web.schedulesView)
	gowatch.POST("schedules/update", web.schedulesUpdate)

	gowatch.GET("backup/view", web.backupView)
	gowatch.GET("backup/create", web.backupCreate)
	gowatch.POST("backup/test", web.backupTest)
	gowatch.POST("backup/restore", web.backupRestore)
	gowatch.POST("backup/delete", web.backupDelete)
	gowatch.GET("backup/download/:id", web.backupDownload)

	web.router.SetTrustedProxies(nil)
}

// initTemplates initializes the templates from EMBED_FS/templates
func (web *Web) initTemplates() {
	web.templates = multitemplate.NewRenderer()

	templatesFS, err := fs.Sub(EMBED_FS, "templates")
	if err != nil {
		log.Fatalln("Could not load templates embed fs")
	}

	web.templates.Add("index", template.Must(template.ParseFS(templatesFS, "base.html", "index.html")))

	web.templates.Add("watchView", template.Must(template.ParseFS(templatesFS, "base.html", "watch/view.html")))
	web.templates.Add("watchCreate", template.Must(template.ParseFS(templatesFS, "base.html", "watch/create.html")))
	web.templates.Add("watchEdit", template.Must(template.ParseFS(templatesFS, "base.html", "watch/edit.html")))

	web.templates.Add("cacheView", template.Must(template.ParseFS(templatesFS, "base.html", "cache/view.html")))

	web.templates.Add("notifiersView", template.Must(template.ParseFS(templatesFS, "base.html", "notifiers.html")))

	web.templates.Add("schedulesView", template.Must(template.ParseFS(templatesFS, "base.html", "schedules.html")))

	web.templates.Add("backupView", template.Must(template.ParseFS(templatesFS, "base.html", "backup/view.html")))
	web.templates.Add("backupTest", template.Must(template.ParseFS(templatesFS, "base.html", "backup/test.html")))
	web.templates.Add("backupRestore", template.Must(template.ParseFS(templatesFS, "base.html", "backup/restore.html")))

	web.templates.Add("500", template.Must(template.ParseFS(templatesFS, "base.html", "500.html")))
}

// initCronJobs reads any 'cron' type filters from the database, and starts a cron job for each
func (web *Web) initCronJobs() {
	var cronFilters []Filter

	// type cron and enabled = yes
	web.db.Model(&Filter{}).Find(&cronFilters, "type = 'cron' AND var2 = 'yes'")

	web.cronWatch = make(map[FilterID]cron.EntryID, len(cronFilters))

	web.cron = cron.New()
	web.cron.Start()

	// db prune job is started if there is a database.prune set
	if viper.IsSet("database.prune") {
		pruneSchedule := viper.GetString("database.prune")
		_, err := web.cron.AddFunc(pruneSchedule, web.pruneDB)
		if err != nil {
			web.startupWarning("Could not parse database.prune:", err)
		}
		log.Println("Started DB prune cronjob:", pruneSchedule)
	}

	// backup job is started if there is a schedule and path
	if viper.IsSet("database.backup.schedule") && viper.IsSet("database.backup.path") {
		backupSchedule := viper.GetString("database.backup.schedule")
		_, err := web.cron.AddFunc(backupSchedule, web.scheduledBackup)
		if err != nil {
			web.startupWarning("Could not parse database.backup.schedule:", err)
		}
		log.Println("Backup schedule set:", backupSchedule)
	}

	// add some delay to cron jobs, so watches with the same schedule don't
	// 'burst' at the same time after restarting GoWatch
	var cronDelayStr string
	if viper.IsSet("schedule.delay") {
		cronDelayStr = viper.GetString("schedule.delay")
	} else {
		cronDelayStr = "100ms"
	}
	cronDelay, delayErr := time.ParseDuration(cronDelayStr)
	if delayErr == nil {
		log.Println("Delaying job startup by:", cronDelay.String())
	} else {
		web.startupWarning("Could not parse schedule.delay: ", cronDelayStr)
	}

	// for every cronFilter, add a new cronjob with the schedule in filter.var1
	for i := range cronFilters {
		cronFilter := &cronFilters[i]
		web.addCronJobIfCronFilter(cronFilter, true)
		if delayErr == nil {
			time.Sleep(cronDelay)
		}
	}
}

// initNotifiers initializes the notifiers configured in the config
func (web *Web) initNotifiers() {
	web.notifiers = make(map[string]notifiers.Notifier, 5)
	if !viper.IsSet("notifiers") {
		web.startupWarning("No notifiers set!")
		return
	}

	// iterates over the map of notifiers, key being the name of the notifier
	notifiersMap := viper.GetStringMap("notifiers")
	for name := range notifiersMap {

		// should probably use notifiersMap.Sub(name), but if it aint broke...
		notifierPath := fmt.Sprintf("notifiers.%s", name)
		notifierMap := viper.GetStringMapString(notifierPath)

		notifierType, exists := notifierMap["type"]
		if !exists {
			web.startupWarning(fmt.Sprintf("No 'type' for '%s' notifier!", name))
			continue
		}

		// create an empty notifier and a success flag,
		// so we can add it to the map at the end instead of each switch case
		success := false
		var notifier notifiers.Notifier
		switch notifierType {
		case "shoutrrr":
			{
				notifier = &notifiers.ShoutrrrNotifier{}
				success = notifier.Open(notifierPath)
				break
			}
		case "apprise":
			{
				notifier = &notifiers.AppriseNotifier{}
				success = notifier.Open(notifierPath)
				break
			}
		case "file":
			{
				notifier = &notifiers.FileNotifier{}
				success = notifier.Open(notifierPath)
				break
			}
		default:
			{
				web.startupWarning("Did not recognize notifier type:", notifierType)
			}
		}
		if success {
			web.notifiers[name] = notifier
		} else {
			web.startupWarning("Could not add notifier:", name)
		}
	}
}

// pruneDB is called by the pruneDB cronjob, it removes repeating values from the database.
func (web *Web) pruneDB() {
	log.Println("Starting database pruning")

	// for every unique (watch.ID, storeFilter.Name)
	var storeFilters []Filter
	web.db.Model(&FilterOutput{}).Distinct("watch_id", "name").Find(&storeFilters)
	for _, storeFilter := range storeFilters {
		// get all the values out of the database
		var values []FilterOutput
		tx := web.db.Model(&FilterOutput{}).Order("time asc").Find(&values, fmt.Sprintf("watch_id = %d AND name = '%s'", storeFilter.WatchID, storeFilter.Name))
		if tx.Error != nil {
			continue
		}

		// a value can be deleted if it's the same as the previous and next value
		IDs := make([]FilterOutputID, 0, len(values))
		for i := range values {
			if i > len(values)-3 {
				break
			}
			a := values[i]
			b := values[i+1]
			c := values[i+2]
			if a.Value == b.Value && b.Value == c.Value {
				IDs = append(IDs, b.ID)
			}
		}
		if len(IDs) > 0 {
			log.Println("Pruned: ", storeFilter.WatchID, "-", storeFilter.Name, "removed", len(IDs), "values")
			web.db.Delete(&FilterOutput{}, IDs)
		} else {
			log.Println("Nothing to prune for", storeFilter.WatchID, "-", storeFilter.Name)
		}
	}
}

// notify sends a message to the notifier with notifierKey name, or all notifiers if key is 'All'
func (web *Web) notify(notifierKey string, message string) {
	if notifierKey == "All" {
		for _, notifier := range web.notifiers {
			notifier.Message(message)
		}
	} else {
		notifier, exists := web.notifiers[notifierKey]
		if !exists {
			log.Println("Could not find notifier with key:", notifierKey)
			return
		}
		notifier.Message(message)
	}
}

// run simply calls router.Run
func (web *Web) Run() {
	web.router.Run("0.0.0.0:8080")
}

// index (/) displays all watches with name, last run, next run, last value
func (web *Web) index(c *gin.Context) {
	watches := []Watch{}
	web.db.Find(&watches)

	// make a map[watch.ID] -> watch so after this we can add data to watches in O(1)
	watchMap := make(map[WatchID]*Watch, len(watches))
	for i := 0; i < len(watches); i++ {
		watchMap[watches[i].ID] = &watches[i]
	}
	// get the schedule for this watch, so we can display last/next run
	// this doesn't work with multiple schedule filters per watch, but meh
	var filters []Filter
	web.db.Model(&Filter{}).Find(&filters, "type = 'cron'")
	for _, filter := range filters {
		entryID, exists := web.cronWatch[filter.ID]
		if !exists {
			log.Println("No cron entry for filter", filter.ID, filter.Name)
			continue
		}
		entry := web.cron.Entry(entryID)
		watchMap[filter.WatchID].CronEntry = &entry
	}

	// get the last value stored, also doesn't really work with multiple values but meh again
	rows, err := web.db.Table("watches").
		Select("watches.id, max(filter_outputs.time) as time, filter_outputs.value").
		Joins("left join filter_outputs on filter_outputs.watch_id = watches.id").
		Order("filter_outputs.name").
		Group("watches.id, time, filter_outputs.value, filter_outputs.name").
		Rows()

	if err != nil {
		log.Println(err)
	} else {
		for rows.Next() {
			var watchID WatchID
			var _time sql.NullString
			var value sql.NullString
			err := rows.Scan(&watchID, &_time, &value)
			if err != nil {
				log.Println(err)
				continue
			}
			if value.Valid {
				watchMap[watchID].LastValue = value.String
			}
		}
	}

	c.HTML(http.StatusOK, "index", gin.H{
		"watches":   watches,
		"warnings":  web.startupWarnings,
		"urlPrefix": web.urlPrefix,
	})
}

// notifiersView (/notifiers/view) shows the notifiers and a test button
func (web *Web) notifiersView(c *gin.Context) {
	c.HTML(http.StatusOK, "notifiersView", gin.H{
		"notifiers": web.notifiers,
		"urlPrefix": web.urlPrefix,
	})
}

// notifiersTest (/notifiers/test) sends a test message to notifier_name
func (web *Web) notifiersTest(c *gin.Context) {
	notifierName := c.PostForm("notifier_name")
	notifier, exists := web.notifiers[notifierName]
	if !exists {
		c.Redirect(http.StatusSeeOther, web.urlPrefix+"notifiers/view")
		return
	}
	notifier.Message("GoWatch Test")
	c.Redirect(http.StatusSeeOther, web.urlPrefix+"notifiers/view")
}

func (web *Web) schedulesView(c *gin.Context) {
	watches := []Watch{}
	web.db.Find(&watches)

	watchMap := make(map[WatchID]*Watch, len(watches))
	for i := 0; i < len(watches); i++ {
		watchMap[watches[i].ID] = &watches[i]
	}

	var filters []Filter
	web.db.Model(&Filter{}).Find(&filters, "type = 'cron'")

	watchSchedules := make(map[*Watch][]*Filter)
	for i := range filters {
		filter := &filters[i]
		entryID, exists := web.cronWatch[filter.ID]

		if exists {
			entry := web.cron.Entry(entryID)
			filter.CronEntry = &entry
		}
		watch := watchMap[filter.WatchID]
		watchSchedules[watch] = append(watchSchedules[watch], filter)
	}
	c.HTML(http.StatusOK, "schedulesView", gin.H{
		"watchSchedules": watchSchedules,
		"urlPrefix":      web.urlPrefix,
	})
}

func (web *Web) schedulesUpdate(c *gin.Context) {
	scheduleStrings := c.PostFormArray("schedules")
	scheduleIDs := make(map[FilterID]bool)
	for _, scheduleString := range scheduleStrings {
		scheduleID, err := strconv.Atoi(scheduleString)
		if err != nil {
			continue
		}
		scheduleIDs[FilterID(scheduleID)] = true
	}

	var cronFilters []Filter
	web.db.Model(&Filter{}).Where("type = 'cron'").Find(&cronFilters)
	for i := range cronFilters {
		cronFilter := &cronFilters[i]
		entryID, exist := web.cronWatch[cronFilter.ID]
		_, checked := scheduleIDs[cronFilter.ID]
		if exist && !checked {
			web.cron.Remove(entryID)
			delete(web.cronWatch, cronFilter.ID)
		} else if !exist && checked {
			cronFilter.Var2 = "yes"
			web.addCronJobIfCronFilter(cronFilter, false)
		}
	}

	if len(scheduleStrings) > 0 {
		web.db.Model(&Filter{}).Where("ID IN ?", scheduleStrings).Update("Var2", "yes")
		web.db.Model(&Filter{}).Where("ID NOT IN ?", scheduleStrings).Update("Var2", "no")
	} else {
		web.db.Model(&Filter{}).Where("TRUE").Update("Var2", "no")
	}

	c.Redirect(http.StatusSeeOther, web.urlPrefix+"schedules/view")
}

// watchCreate (/watch/create) allows user to create a new watch
// A name and an optional template can be picked
func (web *Web) watchCreate(c *gin.Context) {
	templateFiles, err := EMBED_FS.ReadDir("watchTemplates")
	if err != nil {
		log.Fatalln("Could not load templates from embed FS")
	}
	templates := make([]string, 0, len(templateFiles))
	templates = append(templates, "None")
	for _, template := range templateFiles {
		templateFile := template.Name()
		templateName := templateFile[:len(templateFile)-len(".json")]
		templates = append(templates, templateName)
	}
	c.HTML(http.StatusOK, "watchCreate", gin.H{
		"templates": templates,
		"urlPrefix": web.urlPrefix,
	})
}

// watchCreatePost (/watch/create) is where a new watch create will be submitted to
func (web *Web) watchCreatePost(c *gin.Context) {
	var watch Watch
	errMap, err := bindAndValidateWatch(&watch, c)
	if err != nil {
		c.HTML(http.StatusBadRequest, "500", errMap)
		return
	}

	templateID, err := strconv.Atoi(c.PostForm("template"))
	if err != nil {
		log.Println(err)
		templateID = 0
	}

	if templateID == 0 { // empty new watch
		web.db.Create(&watch)
		c.Redirect(http.StatusSeeOther, fmt.Sprintf(web.urlPrefix+"watch/edit/%d", watch.ID))
		return // nothing else to do
	}

	// get the template either from a url or from one of the template files
	var jsn []byte
	if templateID == -1 { // watch from url template
		url := c.PostForm("url")
		if len(url) == 0 {

			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		resp, err := http.Get(url)
		if err != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		jsn = body
	} else if templateID == -2 { // watch from file upload
		file, err := c.FormFile("file")

		if err != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}

		openedFile, err := file.Open()
		if err != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		defer openedFile.Close()
		jsn, _ = ioutil.ReadAll(openedFile)
	} else { // selected one of the templates
		templateFiles, err := EMBED_FS.ReadDir("watchTemplates")
		if err != nil {
			log.Fatalln("Could not load templates from embed FS")
		}

		if templateID > len(templateFiles) {
			log.Println(web.urlPrefix+"watch/create POSTed with", templateID, "but only", len(templateFiles), "templates")
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}

		template := templateFiles[templateID-1] // -1 because of "None" option
		templatePath := fmt.Sprintf("watchTemplates/%s", template.Name())
		_jsn, err := EMBED_FS.ReadFile(templatePath)
		if err != nil {
			log.Println("Could not read template from embed.FS:", err)
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		jsn = _jsn
	}

	export := WatchExport{}
	if err := json.Unmarshal(jsn, &export); err != nil {
		log.Println("Could not unmarshel JSON:", err)
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// create the watch
	web.db.Create(&watch)

	// the IDs of filters and connections have to be 0 when they are added to the database
	// otherwise they will overwrite whatever filters/connections happened to have the same ID
	// so we set them all to 0, but keep a map of 'old filter ID' -> filter
	filterMap := make(map[FilterID]*Filter)
	for i := range export.Filters {
		filter := &export.Filters[i]
		filterMap[filter.ID] = filter
		filter.ID = 0
		filter.WatchID = watch.ID
	}
	if len(export.Filters) > 0 {
		// after web.db.Create, the filters will have their new IDs
		tx := web.db.Create(&export.Filters)
		if tx.Error != nil {
			log.Println("Create filters transaction failed:", err)
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
	}

	for i := range export.Filters {
		filter := &export.Filters[i]
		web.addCronJobIfCronFilter(filter, false)
	}

	// we again set all the connection.ID to 0,
	// but then also swap the old filterIDs to the new IDs the filters got after db.Create
	for i := range export.Connections {
		connection := &export.Connections[i]
		connection.ID = 0
		connection.WatchID = watch.ID
		connection.OutputID = filterMap[connection.OutputID].ID
		connection.InputID = filterMap[connection.InputID].ID
	}
	if len(export.Connections) > 0 {
		tx := web.db.Create(&export.Connections)
		if tx.Error != nil {
			log.Println("Create connections transaction failed:", err)
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
	}

	c.Redirect(http.StatusSeeOther, fmt.Sprintf(web.urlPrefix+"watch/edit/%d", watch.ID))
}

// deleteWatch (/watch/delete) removes a watch and it's cronjobs
func (web *Web) deleteWatch(c *gin.Context) {
	id, err := strconv.Atoi(c.PostForm("watch_id"))
	if err != nil {
		log.Println(err)
		c.Redirect(http.StatusSeeOther, web.urlPrefix)
		return
	}

	web.db.Delete(&FilterConnection{}, "watch_id = ?", id)
	web.db.Delete(&FilterOutput{}, "watch_id = ?", id)
	web.db.Delete(&ExpectFail{}, "watch_id = ?", id)

	var cronFilters []Filter
	web.db.Model(&Filter{}).Find(&cronFilters, "watch_id = ? AND type = 'cron' AND var2 = 'yes'", id)
	for _, filter := range cronFilters {
		entryID, exist := web.cronWatch[filter.ID]
		if exist {
			web.cron.Remove(entryID)
			delete(web.cronWatch, filter.ID)
		}
	}
	web.db.Delete(&Filter{}, "watch_id = ?", id)

	web.db.Delete(&Watch{}, id)
	c.Redirect(http.StatusSeeOther, web.urlPrefix)
}

// watchView (/watch/view) shows the watch page with a graph and/or a table of stored values
func (web *Web) watchView(c *gin.Context) {
	id := c.Param("id")

	var watch Watch
	web.db.Model(&Watch{}).First(&watch, id)

	// get the cron filter for this watch
	var cronFilters []Filter
	web.db.Model(&Filter{}).Find(&cronFilters, "watch_id = ? AND type = 'cron'", id)
	for _, filter := range cronFilters {
		entryID, exists := web.cronWatch[filter.ID]
		if !exists {
			log.Println("Could not find entry for filter", filter.ID, filter.Name)
			continue
		}
		entry := web.cron.Entry(entryID)
		watch.CronEntry = &entry
	}

	// get all the values from this watch from the database
	// split it in 2 groups, numerical and categorical
	var values []FilterOutput
	web.db.Model(&FilterOutput{}).Order("time asc").Where("watch_id = ?", watch.ID).Find(&values)
	numericalMap := make(map[string][]*FilterOutput, len(values))
	categoricalMap := make(map[string][]*FilterOutput, len(values))
	names := make(map[string]bool, 5)
	for i := range values {
		value := &values[i]
		names[value.Name] = true
		_, err := strconv.ParseFloat(value.Value, 64)
		if err == nil {
			numericalMap[value.Name] = append(numericalMap[value.Name], value)
		} else {
			// probably very inefficient to prepend, but want newest values at the top
			valueName := strings.ReplaceAll(value.Name, " ", "_")
			valueName = strings.ToLower(valueName)
			categoricalMap[valueName] = append([]*FilterOutput{value}, categoricalMap[valueName]...)
			log.Println(categoricalMap)
		}
	}

	// give value groups a color, defined in templates/watch/view.html
	colorMap := make(map[string]int, len(names))
	index := 0
	for name := range names {
		colorMap[name] = index % 16 // only 16 colors
		index += 1
	}

	c.HTML(http.StatusOK, "watchView", gin.H{
		"Watch":          watch,
		"numericalMap":   numericalMap,
		"categoricalMap": categoricalMap,
		"colorMap":       colorMap,
		"urlPrefix":      web.urlPrefix,
	})
}

// watchEdit (/watch/edit) shows the node diagram of a watch, allowing the user to modify it
func (web *Web) watchEdit(c *gin.Context) {
	id := c.Param("id")

	var watch Watch
	web.db.Model(&Watch{}).First(&watch, id)

	var filters []Filter
	web.db.Model(&Filter{}).Where("watch_id = ?", watch.ID).Find(&filters)

	var connections []FilterConnection
	web.db.Model(&FilterConnection{}).Where("watch_id = ?", watch.ID).Find(&connections)

	notifiers := make([]string, 0, len(web.notifiers)+1)
	notifiers = append(notifiers, "All")
	for notifier := range web.notifiers {
		notifiers = append(notifiers, notifier)
	}

	buildFilterTree(filters, connections)
	ProcessFilters(filters, web, &watch, true, nil)

	c.HTML(http.StatusOK, "watchEdit", gin.H{
		"Watch":       watch,
		"Filters":     filters,
		"Connections": connections,
		"Notifiers":   notifiers,
		"urlPrefix":   web.urlPrefix,
	})
}

// watchUpdate (/watch/update) is where /watch/edit POSTs to
func (web *Web) watchUpdate(c *gin.Context) {
	// So this function is a simple/lazy way of implementing a watch update.
	// the watch is the only thing that gets 'updated', the rest is just wiped from the database
	// and reinserted
	var watch Watch
	bindAndValidateWatch(&watch, c)

	web.db.Save(&watch)

	var newFilters []Filter
	var filtersJson = c.PostForm("filters")
	if err := json.Unmarshal([]byte(filtersJson), &newFilters); err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	var newConnections []FilterConnection
	var connectionsJson = c.PostForm("connections")
	if err := json.Unmarshal([]byte(connectionsJson), &newConnections); err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// stop/delete cronjobs running for this watch
	var cronFilters []Filter
	web.db.Model(&Filter{}).Where("watch_id = ? AND type = 'cron' AND var2 = 'yes'", watch.ID).Find(&cronFilters)
	for _, filter := range cronFilters {
		entryID, exist := web.cronWatch[filter.ID]
		if exist {
			web.cron.Remove(entryID)
			delete(web.cronWatch, filter.ID)
		} else {
			log.Println("Tried removing cron entry but ID not found ", filter.ID)
			log.Println(web.cronWatch)
		}
	}

	web.db.Delete(&Filter{}, "watch_id = ?", watch.ID)
	web.db.Delete(&ExpectFail{}, "watch_id = ?", watch.ID)

	filterMap := make(map[FilterID]*Filter)
	if len(newFilters) > 0 {
		for i := range newFilters {
			filter := &newFilters[i]
			filterMap[filter.ID] = filter
			filter.ID = 0
		}

		web.db.Create(&newFilters)

		for i := range newFilters {
			filter := &newFilters[i]
			web.addCronJobIfCronFilter(filter, false)
		}
	}

	web.db.Delete(&FilterConnection{}, "watch_id = ?", watch.ID)
	for i := range newConnections {
		connection := &newConnections[i]
		connection.OutputID = filterMap[connection.OutputID].ID
		connection.InputID = filterMap[connection.InputID].ID
	}
	if len(newConnections) > 0 {
		web.db.Create(&newConnections)
	}

	c.Redirect(http.StatusSeeOther, fmt.Sprintf(web.urlPrefix+"watch/edit/%d", watch.ID))
}

// cacheView (/cache/view) shows the items in the web.urlCache
func (web *Web) cacheView(c *gin.Context) {
	c.HTML(http.StatusOK, "cacheView", gin.H{
		"cache":     web.urlCache,
		"urlPrefix": web.urlPrefix,
	})
}

// cacheClear (/cache/clear) clears all items in web.urlCache
func (web *Web) cacheClear(c *gin.Context) {
	web.urlCache = make(map[string]string, 5)
	c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

// backupView (/backup/view) lists the stored backups
func (web *Web) backupView(c *gin.Context) {
	if !viper.IsSet("database.backup") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.schedule") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.schedule not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.path") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.path not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	backupPath := viper.GetString("database.backup.path")

	backupDir, err := filepath.Abs(filepath.Dir(backupPath))
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	filesInBackupDir, err := ioutil.ReadDir(backupDir)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	filePaths := make([]string, 0, len(filesInBackupDir))
	for _, fileInBackupDir := range filesInBackupDir {
		fullPath := filepath.Join(backupDir, fileInBackupDir.Name())
		filePaths = append(filePaths, fullPath)
	}

	c.HTML(http.StatusOK, "backupView", gin.H{
		"Backups":   filePaths,
		"urlPrefix": web.urlPrefix,
	})
}

// backupCreate (/backup/create) creates a new backup
func (web *Web) backupCreate(c *gin.Context) {
	if !viper.IsSet("database.backup") {
		c.HTML(http.StatusBadRequest, "backupView", gin.H{
			"Error":     "database.backup not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.path") {
		c.HTML(http.StatusBadRequest, "backupView", gin.H{
			"Error":     "database.backup.path not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	backupDir := filepath.Dir(viper.GetString("database.backup.path"))
	backupName := fmt.Sprintf("gowatch_%s.gzip", time.Now().Format(time.RFC3339))
	backupName = strings.Replace(backupName, ":", "-", -1)

	backupPath := filepath.Join(backupDir, backupName)
	err := web.createBackup(backupPath)
	if err != nil {
		c.HTML(http.StatusBadRequest, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
}

func (web *Web) scheduledBackup() {
	log.Println("Starting scheduled backup")
	backupPath := viper.GetString("database.backup.path")

	// compare abs backup path to abs dir path, if they are the same, it's a dir
	// avoids an Open(backupPath).Stat, which will fail if it's a file that doesn't exist
	absBackupPath, err := filepath.Abs(backupPath)
	if err != nil {
		log.Println("Could not get abs path of database.backup.path")
		return
	}

	backupDir, err := filepath.Abs(filepath.Dir(backupPath))
	if err != nil {
		log.Println("Could not get abs path of dir(database.backup.path)")
		return
	}
	if absBackupPath == backupDir {
		backupName := fmt.Sprintf("gowatch_%s.gzip", time.Now().Format(time.RFC3339))
		backupPath = filepath.Join(backupPath, backupName)
		log.Println(backupPath)
	} else {
		backupTemplate, err := template.New("backup").Parse(backupPath)
		if err != nil {
			log.Println("Could not parse backup path as template:", err)
			return
		}
		var backupNameBytes bytes.Buffer
		err = backupTemplate.Execute(&backupNameBytes, time.Now())
		if err != nil {
			log.Println("Could not execute backup template:", err)
			return
		}
		backupPath = backupNameBytes.String()
	}
	err = web.createBackup(backupPath)
	if err != nil {
		log.Println("Could not create scheduled backup:", err)
		return
	}
	log.Println("Backup succesful:", backupPath)
}

// createBackup is the function that actually creates the backup
func (web *Web) createBackup(backupPath string) error {
	backupFile, err := os.OpenFile(backupPath, os.O_CREATE|os.O_WRONLY, 0660)
	if err != nil {
		return err
	}
	defer backupFile.Close()

	backupWriter := gzip.NewWriter(backupFile)
	defer backupWriter.Close()

	var watches []Watch
	tx := web.db.Find(&watches)
	if tx.Error != nil {
		return tx.Error
	}

	var filters []Filter
	tx = web.db.Find(&filters)
	if tx.Error != nil {
		return tx.Error
	}

	var connections []FilterConnection
	tx = web.db.Find(&connections)
	if tx.Error != nil {
		return tx.Error
	}

	var values []FilterOutput
	tx = web.db.Find(&values)
	if tx.Error != nil {
		return tx.Error
	}

	backup := Backup{
		Watches:     watches,
		Filters:     filters,
		Connections: connections,
		Values:      values,
	}

	jsn, err := json.Marshal(backup)
	if err != nil {
		return err
	}
	_, err = backupWriter.Write(jsn)

	if err != nil {
		return err
	}

	return nil
}

// backupTest (/backup/test) tests the selected backup file
func (web *Web) backupTest(c *gin.Context) {
	importID, err := strconv.Atoi(c.PostForm("id"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	if importID < -1 {
		c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
		return
	}

	backupFullPath := ""
	var backup Backup
	if importID >= 0 {
		backupFullPath, err = web.backupFromFile(importID, &backup)
	} else { // uploaded backup file
		backupFullPath, err = web.backupFromUpload(c, &backup)
	}

	if err != nil {
		c.HTML(http.StatusOK, "backupTest", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	c.HTML(http.StatusOK, "backupTest", gin.H{
		"Backup":     backup,
		"BackupPath": backupFullPath,
		"urlPrefix":  web.urlPrefix,
	})
}

func (web *Web) backupFromFile(importID int, backup *Backup) (string, error) {
	backupPath := viper.GetString("database.backup.path")

	if !viper.IsSet("database.backup") {
		return "", errors.New("database.backup not set")
	}
	if !viper.IsSet("database.backup.schedule") {
		return "", errors.New("database.backup.schedule not set")
	}
	if !viper.IsSet("database.backup.path") {
		return "", errors.New("database.backup.path not set")
	}

	backupDir, err := filepath.Abs(filepath.Dir(backupPath))
	if err != nil {
		return "", err
	}

	filesInBackupDir, err := ioutil.ReadDir(backupDir)
	if err != nil {
		return "", err
	}
	if importID >= len(filesInBackupDir) {
		return "", err
	}

	backupFileName := filesInBackupDir[importID]
	backupFullPath := filepath.Join(backupDir, backupFileName.Name())
	backupFile, err := os.Open(backupFullPath)
	if err != nil {
		return "", err
	}
	defer backupFile.Close()

	backupReader, err := gzip.NewReader(backupFile)
	if err != nil {
		return "", err
	}
	defer backupReader.Close()
	rawBytes, err := io.ReadAll(backupReader)
	if err != nil {
		return "", err
	}
	err = json.Unmarshal(rawBytes, backup)
	if err != nil {
		return "", err
	}
	return backupFullPath, nil
}

func (web *Web) backupFromUpload(c *gin.Context, backup *Backup) (string, error) {
	upload, err := c.FormFile("upload")
	if err != nil {
		return "", err
	}
	backupFullPath := upload.Filename + " (Uploaded)"

	uploadFile, err := upload.Open()
	if err != nil {
		return "", err
	}
	defer uploadFile.Close()

	backupReader, err := gzip.NewReader(uploadFile)
	if err != nil {
		return "", err
	}
	defer backupReader.Close()
	rawBytes, err := io.ReadAll(backupReader)
	if err != nil {
		return "", err
	}
	err = json.Unmarshal(rawBytes, &backup)
	if err != nil {
		return "", err
	}
	return backupFullPath, nil
}

// backupRestore (/backup/restore/:id) either restores the filesInBackupDir[id] file or from an uploaded file
func (web *Web) backupRestore(c *gin.Context) {
	importID, err := strconv.Atoi(c.PostForm("id"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	if importID < -1 {
		c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
		return
	}

	if !viper.IsSet("database.backup") {
		c.HTML(http.StatusOK, "backupRestore", gin.H{
			"Error":     "database.backup not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.schedule") {
		c.HTML(http.StatusOK, "backupRestore", gin.H{
			"Error":     "database.backup.schedule not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.path") {
		c.HTML(http.StatusOK, "backupRestore", gin.H{
			"Error":     "database.backup.path not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	backupFullPath := ""
	var backup Backup
	if importID >= 0 {
		backupFullPath, err = web.backupFromFile(importID, &backup)
	} else { // uploaded backup file
		backupFullPath, err = web.backupFromUpload(c, &backup)
	}

	if err != nil {
		c.HTML(http.StatusOK, "backupRestore", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	web.cron.Stop()

	err = web.db.Transaction(func(tx *gorm.DB) error {
		connections := tx.Where("1 = 1").Delete(&FilterConnection{})
		if connections.Error != nil {
			return err
		}
		values := tx.Where("1 = 1").Delete(&FilterOutput{})
		if values.Error != nil {
			return err
		}
		filters := tx.Where("1 = 1").Delete(&Filter{})
		if filters.Error != nil {
			return err
		}
		watches := tx.Where("1 = 1").Delete(&Watch{})
		if watches.Error != nil {
			return err
		}
		return nil
	})
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	err = web.db.Transaction(func(tx *gorm.DB) error {
		watches := tx.Create(&backup.Watches)
		if watches.Error != nil {
			return err
		}

		filters := tx.Create(&backup.Filters)
		if filters.Error != nil {
			return err
		}

		connections := tx.Create(&backup.Connections)
		if connections.Error != nil {
			return err
		}

		values := tx.Create(&backup.Values)
		if values.Error != nil {
			return err
		}
		return nil
	})
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	go web.initCronJobs()

	c.HTML(http.StatusOK, "backupRestore", gin.H{
		"Backup":     backup,
		"BackupPath": backupFullPath,
		"urlPrefix":  web.urlPrefix,
	})
}

// backupRestore (/backup/restore/:id) either restores the filesInBackupDir[id] file or from an uploaded file
func (web *Web) backupDelete(c *gin.Context) {
	importID, err := strconv.Atoi(c.PostForm("id"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	if importID < 0 {
		c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
		return
	}

	if !viper.IsSet("database.backup") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.schedule") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.schedule not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.path") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.path not set",
			"urlPrefix": web.urlPrefix})
		return
	}

	backupPath := viper.GetString("database.backup.path")

	backupDir, err := filepath.Abs(filepath.Dir(backupPath))
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	filesInBackupDir, err := ioutil.ReadDir(backupDir)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if importID >= len(filesInBackupDir) {
		c.HTML(http.StatusOK, "backupView", gin.H{"Error": "Wut you doin?",
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	backupFileName := filesInBackupDir[importID]
	backupFullPath := filepath.Join(backupDir, backupFileName.Name())

	err = os.Remove(backupFullPath)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
}

// backupDownload (/backup/download) serves the backup file in index 'id'
func (web *Web) backupDownload(c *gin.Context) {
	importID, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	if importID < 0 {
		c.Redirect(http.StatusSeeOther, web.urlPrefix+"backup/view")
		return
	}

	if !viper.IsSet("database.backup") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.schedule") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.schedule not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if !viper.IsSet("database.backup.path") {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     "database.backup.path not set",
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	backupPath := viper.GetString("database.backup.path")

	backupDir, err := filepath.Abs(filepath.Dir(backupPath))
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	filesInBackupDir, err := ioutil.ReadDir(backupDir)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	if importID >= len(filesInBackupDir) {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	backupFileName := filesInBackupDir[importID]
	backupFullPath := filepath.Join(backupDir, backupFileName.Name())

	backupFile, err := os.Open(backupFullPath)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}
	defer backupFile.Close()

	rawBytes, err := io.ReadAll(backupFile)
	if err != nil {
		c.HTML(http.StatusOK, "backupView", gin.H{
			"Error":     err,
			"urlPrefix": web.urlPrefix,
		})
		return
	}

	c.Header("Content-Disposition", "attachment; filename=\""+backupFileName.Name()+"\"")
	c.Stream(func(w io.Writer) bool {
		w.Write(rawBytes)
		return false
	})
}

// exportWatch (/watch/export/:id) creates a json export of the current watch
func (web *Web) exportWatch(c *gin.Context) {
	watchID := c.Param("id")
	export := WatchExport{}
	var watch Watch
	web.db.Model(&Watch{}).Find(&watch, watchID)
	web.db.Model(&Filter{}).Find(&export.Filters, "watch_id = ?", watchID)
	web.db.Model(&FilterConnection{}).Find(&export.Connections, "watch_id = ?", watchID)

	c.Header("Content-Disposition", "attachment; filename="+watch.Name+".json")
	c.Header("Content-Type", c.Request.Header.Get("Content-Type"))

	c.JSON(http.StatusOK, export)
}

// importWatch (/watch/import/:id) takes a json file and imports it to the current watch
func (web *Web) importWatch(c *gin.Context) {
	watchID, err := strconv.Atoi(c.Param("id"))
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	importType := c.PostForm("type")
	if !(importType == "clear" || importType == "add") {
		c.AbortWithError(http.StatusBadRequest, errors.New("unknown umport type"))
		return
	}
	clearFilters := importType == "clear"

	offsetX := 0
	offsetY := 0

	if !clearFilters {
		offsetX, _ = strconv.Atoi(c.PostForm("offset_x"))
		offsetY, _ = strconv.Atoi(c.PostForm("offset_y"))
		offsetX *= -1
		offsetY *= -1
	}

	file, err := c.FormFile("json")

	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	openedFile, err := file.Open()
	if err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}
	defer openedFile.Close()
	jsn, _ := ioutil.ReadAll(openedFile)

	export := WatchExport{}

	if err := json.Unmarshal(jsn, &export); err != nil {
		c.AbortWithError(http.StatusBadRequest, err)
		return
	}

	// stop/delete cronjobs running for this watch
	var cronFilters []Filter
	if !clearFilters {
		web.db.Model(&Filter{}).Where("watch_id = ? AND type = 'cron'", watchID).Find(&cronFilters)
	}
	for _, filter := range cronFilters {
		entryID, exist := web.cronWatch[filter.ID]
		if exist {
			web.cron.Remove(entryID)
			delete(web.cronWatch, filter.ID)
		}
	}

	filterMap := make(map[FilterID]*Filter)
	for i := range export.Filters {
		filter := &export.Filters[i]
		filterMap[filter.ID] = filter
		filter.ID = 0
		filter.X += offsetX
		filter.Y += offsetY
		filter.WatchID = WatchID(watchID)
	}

	if clearFilters {
		web.db.Delete(&Filter{}, "watch_id = ?", watchID)
	}

	if len(export.Filters) > 0 {
		tx := web.db.Create(&export.Filters)
		if tx.Error != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
		for i := range export.Filters {
			filter := &export.Filters[i]
			web.addCronJobIfCronFilter(filter, false)
		}
	}

	if clearFilters {
		web.db.Delete(&FilterConnection{}, "watch_id = ?", watchID)
	}
	for i := range export.Connections {
		connection := &export.Connections[i]
		connection.ID = 0
		connection.WatchID = WatchID(watchID)
		connection.OutputID = filterMap[connection.OutputID].ID
		connection.InputID = filterMap[connection.InputID].ID
	}
	if len(export.Connections) > 0 {
		tx := web.db.Create(&export.Connections)
		if tx.Error != nil {
			c.AbortWithError(http.StatusBadRequest, err)
			return
		}
	}

	c.Redirect(http.StatusSeeOther, fmt.Sprintf(web.urlPrefix+"watch/edit/%d", watchID))
}
