// This file is part of tdir, the Taler Directory implementation.
// Copyright (C) 2022 Martin Schanzenbach
//
// Taldir 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.
//
// Taldir 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 taldir implements the taler directory service.
package taldir

/* TODO
- ToS compression
- ToS etag
*/

import (
	"crypto/sha512"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"errors"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"net/url"
	"os"
	"regexp"
	"strings"
	"time"

	"database/sql"
	"github.com/gertd/go-pluralize"
	"github.com/gorilla/mux"
	"github.com/kataras/i18n"
	_ "github.com/lib/pq"
	"github.com/schanzen/taler-go/pkg/merchant"
	tos "github.com/schanzen/taler-go/pkg/rest"
	talerutil "github.com/schanzen/taler-go/pkg/util"
	"github.com/skip2/go-qrcode"
	"taler.net/taldir/internal/gana"
	"taler.net/taldir/internal/util"
)

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

	// The main router
	Router *mux.Router

	// The main DB handle
	DB *sql.DB

	// Our configuration from the config.json
	Cfg TaldirConfig

	// Map of supported validators as defined in the configuration
	Validators map[string]Validator

	// Map of supported disseminators as defined in the configuration
	Disseminators map[string]Disseminator

	// imprint page
	ImprintTpl *template.Template

	// landing page
	ValidationTpl *template.Template

	// lookup result/registration page
	LookupResultPageTpl *template.Template

	// landing page
	LandingPageTpl *template.Template

	// about page
	AboutPageTpl *template.Template

	// The alias salt
	Salt string

	// The host base url
	Host string

	// Valid Payment System Address
	ValidPMSRegex string

	// The timeframe for the validation requests
	ValidationTimeframe time.Duration

	// How often may a challenge be requested
	ValidationInitiationMax int64

	// How often may a solution be attempted (in the given timeframe)
	SolutionAttemptsMax int

	// The timeframe for the above solution attempts
	SolutionTimeframe time.Duration

	// Challenge length in bytes before encoding
	ChallengeBytes int

	// Merchant object
	Merchant merchant.Merchant

	// Monthly fee amount
	MonthlyFee *talerutil.Amount

	// Registrar base URL
	BaseURL string

	// Currency Spec
	CurrencySpec talerutil.CurrencySpecification

	// I18n
	I18n *i18n.I18n

	// Logger
	Logger TaldirLogger
}

// VersionResponse is the JSON response of the /config endpoint
type VersionResponse struct {
	// libtool-style representation of the Merchant 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-directory"

	// Supported alias types
	AliasType []AliasType `json:"alias_type"`

	// fee for one month of registration
	MonthlyFee string `json:"monthly_fee"`
}

// AliasType is part of the VersionResponse and contains a supported validator
type AliasType struct {

	// Name of the alias type, e.g. "email" or "sms".
	Name string `json:"name"`

	// per challenge fee
	ChallengeFee string `json:"challenge_fee"`
}

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

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

	// At what frequency are new registrations allowed. FIXME: In what? Currently: In microseconds
	RequestFrequency int64 `json:"request_frequency"`

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

// RegisterMessage is the JSON paylaod when a registration is requested
type RegisterMessage struct {

	// Alias, in type-specific format
	Alias string `json:"alias"`

	// Target URI to associate with this alias
	TargetURI string `json:"target_uri"`

	// For how long should the registration last
	Duration int64 `json:"duration"`
}

// ErrorDetail is the detailed error payload returned from Taldir endpoints
type ErrorDetail struct {

	// Numeric error code unique to the condition.
	// The other arguments are specific to the error value reported here.
	Code int `json:"code"`

	// Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
	// Should give a human-readable hint about the error's nature. Optional, may change without notice!
	Hint string `json:"hint,omitempty"`

	// Optional detail about the specific input value that failed. May change without notice!
	Detail string `json:"detail,omitempty"`

	// Name of the parameter that was bogus (if applicable).
	Parameter string `json:"parameter,omitempty"`

	// Path to the argument that was bogus (if applicable).
	Path string `json:"path,omitempty"`

	// Offset of the argument that was bogus (if applicable).
	Offset string `json:"offset,omitempty"`

	// Index of the argument that was bogus (if applicable).
	Index string `json:"index,omitempty"`

	// Name of the object that was bogus (if applicable).
	Object string `json:"object,omitempty"`

	// Name of the currency than was problematic (if applicable).
	Currency string `json:"currency,omitempty"`

	// Expected type (if applicable).
	TypeExpected string `json:"type_expected,omitempty"`

	// Type that was provided instead (if applicable).
	TypeActual string `json:"type_actual,omitempty"`
}

// ValidationConfirmation is the payload sent by the client t complete a
// registration.
type ValidationConfirmation struct {
	// The solution is the SHA-512 hash of the challenge value
	// chosen by TalDir (encoded as string just as given in the URL, but
	// excluding the 0-termination) concatenated with the binary 32-byte
	// value representing the wallet's EdDSA public key.
	// The hash is provided as string in Crockford base32 encoding.
	Solution string `json:"solution"`
}

