// This file is part of taler-mailbox, the Taler Mailbox implementation.
// Copyright (C) 2022 Martin Schanzenbach
//
// Taler-mailbox is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License,
// or (at your option) any later version.
//
// Taler-mailbox is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: AGPL3.0-or-later

// Package mailbox is a GNU Taler service. See https://docs.taler.net/core/api-mailbox.html
package mailbox

import (
	"crypto/ed25519"
	"crypto/sha512"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/gorilla/mux"
	"github.com/schanzen/taler-go/pkg/merchant"
	tos "github.com/schanzen/taler-go/pkg/rest"
	talerutil "github.com/schanzen/taler-go/pkg/util"
	"gopkg.in/ini.v1"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"taler.net/taler-mailbox/internal/gana"
	"taler.net/taler-mailbox/internal/util"
)

type LogLevel int

const (
	LogError LogLevel = iota
	LogWarning
	LogInfo
	LogDebug
)

var LoglevelStringMap = map[LogLevel]string{
	LogDebug:   "DEBUG",
	LogError:   "ERROR",
	LogWarning: "WARN",
	LogInfo:    "INFO",
}

type MailboxConfig struct {
	// libtool-style representation of the Mailbox protocol version, see
	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
	// The format is "current:revision:age".
	LibtoolVersion string

	// Version
	Version string

	// Data home
	Datahome string

	// Configuration
	Ini *ini.File

	// DB connection
	DB gorm.Dialector

	// Merchant connection
	Merchant merchant.Merchant

	// Loglevel
	Loglevel LogLevel
}

// Mailbox is the primary object of the Mailbox service
type Mailbox struct {

	// The main router
	Router *mux.Router

	// The main DB handle
	DB *gorm.DB

	// Our configuration from the ini
	Cfg MailboxConfig

	// Fixed size of message bodies
	MessageBodyBytes int64 `json:"message_body_bytes"`

	// Merchant object
	Merchant merchant.Merchant

	// Base URL
	BaseURL string

	// Registration fee for each validity month mailbox
	MonthlyFee *talerutil.Amount

	// Registration fee for registering or modifying mailbox
	RegistrationUpdateFee *talerutil.Amount

	// Message fee for receiving a message
	MessageFee *talerutil.Amount

	// The free message quota
	FreeMessageQuota uint64

	// How many messages will a single response
	// contain at maximum.
	MessageResponseLimit uint64

	// Logger
	Logger *log.Logger

	// Currency Spec
	CurrencySpec talerutil.CurrencySpecification
}

type RelativeTime struct {
	Microseconds uint64 `json:"d_us"`
}

type Timestamp struct {
	Seconds uint64 `json:"t_s"`
}

// 1 Month as Go duration
const monthDuration = time.Hour * 24 * 30

// VersionResponse is the JSON response of the /config endpoint
type VersionResponse struct {
	// libtool-style representation of the Mailbox protocol version, see
	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
	// The format is "current:revision:age".
	Version string `json:"version"`

	// Name of the protocol.
	Name string `json:"name"` // "taler-mailbox"

	// Fixed size of message bodies
	MessageBodyBytes int64 `json:"message_body_bytes"`

	// How long will the service store a message
	// before giving up
	DeliveryPeriod RelativeTime `json:"delivery_period" gorm:"embedded;embeddedPrefix:delivery_period_"`

	// How many messages will a single response
	// contain at maximum.
	MessageResponseLimit uint64 `json:"message_response_limit"`

	// How much is the cost of a single
	// registration (update) of a mailbox
	// May be 0 for a free update/registration.
	RegistrationUpdateFee string `json:"registration_update_fee"`

	// How much is the cost of a single
	// registration period (30 days) of a mailbox
	// May be 0 for a free registration.
	MonthlyFee string `json:"monthly_fee"`

	// How much is the cost to send a single
	// message to a mailbox.
	// May be 0 for free message sending.
	MessageFee string `json:"message_fee"`

	// How many messages can be send and
	// are stored by the service for free.
	// After the quota is reached, the
	// regular message_fee applies.
	// May be 0 for no free quota.
	FreeMessageQuota string `json:"free_message_quota"`
}

