// 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

/* TODO
- ToS compression
- ToS etag
*/

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

	"github.com/gertd/go-pluralize"
	"github.com/gorilla/mux"
	"github.com/kataras/i18n"
	"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"
	"gopkg.in/ini.v1"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"taler.net/taldir/internal/gana"
	"taler.net/taldir/internal/util"
)

type TaldirConfig struct {
	// The config to use
	Ini *ini.File

	// The Taldir Version
	Version string

	// The Taldir data home location (usually $datadir/taldir)
	Datahome string

	// The database connection to use
	Db gorm.Dialector

	// The merchant connection to use
	Merchant merchant.Merchant
}

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

	// The main router
	Router *mux.Router

	// The main DB handle
	Db *gorm.DB

	// Our configuration from the config.json
	Cfg TaldirConfig

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

	// 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 address 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 string

	// Registrar base URL
	BaseUrl string

	// Currency Spec
	CurrencySpec talerutil.CurrencySpecification

	// I18n
	I18n *i18n.I18n
}

type ValidatorType string

const (
	ValidatorTypeCommand ValidatorType = "command"
	ValidatorTypeOIDC    ValidatorType = "oidc"
)

type Validator struct {

	// Validator name
	Name string

	// Validator alias regex
	ValidAliasRegex string

	// Validator type
	Type ValidatorType

	// Amount of payment required
	ChallengeFee string

	// Does this validator require payment
	PaymentRequired bool

	// The command to call for validation
	Command string

	// registration/lookup page
	LandingPageTpl *template.Template
}

// 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 registration methods
	Methods []Method `json:"methods"`

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

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

	// Name of the method, 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 {

	// Address, in method-specific format
	Address string `json:"address"`

	// Target URI to associate with this address
	TargetUri string `json:"target_uri"`

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

// Entry is a mapping from the identity key hash to a wallet key
// The identity key hash is sha512(sha512(address)|salt) where identity is
// one of the identity key types supported (e.g. an email address)
type Entry struct {

	// ORM
	gorm.Model `json:"-"`

	// The salted hash (SHA512) of the hashed address (h_address)
	HsAddress string `json:"-"`

	// Target URI to associate with this address
	TargetUri string `json:"target_uri"`

	// How long the registration lasts in microseconds
	Duration time.Duration `json:"-"`
}

// Validation is the object created when a registration for an entry is initiated.
// The Validation stores the identity key (sha256(identity)) the secret
// Validation reference. The Validation reference is sent to the identity
// depending on the out-of-band channel defined through the identity key type.
type Validation struct {

	// ORM
	gorm.Model `json:"-"`

	// The hash (SHA512) of the address
	HAddress string `json:"h_address"`

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

	// Target URI to associate with this address
	TargetUri string `json:"target_uri"`

	// The activation code sent to the client
	Challenge string `json:"-"`

	// The challenge has been sent already
	ChallengeSent bool `json:"-"`

	// true if this validation also requires payment
	RequiresPayment bool `json:"-"`

	// How often was a solution for this validation tried
	SolutionAttemptCount int

	// The beginning of the last solution timeframe
	LastSolutionTimeframeStart time.Time

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

// 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 errors.New(fmt.Sprintf("Payment System Address `%s' invalid", pms)) // TODO i18n
		}
	}
	return
}

func (v *Validator) isAliasValid(alias string) (err error) {
	log.Println(v.ValidAliasRegex)
	if v.ValidAliasRegex != "" {
		matched, _ := regexp.MatchString(v.ValidAliasRegex, alias)
		if !matched {
			return errors.New(fmt.Sprintf("Alias `%s' invalid", alias)) // TODO i18n
		}
	}
	return
}

// Primary lookup function.
// Allows the caller to query a wallet key using the hash(!) of the
// identity, e.g. SHA512(<email address>)
func (t *Taldir) getSingleEntry(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var entry Entry
	hsAddress := saltHAddress(vars["h_address"], t.Salt)
	var err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error
	if err == nil {
		w.Header().Set("Content-Type", "application/json")
		resp, _ := json.Marshal(entry)
		w.Write(resp)
		return
	}
	w.WriteHeader(http.StatusNotFound)
}

