/*
   Copyright 2017-2019 Jan Dittberner

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this program 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 main

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"database/sql"
	"encoding/base64"
	"encoding/pem"
	"flag"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/Masterminds/sprig"
	"github.com/gorilla/csrf"
	"github.com/gorilla/sessions"
	_ "github.com/mattn/go-sqlite3"
	log "github.com/sirupsen/logrus"
	"gopkg.in/yaml.v2"

	"git.cacert.org/cacert-boardvoting/boardvoting"
)

var configFile string
var config *Config
var store *sessions.CookieStore
var csrfKey []byte
var version = "undefined"
var build = "undefined"

const sessionCookieName = "votesession"

func renderTemplate(w http.ResponseWriter, r *http.Request, templates []string, context interface{}) {
	funcMaps := sprig.FuncMap()
	funcMaps["nl2br"] = func(text string) template.HTML {
		return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "<br>", -1))
	}
	funcMaps[csrf.TemplateTag] = func() template.HTML {
		return csrf.TemplateField(r)
	}

	var baseTemplate *template.Template

	for count, t := range templates {
		if assetBytes, err := boardvoting.Asset(fmt.Sprintf("templates/%s", t)); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		} else {
			if count == 0 {
				if baseTemplate, err = template.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
				}
			} else {
				if _, err := baseTemplate.New(t).Funcs(funcMaps).Parse(string(assetBytes)); err != nil {
					http.Error(w, err.Error(), http.StatusInternalServerError)
				}
			}
		}
	}

	if err := baseTemplate.Execute(w, context); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

type contextKey int

const (
	ctxNeedsAuth contextKey = iota
	ctxVoter
	ctxDecision
	ctxVote
	ctxAuthenticatedCert
)

func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) {
	emailsTried := make(map[string]bool)
	for _, cert := range r.TLS.PeerCertificates {
		for _, extKeyUsage := range cert.ExtKeyUsage {
			if extKeyUsage == x509.ExtKeyUsageClientAuth {
				for _, emailAddress := range cert.EmailAddresses {
					emailLower := strings.ToLower(emailAddress)
					emailsTried[emailLower] = true
					voter, err := FindVoterByAddress(emailLower)
					if err != nil {
						http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
						return
					}
					if voter != nil {
						requestContext := context.WithValue(r.Context(), ctxVoter, voter)
						requestContext = context.WithValue(requestContext, ctxAuthenticatedCert, cert)
						handler(w, r.WithContext(requestContext))
						return
					}
				}
			}
		}
	}
	needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool)
	if ok && needsAuth {
		var templateContext struct {
			PageTitle string
			Voter     *Voter
			Flashes   interface{}
			Emails    []string
		}
		for k := range emailsTried {
			templateContext.Emails = append(templateContext.Emails, k)
		}
		sort.Strings(templateContext.Emails)
		w.WriteHeader(http.StatusForbidden)
		renderTemplate(w, r, []string{"denied.html", "header.html", "footer.html"}, templateContext)
		return
	}
	handler(w, r)
}

type motionParameters struct {
	ShowVotes bool
}

type motionListParameters struct {
	Page  int64
	Flags struct {
		Confirmed, Withdraw, Unvoted bool
	}
}

func parseMotionParameters(r *http.Request) motionParameters {
	var m = motionParameters{}
	m.ShowVotes, _ = strconv.ParseBool(r.URL.Query().Get("showvotes"))
	return m
}

func parseMotionListParameters(r *http.Request) motionListParameters {
	var m = motionListParameters{}
	if page, err := strconv.ParseInt(r.URL.Query().Get("page"), 10, 0); err != nil {
		m.Page = 1
	} else {
		m.Page = page
	}
	m.Flags.Withdraw, _ = strconv.ParseBool(r.URL.Query().Get("withdraw"))
	m.Flags.Unvoted, _ = strconv.ParseBool(r.URL.Query().Get("unvoted"))

	if r.Method == http.MethodPost {
		m.Flags.Confirmed, _ = strconv.ParseBool(r.PostFormValue("confirm"))
	}
	return m
}

func motionListHandler(w http.ResponseWriter, r *http.Request) {
	params := parseMotionListParameters(r)
	session, err := store.Get(r, sessionCookieName)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	var templateContext struct {
		Decisions          []*DecisionForDisplay
		Voter              *Voter
		Params             *motionListParameters
		PrevPage, NextPage int64
		PageTitle          string
		Flashes            interface{}
	}
	if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok {
		templateContext.Voter = voter
	}
	if flashes := session.Flashes(); len(flashes) > 0 {
		templateContext.Flashes = flashes
	}
	err = session.Save(r, w)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	templateContext.Params = &params

	if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page, params.Flags.Unvoted, templateContext.Voter); err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}

	if len(templateContext.Decisions) > 0 {
		olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists(params.Flags.Unvoted, templateContext.Voter)
		if err != nil {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
		if olderExists {
			templateContext.NextPage = params.Page + 1
		}
	}

	if params.Page > 1 {
		templateContext.PrevPage = params.Page - 1
	}

	renderTemplate(w, r, []string{
		"motions.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html",
	}, templateContext)
}

func motionHandler(w http.ResponseWriter, r *http.Request) {
	params := parseMotionParameters(r)

	decision, ok := getDecisionFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
		return
	}

	var templateContext struct {
		Decision           *DecisionForDisplay
		Voter              *Voter
		Params             *motionParameters
		PrevPage, NextPage int64
		PageTitle          string
		Flashes            interface{}
	}
	voter, ok := getVoterFromRequest(r)
	if ok {
		templateContext.Voter = voter
	}
	templateContext.Params = &params
	if params.ShowVotes {
		if err := decision.LoadVotes(); err != nil {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}
	}
	templateContext.Decision = decision
	templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title)
	renderTemplate(w, r, []string{"motion.html", "motion_fragments.html", "page_fragments.html", "header.html", "footer.html"}, templateContext)
}

func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) {
	decision, err := FindDecisionForDisplayByTag(tag)
	if err != nil {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	if decision == nil {
		http.NotFound(w, r)
		return
	}
	handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision)))
}

type motionActionHandler interface {
	Handle(w http.ResponseWriter, r *http.Request)
	NeedsAuth() bool
}

type authenticationRequiredHandler struct{}

func (authenticationRequiredHandler) NeedsAuth() bool {
	return true
}

func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) {
	voter, ok = r.Context().Value(ctxVoter).(*Voter)
	return
}

func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) {
	decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay)
	return
}

func getVoteFromRequest(r *http.Request) (vote VoteChoice, ok bool) {
	vote, ok = r.Context().Value(ctxVote).(VoteChoice)
	return
}

type FlashMessageAction struct{}

func (a *FlashMessageAction) AddFlash(w http.ResponseWriter, r *http.Request, message interface{}, tags ...string) {
	session, err := store.Get(r, sessionCookieName)
	if err != nil {
		log.Warnf("could not get session cookie: %v", err)
		return
	}
	session.AddFlash(message, tags...)
	err = session.Save(r, w)
	if err != nil {
		log.Warnf("could not save flash message: %v", err)
		return
	}
}

type withDrawMotionAction struct {
	FlashMessageAction
	authenticationRequiredHandler
}

func (a *withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
	decision, ok := getDecisionFromRequest(r)
	if !ok || decision.Status != voteStatusPending {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
		return
	}
	voter, ok := getVoterFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
		return
	}
	templates := []string{"withdraw_motion_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
	var templateContext struct {
		PageTitle string
		Decision  *DecisionForDisplay
		Flashes   interface{}
		Voter     *Voter
	}

	switch r.Method {
	case http.MethodPost:
		decision.Status = voteStatusWithdrawn
		decision.Modified = time.Now().UTC()
		if err := decision.UpdateStatus(); err != nil {
			log.Errorf("withdrawing motion failed: %v", err)
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		}

		NotifyMailChannel <- NewNotificationWithDrawMotion(&(decision.Decision), voter)

		a.AddFlash(w, r, fmt.Sprintf("Motion %s has been withdrawn!", decision.Tag))

		http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect)
	default:
		templateContext.Decision = decision
		templateContext.Voter = voter
		renderTemplate(w, r, templates, templateContext)
	}
}

type newMotionHandler struct {
	FlashMessageAction
}

func (h *newMotionHandler) Handle(w http.ResponseWriter, r *http.Request) {
	voter, ok := getVoterFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
	}

	templates := []string{"create_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
	var templateContext struct {
		Form      NewDecisionForm
		PageTitle string
		Voter     *Voter
		Flashes   interface{}
	}
	switch r.Method {
	case http.MethodPost:
		form := NewDecisionForm{
			Title:    r.FormValue("Title"),
			Content:  r.FormValue("Content"),
			VoteType: r.FormValue("VoteType"),
			Due:      r.FormValue("Due"),
		}

		if valid, data := form.Validate(); !valid {
			templateContext.Voter = voter
			templateContext.Form = form
			renderTemplate(w, r, templates, templateContext)
		} else {
			data.Proposed = time.Now().UTC()
			data.ProponentId = voter.Id
			if err := data.Create(); err != nil {
				log.Errorf("saving motion failed: %v", err)
				http.Error(w, "Saving motion failed", http.StatusInternalServerError)
				return
			}

			NotifyMailChannel <- &NotificationCreateMotion{decision: *data, voter: *voter}

			h.AddFlash(w, r, "The motion has been proposed!")

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

		return
	default:
		templateContext.Voter = voter
		templateContext.Form = NewDecisionForm{
			VoteType: strconv.FormatInt(voteTypeMotion, 10),
		}
		renderTemplate(w, r, templates, templateContext)
	}
}

type editMotionAction struct {
	FlashMessageAction
	authenticationRequiredHandler
}

func (a editMotionAction) Handle(w http.ResponseWriter, r *http.Request) {
	decision, ok := getDecisionFromRequest(r)
	if !ok || decision.Status != voteStatusPending {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
		return
	}
	voter, ok := getVoterFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed)
		return
	}
	templates := []string{"edit_motion_form.html", "page_fragments.html", "header.html", "footer.html"}
	var templateContext struct {
		Form      EditDecisionForm
		PageTitle string
		Voter     *Voter
		Flashes   interface{}
	}
	switch r.Method {
	case http.MethodPost:
		form := EditDecisionForm{
			Title:    r.FormValue("Title"),
			Content:  r.FormValue("Content"),
			VoteType: r.FormValue("VoteType"),
			Due:      r.FormValue("Due"),
			Decision: &decision.Decision,
		}

		if valid, data := form.Validate(); !valid {
			templateContext.Voter = voter
			templateContext.Form = form
			renderTemplate(w, r, templates, templateContext)
		} else {
			data.Modified = time.Now().UTC()
			if err := data.Update(); err != nil {
				log.Errorf("updating motion failed: %v", err)
				http.Error(w, "Updating the motion failed.", http.StatusInternalServerError)
				return
			}

			NotifyMailChannel <- NewNotificationUpdateMotion(*data, *voter)

			a.AddFlash(w, r, "The motion has been modified!")

			http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
		}
		return
	default:
		templateContext.Voter = voter
		templateContext.Form = EditDecisionForm{
			Title:    decision.Title,
			Content:  decision.Content,
			VoteType: fmt.Sprintf("%d", decision.VoteType),
			Decision: &decision.Decision,
		}
		renderTemplate(w, r, templates, templateContext)
	}
}

type motionsHandler struct{}

func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	subURL := r.URL.Path

	var motionActionMap = map[string]motionActionHandler{
		"withdraw": &withDrawMotionAction{},
		"edit":     &editMotionAction{},
	}

	switch {
	case subURL == "":
		authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler)
		return
	case subURL == "/newmotion/":
		handler := &newMotionHandler{}
		authenticateRequest(
			w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
			handler.Handle)
		return
	case strings.Count(subURL, "/") == 1:
		parts := strings.Split(subURL, "/")
		motionTag := parts[0]
		action, ok := motionActionMap[parts[1]]
		if !ok {
			http.NotFound(w, r)
			return
		}
		authenticateRequest(
			w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())),
			func(w http.ResponseWriter, r *http.Request) {
				singleDecisionHandler(w, r, motionTag, action.Handle)
			})
		return
	case strings.Count(subURL, "/") == 0:
		authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)),
			func(w http.ResponseWriter, r *http.Request) {
				singleDecisionHandler(w, r, subURL, motionHandler)
			})
		return
	default:
		http.NotFound(w, r)
		return
	}
}

type directVoteHandler struct {
	FlashMessageAction
	authenticationRequiredHandler
}

func (h *directVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
	decision, ok := getDecisionFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	voter, ok := getVoterFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	vote, ok := getVoteFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	switch r.Method {
	case http.MethodPost:
		voteResult := &Vote{
			VoterId: voter.Id, Vote: vote, DecisionId: decision.Id, Voted: time.Now().UTC(),
			Notes: fmt.Sprintf("Direct Vote\n\n%s", getPEMClientCert(r))}
		if err := voteResult.Save(); err != nil {
			log.Errorf("Problem saving vote: %v", err)
			http.Error(w, "Problem saving vote", http.StatusInternalServerError)
			return
		}

		NotifyMailChannel <- NewNotificationDirectVote(&decision.Decision, voter, voteResult)

		h.AddFlash(w, r, "Your vote has been registered.")

		http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
	default:
		templates := []string{"direct_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
		var templateContext struct {
			Decision   *DecisionForDisplay
			VoteChoice VoteChoice
			PageTitle  string
			Flashes    interface{}
			Voter      *Voter
		}
		templateContext.Decision = decision
		templateContext.VoteChoice = vote
		templateContext.Voter = voter
		renderTemplate(w, r, templates, templateContext)
	}
}

type proxyVoteHandler struct {
	FlashMessageAction
	authenticationRequiredHandler
}

func getPEMClientCert(r *http.Request) string {
	clientCertPEM := bytes.NewBufferString("")
	authenticatedCertificate := r.Context().Value(ctxAuthenticatedCert).(*x509.Certificate)
	err := pem.Encode(clientCertPEM, &pem.Block{Type: "CERTIFICATE", Bytes: authenticatedCertificate.Raw})
	if err != nil {
		log.Errorf("error encoding client certificate: %v", err)
	}
	return clientCertPEM.String()
}

func (h *proxyVoteHandler) Handle(w http.ResponseWriter, r *http.Request) {
	decision, ok := getDecisionFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	proxy, ok := getVoterFromRequest(r)
	if !ok {
		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
		return
	}
	templates := []string{"proxy_vote_form.html", "header.html", "footer.html", "motion_fragments.html", "page_fragments.html"}
	var templateContext struct {
		Form      ProxyVoteForm
		Decision  *DecisionForDisplay
		Voters    *[]Voter
		PageTitle string
		Flashes   interface{}
		Voter     *Voter
	}
	templateContext.Voter = proxy
	switch r.Method {
	case http.MethodPost:
		form := ProxyVoteForm{
			Voter:         r.FormValue("Voter"),
			Vote:          r.FormValue("Vote"),
			Justification: r.FormValue("Justification"),
		}

		if valid, voter, data, justification := form.Validate(); !valid {
			templateContext.Form = form
			templateContext.Decision = decision
			if voters, err := GetVotersForProxy(proxy); err != nil {
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				return
			} else {
				templateContext.Voters = voters
			}
			renderTemplate(w, r, templates, templateContext)
		} else {
			data.DecisionId = decision.Id
			data.Voted = time.Now().UTC()
			data.Notes = fmt.Sprintf(
				"Proxy-Vote by %s\n\n%s\n\n%s",
				proxy.Name, justification, getPEMClientCert(r))

			if err := data.Save(); err != nil {
				log.Errorf("Error saving vote: %s", err)
				http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
				return
			}

			NotifyMailChannel <- NewNotificationProxyVote(&decision.Decision, proxy, voter, data, justification)

			h.AddFlash(w, r, "The vote has been registered.")

			http.Redirect(w, r, "/motions/", http.StatusMovedPermanently)
		}
		return
	default:
		templateContext.Form = ProxyVoteForm{}
		templateContext.Decision = decision
		if voters, err := GetVotersForProxy(proxy); err != nil {
			http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
			return
		} else {
			templateContext.Voters = voters
		}
		renderTemplate(w, r, templates, templateContext)
	}
}

type decisionVoteHandler struct{}

func (h *decisionVoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch {
	case strings.HasPrefix(r.URL.Path, "/proxy/"):
		motionTag := r.URL.Path[len("/proxy/"):]
		handler := &proxyVoteHandler{}
		authenticateRequest(
			w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
			func(w http.ResponseWriter, r *http.Request) {
				singleDecisionHandler(w, r, motionTag, handler.Handle)
			})
	case strings.HasPrefix(r.URL.Path, "/vote/"):
		parts := strings.Split(r.URL.Path[len("/vote/"):], "/")
		if len(parts) != 2 {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		motionTag := parts[0]
		voteValue, ok := VoteValues[parts[1]]
		if !ok {
			http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
			return
		}
		handler := &directVoteHandler{}
		authenticateRequest(
			w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)),
			func(w http.ResponseWriter, r *http.Request) {
				singleDecisionHandler(
					w, r.WithContext(context.WithValue(r.Context(), ctxVote, voteValue)),
					motionTag, handler.Handle)
			})
		return
	}
}

type Config struct {
	NoticeMailAddress         string `yaml:"notice_mail_address"`
	VoteNoticeMailAddress     string `yaml:"vote_notice_mail_address"`
	NotificationSenderAddress string `yaml:"notification_sender_address"`
	DatabaseFile              string `yaml:"database_file"`
	ClientCACertificates      string `yaml:"client_ca_certificates"`
	ServerCert                string `yaml:"server_certificate"`
	ServerKey                 string `yaml:"server_key"`
	CookieSecret              string `yaml:"cookie_secret"`
	CsrfKey                   string `yaml:"csrf_key"`
	BaseURL                   string `yaml:"base_url"`
	HttpAddress               string `yaml:"http_address"`
	HttpsAddress              string `yaml:"https_address"`
	MailServer                struct {
		Host string `yaml:"host"`
		Port int    `yaml:"port"`
	} `yaml:"mail_server"`
}

func readConfig() {
	source, err := ioutil.ReadFile(configFile)
	if err != nil {
		log.Panicf("Opening configuration file failed: %v", err)
	}
	if err := yaml.Unmarshal(source, &config); err != nil {
		log.Panicf("Loading configuration failed: %v", err)
	}

	if config.HttpsAddress == "" {
		config.HttpsAddress = "127.0.0.1:8443"
	}
	if config.HttpAddress == "" {
		config.HttpAddress = "127.0.0.1:8080"
	}

	cookieSecret, err := base64.StdEncoding.DecodeString(config.CookieSecret)
	if err != nil {
		log.Panicf("Decoding cookie secret failed: %v", err)
		panic(err)
	}
	if len(cookieSecret) < 32 {
		log.Panic("Cookie secret is less than 32 bytes long")
	}
	csrfKey, err = base64.StdEncoding.DecodeString(config.CsrfKey)
	if err != nil {
		log.Panicf("Decoding csrf key failed: %v", err)
	}
	if len(csrfKey) != 32 {
		log.Panicf("CSRF key must be exactly 32 bytes long but is %d bytes long", len(csrfKey))
	}
	store = sessions.NewCookieStore(cookieSecret)
	log.Info("Read configuration")
}

func setupDbConfig(ctx context.Context) {
	database, err := sql.Open("sqlite3", config.DatabaseFile)
	if err != nil {
		log.Panicf("Opening database failed: %v", err)
	}
	db = NewDB(database)

	go func() {
		for range ctx.Done() {
			if err := db.Close(); err != nil {
				_, _ = fmt.Fprintf(os.Stderr, "Problem closing the database: %v", err)
			}
		}
	}()

	log.Infof("opened database connection")
}

func setupNotifications(ctx context.Context) {
	quitMailChannel := make(chan int)
	go MailNotifier(quitMailChannel)

	go func() {
		for range ctx.Done() {
			quitMailChannel <- 1
		}
	}()
}

func setupJobs(ctx context.Context) {
	quitChannel := make(chan int)
	go JobScheduler(quitChannel)

	go func() {
		for range ctx.Done() {
			quitChannel <- 1
		}
	}()
}

func setupHandlers() {
	http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{}))
	http.Handle("/newmotion/", motionsHandler{})
	http.Handle("/proxy/", &decisionVoteHandler{})
	http.Handle("/vote/", &decisionVoteHandler{})
	http.Handle("/static/", http.FileServer(boardvoting.GetAssetFS()))
	http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently))
}

func setupTLSConfig() (tlsConfig *tls.Config) {
	// load CA certificates for client authentication
	caCert, err := ioutil.ReadFile(config.ClientCACertificates)
	if err != nil {
		log.Panicf("Error reading client certificate CAs %v", err)
	}
	caCertPool := x509.NewCertPool()
	if !caCertPool.AppendCertsFromPEM(caCert) {
		log.Panic("could not initialize client CA certificate pool")
	}

	// setup HTTPS server
	tlsConfig = &tls.Config{
		ClientCAs:  caCertPool,
		ClientAuth: tls.VerifyClientCertIfGiven,
	}
	tlsConfig.BuildNameToCertificate()
	return
}

func main() {
	log.SetFormatter(&log.TextFormatter{FullTimestamp: true})
	log.Infof("CAcert Board Voting version %s, build %s", version, build)

	flag.StringVar(
		&configFile, "config", "config.yaml", "Configuration file name")

	flag.Parse()

	var stopAll func()
	executionContext, stopAll := context.WithCancel(context.Background())
	readConfig()
	setupDbConfig(executionContext)
	setupNotifications(executionContext)
	setupJobs(executionContext)
	setupHandlers()
	tlsConfig := setupTLSConfig()

	defer stopAll()

	server := &http.Server{
		Addr:      config.HttpsAddress,
		TLSConfig: tlsConfig,
	}

	server.Handler = csrf.Protect(csrfKey)(http.DefaultServeMux)

	log.Infof("Launching application on https://%s/", server.Addr)

	errs := make(chan error, 1)
	go func() {
		if err := http.ListenAndServe(config.HttpsAddress, http.RedirectHandler(config.BaseURL, http.StatusMovedPermanently)); err != nil {
			errs <- err
		}
		close(errs)
	}()

	if err := server.ListenAndServeTLS(config.ServerCert, config.ServerKey); err != nil {
		log.Panicf("ListenAndServerTLS failed: %v", err)
	}
	if err := <-errs; err != nil {
		log.Panicf("ListenAndServe failed: %v", err)
	}
}
