package tree

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"errors"
	"fmt"

	core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/refs"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
	cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
	frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
)

type message interface {
	SignedDataSize() int
	ReadSignedData([]byte) ([]byte, error)
	GetSignature() *Signature
	SetSignature(*Signature)
}

var (
	errBearerWrongContainer = errors.New("bearer token is created for another container")
	errBearerSignature      = errors.New("invalid bearer token signature")
)

// verifyClient verifies if the request for a client operation
// was signed by a key allowed by (e)ACL rules.
// Operation must be one of:
//   - 1. ObjectPut;
//   - 2. ObjectGet.
func (s *Service) verifyClient(ctx context.Context, req message, cid cidSDK.ID, rawBearer []byte, op acl.Op) error {
	err := verifyMessage(req)
	if err != nil {
		return err
	}

	isAuthorized, err := s.isAuthorized(req, op)
	if isAuthorized || err != nil {
		return err
	}

	cnr, err := s.cnrSource.Get(cid)
	if err != nil {
		return fmt.Errorf("can't get container %s: %w", cid, err)
	}

	bt, err := parseBearer(rawBearer, cid)
	if err != nil {
		return fmt.Errorf("access to operation %s is denied: %w", op, err)
	}

	role, pubKey, err := roleAndPubKeyFromReq(cnr, req, bt)
	if err != nil {
		return fmt.Errorf("can't get request role: %w", err)
	}

	return s.checkAPE(ctx, bt, cnr, cid, op, role, pubKey)
}

// Returns true iff the operation is read-only and request was signed
// with one of the authorized keys.
func (s *Service) isAuthorized(req message, op acl.Op) (bool, error) {
	if op != acl.OpObjectGet {
		return false, nil
	}

	sign := req.GetSignature()
	if sign == nil {
		return false, errors.New("missing signature")
	}

	key := sign.GetKey()
	for i := range s.authorizedKeys {
		if bytes.Equal(s.authorizedKeys[i], key) {
			return true, nil
		}
	}
	return false, nil
}

func parseBearer(rawBearer []byte, cid cidSDK.ID) (*bearer.Token, error) {
	if len(rawBearer) == 0 {
		return nil, nil
	}

	bt := new(bearer.Token)
	if err := bt.Unmarshal(rawBearer); err != nil {
		return nil, fmt.Errorf("invalid bearer token: %w", err)
	}
	if !bt.AssertContainer(cid) {
		return nil, errBearerWrongContainer
	}
	if !bt.VerifySignature() {
		return nil, errBearerSignature
	}
	return bt, nil
}

func verifyMessage(m message) error {
	binBody, err := m.ReadSignedData(nil)
	if err != nil {
		return fmt.Errorf("marshal request body: %w", err)
	}

	sig := m.GetSignature()

	// TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion
	var sigV2 refs.Signature
	sigV2.SetKey(sig.GetKey())
	sigV2.SetSign(sig.GetSign())
	sigV2.SetScheme(refs.ECDSA_SHA512)

	var sigSDK frostfscrypto.Signature
	if err := sigSDK.ReadFromV2(sigV2); err != nil {
		return fmt.Errorf("can't read signature: %w", err)
	}

	if !sigSDK.Verify(binBody) {
		return errors.New("invalid signature")
	}
	return nil
}

// SignMessage uses the provided key and signs any protobuf
// message that was generated for the TreeService by the
// protoc-gen-go-frostfs generator. Returns any errors directly.
func SignMessage(m message, key *ecdsa.PrivateKey) error {
	binBody, err := m.ReadSignedData(nil)
	if err != nil {
		return err
	}

	keySDK := frostfsecdsa.Signer(*key)
	data, err := keySDK.Sign(binBody)
	if err != nil {
		return err
	}

	rawPub := make([]byte, keySDK.Public().MaxEncodedSize())
	rawPub = rawPub[:keySDK.Public().Encode(rawPub)]
	m.SetSignature(&Signature{
		Key:  rawPub,
		Sign: data,
	})

	return nil
}

func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (acl.Role, *keys.PublicKey, error) {
	role := acl.RoleOthers
	owner := cnr.Value.Owner()

	rawKey := req.GetSignature().GetKey()
	if bt != nil && bt.Impersonate() {
		rawKey = bt.SigningKeyBytes()
	}

	pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
	if err != nil {
		return role, nil, fmt.Errorf("invalid public key: %w", err)
	}

	var reqSigner user.ID
	user.IDFromKey(&reqSigner, (ecdsa.PublicKey)(*pub))

	if reqSigner.Equals(owner) {
		role = acl.RoleOwner
	}

	return role, pub, nil
}