/*
Copyright 2017-2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package handlers

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"github.com/go-playground/form/v4"
	"github.com/justinas/nosurf"

	"git.cacert.org/cacert-boardvoting/internal/forms"
	"git.cacert.org/cacert-boardvoting/internal/jobs"
	"git.cacert.org/cacert-boardvoting/internal/models"
	"git.cacert.org/cacert-boardvoting/internal/notifications"
)

func ClientError(w http.ResponseWriter, status int) {
	http.Error(w, http.StatusText(status), status)
}

func NotFound(w http.ResponseWriter) {
	ClientError(w, http.StatusNotFound)
}

type topLevelNavItem string
type subLevelNavItem string

const (
	topLevelNavMotions topLevelNavItem = "motions"
	topLevelNavUsers   topLevelNavItem = "users"

	subLevelNavMotionsAll     subLevelNavItem = "all-motions"
	subLevelNavMotionsUnvoted subLevelNavItem = "unvoted-motions"
	subLevelNavUsers          subLevelNavItem = "users"
	subLevelNavVoters         subLevelNavItem = "manage-voters"
)

const hoursInDay = 24

func choiceFromRequestParam(w http.ResponseWriter, r *http.Request) *models.VoteChoice {
	choice, err := models.VoteChoiceFromString(chi.URLParam(r, "choice"))
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return nil
	}

	return choice
}

type UserAwareHandler interface {
	UserModel() *models.UserModel
}

func getVoter(ctx context.Context, w http.ResponseWriter, u UserAwareHandler, voterID int64) *models.User {
	users := u.UserModel()

	voter, err := users.ByID(ctx, voterID, users.WithRoles())
	if err != nil {
		panic(err)
	}

	var isVoter bool

	if isVoter, err = voter.HasRole(models.RoleVoter); err != nil {
		panic(err)
	}

	if !isVoter {
		ClientError(w, http.StatusBadRequest)

		return nil
	}

	return voter
}

func userFromRequestParam(
	ctx context.Context,
	w http.ResponseWriter, r *http.Request, u UserAwareHandler, options ...models.UserListOption,
) *models.User {
	userID, err := strconv.Atoi(chi.URLParam(r, "id"))
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return nil
	}

	user, err := u.UserModel().ByID(ctx, int64(userID), options...)
	if err != nil {
		panic(err)
	}

	if user == nil {
		NotFound(w)

		return nil
	}

	return user
}

type MotionAwareHandler interface {
	MotionModel() *models.MotionModel
}

func motionFromRequestParam(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
	m MotionAwareHandler,
) *models.Motion {
	withVotes := r.URL.Query().Has("showvotes")

	motion, err := m.MotionModel().ByTag(ctx, chi.URLParam(r, "tag"), withVotes)
	if err != nil {
		panic(err)
	}

	if motion.ID == 0 {
		NotFound(w)

		return nil
	}

	return motion
}

func newTemplateData(
	ctx context.Context,
	r *http.Request,
	flash *FlashHandler,
	nav topLevelNavItem,
	subNav subLevelNavItem,
) *TemplateData {
	user, _ := getUser(r)

	return &TemplateData{
		Request:      r,
		User:         user,
		ActiveNav:    nav,
		ActiveSubNav: subNav,
		Flashes:      flash.flashes(ctx),
		CSRFToken:    nosurf.Token(r),
	}
}

func (m *TemplateData) motionPaginationOptions(limit int, first, last *time.Time) error {
	motions := m.Motions

	if len(motions) == limit && first.Before(motions[len(motions)-1].Proposed) {
		marshaled, err := motions[len(motions)-1].Proposed.MarshalText()
		if err != nil {
			return fmt.Errorf("could not serialize timestamp: %w", err)
		}

		m.NextPage = string(marshaled)
	}

	if len(motions) > 0 && last.After(motions[0].Proposed) {
		marshaled, err := motions[0].Proposed.MarshalText()
		if err != nil {
			return fmt.Errorf("could not serialize timestamp: %w", err)
		}

		m.PrevPage = string(marshaled)
	}

	return nil
}

func calculateMotionListOptions(r *http.Request) (*models.MotionListOptions, error) {
	const (
		queryParamBefore  = "before"
		queryParamAfter   = "after"
		queryParamUnvoted = "unvoted"
		motionsPerPage    = 10
	)

	listOptions := &models.MotionListOptions{Limit: motionsPerPage}

	if r.URL.Query().Has(queryParamAfter) {
		var after time.Time

		err := after.UnmarshalText([]byte(r.URL.Query().Get(queryParamAfter)))
		if err != nil {
			return nil, fmt.Errorf("could not unmarshal timestamp: %w", err)
		}

		listOptions.After = &after
	} else if r.URL.Query().Has(queryParamBefore) {
		var before time.Time

		err := before.UnmarshalText([]byte(r.URL.Query().Get(queryParamBefore)))
		if err != nil {
			return nil, fmt.Errorf("could not unmarshal timestamp: %w", err)
		}

		listOptions.Before = &before
	}

	if r.URL.Query().Has(queryParamUnvoted) {
		listOptions.UnvotedOnly = true

		voter, err := getUser(r)
		if err != nil {
			return nil, fmt.Errorf("could not get voter: %w", err)
		}

		if voter != nil {
			listOptions.VoterID = voter.ID
		}
	}

	return listOptions, nil
}

func emailFromRequestParam(r *http.Request, user *models.User) (string, error) {
	emailAddresses, err := user.EmailAddresses()
	if err != nil {
		return "", fmt.Errorf("could not get email addresses: %w", err)
	}

	emailParam := chi.URLParam(r, "address")

	for _, address := range emailAddresses {
		if emailParam == address {
			return emailParam, nil
		}
	}

	return "", nil
}

type CommonParams struct {
	Flashes       *FlashHandler
	TemplateCache *TemplateCache
	FormDecoder   *form.Decoder
}

func (c *CommonParams) decodePostForm(r *http.Request, dst forms.Form) error {
	err := r.ParseForm()
	if err != nil {
		return fmt.Errorf("could not parse HTML form: %w", err)
	}

	err = c.FormDecoder.Decode(dst, r.PostForm)
	if err != nil {
		var invalidDecoderError *form.InvalidDecoderError

		if errors.As(err, &invalidDecoderError) {
			panic(err)
		}

		return fmt.Errorf("could not decode form: %w", err)
	}

	return nil
}

type MotionHandler struct {
	motions      *models.MotionModel
	mailNotifier *notifications.MailNotifier
	jobScheduler *jobs.JobScheduler
	CommonParams
}

func (m *MotionHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
	return newTemplateData(ctx, r, m.Flashes, topLevelNavMotions, subNav)
}

func (m *MotionHandler) MotionModel() *models.MotionModel {
	return m.motions
}

func (m *MotionHandler) List(w http.ResponseWriter, r *http.Request) {
	var (
		listOptions *models.MotionListOptions
		err         error
	)

	listOptions, err = calculateMotionListOptions(r)
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	ctx := r.Context()

	motions, err := m.motions.List(ctx, listOptions)
	if err != nil {
		panic(err)
	}

	first, last, err := m.motions.TimestampRange(ctx, listOptions)
	if err != nil {
		panic(err)
	}

	templateData := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	if listOptions.UnvotedOnly {
		templateData.ActiveSubNav = subLevelNavMotionsUnvoted
	}

	templateData.Motions = motions

	err = templateData.motionPaginationOptions(listOptions.Limit, first, last)
	if err != nil {
		panic(err)
	}

	m.TemplateCache.render(w, http.StatusOK, "motions.html", templateData)
}

func (m *MotionHandler) Details(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, m)
	if motion == nil {
		return
	}

	data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	data.Motion = motion

	m.TemplateCache.render(w, http.StatusOK, "motion.html", data)
}

func (m *MotionHandler) NewForm(w http.ResponseWriter, r *http.Request) {
	data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
	data.Form = &forms.EditMotionForm{
		Type: models.VoteTypeMotion,
	}

	m.TemplateCache.render(w, http.StatusOK, "create_motion.html", data)
}

func (m *MotionHandler) New(w http.ResponseWriter, r *http.Request) {
	var motionForm forms.EditMotionForm

	err := m.decodePostForm(r, &motionForm)
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err := motionForm.Validate(); err != nil {
		panic(err)
	}

	if !motionForm.Valid() {
		data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
		data.Form = &motionForm

		m.TemplateCache.render(w, http.StatusUnprocessableEntity, "create_motion.html", data)

		return
	}

	motionForm.Normalize()

	user, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	now := time.Now().UTC()
	dueDuration := time.Duration(motionForm.Due) * hoursInDay * time.Hour

	decisionID, err := m.motions.Create(
		r.Context(),
		user,
		motionForm.Type,
		motionForm.Title,
		motionForm.Content,
		now,
		now.Add(dueDuration),
	)
	if err != nil {
		panic(err)
	}

	decision, err := m.motions.ByID(r.Context(), decisionID)
	if err != nil {
		panic(err)
	}

	m.mailNotifier.Notify(&notifications.NewDecisionNotification{
		Decision: decision,
		Proposer: user,
	})

	m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)

	m.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "New motion started",
		Message: fmt.Sprintf("Started new motion %s: %s", decision.Tag, decision.Title),
	})

	http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}

func (m *MotionHandler) EditForm(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, m)
	if motion == nil {
		return
	}

	data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	data.Motion = motion

	data.Form = &forms.EditMotionForm{
		Title:   motion.Title,
		Content: motion.Content,
		Type:    motion.Type,
	}

	m.TemplateCache.render(w, http.StatusOK, "edit_motion.html", data)
}

func (m *MotionHandler) Edit(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, m)
	if motion == nil {
		return
	}

	var motionForm forms.EditMotionForm

	err := m.decodePostForm(r, &motionForm)
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err := motionForm.Validate(); err != nil {
		panic(err)
	}

	if !motionForm.Valid() {
		data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
		data.Form = &motionForm

		m.TemplateCache.render(w, http.StatusUnprocessableEntity, "edit_motion.html", data)

		return
	}

	motionForm.Normalize()

	user, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	now := time.Now().UTC()
	dueDuration := time.Duration(motionForm.Due) * hoursInDay * time.Hour

	err = m.motions.Update(
		r.Context(),
		motion.ID,
		func(m *models.Motion) {
			m.Type = motionForm.Type
			m.Title = strings.TrimSpace(motionForm.Title)
			m.Content = strings.TrimSpace(motionForm.Content)
			m.Due = now.Add(dueDuration)
		},
	)
	if err != nil {
		panic(err)
	}

	decision, err := m.motions.ByID(r.Context(), motion.ID)
	if err != nil {
		panic(err)
	}

	m.mailNotifier.Notify(&notifications.UpdateDecisionNotification{
		Decision: decision,
		User:     user,
	})

	m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)

	m.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashInfo,
		Title:   "Motion modified",
		Message: fmt.Sprintf("The motion %s has been modified!", decision.Tag),
	})

	http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}

func (m *MotionHandler) WithdrawForm(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, m)
	if motion == nil {
		NotFound(w)

		return
	}

	data := m.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	data.Motion = motion

	m.TemplateCache.render(w, http.StatusOK, "withdraw_motion.html", data)
}

func (m *MotionHandler) Withdraw(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, m)
	if motion == nil {
		NotFound(w)

		return
	}

	user, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	err = m.motions.Withdraw(r.Context(), motion.ID)

	if err != nil {
		panic(err)
	}

	m.mailNotifier.Notify(&notifications.WithDrawMotionNotification{Motion: motion, Voter: user})

	m.jobScheduler.Reschedule(jobs.JobIDCloseDecisions, jobs.JobIDRemindVoters)

	m.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashWarning,
		Title:   "Motion withdrawn",
		Message: fmt.Sprintf("The motion %s has been withdrawn!", motion.Tag),
	})

	http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}

func NewMotionHandler(
	motions *models.MotionModel,
	mailNotifier *notifications.MailNotifier,
	jobScheduler *jobs.JobScheduler,
	params CommonParams,
) *MotionHandler {
	return &MotionHandler{
		motions:      motions,
		mailNotifier: mailNotifier,
		jobScheduler: jobScheduler,
		CommonParams: params,
	}
}

type VoteHandler struct {
	motions      *models.MotionModel
	users        *models.UserModel
	mailNotifier *notifications.MailNotifier
	CommonParams
}

func (v *VoteHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
	return newTemplateData(ctx, r, v.Flashes, topLevelNavMotions, subNav)
}

func (v *VoteHandler) UserModel() *models.UserModel {
	return v.users
}

func (v *VoteHandler) MotionModel() *models.MotionModel {
	return v.motions
}

func (v *VoteHandler) VoteForm(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, v)
	if motion == nil {
		NotFound(w)

		return
	}

	choice := choiceFromRequestParam(w, r)
	if choice == nil {
		NotFound(w)

		return
	}

	data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	data.Motion = motion

	data.Form = &forms.DirectVoteForm{
		Choice: choice,
	}

	v.TemplateCache.render(w, http.StatusOK, "direct_vote.html", data)
}

func (v *VoteHandler) Vote(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, v)
	if motion == nil {
		return
	}

	choice := choiceFromRequestParam(w, r)
	if choice == nil {
		return
	}

	user, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	clientCert, err := getPEMClientCert(r)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		_, _ = fmt.Fprint(w, http.StatusText(http.StatusInternalServerError))

		return
	}

	if err := v.motions.UpdateVote(r.Context(), user.ID, motion.ID, func(v *models.Vote) {
		v.Vote = choice
		v.Voted = time.Now().UTC()
		v.Notes = fmt.Sprintf("Direct Vote\n\n%s", clientCert)
	}); err != nil {
		panic(err)
	}

	v.mailNotifier.Notify(&notifications.DirectVoteNotification{
		Decision: motion, User: user, Choice: choice,
	})

	v.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "Vote registered",
		Message: fmt.Sprintf("Your vote for motion %s has been registered.", motion.Tag),
	})

	http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}

func (v *VoteHandler) ProxyVoteForm(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, v)
	if motion == nil {
		return
	}

	data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)

	data.Motion = motion

	potentialVoters, err := v.users.Voters(r.Context())
	if err != nil {
		panic(err)
	}

	data.Form = &forms.ProxyVoteForm{
		Voters: potentialVoters,
	}

	v.TemplateCache.render(w, http.StatusOK, "proxy_vote.html", data)
}

func (v *VoteHandler) ProxyVote(w http.ResponseWriter, r *http.Request) {
	motion := motionFromRequestParam(r.Context(), w, r, v)
	if motion == nil {
		return
	}

	var voteForm forms.ProxyVoteForm

	err := v.decodePostForm(r, &voteForm)
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err := voteForm.Validate(); err != nil {
		panic(err)
	}

	if !voteForm.Valid() {
		data := v.newTemplateData(r.Context(), r, subLevelNavMotionsAll)
		data.Motion = motion

		potentialVoters, err := v.users.Voters(r.Context())
		if err != nil {
			panic(err)
		}

		voteForm.Voters = potentialVoters
		data.Form = &voteForm

		v.TemplateCache.render(w, http.StatusUnprocessableEntity, "proxy_vote.html", data)

		return
	}

	voteForm.Normalize()

	user, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	clientCert, err := getPEMClientCert(r)
	if err != nil {
		panic(err)
	}

	voter := getVoter(r.Context(), w, v, voteForm.Voter.ID)
	if voter == nil {
		return
	}

	if err := v.motions.UpdateVote(r.Context(), voteForm.Voter.ID, motion.ID, func(v *models.Vote) {
		v.Vote = voteForm.Choice
		v.Voted = time.Now().UTC()
		v.Notes = fmt.Sprintf("Proxy-Vote by %s\n\n%s\n\n%s", user.Name, voteForm.Justification, clientCert)
	}); err != nil {
		panic(err)
	}

	v.mailNotifier.Notify(&notifications.ProxyVoteNotification{
		Decision: motion, User: user, Voter: voter, Choice: voteForm.Choice, Justification: voteForm.Justification,
	})

	v.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "Proxy vote registered",
		Message: fmt.Sprintf(
			"Your proxy vote for %s for motion %s has been registered.",
			voter.Name,
			motion.Tag,
		),
	})

	http.Redirect(w, r, "/motions/", http.StatusSeeOther)
}

func NewVoteHandler(
	motions *models.MotionModel,
	users *models.UserModel,
	mailNotifier *notifications.MailNotifier,
	params CommonParams,
) *VoteHandler {
	return &VoteHandler{motions: motions, users: users, mailNotifier: mailNotifier, CommonParams: params}
}

type UserHandler struct {
	users *models.UserModel
	CommonParams
}

func (u *UserHandler) UserModel() *models.UserModel {
	return u.users
}

func (u *UserHandler) newTemplateData(ctx context.Context, r *http.Request, subNav subLevelNavItem) *TemplateData {
	return newTemplateData(ctx, r, u.Flashes, topLevelNavUsers, subNav)
}

func (u *UserHandler) deleteEmailParams(
	ctx context.Context,
	w http.ResponseWriter,
	r *http.Request,
) (*models.User, string, error) {
	userToEdit := userFromRequestParam(ctx, w, r, u, u.users.WithEmailAddresses())
	if userToEdit == nil {
		return nil, "", nil
	}

	emailAddress, err := emailFromRequestParam(r, userToEdit)
	if err != nil {
		return nil, "", err
	}

	return userToEdit, emailAddress, nil
}

func (u *UserHandler) List(w http.ResponseWriter, r *http.Request) {
	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	users, err := u.users.List(
		r.Context(),
		u.users.WithRoles(),
		u.users.CanDelete(),
	)
	if err != nil {
		panic(err)
	}

	data.Users = users

	u.TemplateCache.render(w, http.StatusOK, "users.html", data)
}

func (u *UserHandler) CreateForm(w http.ResponseWriter, r *http.Request) {
	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	data.Form = &forms.NewUserForm{}

	u.TemplateCache.render(w, http.StatusOK, "create_user.html", data)
}

func (u *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
	admin, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	var userForm forms.NewUserForm

	if err := u.decodePostForm(r, &userForm); err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err := userForm.Validate(); err != nil {
		panic(err)
	}

	if !userForm.Valid() {
		data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

		data.Form = &userForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "create_user.html", data)

		return
	}

	userForm.Normalize()

	createUserParams := &models.CreateUserParams{
		Admin:     admin,
		Name:      userForm.Name,
		Reminder:  userForm.EmailAddress,
		Emails:    []string{userForm.EmailAddress},
		Reasoning: userForm.Reasoning,
	}

	userID, err := u.users.Create(r.Context(), createUserParams)
	if err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "User created",
		Message: fmt.Sprintf("Created new user %s", userForm.Name),
	})

	http.Redirect(w, r, fmt.Sprintf("/users/%d", userID), http.StatusSeeOther)
}

func (u *UserHandler) EditForm(w http.ResponseWriter, r *http.Request) {
	userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithRoles(), u.users.WithEmailAddresses())
	if userToEdit == nil {
		return
	}

	roles, err := userToEdit.Roles()
	if err != nil {
		panic(err)
	}

	emailAddresses, err := userToEdit.EmailAddresses()
	if err != nil {
		panic(err)
	}

	roleNames := make([]string, len(roles))
	for i := range roles {
		roleNames[i] = roles[i].Name
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	data.Form = &forms.EditUserForm{
		User:          userToEdit,
		Name:          userToEdit.Name,
		MailAddresses: emailAddresses,
		AllRoles:      models.AllRoles,
		ReminderMail:  userToEdit.Reminder.String,
		Roles:         roleNames,
	}

	u.TemplateCache.render(w, http.StatusOK, "edit_user.html", data)
}

func (u *UserHandler) Edit(w http.ResponseWriter, r *http.Request) {
	userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithRoles(), u.users.WithEmailAddresses())
	if userToEdit == nil {
		NotFound(w)

		return
	}

	var userForm forms.EditUserForm

	if err := u.decodePostForm(r, &userForm); err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	userForm.User = userToEdit
	userForm.AllRoles = models.AllRoles

	if err := userForm.Validate(); err != nil {
		panic(err)
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	if !userForm.Valid() {
		roles, err := userToEdit.Roles()
		if err != nil {
			panic(err)
		}

		emailAddresses, err := userToEdit.EmailAddresses()
		if err != nil {
			panic(err)
		}

		roleNames := make([]string, len(roles))
		for i := range roles {
			roleNames[i] = roles[i].Name
		}

		userForm.MailAddresses = emailAddresses
		userForm.Roles = roleNames

		data.Form = &userForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "edit_user.html", data)

		return
	}

	userForm.Normalize()
	userForm.UpdateUser(userToEdit)

	if err := u.users.EditUser(r.Context(), userToEdit, data.User, userForm.Roles, userForm.Reasoning); err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashInfo,
		Title:   "User modified",
		Message: fmt.Sprintf("User %s has been modified.", userToEdit.Name),
	})

	http.Redirect(w, r, "/users/", http.StatusSeeOther)
}

func (u *UserHandler) AddEmailForm(w http.ResponseWriter, r *http.Request) {
	userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithEmailAddresses())
	if userToEdit == nil {
		NotFound(w)

		return
	}

	emailAddresses, err := userToEdit.EmailAddresses()
	if err != nil {
		panic(err)
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	data.Form = &forms.AddEmailForm{
		User:           userToEdit,
		EmailAddresses: emailAddresses,
	}

	u.TemplateCache.render(w, http.StatusOK, "add_email.html", data)
}

func (u *UserHandler) AddEmail(w http.ResponseWriter, r *http.Request) {
	userToEdit := userFromRequestParam(r.Context(), w, r, u, u.users.WithEmailAddresses())
	if userToEdit == nil {
		NotFound(w)

		return
	}

	var emailForm forms.AddEmailForm

	if err := u.decodePostForm(r, &emailForm); err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	emailForm.User = userToEdit

	if err := emailForm.Validate(); err != nil {
		panic(err)
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	if emailForm.Valid() {
		emailExists, err := u.users.EmailExists(r.Context(), emailForm.EmailAddress)
		if err != nil {
			panic(err)
		}

		if emailExists {
			emailForm.FieldErrors = map[string]string{
				"email_address": "Email address must be unique",
			}
		}
	}

	if !emailForm.Valid() {
		emailAddresses, err := userToEdit.EmailAddresses()
		if err != nil {
			panic(err)
		}

		emailForm.EmailAddresses = emailAddresses

		data.Form = &emailForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "add_email.html", data)

		return
	}

	emailForm.Normalize()

	if err := u.users.AddEmail(
		r.Context(),
		userToEdit,
		data.User,
		emailForm.EmailAddress,
		emailForm.Reasoning,
	); err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "Email address added",
		Message: fmt.Sprintf(
			"Added email address %s for user %s",
			emailForm.EmailAddress,
			userToEdit.Name,
		),
	})

	http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
}

func (u *UserHandler) DeleteEmailForm(w http.ResponseWriter, r *http.Request) {
	userToEdit, emailAddress, err := u.deleteEmailParams(r.Context(), w, r)
	if err != nil {
		panic(err)
	}

	if userToEdit == nil || emailAddress == "" {
		NotFound(w)

		return
	}

	if userToEdit.Reminder.String == emailAddress {
		// delete of reminder address should not happen
		ClientError(w, http.StatusBadRequest)

		return
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	data.Form = &forms.DeleteEmailForm{
		User:         userToEdit,
		EmailAddress: emailAddress,
	}

	u.TemplateCache.render(w, http.StatusOK, "delete_email.html", data)
}

func (u *UserHandler) DeleteEmail(w http.ResponseWriter, r *http.Request) {
	userToEdit, emailAddress, err := u.deleteEmailParams(r.Context(), w, r)
	if err != nil {
		panic(err)
	}

	if userToEdit == nil || emailAddress == "" {
		NotFound(w)

		return
	}

	if userToEdit.Reminder.String == emailAddress {
		// delete of reminder address should not happen
		ClientError(w, http.StatusBadRequest)

		return
	}

	admin, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	var deleteEmailForm forms.DeleteEmailForm

	if err := u.decodePostForm(r, &deleteEmailForm); err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err := deleteEmailForm.Validate(); err != nil {
		panic(err)
	}

	if !deleteEmailForm.Valid() {
		deleteEmailForm.EmailAddress = emailAddress
		deleteEmailForm.User = userToEdit

		data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

		data.Form = &deleteEmailForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "delete_email.html", data)

		return
	}

	deleteEmailForm.Normalize()

	if err := u.users.DeleteEmail(r.Context(), userToEdit, admin, emailAddress, deleteEmailForm.Reasoning); err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashWarning,
		Title:   "Email address deleted",
		Message: fmt.Sprintf(
			"Deleted email address %s of user %s",
			deleteEmailForm.EmailAddress,
			userToEdit.Name,
		),
	})

	http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
}

func (u *UserHandler) DeleteForm(w http.ResponseWriter, r *http.Request) {
	userToDelete := userFromRequestParam(r.Context(), w, r, u, u.users.CanDelete())
	if userToDelete == nil {
		return
	}

	if !userToDelete.CanDelete() {
		NotFound(w)

		return
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	data.Form = &forms.DeleteUserForm{
		User: userToDelete,
	}

	u.TemplateCache.render(w, http.StatusOK, "delete_user.html", data)
}

func (u *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
	userToDelete := userFromRequestParam(r.Context(), w, r, u, u.users.CanDelete())
	if userToDelete == nil {
		return
	}

	if !userToDelete.CanDelete() {
		NotFound(w)

		return
	}

	var deleteUserForm forms.DeleteUserForm

	err := u.decodePostForm(r, &deleteUserForm)
	if err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	deleteUserForm.User = userToDelete

	if err := deleteUserForm.Validate(); err != nil {
		panic(err)
	}

	data := u.newTemplateData(r.Context(), r, subLevelNavUsers)

	if !deleteUserForm.Valid() {
		data.Form = &deleteUserForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "delete_user.html", data)

		return
	}

	deleteUserForm.Normalize()

	if err = u.users.DeleteUser(r.Context(), userToDelete, data.User, deleteUserForm.Reasoning); err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashWarning,
		Title:   "User deleted",
		Message: fmt.Sprintf("User %s has been deleted.", userToDelete.Name),
	})

	http.Redirect(w, r, "/users/", http.StatusSeeOther)
}

func (u *UserHandler) ChangeVotersForm(w http.ResponseWriter, r *http.Request) {
	data := u.newTemplateData(r.Context(), r, subLevelNavVoters)

	allUsers, err := u.users.List(r.Context(), u.users.WithRoles())
	if err != nil {
		panic(err)
	}

	voterIDs := make([]int64, 0)

	for _, user := range allUsers {
		isVoter, err := user.HasRole(models.RoleVoter)
		if err != nil {
			panic(err)
		}

		if isVoter {
			voterIDs = append(voterIDs, user.ID)

			continue
		}
	}

	data.Form = &forms.ChooseVoterForm{
		Users:    allUsers,
		VoterIDs: voterIDs,
	}

	u.TemplateCache.render(w, http.StatusOK, "choose_voters.html", data)
}

func (u *UserHandler) ChangeVoters(w http.ResponseWriter, r *http.Request) {
	admin, err := getUser(r)
	if err != nil {
		ClientError(w, http.StatusUnauthorized)

		return
	}

	var voterForm forms.ChooseVoterForm

	if err = u.decodePostForm(r, &voterForm); err != nil {
		ClientError(w, http.StatusBadRequest)

		return
	}

	if err = voterForm.Validate(); err != nil {
		panic(err)
	}

	if !voterForm.Valid() {
		data := u.newTemplateData(r.Context(), r, subLevelNavVoters)

		allUsers, err := u.users.List(r.Context(), u.users.WithRoles())
		if err != nil {
			panic(err)
		}

		voterForm.Users = allUsers

		data.Form = &voterForm

		u.TemplateCache.render(w, http.StatusUnprocessableEntity, "choose_voters.html", data)

		return
	}

	voterForm.Normalize()

	if err = u.users.ChooseVoters(r.Context(), admin, voterForm.VoterIDs, voterForm.Reasoning); err != nil {
		panic(err)
	}

	u.Flashes.addFlash(r.Context(), &FlashMessage{
		Variant: flashSuccess,
		Title:   "Voters selected",
		Message: "A new list of voters has been selected",
	})

	http.Redirect(w, r, "/users/", http.StatusSeeOther)
}

func NewUserHandler(
	users *models.UserModel,
	params CommonParams,
) *UserHandler {
	return &UserHandler{users: users, CommonParams: params}
}

type HealthCheck struct {
	mailNotifier *notifications.MailNotifier
	motions      *models.MotionModel
}

func NewHealthCheck(notifier *notifications.MailNotifier, motions *models.MotionModel) *HealthCheck {
	return &HealthCheck{mailNotifier: notifier, motions: motions}
}

func (h *HealthCheck) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
	const (
		ok     = "OK"
		failed = "FAILED"
	)

	response := struct {
		DB   string `json:"database"`
		Mail string `json:"mail"`
	}{DB: ok, Mail: ok}

	enc := json.NewEncoder(w)

	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Refresh", "10")
	w.Header().Add("Cache-Control", "no-store")

	var (
		err       error
		hasErrors = false
	)

	if err = h.mailNotifier.Ping(); err != nil {
		hasErrors = true

		response.Mail = failed
	}

	if err = h.motions.DB.Ping(); err != nil {
		hasErrors = true

		response.DB = failed
	}

	if hasErrors {
		w.WriteHeader(http.StatusInternalServerError)
	} else {
		w.WriteHeader(http.StatusOK)
	}

	_ = enc.Encode(response)
}
