/*
   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"
	"fmt"
	"git.cacert.org/cacert-boardvoting/boardvoting"
	"github.com/Masterminds/sprig"
	"gopkg.in/gomail.v2"
	"text/template"

	log "github.com/sirupsen/logrus"
)

type headerData struct {
	name  string
	value []string
}

type headerList []headerData

type recipientData struct {
	field, address, name string
}

type notificationContent struct {
	template   string
	data       interface{}
	subject    string
	headers    headerList
	recipients []recipientData
}

type NotificationMail interface {
	GetNotificationContent() *notificationContent
}

var NotifyMailChannel = make(chan NotificationMail, 1)

func MailNotifier(quitMailNotifier chan int) {
	log.Info("Launched mail notifier")
	for {
		select {
		case notification := <-NotifyMailChannel:
			content := notification.GetNotificationContent()
			mailText, err := buildMail(content.template, content.data)
			if err != nil {
				log.Errorf("building mail failed: %v", err)
				continue
			}

			m := gomail.NewMessage()
			m.SetAddressHeader("From", config.NotificationSenderAddress, "CAcert board voting system")
			for _, recipient := range content.recipients {
				m.SetAddressHeader(recipient.field, recipient.address, recipient.name)
			}
			m.SetHeader("Subject", content.subject)
			for _, header := range content.headers {
				m.SetHeader(header.name, header.value...)
			}
			m.SetBody("text/plain", mailText.String())

			d := gomail.NewDialer(config.MailServer.Host, config.MailServer.Port, "", "")
			if err := d.DialAndSend(m); err != nil {
				log.Errorf("sending mail failed: %v", err)
			}
		case <-quitMailNotifier:
			log.Info("Ending mail notifier")
			return
		}
	}
}

func buildMail(templateName string, context interface{}) (mailText *bytes.Buffer, err error) {
	b, err := boardvoting.Asset(fmt.Sprintf("templates/%s", templateName))
	if err != nil {
		return
	}
	t, err := template.New(templateName).Funcs(sprig.GenericFuncMap()).Parse(string(b))
	if err != nil {
		return
	}

	mailText = bytes.NewBufferString("")
	if err := t.Execute(mailText, context); err != nil {
		log.Errorf("Failed to execute template %s with context %+v: %v", templateName, context, err)
		return nil, err
	}

	return
}

type notificationBase struct{}

func (n *notificationBase) getRecipient() recipientData {
	return recipientData{field: "To", address: config.NoticeMailAddress, name: "CAcert board mailing list"}
}

type decisionReplyBase struct {
	decision Decision
}

func (n *decisionReplyBase) getHeaders() headerList {
	headers := make(headerList, 0)
	headers = append(headers, headerData{
		name: "References", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
	})
	headers = append(headers, headerData{
		name: "In-Reply-To", value: []string{fmt.Sprintf("<%s>", n.decision.Tag)},
	})
	return headers
}

func (n *decisionReplyBase) getSubject() string {
	return fmt.Sprintf("Re: %s - %s", n.decision.Tag, n.decision.Title)
}

type notificationClosedDecision struct {
	notificationBase
	decisionReplyBase
	voteSums  VoteSums
	reasoning string
}

func NewNotificationClosedDecision(decision *Decision, voteSums *VoteSums, reasoning string) NotificationMail {
	notification := &notificationClosedDecision{voteSums: *voteSums, reasoning: reasoning}
	notification.decision = *decision
	return notification
}

func (n *notificationClosedDecision) GetNotificationContent() *notificationContent {
	return &notificationContent{
		template: "closed_motion_mail.txt",
		data: struct {
			*Decision
			*VoteSums
			Reasoning string
		}{&n.decision, &n.voteSums, n.reasoning},
		subject:    fmt.Sprintf("Re: %s - %s - finalised", n.decision.Tag, n.decision.Title),
		headers:    n.decisionReplyBase.getHeaders(),
		recipients: []recipientData{n.notificationBase.getRecipient()},
	}
}

type NotificationCreateMotion struct {
	notificationBase
	decision Decision
	voter    Voter
}

func (n *NotificationCreateMotion) GetNotificationContent() *notificationContent {
	voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
	unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
	return &notificationContent{
		template: "create_motion_mail.txt",
		data: struct {
			*Decision
			Name       string
			VoteURL    string
			UnvotedURL string
		}{&n.decision, n.voter.Name, voteURL, unvotedURL},
		subject:    fmt.Sprintf("%s - %s", n.decision.Tag, n.decision.Title),
		headers:    headerList{headerData{"Message-ID", []string{fmt.Sprintf("<%s>", n.decision.Tag)}}},
		recipients: []recipientData{n.notificationBase.getRecipient()},
	}
}

type notificationUpdateMotion struct {
	notificationBase
	decisionReplyBase
	voter Voter
}

func NewNotificationUpdateMotion(decision Decision, voter Voter) NotificationMail {
	notification := notificationUpdateMotion{voter: voter}
	notification.decision = decision
	return &notification
}

func (n *notificationUpdateMotion) GetNotificationContent() *notificationContent {
	voteURL := fmt.Sprintf("%s/vote/%s", config.BaseURL, n.decision.Tag)
	unvotedURL := fmt.Sprintf("%s/motions/?unvoted=1", config.BaseURL)
	return &notificationContent{
		template: "update_motion_mail.txt",
		data: struct {
			*Decision
			Name       string
			VoteURL    string
			UnvotedURL string
		}{&n.decision, n.voter.Name, voteURL, unvotedURL},
		subject:    n.decisionReplyBase.getSubject(),
		headers:    n.decisionReplyBase.getHeaders(),
		recipients: []recipientData{n.notificationBase.getRecipient()},
	}
}

type notificationWithDrawMotion struct {
	notificationBase
	decisionReplyBase
	voter Voter
}

func NewNotificationWithDrawMotion(decision *Decision, voter *Voter) NotificationMail {
	notification := &notificationWithDrawMotion{voter: *voter}
	notification.decision = *decision
	return notification
}

func (n *notificationWithDrawMotion) GetNotificationContent() *notificationContent {
	return &notificationContent{
		template: "withdraw_motion_mail.txt",
		data: struct {
			*Decision
			Name string
		}{&n.decision, n.voter.Name},
		subject:    fmt.Sprintf("Re: %s - %s - withdrawn", n.decision.Tag, n.decision.Title),
		headers:    n.decisionReplyBase.getHeaders(),
		recipients: []recipientData{n.notificationBase.getRecipient()},
	}
}

type RemindVoterNotification struct {
	voter     Voter
	decisions []Decision
}

func (n *RemindVoterNotification) GetNotificationContent() *notificationContent {
	return &notificationContent{
		template: "remind_voter_mail.txt",
		data: struct {
			Decisions []Decision
			Name      string
			BaseURL   string
		}{n.decisions, n.voter.Name, config.BaseURL},
		subject:    "Outstanding CAcert board votes",
		recipients: []recipientData{{"To", n.voter.Reminder, n.voter.Name}},
	}
}

type voteNotificationBase struct{}

func (n *voteNotificationBase) getRecipient() recipientData {
	return recipientData{"To", config.VoteNoticeMailAddress, "CAcert board votes mailing list"}
}

type notificationProxyVote struct {
	voteNotificationBase
	decisionReplyBase
	proxy         Voter
	voter         Voter
	vote          Vote
	justification string
}

func NewNotificationProxyVote(decision *Decision, proxy *Voter, voter *Voter, vote *Vote, justification string) NotificationMail {
	notification := &notificationProxyVote{proxy: *proxy, voter: *voter, vote: *vote, justification: justification}
	notification.decision = *decision
	return notification
}

func (n *notificationProxyVote) GetNotificationContent() *notificationContent {
	return &notificationContent{
		template: "proxy_vote_mail.txt",
		data: struct {
			Proxy         string
			Vote          VoteChoice
			Voter         string
			Decision      *Decision
			Justification string
		}{n.proxy.Name, n.vote.Vote, n.voter.Name, &n.decision, n.justification},
		subject:    n.decisionReplyBase.getSubject(),
		headers:    n.decisionReplyBase.getHeaders(),
		recipients: []recipientData{n.voteNotificationBase.getRecipient()},
	}
}

type notificationDirectVote struct {
	voteNotificationBase
	decisionReplyBase
	voter Voter
	vote  Vote
}

func NewNotificationDirectVote(decision *Decision, voter *Voter, vote *Vote) NotificationMail {
	notification := &notificationDirectVote{voter: *voter, vote: *vote}
	notification.decision = *decision
	return notification
}

func (n *notificationDirectVote) GetNotificationContent() *notificationContent {
	return &notificationContent{
		template: "direct_vote_mail.txt",
		data: struct {
			Vote     VoteChoice
			Voter    string
			Decision *Decision
		}{n.vote.Vote, n.voter.Name, &n.decision},
		subject:    n.decisionReplyBase.getSubject(),
		headers:    n.decisionReplyBase.getHeaders(),
		recipients: []recipientData{n.voteNotificationBase.getRecipient()},
	}
}
