// request
package repositories

import (
	"bytes"
	"crypto/rand"
	"database/sql"
	"errors"
	"fmt"
	"html/template"
	"log"
	"math/big"
	"net/smtp"
	"officeHours/config"
	"officeHours/models"
	"officeHours/templating"
)

type RequestRepo struct {
	db             *sql.DB
	officeHourRepo models.OfficeHourRepository
	config         config.Config
}

func NewRequestRepo(db *sql.DB, officeHourRepo models.OfficeHourRepository, config config.Config) *RequestRepo {
	return &RequestRepo{
		db:             db,
		officeHourRepo: officeHourRepo,
		config:         config,
	}
}

func (r *RequestRepo) FindBySecret(secret string) (models.Request, error) {
	// This query is not safe against timing sidechannel attacks – I don't care.
	row := r.db.QueryRow("SELECT * FROM request WHERE secret=?", secret)
	var request models.Request
	var officeHourId int
	err := row.Scan(&request.Id, &officeHourId, &request.Action, &request.Secret)
	if err != nil {
		return models.Request{}, fmt.Errorf("SQL-error scanning request row: %w", err)
	}
	request.OfficeHour, err = r.officeHourRepo.FindById(officeHourId)
	return request, err
}

func (r *RequestRepo) FindByOfficeHour(officeHour models.OfficeHour) ([]models.Request, error) {
	rows, err := r.db.Query("SELECT * FROM request WHERE officeHour=?", officeHour.Id)
	if err != nil {
		return nil, fmt.Errorf("SQL-error selecting requests by office hour: %w", err)
	}
	defer rows.Close()

	var requests []models.Request
	for rows.Next() {
		var request models.Request
		var officeHourId int
		if err := rows.Scan(&request.Id, &officeHourId, &request.Action, &request.Secret); err != nil {
			err = fmt.Errorf("error scanning request row: %w", err)
			log.Println(err.Error())
			return requests, err
		}
		request.OfficeHour, err = r.officeHourRepo.FindById(officeHourId)
		if err != nil {
			return requests, err
		}
		requests = append(requests, request)
	}
	return requests, nil
}

// Add a request to the database if it doesnt already exist.
// Send a mail with the secret to the confirmation address in any case.
func (r *RequestRepo) Add(officeHour models.OfficeHour, action models.RequestAction) (int, error) {
	existents, err := r.FindByOfficeHour(officeHour)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return 0, err
	}
	/* Resend confirmation mail if identical request exists,
	 * but don't insert new request into database.
	 */
	for _, request := range existents {
		if request.Action == action { // already covered by selection: && request.OfficeHour == officeHour {
			return request.Id, r.sendConfirmationMail(request)
		}
	}
	secret, err := r.newSecret()
	if err != nil {
		return 0, err
	}
	request := models.Request{Id: 0, OfficeHour: officeHour, Action: action, Secret: secret}
	_, err = r.db.Exec("INSERT INTO `request` (officeHour, action, secret) VALUES (?,?,?)", officeHour.Id, action, secret)
	if err != nil {
		return 0, fmt.Errorf("SQL-error inserting new request: %w", err)
	}
	request, err = r.FindBySecret(secret)
	if err != nil {
		return request.Id, err
	}
	return request.Id, r.sendConfirmationMail(request)
}

// Execute a request and delete it.
func (r *RequestRepo) Execute(request models.Request) error {
	var err error
	switch request.Action {
	case models.RequestActivate:
		_, err = r.db.Exec("UPDATE officeHour SET active=true WHERE id=?", request.OfficeHour.Id)
		r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
	case models.RequestDelete:
		r.db.Exec("DELETE FROM request WHERE officeHour=?", request.OfficeHour.Id)
		err = r.officeHourRepo.Delete(request.OfficeHour)
	default:
		log.Printf("executing request: Action type %d unknown.", request.Action)
		_, err = r.db.Exec("DELETE FROM request WHERE id=?", request.Id)
	}
	return err
}

// Find a new secret token with configured length that is currently unused.
func (r *RequestRepo) newSecret() (string, error) {
	var err error
	var secret string
	// find unused secret
	for !errors.Is(err, sql.ErrNoRows) {
		secret = randomString(r.config.Request.SecretLength)
		_, err = r.FindBySecret(secret)
		if err != nil && !errors.Is(err, sql.ErrNoRows) {
			return "", err
		}
	}
	return secret, nil
}

func (r *RequestRepo) sendConfirmationMail(request models.Request) error {
	var message bytes.Buffer
	var data = struct {
		Config    config.Config
		Request   models.Request
		MessageId template.HTML
	}{r.config, request, template.HTML("<" + randomString(15) + "@" + r.config.Server.Domain + ">")}
	err := templating.WriteTemplate(&message, "confirmationMail", data)
	if err != nil {
		err = fmt.Errorf("error parsing confirmation Mail: %w", err)
		log.Println(err.Error())
		return err
	}

	switch r.config.Mailer.Type {
	case "Stdout":
		fmt.Println(message.String())
	case "Smtp":
		to := []string{request.OfficeHour.Tutor.Email}
		var auth smtp.Auth
		if r.config.Mailer.SmtpUseAuth {
			auth = smtp.PlainAuth(r.config.Mailer.SmtpIdentity, r.config.Mailer.FromAddress, r.config.Mailer.SmtpPassword, r.config.Mailer.SmtpHost)
		}
		err = smtp.SendMail(fmt.Sprintf("%s:%d", r.config.Mailer.SmtpHost, r.config.Mailer.SmtpPort), auth, string(r.config.Mailer.FromName), to, message.Bytes())
		if err != nil {
			err = fmt.Errorf("error sending mail by smtp: %w", err)
			log.Println(err.Error())
		}
		return err
	}
	return nil
}

func randomString(n int) string {
	// []byte would be faster and also work, but []rune keeps working for UTF8 characters
	// if someone would like them.
	var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

	s := make([]rune, n)
	for i := range s {
		position, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
		if err != nil {
			log.Printf("Error getting random position in randomString(n int): %s", err.Error())
		}
		s[i] = letters[position.Int64()]
	}
	return string(s)
}
