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

import (
	"bytes"
	"crypto/ed25519"
	"crypto/sha256"
	"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 TalerMailboxBoxySize int

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
}

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

	// MessageFee for sending message
	MessageFee *talerutil.Amount

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

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

	// fee for one month of registration
	MessageFee string `json:"message_fee"`

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

	// How long will the service store a message
	// before giving up
	DeliveryPeriod uint64 `json:"delivery_period"`

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

// MessageDeletionRequest is used to request the deletion of already received
// messages from the mailbox.
type MessageDeletionRequest struct {

	// Number of messages to delete. (Starting from the beginning
	// of the latest GET response).
	Count int

	// SHA-512 hash over all messages to delete.
	Checksum string

	// Signature by the mailbox's private key affirming
	// the deletion of the messages, of purpose
	// TALER_SIGNATURE_WALLET_MAILBOX_DELETE_MESSAGES.
	WalletSig string `json:"wallet_sig"`
}

// 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
	HMailbox 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,
		MessageFee:           m.MessageFee.String(),
		DeliveryPeriod:       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
	err := m.Db.Where("h_mailbox = ?", vars["h_mailbox"]).Limit(int(m.MessageResponseLimit)).Find(&entries).Error
	if err != nil {
		log.Printf("%v", err)
		w.WriteHeader(http.StatusInternalServerError)
		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
	//var body = make([]byte, m.MessageBodyBytes)
	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) //.Read(body)
	if err != nil {
		log.Printf("%v", err)
		http.Error(w, "Cannot read body", http.StatusBadRequest)
		return
	}
	err = m.Db.First(&entry, "h_mailbox = ? AND body = ?", vars["h_mailbox"], body, true).Error
	if err == nil {
		w.WriteHeader(http.StatusNotModified)
		return
	}
	entry.HMailbox = vars["h_mailbox"]
	entry.Body = body
	if m.MessageFee.IsZero() {
		m.Db.Save(&entry)
		w.WriteHeader(http.StatusNoContent)
		return
	}

	// Check if order exists and was paid already.
	h := sha256.New()
	h.Sum(body)
	orderId := util.Base32CrockfordEncode(h.Sum(nil))
	httpStatus, paymentStatus, payto, paytoErr := m.Merchant.IsOrderPaid(orderId)
	if paytoErr != nil {
		fmt.Println(paytoErr)
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	switch httpStatus {
	case http.StatusNotFound:
		// Not found. Create new order.
		var order merchant.CommonOrder
		order.OrderId = orderId
		order.Amount = m.MessageFee.String()
		order.Summary = "Mailbox message dispatch"
		order.MerchantBaseUrl = m.BaseUrl
		_, newOrderErr := m.Merchant.CreateOrder(order)
		if newOrderErr != nil {
			fmt.Println(newOrderErr)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		// Check for order again to get payto.
		_, _, payto, paytoErr = m.Merchant.IsOrderPaid(orderId)
		if paytoErr != nil {
			fmt.Println(paytoErr)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		if payto == "" {
			fmt.Println(paytoErr)
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.WriteHeader(http.StatusPaymentRequired)
		w.Header().Set("Taler", payto)
		return
	case http.StatusOK:
		// Check if it was actually paid.
		if paymentStatus == "paid" {
			m.Db.Save(&entry)
			w.WriteHeader(http.StatusNoContent)
			return
		}
		w.WriteHeader(http.StatusPaymentRequired)
		if payto != "" {
			w.Header().Set("Taler", payto)
		}
		return
	}
}

func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	var msg MessageDeletionRequest
	var entries []InboxEntry
	if r.Body == nil {
		http.Error(w, "No request body", 400)
		return
	}
	err := json.NewDecoder(r.Body).Decode(&msg)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	etag_hdr := r.Header.Get("If-Match")
	if etag_hdr == "" {
		http.Error(w, "If-Match header missing", 400)
		return
	}
	if strings.Contains(etag_hdr, ",") {
		http.Error(w, "If-Match contains multiple values", 400)
		return
	}
	etag_expected, err := strconv.Atoi(etag_hdr)
	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
	}
	checksum, err := util.Base32CrockfordDecode(msg.Checksum, 64)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	pk := ed25519.PublicKey(pkey)
	sig, err := util.Base32CrockfordDecode(msg.WalletSig, 64)
	if nil != err {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	var signed_msg [72]byte
	size := signed_msg[0:4]
	binary.BigEndian.PutUint32(size, 64+4+4)
	purp := signed_msg[4:8]
	binary.BigEndian.PutUint32(purp, gana.TALER_SIGNATURE_PURPOSE_MAILBOX_MESSAGES_DELETE)
	copy(signed_msg[8:], checksum)
	if !ed25519.Verify(pk, signed_msg[0:], sig) {
		w.WriteHeader(http.StatusForbidden)
		return
	}
	h := sha512.New()
	h.Write(pkey)
	h_mailbox := util.Base32CrockfordEncode(h.Sum(nil))
	err = m.Db.Where("h_mailbox = ? AND id >= ?", h_mailbox, etag_expected).Limit(msg.Count).Find(&entries).Error
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}
	if len(entries) != msg.Count {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	if entries[0].ID != uint(etag_expected) {
		w.WriteHeader(http.StatusPreconditionFailed)
		return
	}
	h_all := sha512.New()
	for _, entry := range entries {
		h_all.Write(entry.Body)
	}
	if !bytes.Equal(h_all.Sum(nil), checksum) {
		w.WriteHeader(http.StatusNotFound)
		return
	}
	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("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
	m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
	m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
}

// Initialize the Mailbox instance with cfgfile
func (m *Mailbox) Initialize(cfg MailboxConfig) {
	m.Cfg = cfg
	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)
	// FIXME actual cost
	fee, err := talerutil.ParseAmount(cfg.Ini.Section("mailbox").Key("message_fee").MustString("KUDOS:1"))
	if err != nil {
		fmt.Printf("Failed to parse cost: %v", err)
		os.Exit(1)
	}
	m.MessageFee = fee
	_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)
	}

	m.Merchant = cfg.Merchant
	m.setupHandlers()
}