type MailboxMetadata struct {
	// ORM
	gorm.Model `json:"-"`

	// ORM helper hash of signing key
	HashedSigningKey string `json:"-"`

	// The mailbox signing key.
	// Note that $H_MAILBOX == H(singingKey).
	// Note also how this key cannot be updated
	// as it identifies the mailbox.
	SigningKey string `json:"signing_key"`

	// Type of key.
	// Optional, as currently only
	// EdDSA keys are supported.
	SigningKeyType string `json:"signing_key_type"`

	// The mailbox encryption key.
	// This is an HPKE public key
	// in the X25519 format for use
	// in a X25519-DHKEM (RFC 9180).
	// Base32 crockford-encoded.
	EncryptionKey string `json:"encryption_key"`

	// Type of key.
	// Optional, as currently only
	// X25519 keys are supported.
	EncryptionKeyType string `json:"encryption_key_type"`

	// Expiration of this mapping.
	Expiration Timestamp `json:"expiration" gorm:"embedded;embeddedPrefix:expiration_"`
}

type PendingMailboxRegistration struct {
	// ORM
	gorm.Model `json:"-"`

	// Hash of the inbox for this entry
	HashedSigningKey string // Requested registration duration

	Duration time.Duration

	// The order ID associated with this registration
	OrderID string `json:"-"`
}

type MailboxRegistrationRequest struct {

	// Keys to add/update for a mailbox.
	MailboxMetadata MailboxMetadata `json:"mailbox_metadata"`

	// Signature by the mailbox's signing key affirming
	// the update of keys, of purpose
	// TALER_SIGNATURE_WALLET_MAILBOX_KEYS_UPDATE.
	// The signature is created over the SHA-512 hash
	// of (encryptionKeyType||encryptionKey||expiration)
	Signature string `json:"signature"`
}

// MailboxRateLimitedResponse is the JSON response when a rate limit is hit
type MailboxRateLimitedResponse struct {

	// Taler error code, TALER_EC_mailbox_REGISTER_RATE_LIMITED.
	Code int `json:"code"`

	// When the client should retry. Currently: In microseconds
	RetryDelay int64 `json:"retry_delay"`

	// The human readable error message.
	Hint string `json:"hint"`
}

type InboxEntry struct {
	// ORM
	gorm.Model `json:"-"`

	// Encrypted message. Must be exactly 256-32 bytes long.
	Body []byte

	// Hash of the inbox for this entry
	HashedSigningKey string
}

func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
	dpStr := m.Cfg.Ini.Section("mailbox").Key("delivery_period").MustString("72h")
	dp, err := time.ParseDuration(dpStr)
	if err != nil {
		log.Fatal(err)
	}
	cfg := VersionResponse{
		Version:               m.Cfg.LibtoolVersion,
		Name:                  "taler-mailbox",
		MessageBodyBytes:      m.MessageBodyBytes,
		MessageResponseLimit:  m.MessageResponseLimit,
		MonthlyFee:            m.MonthlyFee.String(),
		RegistrationUpdateFee: m.RegistrationUpdateFee.String(),
		DeliveryPeriod:        RelativeTime{Microseconds: uint64(dp.Microseconds())},
	}
	w.Header().Set("Content-Type", "application/json")
	response, _ := json.Marshal(cfg)
	w.Write(response)
}

func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	//to, toSet := vars["timeout_ms"]
	var entries []InboxEntry
	// FIXME rate limit
	// FIXME timeout
	// FIXME possibly limit results here
	m.checkPendingRegistrationUpdates(vars["h_mailbox"])
	err := m.DB.Where("hashed_signing_key = ?", vars["h_mailbox"]).Limit(int(m.MessageResponseLimit)).Find(&entries).Error
	if err != nil {
		log.Printf("%v", err)
		w.WriteHeader(http.StatusNotFound)
		return
	}
	if len(entries) == 0 {
		w.WriteHeader(http.StatusNoContent)
		return
	}
	// Add ETag of first message ID
	etag := entries[0].ID
	w.Header().Add("ETag", fmt.Sprintf("%d", etag))
	for _, entry := range entries {
		w.Write(entry.Body)
	}
}

