package main

import (
	"crypto/subtle"
	"net/http"
	"net/url"
	"regexp"
	"strconv"

	"github.com/labstack/echo"
	"github.com/labstack/echo/middleware"
)

const (
	// stdInputMaxLen is the maximum allowed length for a standard input field.
	stdInputMaxLen = 200

	sortAsc  = "asc"
	sortDesc = "desc"
)

type okResp struct {
	Data interface{} `json:"data"`
}

// pagination represents a query's pagination (limit, offset) related values.
type pagination struct {
	PerPage int `json:"per_page"`
	Page    int `json:"page"`
	Offset  int `json:"offset"`
	Limit   int `json:"limit"`
}

var (
	reUUID     = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
	reLangCode = regexp.MustCompile("[^a-zA-Z_0-9]")
)

// registerHandlers registers HTTP handlers.
func registerHTTPHandlers(e *echo.Echo, app *App) {
	// Group of private handlers with BasicAuth.
	var g *echo.Group

	if len(app.constants.AdminUsername) == 0 ||
		len(app.constants.AdminPassword) == 0 {
		g = e.Group("")
	} else {
		g = e.Group("", middleware.BasicAuth(basicAuth))
	}

	g.GET("/", handleIndexPage)
	g.GET("/api/health", handleHealthCheck)
	g.GET("/api/config", handleGetServerConfig)
	g.GET("/api/lang/:lang", handleGetI18nLang)
	g.GET("/api/dashboard/charts", handleGetDashboardCharts)
	g.GET("/api/dashboard/counts", handleGetDashboardCounts)

	g.GET("/api/settings", handleGetSettings)
	g.PUT("/api/settings", handleUpdateSettings)
	g.POST("/api/admin/reload", handleReloadApp)
	g.GET("/api/logs", handleGetLogs)

	g.GET("/api/subscribers/:id", handleGetSubscriber)
	g.GET("/api/subscribers/:id/export", handleExportSubscriberData)
	g.POST("/api/subscribers", handleCreateSubscriber)
	g.PUT("/api/subscribers/:id", handleUpdateSubscriber)
	g.POST("/api/subscribers/:id/optin", handleSubscriberSendOptin)
	g.PUT("/api/subscribers/blocklist", handleBlocklistSubscribers)
	g.PUT("/api/subscribers/:id/blocklist", handleBlocklistSubscribers)
	g.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
	g.PUT("/api/subscribers/lists", handleManageSubscriberLists)
	g.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
	g.DELETE("/api/subscribers", handleDeleteSubscribers)

	// Subscriber operations based on arbitrary SQL queries.
	// These aren't very REST-like.
	g.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
	g.PUT("/api/subscribers/query/blocklist", handleBlocklistSubscribersByQuery)
	g.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
	g.GET("/api/subscribers", handleQuerySubscribers)
	g.GET("/api/subscribers/export",
		middleware.GzipWithConfig(middleware.GzipConfig{Level: 9})(handleExportSubscribers))

	g.GET("/api/import/subscribers", handleGetImportSubscribers)
	g.GET("/api/import/subscribers/logs", handleGetImportSubscriberStats)
	g.POST("/api/import/subscribers", handleImportSubscribers)
	g.DELETE("/api/import/subscribers", handleStopImportSubscribers)

	g.GET("/api/lists", handleGetLists)
	g.GET("/api/lists/:id", handleGetLists)
	g.POST("/api/lists", handleCreateList)
	g.PUT("/api/lists/:id", handleUpdateList)
	g.DELETE("/api/lists/:id", handleDeleteLists)

	g.GET("/api/campaigns", handleGetCampaigns)
	g.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
	g.GET("/api/campaigns/:id", handleGetCampaigns)
	g.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
	g.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
	g.POST("/api/campaigns/:id/content", handleCampaignContent)
	g.POST("/api/campaigns/:id/text", handlePreviewCampaign)
	g.POST("/api/campaigns/:id/test", handleTestCampaign)
	g.POST("/api/campaigns", handleCreateCampaign)
	g.PUT("/api/campaigns/:id", handleUpdateCampaign)
	g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
	g.DELETE("/api/campaigns/:id", handleDeleteCampaign)

	g.GET("/api/media", handleGetMedia)
	g.POST("/api/media", handleUploadMedia)
	g.DELETE("/api/media/:id", handleDeleteMedia)

	g.GET("/api/templates", handleGetTemplates)
	g.GET("/api/templates/:id", handleGetTemplates)
	g.GET("/api/templates/:id/preview", handlePreviewTemplate)
	g.POST("/api/templates/preview", handlePreviewTemplate)
	g.POST("/api/templates", handleCreateTemplate)
	g.PUT("/api/templates/:id", handleUpdateTemplate)
	g.PUT("/api/templates/:id/default", handleTemplateSetDefault)
	g.DELETE("/api/templates/:id", handleDeleteTemplate)

	// Static admin views.
	g.GET("/lists", handleIndexPage)
	g.GET("/lists/forms", handleIndexPage)
	g.GET("/subscribers", handleIndexPage)
	g.GET("/subscribers/lists/:listID", handleIndexPage)
	g.GET("/subscribers/import", handleIndexPage)
	g.GET("/campaigns", handleIndexPage)
	g.GET("/campaigns/new", handleIndexPage)
	g.GET("/campaigns/media", handleIndexPage)
	g.GET("/campaigns/templates", handleIndexPage)
	g.GET("/campaigns/:campignID", handleIndexPage)
	g.GET("/settings", handleIndexPage)
	g.GET("/settings/logs", handleIndexPage)

	// Public subscriber facing views.
	e.GET("/subscription/form", handleSubscriptionFormPage)
	e.POST("/subscription/form", handleSubscriptionForm)
	e.GET("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
		"campUUID", "subUUID"))
	e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
		"campUUID", "subUUID"))
	e.GET("/subscription/optin/:subUUID", validateUUID(subscriberExists(handleOptinPage), "subUUID"))
	e.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(handleOptinPage), "subUUID"))
	e.POST("/subscription/export/:subUUID", validateUUID(subscriberExists(handleSelfExportSubscriberData),
		"subUUID"))
	e.POST("/subscription/wipe/:subUUID", validateUUID(subscriberExists(handleWipeSubscriberData),
		"subUUID"))
	e.GET("/link/:linkUUID/:campUUID/:subUUID", validateUUID(handleLinkRedirect,
		"linkUUID", "campUUID", "subUUID"))
	e.GET("/campaign/:campUUID/:subUUID", validateUUID(handleViewCampaignMessage,
		"campUUID", "subUUID"))
	e.GET("/campaign/:campUUID/:subUUID/px.png", validateUUID(handleRegisterCampaignView,
		"campUUID", "subUUID"))
}