// NOTE: Go stores durations as nanoseconds. TalDir usually operates on microseconds
const monthDurationUs = 2592000000000

// 1 Month as Go duration
const monthDuration = time.Duration(monthDurationUs * 1000)

func (t *Taldir) isPMSValid(pms string) (err error) {
	if t.ValidPMSRegex != "" {
		matched, _ := regexp.MatchString(t.ValidPMSRegex, pms)
		if !matched {
			return fmt.Errorf("payment System Address `%s' invalid", pms) // TODO i18n
		}
	}
	return
}

// Primary lookup function.
// Allows the caller to query a wallet key using the hash(!) of the
// alias
func (t *Taldir) getSingleEntry(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var entry Entry
	hsAlias := saltHAlias(vars["h_alias"], t.Salt)
	var err = GetEntryByHsAliasFromDatabase(t.DB, &entry, hsAlias)
	if err == nil {
		w.Header().Set("Content-Type", "application/json")
		resp, _ := json.Marshal(entry)
		w.Write(resp)
		return
	}
	w.WriteHeader(http.StatusNotFound)
}

// Disseminate entry
func (t *Taldir) disseminateStop(e Entry) error {
	for _, d := range t.Disseminators {
		err := d.DisseminateStop(&e)
		if err != nil {
			t.Logger.Logf(LogWarning, "Dissemination stop failed for disseminator `%s' and entry `%s'", d.Name(), e.HsAlias)
		}
	}
	return nil
}

// Disseminate entry
func (t *Taldir) disseminateStart(e Entry) {
	for _, d := range t.Disseminators {
		err := d.DisseminateStart(&e)
		if err != nil {
			t.Logger.Logf(LogWarning, "Dissemination start failed for disseminator `%s' and entry `%s': %v", d.Name(), e.HsAlias, err)
		}
	}
}

// Disseminate all entries
func (t *Taldir) disseminateEntries() error {
	entries, err := GetAllEntriesFromDatabase(t.DB)
	if nil != err {
		return err
	}
	for _, e := range entries {
		t.disseminateStart(e)
	}
	return nil
}

// HashAlias hashes the alias with its type in a prefix-free fashion
// SHA512(len(atype||alias)||atype||alias)
func HashAlias(atype string, alias string) []byte {
	h := sha512.New()
	b := make([]byte, 4)
	binary.BigEndian.PutUint32(b, uint32(len(atype)+len(alias)))
	h.Write(b)
	h.Write([]byte(atype))
	h.Write([]byte(alias))
	return h.Sum(nil)
}

// Hashes an identity key (see hashAlias) with a salt for
// Lookup and storage.
func saltHAlias(hAlias string, salt string) string {
	h := sha512.New()
	h.Write([]byte(hAlias))
	h.Write([]byte(salt))
	return util.Base32CrockfordEncode(h.Sum(nil))
}