func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var entry InboxEntry
	if r.Body == nil {
		http.Error(w, "No request body", http.StatusBadRequest)
		return
	}
	if r.ContentLength != m.MessageBodyBytes {
		http.Error(w, "Wrong message size", http.StatusBadRequest)
		return
	}
	body, err := io.ReadAll(r.Body)
	if err != nil {
		log.Printf("%v", err)
		http.Error(w, "Cannot read body", http.StatusBadRequest)
		return
	}
	if !m.MessageFee.IsZero() {
		var count int64
		err = m.DB.Model(&InboxEntry{}).Where("hashed_signing_key = ?", vars["h_mailbox"]).Count(&count).Error
		if nil != err {
			m.Logf(LogError, "%v", err)
			http.Error(w, "Cannot look for entries", http.StatusBadRequest)
			return
		}
		if count >= int64(m.FreeMessageQuota) {
			w.WriteHeader(http.StatusPaymentRequired)
			//w.Header().Set("Taler", payto) FIXME generate payto
			return
		}
	}
	m.checkPendingRegistrationUpdates(vars["h_mailbox"])
	err = m.DB.First(&entry, "hashed_signing_key = ? AND body = ?", vars["h_mailbox"], body, true).Error
	if err == nil {
		w.WriteHeader(http.StatusNotModified)
		return
	}
	entry.HashedSigningKey = vars["h_mailbox"]
	entry.Body = body
	m.DB.Save(&entry)
	w.WriteHeader(http.StatusNoContent)
}

func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var keyEntry MailboxMetadata
	m.checkPendingRegistrationUpdates(vars["h_mailbox"])
	err := m.DB.First(&keyEntry, "hashed_signing_key = ?", vars["h_mailbox"]).Error
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	m.Logf(LogDebug, "entry expires at %d, have %d", keyEntry.Expiration.Seconds, time.Now().Unix())
	if keyEntry.Expiration.Seconds < uint64(time.Now().Unix()) {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	response, _ := json.Marshal(keyEntry)
	w.Write(response)
}

func (m *Mailbox) validateRegistrationSignature(msg MailboxRegistrationRequest) error {
	var expNbo [8]byte
	var signedMsg [72]byte
	encPk, err := util.Base32CrockfordDecode(msg.MailboxMetadata.EncryptionKey, 32)
	if err != nil {
		return fmt.Errorf("unable to decode encryption key")
	}
	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
	if err != nil {
		return fmt.Errorf("unable to decode pubkey")
	}
	pk := ed25519.PublicKey(pkey)
	sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
	if nil != err {
		return fmt.Errorf("unable to decode signature")
	}
	binary.BigEndian.PutUint64(expNbo[:], msg.MailboxMetadata.Expiration.Seconds)
	size := signedMsg[0:4]
	binary.BigEndian.PutUint32(size, 64+4+4)
	purp := signedMsg[4:8]
	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
	h := sha512.New()
	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType)) // Currently always X25519
	h.Write(encPk)
	h.Write(expNbo[:])
	copy(signedMsg[8:], h.Sum(nil))
	if !ed25519.Verify(pk, signedMsg[0:], sig) {
		return fmt.Errorf("signature invalid")
	}
	return nil
}

// Check if this is a non-zero, positive amount
func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) {
	sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds()))
	sliceCost, err := talerutil.ParseAmount(sliceCostAmount)
	if nil != err {
		return nil, err
	}
	fixedCost, err := talerutil.ParseAmount(fixedCostAmount)
	if nil != err {
		return nil, err
	}
	sum := &talerutil.Amount{
		Currency: sliceCost.Currency,
		Value:    0,
		Fraction: 0,
	}
	for range sliceCount {
		sum, err = sum.Add(*sliceCost)
		if nil != err {
			return nil, err
		}
	}
	sum, err = sum.Add(*fixedCost)
	if nil != err {
		return nil, err
	}
	return sum, nil
}