// handleIndex is the root handler that renders the Javascript frontend.
func handleIndexPage(c echo.Context) error {
	app := c.Get("app").(*App)

	b, err := app.fs.Read("/frontend/index.html")
	if err != nil {
		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
	}

	c.Response().Header().Set("Content-Type", "text/html")
	return c.String(http.StatusOK, string(b))
}

// handleHealthCheck is a healthcheck endpoint that returns a 200 response.
func handleHealthCheck(c echo.Context) error {
	return c.JSON(http.StatusOK, okResp{true})
}

// basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
func basicAuth(username, password string, c echo.Context) (bool, error) {
	app := c.Get("app").(*App)

	// Auth is disabled.
	if len(app.constants.AdminUsername) == 0 &&
		len(app.constants.AdminPassword) == 0 {
		return true, nil
	}

	if subtle.ConstantTimeCompare([]byte(username), app.constants.AdminUsername) == 1 &&
		subtle.ConstantTimeCompare([]byte(password), app.constants.AdminPassword) == 1 {
		return true, nil
	}
	return false, nil
}

// validateUUID middleware validates the UUID string format for a given set of params.
func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
	return func(c echo.Context) error {
		app := c.Get("app").(*App)

		for _, p := range params {
			if !reUUID.MatchString(c.Param(p)) {
				return c.Render(http.StatusBadRequest, tplMessage,
					makeMsgTpl(app.i18n.T("public.errorTitle"), "",
						app.i18n.T("globals.messages.invalidUUID")))
			}
		}
		return next(c)
	}
}

// subscriberExists middleware checks if a subscriber exists given the UUID
// param in a request.
func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
	return func(c echo.Context) error {
		var (
			app     = c.Get("app").(*App)
			subUUID = c.Param("subUUID")
		)

		var exists bool
		if err := app.queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
			app.log.Printf("error checking subscriber existence: %v", err)
			return c.Render(http.StatusInternalServerError, tplMessage,
				makeMsgTpl(app.i18n.T("public.errorTitle"), "",
					app.i18n.T("public.errorProcessingRequest")))
		}

		if !exists {
			return c.Render(http.StatusNotFound, tplMessage,
				makeMsgTpl(app.i18n.T("public.notFoundTitle"), "",
					app.i18n.T("public.subNotFound")))
		}
		return next(c)
	}
}

// getPagination takes form values and extracts pagination values from it.
func getPagination(q url.Values, perPage, maxPerPage int) pagination {
	page, _ := strconv.Atoi(q.Get("page"))
	pp := q.Get("per_page")
	if pp == "all" {
		// No limit.
		perPage = 0
	} else {
		ppi, _ := strconv.Atoi(pp)
		if ppi > 0 && ppi <= maxPerPage {
			perPage = ppi
		}
	}

	if page < 1 {
		page = 0
	} else {
		page--
	}

	return pagination{
		Page:    page + 1,
		PerPage: perPage,
		Offset:  page * perPage,
		Limit:   perPage,
	}
}

// copyEchoCtx returns a copy of the the current echo.Context in a request
// with the given params set for the active handler to proxy the request
// to another handler without mutating its context.
func copyEchoCtx(c echo.Context, params map[string]string) echo.Context {
	var (
		keys = make([]string, 0, len(params))
		vals = make([]string, 0, len(params))
	)
	for k, v := range params {
		keys = append(keys, k)
		vals = append(vals, v)
	}

	b := c.Echo().NewContext(c.Request(), c.Response())
	b.Set("app", c.Get("app").(*App))
	b.SetParamNames(keys...)
	b.SetParamValues(vals...)
	return b
}