// Called by the registrant to validate the registration request. The reference ID was
// provided "out of band" using a validation method such as email or SMS
func (t *Taldir) validationRequest(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var entry Entry
	var validation Validation
	var confirm ValidationConfirmation
	var errDetail ErrorDetail
	if r.Body == nil {
		http.Error(w, "No request body", http.StatusBadRequest)
		return
	}
	err := json.NewDecoder(r.Body).Decode(&confirm)
	if err != nil {
		errDetail.Code = 1006 //TALER_EC_JSON_INVALID
		errDetail.Hint = "Unable to parse JSON"
		resp, _ := json.Marshal(errDetail)
		w.WriteHeader(http.StatusBadRequest)
		w.Write(resp)
		return
	}
	err = GetFirstValidationByHAliasFromDatabase(t.DB, &validation, vars["h_alias"])
	t.Logger.Logf(LogDebug, "Got validation %v", validation)
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	validation.SolutionAttemptCount++
	if time.UnixMicro(validation.LastSolutionTimeframeStart + t.SolutionTimeframe.Microseconds()).After(time.Now()) {
		if validation.SolutionAttemptCount > t.SolutionAttemptsMax {
			w.WriteHeader(http.StatusTooManyRequests)
			return
		}
	} else {
		t.Logger.Logf(LogDebug, "New solution timeframe set.")
		validation.LastSolutionTimeframeStart = time.Now().UnixMicro()
		validation.SolutionAttemptCount = 1
	}
	UpdateValidationInDatabase(t.DB, &validation)
	t.Logger.Logf(LogDebug, "Generating solution from %s and %s", validation.TargetURI, validation.Challenge)
	expectedSolution := util.GenerateSolution(validation.TargetURI, validation.Challenge)

	t.Logger.Logf(LogDebug, "Expected solution: `%s', given: `%s'\n", expectedSolution, confirm.Solution)
	if confirm.Solution != expectedSolution {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	_, err = DeleteValidationFromDatabase(t.DB, &validation)
	if err != nil {
		t.Logger.Logf(LogError, "Error deleting validation: %v", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	entry.HsAlias = saltHAlias(validation.HAlias, t.Salt)
	tmpDuration := (entry.Duration + validation.Duration) * 1000
	err = GetEntryByHsAliasFromDatabase(t.DB, &entry, entry.HsAlias)
	if err == nil {
		if validation.TargetURI == "" {
			_, err = DeleteEntryFromDatabase(t.DB, &entry)
			if err != nil {
				t.Logger.Logf(LogError, "Error deleting entry: %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			t.Logger.Logf(LogDebug, "Deleted entry for '%s´\n", entry.HsAlias)
			t.disseminateStop(entry)
		} else {
			entry.TargetURI = validation.TargetURI
			entry.Duration = tmpDuration
			err = UpdateEntryInDatabase(t.DB, &entry)
			t.Logger.Logf(LogDebug, "Updated entry in database to: %v", entry)
			if err != nil {
				t.Logger.Logf(LogError, "Error updating entry: %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			t.disseminateStart(entry)
		}
	} else {
		t.Logger.Logf(LogError, "Entry does not yet exist: %v", err)
		if validation.TargetURI == "" {
			t.Logger.Logf(LogWarning, "Validated a deletion request but no entry found for `%s'\n", entry.HsAlias)
		} else {
			entry.TargetURI = validation.TargetURI
			err = InsertEntryIntoDatabase(t.DB, &entry)
			if err != nil {
				t.Logger.Logf(LogError, "Error inserting entry: %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			t.Logger.Logf(LogError, "Inserted entry: %v", entry)
		}
	}
	w.WriteHeader(http.StatusNoContent)
}

func (t *Taldir) isRateLimited(hAlias string) (bool, error) {
	validations, err := GetAllValidationsByHAliasFromDatabase(t.DB, hAlias)
	// NOTE: Check rate limit
	if err == nil {
		// Limit re-initiation attempts to ValidationInitiationMax times
		// within the expiration timeframe of a validation.
		t.Logger.Logf(LogDebug, "Pending validations are %d", len(validations))
		return len(validations) >= int(t.ValidationInitiationMax), nil
	}
	return false, nil
}

func (t *Taldir) registerRequest(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var req RegisterMessage
	var errDetail ErrorDetail
	var validation Validation
	var entry Entry

	// Check if this validation method is supported or not.
	validator, ok := t.Validators[vars["alias_type"]]
	if !ok {
		// FIXME rename GANA entry to alias type
		errDetail.Code = gana.TALDIR_METHOD_NOT_SUPPORTED
		errDetail.Hint = "Unsupported alias_type"
		errDetail.Detail = "Given alias_type: " + vars["alias_type"]
		resp, _ := json.Marshal(errDetail)
		w.WriteHeader(http.StatusNotFound)
		w.Write(resp)
		return
	}
	if r.Body == nil {
		http.Error(w, "No request body", http.StatusBadRequest)
		return
	}
	err := json.NewDecoder(r.Body).Decode(&req)
	if err != nil {
		errDetail.Code = gana.GENERIC_JSON_INVALID
		errDetail.Hint = "Unable to parse JSON"
		resp, _ := json.Marshal(errDetail)
		w.WriteHeader(http.StatusBadRequest)
		w.Write(resp)
		return
	}
	t.Logger.Logf(LogDebug, "Received registerRequest %v", req)

	if req.TargetURI != "" {
		err = t.isPMSValid(req.TargetURI)
		if nil != err {
			errDetail.Code = gana.GENERIC_JSON_INVALID
			errDetail.Hint = err.Error()
			w.Header().Set("Content-Type", "application/json")
			resp, _ := json.Marshal(errDetail)
			w.WriteHeader(http.StatusBadRequest)
			w.Write(resp)
			return
		}
	}

	// Setup validation object. Retrieve object from DB if it already
	// exists.
	hAliasBin := HashAlias(validator.Name(), req.Alias)
	hAlias := util.Base32CrockfordEncode(hAliasBin)
	validation.HAlias = hAlias
	validation.ValidatorName = validator.Name()
	hsAlias := saltHAlias(validation.HAlias, t.Salt)
	err = GetEntryByHsAliasFromDatabase(t.DB, &entry, hsAlias)
	// Round to the nearest multiple of a month
	reqDuration := time.Duration(req.Duration * 1000)
	reqDuration = reqDuration.Round(monthDuration)
	if err == nil {
		t.Logger.Logf(LogDebug, "Found entry in database %v matching %v", entry, hsAlias)
		// Check if  this entry is to be modified or extended
		entryModified := (req.TargetURI != entry.TargetURI)
		entryValidity := time.UnixMicro(entry.CreatedAt + entry.Duration)
		// NOTE: The extension must be at least one month
		t.Logger.Logf(LogDebug, "Entry to be modified: %t, requested (rounded) duration: %d", entryModified, reqDuration.Microseconds())
		if (reqDuration.Microseconds() == 0) && !entryModified {
			// Nothing changed. Return validity
			t.Logger.Logf(LogDebug, "Returning validity of entry")
			w.WriteHeader(http.StatusOK)
			w.Header().Set("Content-Type", "application/json")
			w.Write(fmt.Appendf(make([]byte, 0), "{\"valid_for\": %d}", time.Until(entryValidity).Microseconds()))
			return
		}
	}
	rateLimited, err := t.isRateLimited(hAlias)
	if nil != err {
		t.Logger.Logf(LogError, "Error checking rate limit! %v", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	} else if rateLimited {
		w.WriteHeader(http.StatusTooManyRequests)
		rlResponse := RateLimitedResponse{
			Code:             gana.TALDIR_REGISTER_RATE_LIMITED,
			RequestFrequency: t.ValidationTimeframe.Microseconds() / t.ValidationInitiationMax,
			Hint:             "Registration rate limit reached",
		}
		jsonResp, _ := json.Marshal(rlResponse)
		w.Write(jsonResp)
		DeleteValidationFromDatabase(t.DB, &validation)
		return
	}
	t.Logger.Logf(LogDebug, "Looking for validation with %v %v %v\n", hAlias, req.TargetURI, reqDuration.Microseconds())
	err = GetValidationFromDatabase(t.DB, &validation, hAlias, req.TargetURI, reqDuration)
	validationExists := (nil == err)
	t.Logger.Logf(LogDebug, "Validation exists %v\n", validationExists)
	// FIXME: Always set new challenge?
	validation.Challenge = util.GenerateChallenge(t.ChallengeBytes)
	validation.TargetURI = req.TargetURI
	validation.SolutionAttemptCount = 0
	validation.LastSolutionTimeframeStart = time.Now().UnixMicro()
	validation.Duration = reqDuration.Microseconds()
	validation.CreatedAt = validation.LastSolutionTimeframeStart
	t.Logger.Logf(LogDebug, "Storing new validation %v\n", validation)
	err = InsertValidationIntoDatabase(t.DB, &validation)
	if nil != err {
		t.Logger.Logf(LogError, "Error inserting validation! %v", err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	sliceDuration := time.Duration(validation.Duration * 1000)
	cost, err := util.CalculateCost(t.MonthlyFee.String(),
		validator.ChallengeFee(),
		sliceDuration,
		monthDuration)
	if err != nil {
		fmt.Println(err)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if !cost.IsZero() {
		validation.RequiresPayment = true
		if len(validation.OrderID) == 0 {
			// Add new order for new validations
			// FIXME: What is the URL we want to provide here?
			orderID, newOrderErr := t.Merchant.AddNewOrder(*cost, "Taldir registration", t.BaseURL)
			if newOrderErr != nil {
				fmt.Println(newOrderErr)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			validation.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 := t.Merchant.IsOrderPaid(validation.OrderID)
		if paytoErr != nil {
			w.WriteHeader(http.StatusInternalServerError)
			t.Logger.Logf(LogError, "%s\n", paytoErr)
			return
		}
		if len(payto) != 0 {
			err = UpdateValidationInDatabase(t.DB, &validation)
			if nil != err {
				t.Logger.Logf(LogError, "Error inserting validation! %v", err)
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			w.WriteHeader(http.StatusPaymentRequired)
			w.Header().Set("Taler", payto) // FIXME no idea what to do with this.
			return
		}
		// In this case, this order was paid
	}
	err = UpdateValidationInDatabase(t.DB, &validation)
	if err != nil {
		t.Logger.Logf(LogError, "%s\n", err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	topic := t.I18n.GetLocale(r).GetMessage("taldirRegTopic")
	link := t.Host + "/register/" + url.QueryEscape(validation.HAlias) + "/" + url.QueryEscape(validation.Challenge) + "?alias=" + url.QueryEscape(req.Alias)
	message := t.I18n.GetLocale(r).GetMessage("taldirRegMessage", link)
	redirectionLink, err := validator.RegistrationStart(topic, link, message, req.Alias, validation.Challenge)
	if err != nil {
		t.Logger.Logf(LogError, "%s\n", err.Error())
		DeleteValidationFromDatabase(t.DB, &validation)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	// FIXME does this persist this boolean or do we need to call Db.Save again?
	validation.ChallengeSent = true
	if len(redirectionLink) > 0 {
		// This is dangerous, of course, but our validators are trusted, right?
		w.Header().Set("Location", redirectionLink)
	}
	w.WriteHeader(http.StatusAccepted)
}

func (t *Taldir) oidcValidatorResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	for name, validator := range t.Validators {
		if validator.Type() != ValidatorTypeOIDC {
			continue
		}
		if name != vars["validator"] {
			continue
		}
		oidcValidator := validator.(OidcValidator)
		alias, challenge, err := oidcValidator.ProcessOidcCallback(r)
		if err != nil {
			t.Logger.Logf(LogError, "%s\n", err.Error())
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		ha := HashAlias(validator.Name(), alias)
		hAlias := util.Base32CrockfordEncode(ha)
		http.Redirect(w, r, fmt.Sprintf("/register/%s/%s?alias=%s", hAlias, challenge, alias), http.StatusSeeOther)
		return
	}
	w.WriteHeader(http.StatusNotFound)
}

func (t *Taldir) configResponse(w http.ResponseWriter, r *http.Request) {
	meths := []AliasType{}
	i := 0
	for key := range t.Validators {
		var meth AliasType
		meth.Name = key
		meth.ChallengeFee = t.Validators[key].ChallengeFee()
		i++
		meths = append(meths, meth)
	}
	cfg := VersionResponse{
		Version:    "0:0:0",
		Name:       "taler-directory",
		MonthlyFee: t.Cfg.Ini.GetString("directory", "monthly_fee", "KUDOS:1"),
		AliasType:  meths,
	}
	w.Header().Set("Content-Type", "application/json")
	response, _ := json.Marshal(cfg)
	w.Write(response)
}

func (t *Taldir) validationPage(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var walletLink string
	var alias string
	var png []byte
	var validation Validation

	err := GetFirstValidationByHAliasFromDatabase(t.DB, &validation, vars["h_alias"])
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err != nil {
		// This validation does not exist.
		w.WriteHeader(http.StatusNotFound)
		return
	}
	if vars["challenge"] != validation.Challenge {
		t.Logger.Logf(LogWarning, "Solution does not match challenge!\n")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	alias = r.URL.Query().Get("alias")

	if alias == "" {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	// FIXME requires a prefix-free encoding
	hAliasBin := HashAlias(validation.ValidatorName, alias)
	expectedHAlias := util.Base32CrockfordEncode(hAliasBin)

	if expectedHAlias != validation.HAlias {
		t.Logger.Logf(LogWarning, "Alias does not match challenge!\n")
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// FIXME: This is kind of broken and probably requires wallet support/integration first
	if validation.RequiresPayment {
		t.Logger.Logf(LogWarning, "Validation requires payment\n")
		walletLink = "taler://taldir/" + vars["h_alias"] + "/" + vars["challenge"] + "-wallet"
		png, err = qrcode.Encode(walletLink, qrcode.Medium, 256)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		encodedPng := base64.StdEncoding.EncodeToString(png)

		fullData := map[string]any{
			"version":                t.Cfg.Version,
			"QRCode":                 template.URL("data:image/png;base64," + encodedPng),
			"WalletLink":             template.URL(walletLink),
			"productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")),
		}
		t.ValidationTpl.Execute(w, fullData)
	} else {
		expectedSolution := util.GenerateSolution(validation.TargetURI, validation.Challenge)
		confirmDeletionOrRegistration := ""
		if validation.TargetURI == "" {
			confirmDeletionOrRegistration = t.I18n.GetLocale(r).GetMessage("confirmDelete", alias)
		} else {
			confirmDeletionOrRegistration = t.I18n.GetLocale(r).GetMessage("confirmReg", alias, validation.TargetURI)
		}
		fullData := map[string]any{
			"version":                       t.Cfg.Version,
			"error":                         r.URL.Query().Get("error"),
			"target_uri":                    template.URL(validation.TargetURI),
			"alias":                         template.URL(alias),
			"halias":                        template.URL(validation.HAlias),
			"solution":                      template.URL(expectedSolution),
			"confirmDeletionOrRegistration": template.HTML(confirmDeletionOrRegistration),
			"productDisclaimerShort":        template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")),
			"tr":                            t.I18n.GetLocale(r).GetMessage,
		}
		t.ValidationTpl.Execute(w, fullData)
	}
}

// ClearDatabase nukes the database (for tests)
func (t *Taldir) ClearDatabase() {
	err := ClearDatabase(t.DB)
	if err != nil {
		t.Logger.Logf(LogWarning, "Error clearing database: %v", err)
	}
}

func (t *Taldir) termsResponse(w http.ResponseWriter, r *http.Request) {
	termspath := t.Cfg.Ini.GetFilename("directory", "default_terms_path", "terms/", t.Cfg.Datahome)
	tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{
		DefaultFileType:    t.Cfg.Ini.GetString("directory", "default_doc_filetype", "text/html"),
		DefaultLanguage:    t.Cfg.Ini.GetString("directory", "default_doc_lang", "en"),
		SupportedFileTypes: strings.Split(t.Cfg.Ini.GetString("directory", "supported_doc_filetypes", ""), " "),
	})
}

func (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) {
	pppath := t.Cfg.Ini.GetFilename("directory", "default_pp_path", "privacy/", t.Cfg.Datahome)
	tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{
		DefaultFileType:    t.Cfg.Ini.GetString("directory", "default_doc_filetype", "text/html"),
		DefaultLanguage:    t.Cfg.Ini.GetString("directory", "default_doc_lang", "en"),
		SupportedFileTypes: strings.Split(t.Cfg.Ini.GetString("directory", "supported_doc_filetypes", ""), " "),
	})
}

func (t *Taldir) landingPage(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	translateFunc := t.I18n.GetLocale(r).GetMessage
	fullData := map[string]any{
		"validators":                  t.Validators,
		"version":                     t.Cfg.Version,
		"lookupOrRegisterCardTitle":   template.HTML(translateFunc("lookup")),
		"selectAliasToLookupCardText": template.HTML(translateFunc("selectAliasToLookup")),
		"registerCardText":            template.HTML(translateFunc("howtoRegisterOrModify")),
		"productDisclaimerShort":      template.HTML(translateFunc("productDisclaimerShort")),
		"error":                       translateFunc(r.URL.Query().Get("error")),
		"tr":                          translateFunc,
	}
	err := t.LandingPageTpl.Execute(w, fullData)
	if err != nil {
		fmt.Println(err)
	}
}

func (t *Taldir) imprintPage(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	translateFunc := t.I18n.GetLocale(r).GetMessage
	fullData := map[string]any{
		"validators":             t.Validators,
		"version":                t.Cfg.Version,
		"productDisclaimerShort": template.HTML(translateFunc("productDisclaimerShort")),
		"error":                  translateFunc(r.URL.Query().Get("error")),
		"tr":                     translateFunc,
	}
	err := t.ImprintTpl.Execute(w, fullData)
	if err != nil {
		fmt.Println(err)
	}
}

func (t *Taldir) aboutPage(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	translateFunc := t.I18n.GetLocale(r).GetMessage
	fullData := map[string]any{
		"validators":             t.Validators,
		"version":                t.Cfg.Version,
		"productDisclaimerShort": template.HTML(translateFunc("productDisclaimerShort")),
		"productDisclaimer":      template.HTML(translateFunc("productDisclaimer")),
		"error":                  translateFunc(r.URL.Query().Get("error")),
		"tr":                     translateFunc,
	}
	err := t.AboutPageTpl.Execute(w, fullData)
	if err != nil {
		fmt.Println(err)
	}
}

func (t *Taldir) typeLookupResultPage(w http.ResponseWriter, r *http.Request) {
	var entry Entry
	vars := mux.Vars(r)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Check if this alias type is supported or not.
	val, ok := t.Validators[vars["alias_type"]]
	if !ok {
		w.WriteHeader(http.StatusNotFound)
		return
	}

	// Check if alias is valid
	alias := r.URL.Query().Get("alias")
	err := val.IsAliasValid(alias)
	emsg := ""
	found := false
	if nil != err {
		t.Logger.Logf(LogWarning, "Not a valid alias\n")
		emsg = t.I18n.GetLocale(r).GetMessage("aliasInvalid", alias)
		http.Redirect(w, r, fmt.Sprintf("/landing/"+val.Name()+"?error=%s", emsg), http.StatusSeeOther)
		return
	} else {
		hAliasBin := HashAlias(val.Name(), r.URL.Query().Get("alias"))
		hAlias := util.Base32CrockfordEncode(hAliasBin[:])
		hsAlias := saltHAlias(hAlias, t.Salt)
		err := GetEntryByHsAliasFromDatabase(t.DB, &entry, hsAlias)
		if err != nil {
			t.Logger.Logf(LogError, "`%s` not found.\n", hAlias)
		} else {
			found = true
		}
	}
	encodedPng := ""
	talerAddContactURI := ""
	if found && strings.HasPrefix(entry.TargetURI, "https://") {
		// This could be a mailbox URI and we can create a helper QR code for import
		hostDomain := strings.TrimPrefix(entry.TargetURI, "https://")
		talerAddContactURI, err = url.JoinPath("taler://add-contact", val.Name(), r.URL.Query().Get("alias"), hostDomain)
		if nil == err {
			talerAddContactURI += "?sourceBaseUrl=" + url.QueryEscape(t.BaseURL)
			qrPng, qrErr := qrcode.Encode(talerAddContactURI, qrcode.Medium, 256)
			if qrErr != nil {
				t.Logger.Logf(LogError, "Failed to create QR code")
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
			encodedPng = base64.StdEncoding.EncodeToString(qrPng)
		}
	}

	fullData := map[string]any{
		"version":                t.Cfg.Version,
		"qrCode":                 template.URL("data:image/png;base64," + encodedPng),
		"talerAddContactURI":     template.URL(talerAddContactURI),
		"available":              !found,
		"alias_type":             val.Name(),
		"alias":                  r.URL.Query().Get("alias"),
		"result":                 entry.TargetURI,
		"error":                  emsg,
		"productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")),
		"tr":                     t.I18n.GetLocale(r).GetMessage,
	}
	err = t.LookupResultPageTpl.Execute(w, fullData)
	if err != nil {
		fmt.Println(err)
	}
}

func (t *Taldir) typeLandingPage(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Check if this alias type is supported or not.
	val, ok := t.Validators[vars["alias_type"]]
	if !ok {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	fullData := map[string]any{
		"version":                t.Cfg.Version,
		"error":                  r.URL.Query().Get("error"),
		"productDisclaimerShort": template.HTML(t.I18n.GetLocale(r).GetMessage("productDisclaimerShort")),
		"tr":                     t.I18n.GetLocale(r).GetMessage,
	}
	err := val.LandingPageTpl().Execute(w, fullData)
	if err != nil {
		fmt.Println(err)
	}
}

func (t *Taldir) setupHandlers() {
	t.Router = mux.NewRouter().StrictSlash(true)

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

	/* About page */
	t.Router.HandleFunc("/about", t.aboutPage).Methods("GET")

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

	/* Assets HTML */
	t.Router.PathPrefix("/css").Handler(http.StripPrefix("/css", http.FileServer(http.Dir(t.getFileName("static/css")))))
	t.Router.PathPrefix("/images").Handler(http.StripPrefix("/images", http.FileServer(http.Dir(t.getFileName("static/images")))))
	t.Router.PathPrefix("/fontawesome").Handler(http.StripPrefix("/fontawesome", http.FileServer(http.Dir(t.getFileName("static/fontawesome")))))

	/* Registration API */
	t.Router.HandleFunc("/", t.landingPage).Methods("GET")
	t.Router.HandleFunc("/{h_alias}", t.getSingleEntry).Methods("GET")
	t.Router.HandleFunc("/lookup/{alias_type}", t.typeLookupResultPage).Methods("GET")
	t.Router.HandleFunc("/landing/{alias_type}", t.typeLandingPage).Methods("GET")
	t.Router.HandleFunc("/register/{alias_type}", t.registerRequest).Methods("POST")
	t.Router.HandleFunc("/register/{h_alias}/{challenge}", t.validationPage).Methods("GET")
	t.Router.HandleFunc("/{h_alias}", t.validationRequest).Methods("POST")

	// OIDC validator callback URI(s)
	t.Router.HandleFunc("/oidc_validator/{validator}", t.oidcValidatorResponse).Methods("GET")

}

var pluralizeClient = pluralize.NewClient()

func getFuncs(current *i18n.Locale) template.FuncMap {
	return template.FuncMap{
		"plural": func(word string, count int) string {
			return pluralizeClient.Pluralize(word, count, true)
		},
	}
}

func (t *Taldir) getFileName(relativeFileName string) string {
	_, err := os.Stat(relativeFileName)
	if errors.Is(err, os.ErrNotExist) {
		_, err := os.Stat(t.Cfg.Datahome + "/" + relativeFileName)
		if errors.Is(err, os.ErrNotExist) {
			t.Logger.Logf(LogError, "Tried fallback not found %s\n", t.Cfg.Datahome+"/"+relativeFileName)
			return ""
		}
		return t.Cfg.Datahome + "/" + relativeFileName
	}
	return relativeFileName
}

// Initialize the Taldir instance with cfgfile
func (t *Taldir) Initialize(cfg TaldirConfig) {
	t.Cfg = cfg
	t.Logger = TaldirLogger{
		InternalLogger: log.New(os.Stdout, "taler-directory:", log.LstdFlags),
		logLevel:       cfg.Loglevel,
	}
	// FIXME localedir
	i18n, err := i18n.New(i18n.Glob("./locales/*/*", i18n.LoaderConfig{
		// Set custom functions per locale!
		Funcs: getFuncs,
	}), "en-US", "de-DE")
	if err != nil {
		panic(err)
	}
	t.I18n = i18n

	navTplFile := cfg.Ini.GetFilename("directory", "navigation", "web/templates/nav.html", t.Cfg.Datahome)
	footerTplFile := cfg.Ini.GetFilename("directory", "footer", "web/templates/footer.html", t.Cfg.Datahome)
	t.BaseURL = cfg.Ini.GetString("directory", "base_url", "http://localhost:11000")
	t.Validators = make(map[string]Validator)
	for _, sec := range cfg.Ini.IterateSections("directory-validator-") {
		if !strings.HasPrefix(sec, "directory-validator-") {
			continue
		}
		vname := strings.TrimPrefix(sec, "directory-validator-")
		if !cfg.Ini.GetBool(sec, "enabled", false) {
			t.Logger.Logf(LogWarning, "`Validator `%s' disabled.\n", vname)
			continue
		}
		vlandingPageTplFile := cfg.Ini.GetFilename(sec, "registration_page", "web/templates/landing_"+vname+".html", t.Cfg.Datahome)
		vlandingPageTpl, err := template.ParseFiles(vlandingPageTplFile, navTplFile, footerTplFile)
		if err != nil {
			t.Logger.Logf(LogWarning, "`%s` template not found, disabling validator `%s`: `%v`\n", vlandingPageTplFile, vname, err)
			continue
		}
		var v Validator
		vtype := cfg.Ini.GetString(sec, "type", "")
		if len(vtype) == 0 {
			t.Logger.Logf(LogWarning, "`type` key in section `[%s]` not found, disabling validator.\n", sec)
			continue
		}
		switch vtype {
		case string(ValidatorTypeCommand):
			v = Validator(makeCommandValidator(&cfg, vname, vlandingPageTpl))
		case string(ValidatorTypeOIDC):
			v = makeOidcValidator(&cfg, vname, vlandingPageTpl)
		default:
			t.Logger.Logf(LogWarning, "`%s` type unknown, disabling validator `%s`.\n", vtype, vname)
			continue
		}
		t.Validators[vname] = v
		t.Logger.Logf(LogDebug, "`%s` validator enabled.\n", vname)
	}
	t.Logger.Logf(LogDebug, "Found %d validators.\n", len(t.Validators))
	t.Disseminators = make(map[string]Disseminator)
	gnsdisseminator := makeGnsDisseminator(&cfg)
	if gnsdisseminator.IsEnabled() {
		t.Disseminators[gnsdisseminator.Name()] = &gnsdisseminator
		t.Logger.Logf(LogInfo, "Disseminator `%s' enabled.\n", gnsdisseminator.Name())
	}
	t.ChallengeBytes = cfg.Ini.GetInt("directory", "challenge_bytes", 16)
	t.ValidationInitiationMax = cfg.Ini.GetInt64("directory", "validation_initiation_max", 3)
	t.SolutionAttemptsMax = cfg.Ini.GetInt("directory", "solution_attempt_max", 3)

	t.ValidPMSRegex = cfg.Ini.GetString("directory", "valid_payment_system_address_regex", ".*")
	t.ValidationTimeframe, err = cfg.Ini.GetDuration("directory", "validation_timeframe", time.Minute*5)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}

	t.SolutionTimeframe, err = cfg.Ini.GetDuration("directory", "solution_attempt_timeframe", time.Hour)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	t.MonthlyFee, err = cfg.Ini.GetAmount("directory", "monthly_fee", &talerutil.Amount{})
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}

	t.DB = cfg.Db
	if cfg.Ini.GetBool("directory", "purge_mappings_on_startup_dangerous", false) {
		t.Logger.Logf(LogWarning, "DANGER Purging mappings!")
		num, err := DeleteAllEntriesFromDatabase(t.DB)
		if err != nil {
			t.Logger.Logf(LogDebug, "Error purging entries: `%v'.\n", err)
		}
		t.Logger.Logf(LogDebug, "Deleted %d entries.\n", num)
	}
	// Clean up validations
	validationExp, err := cfg.Ini.GetDuration("directory", "validation_expiration", time.Hour*24)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	go func() {
		for {
			num, err := DeleteStaleValidationsFromDatabase(t.DB, validationExp)
			if err != nil {
				t.Logger.Logf(LogDebug, "Error purging stale validations: `%v'.\n", err)
			}
			t.Logger.Logf(LogInfo, "Cleaned up %d stale validations.\n", num)
			time.Sleep(validationExp)
		}
	}()
	imprintTplFile := cfg.Ini.GetFilename("directory", "imprint_page", "web/templates/imprint.html", t.Cfg.Datahome)
	t.ImprintTpl, err = template.ParseFiles(imprintTplFile, navTplFile, footerTplFile)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	validationLandingTplFile := cfg.Ini.GetFilename("directory", "validation_landing", "web/templates/validation_landing.html", t.Cfg.Datahome)
	t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile, navTplFile, footerTplFile)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	landingTplFile := cfg.Ini.GetFilename("directory", "landing_page", "web/templates/landing.html", t.Cfg.Datahome)
	t.LandingPageTpl, err = template.ParseFiles(landingTplFile, navTplFile, footerTplFile)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	lookupResultTplFile := cfg.Ini.GetFilename("directory", "lookup_result_page", "web/templates/lookup_result.html", t.Cfg.Datahome)
	t.LookupResultPageTpl, err = template.ParseFiles(lookupResultTplFile, navTplFile, footerTplFile)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	aboutTplFile := cfg.Ini.GetFilename("directory", "about_page", "web/templates/about.html", t.Cfg.Datahome)
	t.AboutPageTpl, err = template.ParseFiles(aboutTplFile, navTplFile, footerTplFile)
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	t.Salt = os.Getenv("TALDIR_SALT")
	if t.Salt == "" {
		t.Salt = cfg.Ini.GetString("directory", "salt", "ChangeMe")
	}
	t.Host = cfg.Ini.GetString("directory", "base_url", "http://localhost")
	t.Merchant = cfg.Merchant
	merchConfig, err := t.Merchant.GetConfig()
	if err != nil {
		t.Logger.InternalLogger.Fatal(err)
		os.Exit(1)
	}
	currencySpec, currencySupported := merchConfig.Currencies[t.MonthlyFee.Currency]
	for !currencySupported {
		t.Logger.InternalLogger.Fatalf("Currency `%s' not supported by merchant!\n", t.MonthlyFee.Currency)
		os.Exit(1)
	}
	t.CurrencySpec = currencySpec
	t.setupHandlers()
	t.disseminateEntries()
}