func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) {
	var msg MailboxRegistrationRequest
	var pendingRegistration PendingMailboxRegistration
	var registrationEntry MailboxMetadata
	if r.Body == nil {
		m.Logf(LogError, "no request body")
		http.Error(w, "No request body", http.StatusBadRequest)
		return
	}
	err := json.NewDecoder(r.Body).Decode(&msg)
	if err != nil {
		http.Error(w, "Malformed request body", http.StatusBadRequest)
		return
	}
	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
	if err != nil {
		http.Error(w, "Public key invalid", http.StatusBadRequest)
		return
	}
	pk := ed25519.PublicKey(pkey)
	err = m.validateRegistrationSignature(msg)
	if nil != err {
		http.Error(w, "Signature verification failed", http.StatusBadRequest)
		return
	}
	h := sha512.New()
	h.Write(pkey)
	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
	pendingRegistration.HashedSigningKey = hMailbox
	// Round to the nearest multiple of a month
	reqExpiration := time.Unix(int64(msg.MailboxMetadata.Expiration.Seconds), 0)
	now := time.Now()
	reqDuration := reqExpiration.Sub(now).Round(monthDuration)
	err = m.DB.First(&registrationEntry, "hashed_signing_key = ?", hMailbox).Error
	if err == nil {
		// This probably meansthe registration is modified or extended or both
		entryModified := (registrationEntry.EncryptionKey != msg.MailboxMetadata.EncryptionKey)
		// At least one MonthlyFee
		if reqDuration.Microseconds() == 0 && !entryModified {
			// Nothing changed. Return validity
			w.WriteHeader(http.StatusNotModified)
			return
		}
	} else {
		// Entry does not yet exist, add but immediately expire it
		registrationEntry = msg.MailboxMetadata
		registrationEntry.Expiration.Seconds = uint64(time.Now().Unix() - 1)
		hAddr := sha512.New()
		hAddr.Write(pk)
		registrationEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
		err = m.DB.Create(&registrationEntry).Error
		if nil != err {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
	}
	err = m.DB.First(&pendingRegistration, "hashed_signing_key = ?", hMailbox).Error
	pendingRegistrationExists := (nil == err)
	if !pendingRegistrationExists {
		pendingRegistration.HashedSigningKey = hMailbox
		pendingRegistration.Duration = reqDuration
	}
	// At least the update fee needs to be paid
	cost, err := calculateCost(m.MonthlyFee.String(),
		m.RegistrationUpdateFee.String(),
		reqDuration,
		monthDuration)
	if err != nil {
		fmt.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if !cost.IsZero() {
		if len(pendingRegistration.OrderID) == 0 {
			// Add new order
			orderID, newOrderErr := m.Merchant.AddNewOrder(*cost, "Mailbox registration", m.BaseURL)
			if newOrderErr != nil {
				m.Logf(LogError, "%v", newOrderErr)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			m.Logf(LogDebug, "New order ID %s for pending registration for %s", orderID, pendingRegistration.HashedSigningKey)
			pendingRegistration.OrderID = orderID
		}
		// Check if order paid.
		// FIXME: Remember that it was activated and paid
		// FIXME: We probably need to handle the return code here (see gns registrar for how)
		_, _, payto, paytoErr := m.Merchant.IsOrderPaid(pendingRegistration.OrderID)
		if paytoErr != nil {
			fmt.Println(paytoErr)
			w.WriteHeader(http.StatusInternalServerError)
			m.Logf(LogError, "%s\n", paytoErr.Error())
			return
		}
		if len(payto) != 0 {
			m.DB.Save(&pendingRegistration)
			w.WriteHeader(http.StatusPaymentRequired)
			w.Header().Set("Taler", payto)
			return
		}
	}
	// Update expiration time of registration.
	registrationEntry.Expiration.Seconds += uint64(reqDuration.Seconds())
	m.DB.Delete(pendingRegistration)
	err = m.DB.Save(&registrationEntry).Error
	if nil != err {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusNoContent)
}

func (m *Mailbox) checkPendingRegistrationUpdates(hMailbox string) {
	var pendingEntry PendingMailboxRegistration
	var registrationEntry MailboxMetadata
	err := m.DB.First(&pendingEntry, "hashed_signing_key = ?", hMailbox).Error
	if err != nil {
		return
	}
	m.Logf(LogDebug, "Found pending registration for %s, OrderID: %s", hMailbox, pendingEntry.OrderID)
	rc, orderStatus, _, paytoErr := m.Merchant.IsOrderPaid(pendingEntry.OrderID)
	if nil != paytoErr {
		if rc == http.StatusNotFound {
			m.Logf(LogInfo, "Registration order for `%s' not found, removing\n", hMailbox)
		}
		err := m.DB.Delete(&pendingEntry)
		if nil != err {
			m.Logf(LogInfo, "%v\n", err)
		}
		return
	}
	m.Logf(LogDebug, "Order status for %s is %s", pendingEntry.HashedSigningKey, orderStatus)
	if merchant.OrderPaid == orderStatus {
		m.Logf(LogDebug, "Order for %v appears to be paid", pendingEntry)
		err = m.DB.First(&registrationEntry, "hashed_signing_key = ?", hMailbox).Error
		if err == nil {
			m.Logf(LogDebug, "Adding %d seconds to entry expiration", uint64(pendingEntry.Duration.Seconds()))
			registrationEntry.Expiration.Seconds += uint64(pendingEntry.Duration.Seconds())
			m.DB.Save(&registrationEntry)
			err := m.DB.Delete(&pendingEntry)
			if nil != err {
				m.Logf(LogInfo, "%v\n", err)
			}
		}
		return
	}
}

func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	etagHeader := r.Header.Get("If-Match")
	if etagHeader == "" {
		http.Error(w, "If-Match header missing", 400)
		return
	}
	if strings.Contains(etagHeader, ",") {
		http.Error(w, "If-Match contains multiple values", 400)
		return
	}
	expectedETag, err := strconv.Atoi(etagHeader)
	if err != nil {
		http.Error(w, "If-Match contains malformed etag number", 400)
		return
	}
	pkey, err := util.Base32CrockfordDecode(vars["mailbox"], 32)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	count := 1
	countStr := r.URL.Query().Get("count")
	if len(countStr) > 0 {
		count, err = strconv.Atoi(countStr)
		if err != nil {
			http.Error(w, "Malformed count parameter", http.StatusBadRequest)
			w.WriteHeader(http.StatusBadRequest)
			return
		}
	}
	headerSig := r.Header["Taler-Mailbox-Delete-Signature"]
	if nil == headerSig {
		http.Error(w, "Missing signature", http.StatusBadRequest)
		return
	}
	pk := ed25519.PublicKey(pkey)
	sig, err := util.Base32CrockfordDecode(headerSig[0], 64)
	if nil != err {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	h := sha512.New()
	h.Write(pkey)
	hHailbox := util.Base32CrockfordEncode(h.Sum(nil))
	m.checkPendingRegistrationUpdates(hHailbox)
	var signedMsg [4 * 4]byte
	binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
	binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
	binary.BigEndian.PutUint32(signedMsg[8:12], uint32(expectedETag))
	binary.BigEndian.PutUint32(signedMsg[12:16], uint32(count))
	if !ed25519.Verify(pk, signedMsg[0:], sig) {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	// Check that expectedETag actually exists
	err = m.DB.Where("hashed_signing_key = ? AND id = ?", hHailbox, expectedETag).Find(&InboxEntry{}).Error
	if err != nil {
		m.Logf(LogDebug, "Message to delete not found with ID %d", expectedETag)
		w.WriteHeader(http.StatusNotFound)
		return
	}
	var entries []InboxEntry
	err = m.DB.Where("hashed_signing_key = ? AND id >= ?", hHailbox, expectedETag).Limit(count).Find(&entries).Error
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	m.Logf(LogDebug, "Found matching ID, deleting %d messages", len(entries))
	m.DB.Delete(entries)
	w.WriteHeader(http.StatusNoContent)
}

func (m *Mailbox) getFileName(relativeFileName string) string {
	_, err := os.Stat(relativeFileName)
	if errors.Is(err, os.ErrNotExist) {
		_, err := os.Stat(m.Cfg.Datahome + "/" + relativeFileName)
		if errors.Is(err, os.ErrNotExist) {
			log.Printf("Tried fallback not found %s\n", m.Cfg.Datahome+"/"+relativeFileName)
			return ""
		}
		return m.Cfg.Datahome + "/" + relativeFileName
	}
	return relativeFileName
}

func (m *Mailbox) termsResponse(w http.ResponseWriter, r *http.Request) {
	s := m.Cfg.Ini.Section("mailbox")
	termspath := m.getFileName(s.Key("default_terms_path").MustString("terms/"))
	tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{
		DefaultFileType:    s.Key("default_doc_filetype").MustString("text/html"),
		DefaultLanguage:    s.Key("default_doc_lang").MustString("en"),
		SupportedFileTypes: strings.Split(s.Key("supported_doc_filetypes").String(), " "),
	})
}

func (m *Mailbox) privacyResponse(w http.ResponseWriter, r *http.Request) {
	s := m.Cfg.Ini.Section("mailbox")
	pppath := m.getFileName(s.Key("default_pp_path").MustString("privacy/"))
	tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{
		DefaultFileType:    s.Key("default_doc_filetype").MustString("text/html"),
		DefaultLanguage:    s.Key("default_doc_lang").MustString("en"),
		SupportedFileTypes: strings.Split(s.Key("supported_doc_filetypes").String(), " "),
	})
}

func (m *Mailbox) setupHandlers() {
	m.Router = mux.NewRouter().StrictSlash(true)

	/* ToS API */
	m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
	m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")

	/* Config API */
	m.Router.HandleFunc("/config", m.configResponse).Methods("GET")

	/* Mailbox API */
	m.Router.HandleFunc("/register", m.registerMailboxResponse).Methods("POST")
	m.Router.HandleFunc("/info/{h_mailbox}", m.getKeysResponse).Methods("GET")
	m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
	m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
	m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
}

func (m *Mailbox) Logf(loglevel LogLevel, fmt string, args ...any) {
	if loglevel < m.Cfg.Loglevel {
		return
	}
	m.Logger.SetPrefix("taler-mailbox - " + LoglevelStringMap[loglevel] + " ")
	m.Logger.Printf(fmt, args...)
}

// Initialize the Mailbox instance with cfgfile
func (m *Mailbox) Initialize(cfg MailboxConfig) {
	m.Cfg = cfg
	m.Logger = log.New(os.Stdout, "taler-mailbox:", log.LstdFlags)
	if cfg.Ini.Section("mailbox").Key("production").MustBool(false) {
		fmt.Println("Production mode enabled")
	}
	m.BaseURL = cfg.Ini.Section("mailbox").Key("base_url").MustString("https://example.com")
	m.MessageBodyBytes = cfg.Ini.Section("mailbox").Key("message_body_bytes").MustInt64(256)
	m.MessageResponseLimit = cfg.Ini.Section("mailbox").Key("message_response_limit").MustUint64(50)
	monthlyFee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("monthly_fee").MustString("KUDOS:0"))
	if err != nil {
		fmt.Printf("Failed to parse monthly fee: %v", err)
		os.Exit(1)
	}
	m.MonthlyFee = monthlyFee
	updateFee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("registration_update_fee").MustString("KUDOS:0"))
	if err != nil {
		fmt.Printf("Failed to parse update fee: %v", err)
		os.Exit(1)
	}
	m.RegistrationUpdateFee = updateFee
	messageFee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("message_fee").MustString("KUDOS:0"))
	if err != nil {
		fmt.Printf("Failed to parse message fee: %v", err)
		os.Exit(1)
	}
	m.MessageFee = messageFee
	m.FreeMessageQuota = cfg.Ini.Section("mailbox").Key("free_message_quota").MustUint64(0)
	_db, err := gorm.Open(cfg.DB, &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		panic(err)
	}
	m.DB = _db
	if err := m.DB.AutoMigrate(&InboxEntry{}); err != nil {
		panic(err)
	}
	if err := m.DB.AutoMigrate(&MailboxMetadata{}); err != nil {
		panic(err)
	}
	go func() {
		for {
			tx := m.DB.Where("expiration < ?", time.Now()).Delete(&MailboxMetadata{})
			m.Logf(LogInfo, "Cleaned up %d stale registrations.\n", tx.RowsAffected)
			time.Sleep(time.Hour * 24)
		}
	}()
	if err := m.DB.AutoMigrate(&PendingMailboxRegistration{}); err != nil {
		panic(err)
	}
	// Clean up pending
	pendingExpStr := cfg.Ini.Section("mailbox").Key("pending_registration_expiration").MustString("24h")
	pendingExp, err := time.ParseDuration(pendingExpStr)
	if err != nil {
		fmt.Printf("Failed to parse pending registration expiration: %v", err)
		os.Exit(1)
	}
	go func() {
		for {
			tx := m.DB.Where("created_at < ?", time.Now().Add(-pendingExp)).Delete(&PendingMailboxRegistration{})
			m.Logf(LogInfo, "Cleaned up %d stale pending registrations.\n", tx.RowsAffected)
			time.Sleep(pendingExp)
		}
	}()

	m.Merchant = cfg.Merchant
	if !monthlyFee.IsZero() {
		merchConfig, err := m.Merchant.GetConfig()
		if err != nil {
			fmt.Printf("Failed to get merchant config: %v", err)
			os.Exit(1)
		}
		currencySpec, currencySupported := merchConfig.Currencies[monthlyFee.Currency]
		for !currencySupported {
			fmt.Printf("Currency `%s' not supported by merchant!\n", monthlyFee.Currency)
			os.Exit(1)
		}
		m.CurrencySpec = currencySpec
	}
	m.setupHandlers()
}
