/*
Copyright 2017-2021 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 (
	"database/sql"
	"embed"
	"errors"
	"fmt"
	"time"

	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database/sqlite3"
	"github.com/jmoiron/sqlx"
	"github.com/johejo/golang-migrate-extra/source/iofs"

	log "github.com/sirupsen/logrus"
)

type sqlKey int

const (
	sqlLoadDecisions sqlKey = iota
	sqlLoadUnVotedDecisions
	sqlLoadDecisionByTag
	sqlLoadDecisionByID
	sqlLoadVoteCountsForDecision
	sqlLoadVotesForDecision
	sqlLoadEnabledVoterByEmail
	sqlCountOlderThanDecision
	sqlCountOlderThanUnVotedDecision
	sqlCreateDecision
	sqlUpdateDecision
	sqlUpdateDecisionStatus
	sqlSelectClosableDecisions
	sqlGetNextPendingDecisionDue
	sqlGetReminderVoters
	sqlFindUnVotedDecisionsForVoter
	sqlGetEnabledVoterByID
	sqlCreateVote
	sqlLoadVote
	sqlGetVotersForProxy
)

var sqlStatements = map[sqlKey]string{
	sqlLoadDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
       decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM   decisions
JOIN   voters ON decisions.proponent=voters.id
ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $1`,
	sqlLoadUnVotedDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
       decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM   decisions
JOIN   voters ON decisions.proponent=voters.id
WHERE  decisions.status = 0 AND decisions.id NOT IN (SELECT votes.decision FROM votes WHERE votes.voter = $1)
ORDER BY proposed DESC
LIMIT 10 OFFSET 10 * $2;`,
	sqlLoadDecisionByTag: `
SELECT decisions.id, decisions.tag, decisions.proponent, voters.name AS proposer, decisions.proposed, decisions.title,
       decisions.content, decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM   decisions
JOIN   voters ON decisions.proponent=voters.id
WHERE  decisions.tag=$1;`,
	sqlLoadDecisionByID: `
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
       decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM   decisions
WHERE  decisions.id=$1;`,
	sqlLoadVoteCountsForDecision: `
SELECT vote, COUNT(vote) FROM votes WHERE decision=$1 GROUP BY vote`,
	sqlLoadVotesForDecision: `
SELECT votes.decision, votes.voter, voters.name, votes.vote, votes.voted, votes.notes
FROM   votes
JOIN   voters ON votes.voter=voters.id
WHERE  decision=$1`,
	sqlLoadEnabledVoterByEmail: `
SELECT voters.id, voters.name, voters.reminder
FROM   voters
JOIN   emails ON voters.id=emails.voter
JOIN   user_roles ON user_roles.voter_id=voters.id
WHERE  emails.address=$1 AND user_roles.role='VOTER'`,
	sqlGetEnabledVoterByID: `
SELECT voters.id, voters.name, voters.reminder
FROM   voters
JOIN   user_roles ON user_roles.voter_id=voters.id
WHERE  user_roles.role='VOTER' AND voters.id=$1`,
	sqlCountOlderThanDecision: `
SELECT COUNT(*) > 0 FROM decisions WHERE proposed < $1`,
	sqlCountOlderThanUnVotedDecision: `
SELECT COUNT(*) > 0 FROM decisions
WHERE  proposed < $1 AND status=0 AND id NOT IN (SELECT decision FROM votes WHERE votes.voter=$2)`,
	sqlCreateDecision: `
INSERT INTO decisions (proposed, proponent, title, content, votetype, status, due, modified,tag)
VALUES (
    :proposed, :proponent, :title, :content, :votetype, 0, :due, :proposed,
    'm' || strftime('%Y%m%d', :proposed) || '.' || (
	 	SELECT COUNT(*)+1 AS num
  		FROM decisions
  		WHERE proposed BETWEEN date(:proposed) AND date(:proposed, '1 day')
    )
)`,
	sqlUpdateDecision: `
UPDATE decisions
SET    proponent=:proponent, title=:title, content=:content, votetype=:votetype, due=:due, modified=:modified
WHERE  id=:id`,
	sqlUpdateDecisionStatus: `
UPDATE decisions SET status=:status, modified=:modified WHERE id=:id`,
	sqlSelectClosableDecisions: `
SELECT decisions.id, decisions.tag, decisions.proponent, decisions.proposed, decisions.title, decisions.content,
       decisions.votetype, decisions.status, decisions.due, decisions.modified
FROM   decisions
WHERE  decisions.status=0 AND :now > due`,
	sqlGetNextPendingDecisionDue: `
SELECT due FROM decisions WHERE status=0 ORDER BY due LIMIT 1`,
	sqlGetVotersForProxy: `
SELECT voters.id, voters.name, voters.reminder
FROM   voters
JOIN   user_roles ON user_roles.voter_id=voters.id
WHERE  user_roles.role='VOTER' AND voters.id != $1`,
	sqlGetReminderVoters: `
SELECT voters.id, voters.name, voters.reminder
FROM   voters
JOIN   user_roles ON user_roles.voter_id=voters.id
WHERE  user_roles.role='VOTER' AND reminder!='' AND reminder IS NOT NULL`,
	sqlFindUnVotedDecisionsForVoter: `
SELECT tag, title, votetype, due
FROM   decisions
WHERE  status = 0 AND id NOT IN (SELECT decision FROM votes WHERE voter = $1)
ORDER BY due ASC`,
	sqlCreateVote: `
INSERT OR REPLACE INTO votes (decision, voter, vote, voted, notes)
VALUES (:decision, :voter, :vote, :voted, :notes)`,
	sqlLoadVote: `
SELECT decision, voter, vote, voted, notes
FROM   votes
WHERE  decision=$1 AND voter=$2`,
}

type VoteType uint8
type VoteStatus int8

type Decision struct {
	ID          int64 `db:"id"`
	Proposed    time.Time
	ProponentID int64 `db:"proponent"`
	Title       string
	Content     string
	Quorum      int
	Majority    int
	Status      VoteStatus
	Due         time.Time
	Modified    time.Time
	Tag         string
	VoteType    VoteType
}

type Voter struct {
	ID       int64 `db:"id"`
	Name     string
	Reminder string // reminder email address
}

type VoteChoice int

const (
	voteAye     = 1
	voteNaye    = -1
	voteAbstain = 0
)

const (
	voteTypeMotion = 0
	voteTypeVeto   = 1
)

const (
	voteTypeLabelMotion  = "motion"
	voteTypeLabelUnknown = "unknown"
	voteTypeLabelVeto    = "veto"
)

func (v VoteType) String() string {
	switch v {
	case voteTypeMotion:
		return voteTypeLabelMotion
	case voteTypeVeto:
		return voteTypeLabelVeto
	default:
		return voteTypeLabelUnknown
	}
}

func (v VoteType) QuorumAndMajority() (int, float32) {
	const (
		majorityDefault = 0.99
		majorityMotion  = 0.50
		quorumDefault   = 1
		quorumMotion    = 3
	)

	switch v {
	case voteTypeMotion:
		return quorumMotion, majorityMotion
	default:
		return quorumDefault, majorityDefault
	}
}

func (v VoteChoice) String() string {
	switch v {
	case voteAye:
		return "aye"
	case voteNaye:
		return "naye"
	case voteAbstain:
		return "abstain"
	default:
		return "unknown"
	}
}

var VoteValues = map[string]VoteChoice{
	"aye":     voteAye,
	"naye":    voteNaye,
	"abstain": voteAbstain,
}

var VoteChoices = map[int64]VoteChoice{
	1:  voteAye,
	0:  voteAbstain,
	-1: voteNaye,
}

const (
	voteStatusDeclined  = -1
	voteStatusPending   = 0
	voteStatusApproved  = 1
	voteStatusWithdrawn = -2
)

func (v VoteStatus) String() string {
	switch v {
	case voteStatusDeclined:
		return "declined"
	case voteStatusPending:
		return "pending"
	case voteStatusApproved:
		return "approved"
	case voteStatusWithdrawn:
		return "withdrawn"
	default:
		return "unknown"
	}
}

type Vote struct {
	DecisionID int64 `db:"decision"`
	VoterID    int64 `db:"voter"`
	Vote       VoteChoice
	Voted      time.Time
	Notes      string
}

type DbHandler struct {
	db *sqlx.DB
}

var db *DbHandler

//go:embed boardvoting/migrations/*
var migrations embed.FS

func NewDB(database *sql.DB) *DbHandler {
	handler := &DbHandler{db: sqlx.NewDb(database, "sqlite3")}

	source, err := iofs.New(migrations, "boardvoting/migrations")
	if err != nil {
		log.Panicf("could not create migration source: %v", err)
	}

	driver, err := sqlite3.WithInstance(database, &sqlite3.Config{})
	if err != nil {
		log.Panicf("could not create migration driver: %v", err)
	}

	m, err := migrate.NewWithInstance("iofs", source, "sqlite3", driver)
	if err != nil {
		log.Panicf("could not create migration instance: %v", err)
	}

	m.Log = NewLogger()

	err = m.Up()
	if err != nil {
		if !errors.Is(err, migrate.ErrNoChange) {
			log.Panicf("running database migration failed: %v", err)
		}

		log.Info("no database migrations required")
	} else {
		log.Info("applied database migrations")
	}

	failedStatements := make([]string, 0)

	for _, sqlStatement := range sqlStatements {
		var stmt *sqlx.Stmt

		stmt, err := handler.db.Preparex(sqlStatement)
		if err != nil {
			log.Errorf("error parsing statement %s: %s", sqlStatement, err)
			failedStatements = append(failedStatements, sqlStatement)
		}

		// nolint:sqlclosecheck
		_ = stmt.Close()
	}

	if len(failedStatements) > 0 {
		log.Panicf("%d statements failed to prepare", len(failedStatements))
	}

	return handler
}

type migrationLogger struct{}

func (m migrationLogger) Printf(format string, v ...interface{}) {
	log.Printf(format, v...)
}

func (m migrationLogger) Verbose() bool {
	return log.IsLevelEnabled(log.DebugLevel)
}

func NewLogger() migrate.Logger {
	return &migrationLogger{}
}

func (d *DbHandler) Close() error {
	return d.db.Close()
}

func (d *DbHandler) getPreparedNamedStatement(statementKey sqlKey) *sqlx.NamedStmt {
	statement, err := d.db.PrepareNamed(sqlStatements[statementKey])
	if err != nil {
		log.Panicf("Preparing statement failed: %v", err)
	}

	return statement
}

func (d *DbHandler) getPreparedStatement(statementKey sqlKey) *sqlx.Stmt {
	statement, err := d.db.Preparex(sqlStatements[statementKey])
	if err != nil {
		log.Panicf("Preparing statement failed: %v", err)
	}

	return statement
}

func (v *Vote) Save() error {
	insertVoteStmt := db.getPreparedNamedStatement(sqlCreateVote)

	defer func() { _ = insertVoteStmt.Close() }()

	var err error

	if _, err = insertVoteStmt.Exec(v); err != nil {
		return fmt.Errorf("saving vote failed: %w", err)
	}

	getVoteStmt := db.getPreparedStatement(sqlLoadVote)

	defer func() { _ = getVoteStmt.Close() }()

	if err = getVoteStmt.Get(v, v.DecisionID, v.VoterID); err != nil {
		return fmt.Errorf("getting inserted vote failed: %w", err)
	}

	return nil
}

type VoteSums struct {
	Ayes     int
	Nayes    int
	Abstains int
}

func (v *VoteSums) VoteCount() int {
	return v.Ayes + v.Nayes + v.Abstains
}

func (v *VoteSums) TotalVotes() int {
	return v.Ayes + v.Nayes
}

func (v *VoteSums) Percent() int {
	totalVotes := v.TotalVotes()
	if totalVotes == 0 {
		return 0
	}

	return v.Ayes * 100 / totalVotes
}

func (v *VoteSums) CalculateResult(quorum int, majority float32) (VoteStatus, string) {
	if v.VoteCount() < quorum {
		return voteStatusDeclined, fmt.Sprintf("Needed quorum of %d has not been reached.", quorum)
	}

	if (float32(v.Ayes) / float32(v.TotalVotes())) < majority {
		return voteStatusDeclined, fmt.Sprintf("Needed majority of %0.2f%% has not been reached.", majority)
	}

	return voteStatusApproved, "Quorum and majority have been reached"
}

type VoteForDisplay struct {
	Vote
	Name string
}

type DecisionForDisplay struct {
	Decision
	Proposer string `db:"proposer"`
	*VoteSums
	Votes []VoteForDisplay
}

func FindDecisionForDisplayByTag(tag string) (*DecisionForDisplay, error) {
	decisionStmt := db.getPreparedStatement(sqlLoadDecisionByTag)

	defer func() { _ = decisionStmt.Close() }()

	decision := &DecisionForDisplay{}

	var err error

	if err = decisionStmt.Get(decision, tag); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}

		return nil, fmt.Errorf("getting motion %s failed: %w", tag, err)
	}

	decision.VoteSums, err = decision.Decision.VoteSums()

	return decision, err
}

// FindDecisionsForDisplayOnPage loads a set of decisions from the database.
//
// This function uses OFFSET for pagination which is not a good idea for larger data sets.
//
// TODO: migrate to timestamp base pagination
func FindDecisionsForDisplayOnPage(page int64, unVoted bool, voter *Voter) ([]*DecisionForDisplay, error) {
	var decisionsStmt *sqlx.Stmt

	if unVoted && voter != nil {
		decisionsStmt = db.getPreparedStatement(sqlLoadUnVotedDecisions)
	} else {
		decisionsStmt = db.getPreparedStatement(sqlLoadDecisions)
	}

	defer func() { _ = decisionsStmt.Close() }()

	var (
		rows      *sqlx.Rows
		err       error
		decisions []*DecisionForDisplay
	)

	if unVoted && voter != nil {
		rows, err = decisionsStmt.Queryx(voter.ID, page-1)
	} else {
		rows, err = decisionsStmt.Queryx(page - 1)
	}

	if err != nil {
		return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
	}

	defer func() { _ = rows.Close() }()

	for rows.Next() {
		var d DecisionForDisplay

		if err = rows.StructScan(&d); err != nil {
			return nil, fmt.Errorf("loading motions for page %d failed: %w", page, err)
		}

		d.VoteSums, err = d.Decision.VoteSums()

		if err != nil {
			return nil, err
		}

		decisions = append(decisions, &d)
	}

	return decisions, nil
}

func (d *Decision) VoteSums() (*VoteSums, error) {
	votesStmt := db.getPreparedStatement(sqlLoadVoteCountsForDecision)

	defer func() { _ = votesStmt.Close() }()

	voteRows, err := votesStmt.Queryx(d.ID)
	if err != nil {
		return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
	}

	defer func() { _ = voteRows.Close() }()

	sums := &VoteSums{}

	for voteRows.Next() {
		var (
			vote  VoteChoice
			count int
		)

		if err = voteRows.Scan(&vote, &count); err != nil {
			return nil, fmt.Errorf("fetching vote sums for motion %s failed: %w", d.Tag, err)
		}

		switch vote {
		case voteAye:
			sums.Ayes = count
		case voteNaye:
			sums.Nayes = count
		case voteAbstain:
			sums.Abstains = count
		}
	}

	return sums, nil
}

func (d *DecisionForDisplay) LoadVotes() (err error) {
	votesStmt := db.getPreparedStatement(sqlLoadVotesForDecision)

	defer func() { _ = votesStmt.Close() }()

	err = votesStmt.Select(&d.Votes, d.ID)
	if err != nil {
		log.Errorf("selecting votes for motion %s failed: %v", d.Tag, err)

		return
	}

	return
}

func (d *Decision) OlderExists(unvoted bool, voter *Voter) (bool, error) {
	var result bool

	if unvoted && voter != nil {
		olderStmt := db.getPreparedStatement(sqlCountOlderThanUnVotedDecision)

		defer func() { _ = olderStmt.Close() }()

		if err := olderStmt.Get(&result, d.Proposed, voter.ID); err != nil {
			return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
		}
	} else {
		olderStmt := db.getPreparedStatement(sqlCountOlderThanDecision)

		defer func() { _ = olderStmt.Close() }()

		if err := olderStmt.Get(&result, d.Proposed); err != nil {
			return false, fmt.Errorf("finding older motions than %s failed: %w", d.Tag, err)
		}
	}

	return result, nil
}

func (d *Decision) Create() error {
	insertDecisionStmt := db.getPreparedNamedStatement(sqlCreateDecision)

	defer func() { _ = insertDecisionStmt.Close() }()

	result, err := insertDecisionStmt.Exec(d)
	if err != nil {
		return fmt.Errorf("creating motion failed: %w", err)
	}

	decisionID, err := result.LastInsertId()
	if err != nil {
		return fmt.Errorf("getting id of inserted motion failed: %w", err)
	}
	rescheduleChannel <- JobIDCloseDecisions

	getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)

	defer func() { _ = getDecisionStmt.Close() }()

	err = getDecisionStmt.Get(d, decisionID)
	if err != nil {
		return fmt.Errorf("getting inserted motion failed: %w", err)
	}

	return nil
}

func (d *Decision) LoadWithID() (err error) {
	getDecisionStmt := db.getPreparedStatement(sqlLoadDecisionByID)

	defer func() { _ = getDecisionStmt.Close() }()

	err = getDecisionStmt.Get(d, d.ID)
	if err != nil {
		log.Errorf("loading updated motion failed: %v", err)

		return
	}

	return
}

func (d *Decision) Update() (err error) {
	updateDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecision)

	defer func() { _ = updateDecisionStmt.Close() }()

	result, err := updateDecisionStmt.Exec(d)
	if err != nil {
		log.Errorf("updating motion failed: %v", err)

		return
	}

	affectedRows, err := result.RowsAffected()
	if err != nil {
		log.Error("Problem determining the affected rows")

		return
	} else if affectedRows != 1 {
		log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
	}

	rescheduleChannel <- JobIDCloseDecisions

	err = d.LoadWithID()

	return
}

func (d *Decision) UpdateStatus() error {
	updateStatusStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)

	defer func() { _ = updateStatusStmt.Close() }()

	result, err := updateStatusStmt.Exec(d)
	if err != nil {
		return fmt.Errorf("setting motion status failed: %w", err)
	}

	affectedRows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("determining the affected rows failed: %w", err)
	} else if affectedRows != 1 {
		log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
	}

	rescheduleChannel <- JobIDCloseDecisions

	err = d.LoadWithID()

	return err
}

func (d *Decision) String() string {
	return fmt.Sprintf("%s %s (ID %d)", d.Tag, d.Title, d.ID)
}

func FindVoterByAddress(emailAddress string) (*Voter, error) {
	findVoterStmt := db.getPreparedStatement(sqlLoadEnabledVoterByEmail)

	defer func() { _ = findVoterStmt.Close() }()

	voter := &Voter{}
	if err := findVoterStmt.Get(voter, emailAddress); err != nil {
		if !errors.Is(err, sql.ErrNoRows) {
			return nil, fmt.Errorf("getting voter for address %s failed: %w", emailAddress, err)
		}

		voter = nil
	}

	return voter, nil
}

func (d *Decision) Close() error {
	quorum, majority := d.VoteType.QuorumAndMajority()

	var (
		voteSums *VoteSums
		err      error
	)

	if voteSums, err = d.VoteSums(); err != nil {
		log.Errorf("getting vote sums failed: %v", err)

		return err
	}

	var reasoning string

	d.Status, reasoning = voteSums.CalculateResult(quorum, majority)

	closeDecisionStmt := db.getPreparedNamedStatement(sqlUpdateDecisionStatus)

	defer func() { _ = closeDecisionStmt.Close() }()

	result, err := closeDecisionStmt.Exec(d)
	if err != nil {
		return fmt.Errorf("closing vote failed: %w", err)
	}

	affectedRows, err := result.RowsAffected()
	if err != nil {
		return fmt.Errorf("getting affected rows failed: %w", err)
	}

	if affectedRows != 1 {
		log.Warningf("wrong number of affected rows: %d (1 expected)", affectedRows)
	}

	NotifyMailChannel <- NewNotificationClosedDecision(d, voteSums, reasoning)

	log.Infof("decision %s closed with result %s: reasoning %s", d.Tag, d.Status, reasoning)

	return nil
}

func CloseDecisions() error {
	getClosableDecisionsStmt := db.getPreparedNamedStatement(sqlSelectClosableDecisions)

	defer func() { _ = getClosableDecisionsStmt.Close() }()

	decisions := make([]*Decision, 0)

	rows, err := getClosableDecisionsStmt.Queryx(struct{ Now time.Time }{time.Now().UTC()})
	if err != nil {
		return fmt.Errorf("fetching closable decisions failed: %w", err)
	}

	defer func() { _ = rows.Close() }()

	for rows.Next() {
		decision := &Decision{}
		if err = rows.StructScan(decision); err != nil {
			return fmt.Errorf("scanning row failed: %w", err)
		}

		decisions = append(decisions, decision)
	}

	defer func() { _ = rows.Close() }()

	for _, decision := range decisions {
		log.Infof("found closable decision %s", decision.Tag)

		if err = decision.Close(); err != nil {
			return fmt.Errorf("closing decision %s failed: %w", decision.Tag, err)
		}
	}

	return nil
}

func GetNextPendingDecisionDue() (*time.Time, error) {
	getNextPendingDecisionDueStmt := db.getPreparedStatement(sqlGetNextPendingDecisionDue)

	defer func() { _ = getNextPendingDecisionDueStmt.Close() }()

	row := getNextPendingDecisionDueStmt.QueryRow()

	due := &time.Time{}
	if err := row.Scan(due); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			log.Debug("No pending decisions")

			return nil, nil
		}

		return nil, fmt.Errorf("parsing result failed: %w", err)
	}

	return due, nil
}

func GetReminderVoters() ([]Voter, error) {
	getReminderVotersStmt := db.getPreparedStatement(sqlGetReminderVoters)

	defer func() { _ = getReminderVotersStmt.Close() }()

	var voters []Voter

	if err := getReminderVotersStmt.Select(&voters); err != nil {
		return nil, fmt.Errorf("getting voters failed: %w", err)
	}

	return voters, nil
}

func FindUnVotedDecisionsForVoter(voter *Voter) ([]Decision, error) {
	findUnVotedDecisionsForVoterStmt := db.getPreparedStatement(sqlFindUnVotedDecisionsForVoter)

	defer func() { _ = findUnVotedDecisionsForVoterStmt.Close() }()

	var decisions []Decision

	if err := findUnVotedDecisionsForVoterStmt.Select(&decisions, voter.ID); err != nil {
		return nil, fmt.Errorf("getting unvoted decisions failed: %w", err)
	}

	return decisions, nil
}

func GetVoterByID(id int64) (*Voter, error) {
	getVoterByIDStmt := db.getPreparedStatement(sqlGetEnabledVoterByID)

	defer func() { _ = getVoterByIDStmt.Close() }()

	voter := &Voter{}
	if err := getVoterByIDStmt.Get(voter, id); err != nil {
		return nil, fmt.Errorf("getting voter failed: %w", err)
	}

	return voter, nil
}

func GetVotersForProxy(proxy *Voter) (voters *[]Voter, err error) {
	getVotersForProxyStmt := db.getPreparedStatement(sqlGetVotersForProxy)

	defer func() { _ = getVotersForProxyStmt.Close() }()

	votersSlice := make([]Voter, 0)

	if err = getVotersForProxyStmt.Select(&votersSlice, proxy.ID); err != nil {
		log.Errorf("Error getting voters for proxy failed: %v", err)

		return
	}

	voters = &votersSlice

	return
}
