package pool

import (
	"context"
	"crypto/ecdsa"
	"math"

	"github.com/nspcc-dev/neofs-api-go/refs"
	"github.com/nspcc-dev/neofs-api-go/service"
	"github.com/nspcc-dev/neofs-api-go/session"
	crypto "github.com/nspcc-dev/neofs-crypto"
	"github.com/pkg/errors"
	"google.golang.org/grpc"
)

type (
	queryParams struct {
		key  *ecdsa.PrivateKey
		addr refs.Address
		verb service.Token_Info_Verb
	}

	SessionParams struct {
		Addr refs.Address
		Conn *grpc.ClientConn
		Verb service.Token_Info_Verb
	}
)

func (p *pool) fetchToken(ctx context.Context, con *grpc.ClientConn) (*session.Token, error) {
	p.Lock()
	defer p.Unlock()

	// if we had token for current connection - return it
	if tkn, ok := p.tokens[con.Target()]; ok {
		return tkn, nil
	}

	// try to generate token for connection
	tkn, err := generateToken(ctx, con, p.key)
	if err != nil {
		return nil, err
	}

	p.tokens[con.Target()] = tkn
	return tkn, nil
}

// SessionToken returns session token for connection
func (p *pool) SessionToken(ctx context.Context, params *SessionParams) (*service.Token, error) {
	var (
		err error
		tkn *session.Token
	)

	if params.Conn == nil {
		return nil, errors.New("empty connection")
	} else if tkn, err = p.fetchToken(ctx, params.Conn); err != nil {
		return nil, err
	}

	return prepareToken(tkn, queryParams{
		key:  p.key,
		addr: params.Addr,
		verb: params.Verb,
	})
}

// creates token using
func generateToken(ctx context.Context, con *grpc.ClientConn, key *ecdsa.PrivateKey) (*service.Token, error) {
	owner, err := refs.NewOwnerID(&key.PublicKey)
	if err != nil {
		return nil, err
	}

	token := new(service.Token)
	token.SetOwnerID(owner)
	token.SetExpirationEpoch(math.MaxUint64)
	token.SetOwnerKey(crypto.MarshalPublicKey(&key.PublicKey))

	creator, err := session.NewGRPCCreator(con, key)
	if err != nil {
		return nil, err
	}

	res, err := creator.Create(ctx, token)
	if err != nil {
		return nil, err
	}

	token.SetID(res.GetID())
	token.SetSessionKey(res.GetSessionKey())

	return token, nil
}

func prepareToken(t *service.Token, p queryParams) (*service.Token, error) {
	sig := make([]byte, len(t.Signature))
	copy(sig, t.Signature)

	token := &service.Token{
		Token_Info: service.Token_Info{
			ID:            t.ID,
			OwnerID:       t.OwnerID,
			Verb:          t.Verb,
			Address:       t.Address,
			TokenLifetime: t.TokenLifetime,
			SessionKey:    t.SessionKey,
			OwnerKey:      t.OwnerKey,
		},
		Signature: sig,
	}

	token.SetAddress(p.addr)
	token.SetVerb(p.verb)

	err := service.AddSignatureWithKey(p.key, service.NewSignedSessionToken(token))
	if err != nil {
		return nil, err
	}

	return token, nil
}