// Package identity implements keypair-based sign-in.
//
// There are no accounts in the usual sense: a player's ed25519 public key is
// their identity. To prove ownership they sign a server-issued challenge, which
// is single-use and short-lived. There is no password to leak, no email to
// verify, and nothing to reset — losing the key loses the balance, which is
// stated plainly in the interface.
package identity

import (
	"crypto/ed25519"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"sync"
	"time"
)

var (
	ErrUnknownChallenge = errors.New("identity: challenge not found or expired")
	ErrBadSignature     = errors.New("identity: signature does not verify")
	ErrBadPublicKey     = errors.New("identity: malformed public key")
)

// ChallengeTTL is how long a challenge stays valid. Short, because a client
// signs it immediately.
const ChallengeTTL = 2 * time.Minute

type challenge struct {
	nonce   [32]byte
	expires time.Time
}

// Authenticator issues and verifies sign-in challenges.
type Authenticator struct {
	mu         sync.Mutex
	challenges map[string]challenge
	now        func() time.Time
}

func NewAuthenticator() *Authenticator {
	return &Authenticator{
		challenges: make(map[string]challenge),
		now:        time.Now,
	}
}

// Challenge issues a fresh nonce for a public key to sign.
func (a *Authenticator) Challenge(pubkeyHex string) (string, error) {
	pk, err := ParsePublicKey(pubkeyHex)
	if err != nil {
		return "", err
	}
	var n [32]byte
	if _, err := rand.Read(n[:]); err != nil {
		panic("identity: system randomness unavailable: " + err.Error())
	}

	a.mu.Lock()
	defer a.mu.Unlock()
	a.sweepLocked()
	a.challenges[hex.EncodeToString(pk)] = challenge{
		nonce:   n,
		expires: a.now().Add(ChallengeTTL),
	}
	return hex.EncodeToString(n[:]), nil
}

// Verify checks a signature over the outstanding challenge for that key and
// consumes it, so a captured signature cannot be replayed.
func (a *Authenticator) Verify(pubkeyHex, signatureHex string) error {
	pk, err := ParsePublicKey(pubkeyHex)
	if err != nil {
		return err
	}
	sig, err := hex.DecodeString(signatureHex)
	if err != nil || len(sig) != ed25519.SignatureSize {
		return ErrBadSignature
	}

	a.mu.Lock()
	key := hex.EncodeToString(pk)
	c, ok := a.challenges[key]
	if ok {
		delete(a.challenges, key) // single use, whether or not it verifies
	}
	now := a.now()
	a.mu.Unlock()

	if !ok || now.After(c.expires) {
		return ErrUnknownChallenge
	}
	if !ed25519.Verify(pk, c.nonce[:], sig) {
		return ErrBadSignature
	}
	return nil
}

// sweepLocked drops expired challenges. Called under the mutex.
func (a *Authenticator) sweepLocked() {
	now := a.now()
	for k, c := range a.challenges {
		if now.After(c.expires) {
			delete(a.challenges, k)
		}
	}
}

// ParsePublicKey decodes and validates a hex-encoded ed25519 public key.
func ParsePublicKey(s string) (ed25519.PublicKey, error) {
	b, err := hex.DecodeString(s)
	if err != nil || len(b) != ed25519.PublicKeySize {
		return nil, ErrBadPublicKey
	}
	return ed25519.PublicKey(b), nil
}
