// Copyright (c) 2022 BitMaelum Authors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

package signature

import (
	"crypto/sha256"
	"encoding/base64"
	"strconv"
	"strings"
	"time"

	"github.com/bitmaelum/bitmaelum-suite/internal"
	"github.com/bitmaelum/bitmaelum-suite/pkg/bmcrypto"
	"github.com/bitmaelum/bitmaelum-suite/pkg/hash"
)

// override for testing purposes
var timeNow = internal.TimeNow

// Token holds all info for an invitation
type Token struct {
	AddrHash  hash.Hash
	RoutingID string
	Expiry    time.Time
	Signature []byte
}

// ParseInviteToken parses data as generated by inviteToken.String() into a token
func ParseInviteToken(data string) (*Token, error) {
	it := &Token{}

	tokenData, err := base64.StdEncoding.DecodeString(data)
	if err != nil {
		return nil, err
	}

	parts := strings.SplitN(string(tokenData), ":", 4)
	if len(parts) != 4 {
		return nil, err
	}

	// Convert all parts to the correct formats
	h, err := hash.NewFromHash(parts[0])
	if err != nil {
		return nil, err
	}
	it.AddrHash = *h

	it.RoutingID = parts[1]
	i, err := strconv.ParseInt(parts[2], 10, 64)
	if err != nil {
		return nil, err
	}

	it.Expiry = time.Unix(i, 0)
	it.Signature = []byte(parts[3])

	return it, nil
}

// NewInviteToken will create a new invitation token that can be used to create an address on a mailserver
func NewInviteToken(addrHash hash.Hash, routingID string, validUntil time.Time, pk bmcrypto.PrivKey) (*Token, error) {
	h := generateHash(addrHash, routingID, validUntil)
	sig, err := bmcrypto.Sign(pk, h)
	if err != nil {
		return nil, err
	}

	return &Token{
		AddrHash:  addrHash,
		RoutingID: routingID,
		Expiry:    validUntil,
		Signature: sig,
	}, nil
}

// String converts a invite token to a string representation
func (token *Token) String() string {
	s := token.AddrHash.String() + ":" + token.RoutingID + ":" + strconv.FormatInt(token.Expiry.Unix(), 10) + ":" + string(token.Signature)
	return base64.StdEncoding.EncodeToString([]byte(s))
}

// Verify will verify a token/signature against the given data
func (token *Token) Verify(routingID string, pubKey bmcrypto.PubKey) bool {
	// check signature
	h := generateHash(token.AddrHash, token.RoutingID, token.Expiry)
	ok, err := bmcrypto.Verify(pubKey, h, token.Signature)
	if err != nil || !ok {
		return false
	}

	// Make sure we don't accept tokens after the valid Until date
	if timeNow().After(token.Expiry) {
		return false
	}

	// Must be the same routing ID
	if token.RoutingID != routingID {
		return false
	}

	return ok
}

func generateHash(addrHash hash.Hash, routingID string, validUntil time.Time) []byte {
	h := sha256.Sum256([]byte(addrHash.String() + routingID + strconv.FormatInt(validUntil.Unix(), 10)))

	return h[:]
}