// Hashes an identity key (e.g. sha256(<email address>)) with a salt for
// Lookup and storage.
func saltHAddress(hAddress string, salt string) string {
	h := sha512.New()
	h.Write([]byte(hAddress))
	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", 400)
		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(400)
		w.Write(resp)
		return
	}
	err = t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error
	if err != nil {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	validation.SolutionAttemptCount++
	if validation.LastSolutionTimeframeStart.Add(t.SolutionTimeframe).After(time.Now()) {
		if validation.SolutionAttemptCount > t.SolutionAttemptsMax {
			w.WriteHeader(429)
			return
		}
	} else {
		log.Println("New solution timeframe set.")
		validation.LastSolutionTimeframeStart = time.Now()
		validation.SolutionAttemptCount = 1
	}
	t.Db.Save(&validation)
	expectedSolution := util.GenerateSolution(validation.TargetUri, validation.Challenge)
	log.Printf("Expected solution: `%s', given: `%s'\n", expectedSolution, confirm.Solution)
	if confirm.Solution != expectedSolution {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	err = t.Db.Delete(&validation).Error
	if err != nil {
		log.Println("Error deleting validation")
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	entry.HsAddress = saltHAddress(validation.HAddress, t.Salt)
	entry.TargetUri = validation.TargetUri
	tmpDuration := (entry.Duration.Microseconds() + validation.Duration) * 1000
	entry.Duration = time.Duration(tmpDuration)
	err = t.Db.First(&entry, "hs_address = ?", entry.HsAddress).Error
	if err == nil {
		if validation.TargetUri == "" {
			log.Printf("Deleted entry for '%s´\n", entry.HsAddress)
			err = t.Db.Delete(&entry).Error
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
		} else {
			t.Db.Save(&entry)
		}
	} else {
		if validation.TargetUri == "" {
			log.Printf("Validated a deletion request but no entry found for `%s'\n", entry.HsAddress)
		} else {
			err = t.Db.Create(&entry).Error
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				return
			}
		}
	}
	w.WriteHeader(http.StatusNoContent)
}

func (t *Taldir) isRateLimited(hAddress string) (bool, error) {
	var validations []Validation
	res := t.Db.Where("h_address = ?", hAddress).Find(&validations)
	// NOTE: Check rate limit
	if res.Error == nil {
		// Limit re-initiation attempts to ValidationInitiationMax times
		// within the expiration timeframe of a validation.
		return res.RowsAffected >= 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["method"]]
	if !ok {
		errDetail.Code = gana.TALDIR_METHOD_NOT_SUPPORTED
		errDetail.Hint = "Unsupported method"
		errDetail.Detail = "Given method: " + vars["method"]
		resp, _ := json.Marshal(errDetail)
		w.WriteHeader(http.StatusNotFound)
		w.Write(resp)
		return
	}
	if r.Body == nil {
		http.Error(w, "No request body", 400)
		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(400)
		w.Write(resp)
		return
	}

	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(400)
			w.Write(resp)
			return
		}
	}

	// Setup validation object. Retrieve object from DB if it already
	// exists.
	h := sha512.New()
	h.Write([]byte(req.Address))
	hAddress := util.Base32CrockfordEncode(h.Sum(nil))
	validation.HAddress = hAddress
	hsAddress := saltHAddress(validation.HAddress, t.Salt)
	err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error
	// Round to the nearest multiple of a month
	reqDuration := time.Duration(req.Duration * 1000)
	reqDuration = reqDuration.Round(monthDuration)
	if err == nil {
		// Check if  this entry is to be modified or extended
		entryModified := (req.TargetUri != entry.TargetUri)
		entryValidity := entry.CreatedAt.Add(entry.Duration)
		// NOTE: The extension must be at least one month
		if (reqDuration.Microseconds() == 0) && !entryModified {
			// Nothing changed. Return validity
			w.WriteHeader(http.StatusOK)
			w.Header().Set("Content-Type", "application/json")
			w.Write([]byte(fmt.Sprintf("{\"valid_for\": %d}", time.Until(entryValidity).Microseconds())))
			return
		}
	}
	rateLimited, err := t.isRateLimited(hAddress)
	if nil != err {
		log.Printf("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)
		t.Db.Delete(&validation)
		return
	}
	err = t.Db.First(&validation, "h_address = ? AND target_uri = ? AND duration = ?",
		hAddress, req.TargetUri, reqDuration).Error
	validationExists := (nil == err)
	// FIXME: Always set new challenge?
	validation.Challenge = util.GenerateChallenge(t.ChallengeBytes)
	if !validationExists {
		validation.TargetUri = req.TargetUri
		validation.SolutionAttemptCount = 0
		validation.LastSolutionTimeframeStart = time.Now()
		validation.Duration = reqDuration.Microseconds()
	}

	sliceDuration := time.Duration(validation.Duration * 1000)
	cost, err := util.CalculateCost(t.MonthlyFee,
		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 {
			fmt.Println(paytoErr)
			w.WriteHeader(http.StatusInternalServerError)
			log.Println(paytoErr)
			return
		}
		if len(payto) != 0 {
			t.Db.Save(&validation)
			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 = t.Db.Save(&validation).Error
	if err != nil {
		log.Println(err)
		w.WriteHeader(500)
		return
	}
	path, err := exec.LookPath(validator.Command)
	if err != nil {
		log.Println(err)
		t.Db.Delete(&validation)
		w.WriteHeader(500)
		return
	}
	log.Printf("Found `%s' in path as `%s'\n", validator.Command, path)
	topic := t.I18n.GetLocale(r).GetMessage("taldirRegTopic")
	link := t.Host + "/register/" + url.QueryEscape(validation.HAddress) + "/" + url.QueryEscape(validation.Challenge) + "?address=" + url.QueryEscape(req.Address)
	message := t.I18n.GetLocale(r).GetMessage("taldirRegMessage", link)
	out, err := exec.Command(path, req.Address, validation.Challenge, topic, message).Output()
	log.Printf("Executing `%s %s %s %s %s`\n", path, req.Address, validation.Challenge, topic, message)
	if err != nil {
		fmt.Printf("%s, %v\n", out, err)
		t.Db.Delete(&validation)
		w.WriteHeader(500)
		return
	}
	// FIXME does this persist this boolean or do we need to call Db.Save again?
	validation.ChallengeSent = true
	w.WriteHeader(202)
}

func (t *Taldir) configResponse(w http.ResponseWriter, r *http.Request) {
	meths := []Method{}
	i := 0
	for key := range t.Validators {
		var meth Method
		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.Section("taldir").Key("monthly_fee").MustString("KUDOS:1"),
		Methods:    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 address string
	var png []byte
	var validation Validation

	err := t.Db.First(&validation, "h_address = ?", vars["h_address"]).Error
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err != nil {
		// This validation does not exist.
		w.WriteHeader(404)
		return
	}
	if vars["challenge"] != validation.Challenge {
		log.Println("Solution does not match challenge!")
		w.WriteHeader(400)
		return
	}

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

	if address == "" {
		w.WriteHeader(404)
		return
	}

	h := sha512.New()
	h.Write([]byte(address))
	expectedHAddress := util.Base32CrockfordEncode(h.Sum(nil))

	if expectedHAddress != validation.HAddress {
		log.Println("Address does not match challenge!")
		w.WriteHeader(400)
		return
	}

	// FIXME: This is kind of broken and probably requires wallet support/integration first
	if validation.RequiresPayment {
		log.Println("Validation requires payment")
		walletLink = "taler://taldir/" + vars["h_address"] + "/" + vars["challenge"] + "-wallet"
		png, err = qrcode.Encode(walletLink, qrcode.Medium, 256)
		if err != nil {
			w.WriteHeader(500)
			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", address)
		} else {
			confirmDeletionOrRegistration = t.I18n.GetLocale(r).GetMessage("confirmReg", address, validation.TargetUri)
		}
		fullData := map[string]any{
			"version":                       t.Cfg.Version,
			"error":                         r.URL.Query().Get("error"),
			"target_uri":                    template.URL(validation.TargetUri),
			"address":                       template.URL(address),
			"haddress":                      template.URL(validation.HAddress),
			"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)
	}
	return
}

// ClearDatabase nukes the database (for tests)
func (t *Taldir) ClearDatabase() {
	t.Db.Where("1 = 1").Delete(&Entry{})
	t.Db.Where("1 = 1").Delete(&Validation{})
}

func (t *Taldir) termsResponse(w http.ResponseWriter, r *http.Request) {
	s := t.Cfg.Ini.Section("taldir")
	termspath := t.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 (t *Taldir) privacyResponse(w http.ResponseWriter, r *http.Request) {
	s := t.Cfg.Ini.Section("mailbox")
	pppath := t.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 (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]interface{}{
		"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)
	}
	return
}

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]interface{}{
		"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)
	}
	return
}

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]interface{}{
		"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)
	}
	return
}

func (t *Taldir) methodLookupResultPage(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 validation method is supported or not.
	val, ok := t.Validators[vars["method"]]
	if !ok {
		w.WriteHeader(404)
		return
	}

	// Check if alias is valid
	alias := r.URL.Query().Get("address")
	err := val.isAliasValid(alias)
	emsg := ""
	found := false
	if nil != err {
		log.Printf("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 {
		hAddressBin := sha512.Sum512([]byte(r.URL.Query().Get("address")))
		hAddress := util.Base32CrockfordEncode(hAddressBin[:])
		hsAddress := saltHAddress(hAddress, t.Salt)
		err = t.Db.First(&entry, "hs_address = ?", hsAddress).Error
		if err != nil {
			log.Printf("`%s` not found.\n", hAddress)
		} else {
			found = true
		}
	}
	fullData := map[string]interface{}{
		"version":                t.Cfg.Version,
		"available":              !found,
		"method":                 val.Name,
		"address":                r.URL.Query().Get("address"),
		"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) methodLandingPage(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	w.Header().Set("Content-Type", "text/html; charset=utf-8")

	// Check if this validation method is supported or not.
	val, ok := t.Validators[vars["method"]]
	if !ok {
		w.WriteHeader(404)
		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("./static/css"))))
	t.Router.PathPrefix("/images").Handler(http.StripPrefix("/images", http.FileServer(http.Dir("./static/images"))))
	t.Router.PathPrefix("/fontawesome").Handler(http.StripPrefix("/fontawesome", http.FileServer(http.Dir("./static/fontawesome"))))

	/* Registration API */
	t.Router.HandleFunc("/", t.landingPage).Methods("GET")
	t.Router.HandleFunc("/{h_address}", t.getSingleEntry).Methods("GET")
	t.Router.HandleFunc("/lookup/{method}", t.methodLookupResultPage).Methods("GET")
	t.Router.HandleFunc("/landing/{method}", t.methodLandingPage).Methods("GET")
	t.Router.HandleFunc("/register/{method}", t.registerRequest).Methods("POST")
	t.Router.HandleFunc("/register/{h_address}/{challenge}", t.validationPage).Methods("GET")
	t.Router.HandleFunc("/{h_address}", t.validationRequest).Methods("POST")

}

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) {
			log.Printf("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
	// 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
	if t.Cfg.Ini.Section("taldir").Key("production").MustBool(false) {
		fmt.Println("Production mode enabled")
	}

	navTplFile := cfg.Ini.Section("taldir").Key("navigation").MustString(t.getFileName("web/templates/nav.html"))
	footerTplFile := cfg.Ini.Section("taldir").Key("footer").MustString(t.getFileName("web/templates/footer.html"))
	t.BaseUrl = cfg.Ini.Section("taldir").Key("base_url").MustString("http://localhost:11000")
	t.Validators = make(map[string]Validator)
	for _, sec := range cfg.Ini.Sections() {
		if !strings.HasPrefix(sec.Name(), "taldir-validator-") {
			continue
		}
		if !sec.HasKey("type") {
			log.Printf("`type` key in section `[%s]` not found, disabling validator.\n", sec.Name())
			continue
		}
		vname := strings.TrimPrefix(sec.Name(), "taldir-validator-")
		vlandingPageTplFile := sec.Key("registration_page").MustString(t.getFileName("web/templates/landing_" + vname + ".html"))
		vlandingPageTpl, err := template.ParseFiles(vlandingPageTplFile, navTplFile, footerTplFile)
		if err != nil {
			log.Printf("`%s` template not found, disabling validator `%s`.\n", vlandingPageTplFile, vname)
			continue
		}
		t.Validators[vname] = Validator{
			Name:            vname,
			LandingPageTpl:  vlandingPageTpl,
			ChallengeFee:    sec.Key("challenge_fee").MustString("KUDOS:0"),
			PaymentRequired: sec.Key("enabled").MustBool(false),
			Command:         sec.Key("command").MustString(""),
			Type:            ValidatorType(sec.Key("type").MustString("")),
			ValidAliasRegex: sec.Key("valid_alias_regex").MustString(""),
		}
	}
	t.ChallengeBytes = cfg.Ini.Section("taldir").Key("challenge_bytes").MustInt(16)
	t.ValidationInitiationMax = cfg.Ini.Section("taldir").Key("validation_initiation_max").MustInt64(3)
	t.SolutionAttemptsMax = cfg.Ini.Section("taldir").Key("solution_attempt_max").MustInt(3)

	validationTTLStr := cfg.Ini.Section("taldir").Key("validation_timeframe").MustString("5m")
	t.ValidPMSRegex = cfg.Ini.Section("taldir").Key("valid_payment_system_address_regex").MustString(".*")
	t.ValidationTimeframe, err = time.ParseDuration(validationTTLStr)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}

	retryTimeframeStr := cfg.Ini.Section("taldir").Key("solution_attempt_timeframe").MustString("1h")
	t.SolutionTimeframe, err = time.ParseDuration(retryTimeframeStr)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	t.MonthlyFee = cfg.Ini.Section("taldir").Key("monthly_fee").MustString("KUDOS:0")

	_db, err := gorm.Open(cfg.Db, &gorm.Config{
		Logger: logger.Default.LogMode(logger.Silent),
	})
	if err != nil {
		panic(err)
	}
	t.Db = _db
	if err := t.Db.AutoMigrate(&Entry{}); err != nil {
		panic(err)
	}
	if err := t.Db.AutoMigrate(&Validation{}); err != nil {
		panic(err)
	}
	if cfg.Ini.Section("taldir").Key("purge_mappings_on_startup_dangerous").MustBool(false) {
		log.Println("DANGER Purging mappings!")
		tx := t.Db.Where("1 = 1").Delete(&Entry{})
		log.Printf("Deleted %d entries.\n", tx.RowsAffected)
	}
	// Clean up validations
	validationExpStr := cfg.Ini.Section("taldir").Key("validation_expiration").MustString("24h")
	validationExp, err := time.ParseDuration(validationExpStr)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	go func() {
		for {
			tx := t.Db.Where("created_at < ?", time.Now().Add(-validationExp)).Delete(&Validation{})
			log.Printf("Cleaned up %d stale validations.\n", tx.RowsAffected)
			time.Sleep(validationExp)
		}
	}()
	imprintTplFile := cfg.Ini.Section("taldir").Key("imprint_page").MustString(t.getFileName("web/templates/imprint.html"))
	t.ImprintTpl, err = template.ParseFiles(imprintTplFile, navTplFile, footerTplFile)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	validationLandingTplFile := cfg.Ini.Section("taldir").Key("validation_landing").MustString(t.getFileName("web/templates/validation_landing.html"))
	t.ValidationTpl, err = template.ParseFiles(validationLandingTplFile, navTplFile, footerTplFile)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	landingTplFile := cfg.Ini.Section("taldir").Key("landing_page").MustString(t.getFileName("web/templates/landing.html"))
	t.LandingPageTpl, err = template.ParseFiles(landingTplFile, navTplFile, footerTplFile)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	lookupResultTplFile := cfg.Ini.Section("taldir").Key("lookup_result_page").MustString(t.getFileName("web/templates/lookup_result.html"))
	t.LookupResultPageTpl, err = template.ParseFiles(lookupResultTplFile, navTplFile, footerTplFile)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	aboutTplFile := cfg.Ini.Section("taldir").Key("about_page").MustString(t.getFileName("web/templates/about.html"))
	t.AboutPageTpl, err = template.ParseFiles(aboutTplFile, navTplFile, footerTplFile)
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	t.Salt = os.Getenv("TALDIR_SALT")
	if t.Salt == "" {
		t.Salt = cfg.Ini.Section("taldir").Key("salt").MustString("ChangeMe")
	}
	t.Host = cfg.Ini.Section("taldir").Key("base_url").MustString("http://localhost")
	t.Merchant = cfg.Merchant
	registrationCost, _ := talerutil.ParseAmount(t.MonthlyFee)
	merchConfig, err := t.Merchant.GetConfig()
	if err != nil {
		log.Fatal(err)
		os.Exit(1)
	}
	currencySpec, currencySupported := merchConfig.Currencies[registrationCost.Currency]
	for !currencySupported {
		log.Fatalf("Currency `%s' not supported by merchant!\n", registrationCost.Currency)
		os.Exit(1)
	}
	t.CurrencySpec = currencySpec
	t.setupHandlers()
}
