package container

import (
	"bytes"
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/sha256"
	"encoding/hex"
	"errors"
	"fmt"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/container"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
	"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
	apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
	cnrSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
	sessionSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	policyengine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"git.frostfs.info/TrueCloudLab/policy-engine/pkg/resource"
	nativeschema "git.frostfs.info/TrueCloudLab/policy-engine/schema/native"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
)

var (
	errMissingContainerID           = errors.New("missing container ID")
	errSessionContainerMissmatch    = errors.New("requested container is not related to the session")
	errMissingVerificationHeader    = errors.New("malformed request: empty verification header")
	errInvalidSessionTokenSignature = errors.New("malformed request: invalid session token signature")
	errInvalidSessionTokenOwner     = errors.New("malformed request: invalid session token owner")
	errEmptyBodySignature           = errors.New("malformed request: empty body signature")
	errMissingOwnerID               = errors.New("malformed request: missing owner ID")
	errSubjectNotFound              = errors.New("subject not found")

	undefinedContainerID = cid.ID{}
)

type ir interface {
	InnerRingKeys() ([][]byte, error)
}

type containers interface {
	Get(cid.ID) (*containercore.Container, error)
}

type frostfsidSubjectProvider interface {
	GetSubject(util.Uint160) (*client.Subject, error)
}

type apeChecker struct {
	router policyengine.ChainRouter
	reader containers
	ir     ir
	nm     netmap.Source

	frostFSIDClient frostfsidSubjectProvider

	next Server
}

func NewAPEServer(router policyengine.ChainRouter, reader containers, ir ir, nm netmap.Source, frostFSIDClient frostfsidSubjectProvider, srv Server) Server {
	return &apeChecker{
		router:          router,
		reader:          reader,
		ir:              ir,
		next:            srv,
		nm:              nm,
		frostFSIDClient: frostFSIDClient,
	}
}

func (ac *apeChecker) AnnounceUsedSpace(ctx context.Context, req *container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.AnnounceUsedSpace")
	defer span.End()

	// this method is not used, so not checked

	return ac.next.AnnounceUsedSpace(ctx, req)
}

func (ac *apeChecker) Delete(ctx context.Context, req *container.DeleteRequest) (*container.DeleteResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Delete")
	defer span.End()

	if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
		nativeschema.MethodDeleteContainer); err != nil {
		return nil, err
	}

	return ac.next.Delete(ctx, req)
}

func (ac *apeChecker) Get(ctx context.Context, req *container.GetRequest) (*container.GetResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Get")
	defer span.End()

	if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
		nativeschema.MethodGetContainer); err != nil {
		return nil, err
	}

	return ac.next.Get(ctx, req)
}

func (ac *apeChecker) GetExtendedACL(ctx context.Context, req *container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.GetExtendedACL")
	defer span.End()

	if err := ac.validateContainerBoundedOperation(req.GetBody().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
		nativeschema.MethodGetContainerEACL); err != nil {
		return nil, err
	}

	return ac.next.GetExtendedACL(ctx, req)
}

func (ac *apeChecker) List(ctx context.Context, req *container.ListRequest) (*container.ListResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.List")
	defer span.End()

	role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader())
	if err != nil {
		return nil, err
	}

	reqProps := map[string]string{
		nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(pk.Bytes()),
		nativeschema.PropertyKeyActorRole:      role,
	}

	namespace, err := ac.namespaceByOwner(req.GetBody().GetOwnerID())
	if err != nil {
		return nil, fmt.Errorf("could not get owner namespace: %w", err)
	}
	if err := ac.validateNamespaceByPublicKey(pk, namespace); err != nil {
		return nil, err
	}

	request := &apeRequest{
		resource: &apeResource{
			name:  resourceName(namespace, ""),
			props: make(map[string]string),
		},
		op:    nativeschema.MethodListContainers,
		props: reqProps,
	}

	s, found, err := ac.router.IsAllowed(apechain.Ingress,
		policyengine.NewRequestTargetWithNamespace(namespace),
		request)
	if err != nil {
		return nil, err
	}

	if !found || s == apechain.Allow {
		return ac.next.List(ctx, req)
	}

	return nil, apeErr(nativeschema.MethodListContainers, s)
}

func (ac *apeChecker) Put(ctx context.Context, req *container.PutRequest) (*container.PutResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.Put")
	defer span.End()

	role, pk, err := ac.getRoleWithoutContainerID(req.GetBody().GetContainer().GetOwnerID(), req.GetMetaHeader(), req.GetVerificationHeader())
	if err != nil {
		return nil, err
	}

	reqProps := map[string]string{
		nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(pk.Bytes()),
		nativeschema.PropertyKeyActorRole:      role,
	}

	namespace, err := ac.namespaceByOwner(req.GetBody().GetContainer().GetOwnerID())
	if err != nil {
		return nil, fmt.Errorf("get namespace error: %w", err)
	}
	if err = validateNamespace(req.GetBody().GetContainer(), namespace); err != nil {
		return nil, err
	}

	request := &apeRequest{
		resource: &apeResource{
			name:  resourceName(namespace, ""),
			props: make(map[string]string),
		},
		op:    nativeschema.MethodPutContainer,
		props: reqProps,
	}

	s, found, err := ac.router.IsAllowed(apechain.Ingress,
		policyengine.NewRequestTargetWithNamespace(namespace),
		request)
	if err != nil {
		return nil, err
	}

	if !found || s == apechain.Allow {
		return ac.next.Put(ctx, req)
	}

	return nil, apeErr(nativeschema.MethodPutContainer, s)
}

func (ac *apeChecker) getRoleWithoutContainerID(oID *refs.OwnerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader) (string, *keys.PublicKey, error) {
	if vh == nil {
		return "", nil, errMissingVerificationHeader
	}

	if oID == nil {
		return "", nil, errMissingOwnerID
	}
	var ownerID user.ID
	if err := ownerID.ReadFromV2(*oID); err != nil {
		return "", nil, err
	}

	actor, pk, err := ac.getActorAndPublicKey(mh, vh, undefinedContainerID)
	if err != nil {
		return "", nil, err
	}

	if actor.Equals(ownerID) {
		return nativeschema.PropertyValueContainerRoleOwner, pk, nil
	}

	pkBytes := pk.Bytes()
	isIR, err := ac.isInnerRingKey(pkBytes)
	if err != nil {
		return "", nil, err
	}
	if isIR {
		return nativeschema.PropertyValueContainerRoleIR, pk, nil
	}

	return nativeschema.PropertyValueContainerRoleOthers, pk, nil
}

func (ac *apeChecker) SetExtendedACL(ctx context.Context, req *container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) {
	ctx, span := tracing.StartSpanFromContext(ctx, "apeChecker.SetExtendedACL")
	defer span.End()

	if err := ac.validateContainerBoundedOperation(req.GetBody().GetEACL().GetContainerID(), req.GetMetaHeader(), req.GetVerificationHeader(),
		nativeschema.MethodSetContainerEACL); err != nil {
		return nil, err
	}

	return ac.next.SetExtendedACL(ctx, req)
}

func (ac *apeChecker) validateContainerBoundedOperation(containerID *refs.ContainerID, mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, op string) error {
	if vh == nil {
		return errMissingVerificationHeader
	}

	id, err := getContainerID(containerID)
	if err != nil {
		return err
	}

	cont, err := ac.reader.Get(id)
	if err != nil {
		return err
	}

	reqProps, err := ac.getRequestProps(mh, vh, cont, id)
	if err != nil {
		return err
	}

	namespace := ""
	cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cont.Value).Zone(), ".ns")
	if hasNamespace {
		namespace = cntNamespace
	}

	request := &apeRequest{
		resource: &apeResource{
			name:  resourceName(namespace, id.EncodeToString()),
			props: ac.getContainerProps(cont),
		},
		op:    op,
		props: reqProps,
	}

	s, found, err := ac.router.IsAllowed(apechain.Ingress,
		policyengine.NewRequestTarget(cntNamespace, id.EncodeToString()),
		request)
	if err != nil {
		return err
	}

	if !found || s == apechain.Allow {
		return nil
	}

	return apeErr(op, s)
}

func apeErr(operation string, status apechain.Status) error {
	errAccessDenied := &apistatus.ObjectAccessDenied{}
	errAccessDenied.WriteReason(fmt.Sprintf("access to container operation %s is denied by access policy engine: %s", operation, status.String()))
	return errAccessDenied
}

func getContainerID(reqContID *refs.ContainerID) (cid.ID, error) {
	if reqContID == nil {
		return cid.ID{}, errMissingContainerID
	}
	var id cid.ID
	err := id.ReadFromV2(*reqContID)
	if err != nil {
		return cid.ID{}, fmt.Errorf("invalid container ID: %w", err)
	}
	return id, nil
}

type apeRequest struct {
	resource *apeResource
	op       string
	props    map[string]string
}

// Operation implements resource.Request.
func (r *apeRequest) Operation() string {
	return r.op
}

// Property implements resource.Request.
func (r *apeRequest) Property(key string) string {
	return r.props[key]
}

// Resource implements resource.Request.
func (r *apeRequest) Resource() resource.Resource {
	return r.resource
}

type apeResource struct {
	name  string
	props map[string]string
}

func (r *apeResource) Name() string {
	return r.name
}

func (r *apeResource) Property(key string) string {
	return r.props[key]
}

func resourceName(namespace string, container string) string {
	if namespace == "" && container == "" {
		return nativeschema.ResourceFormatRootContainers
	}
	if namespace == "" && container != "" {
		return fmt.Sprintf(nativeschema.ResourceFormatRootContainer, container)
	}
	if namespace != "" && container == "" {
		return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainers, namespace)
	}
	return fmt.Sprintf(nativeschema.ResourceFormatNamespaceContainer, namespace, container)
}

func (ac *apeChecker) getContainerProps(c *containercore.Container) map[string]string {
	return map[string]string{
		nativeschema.PropertyKeyContainerOwnerID: c.Value.Owner().EncodeToString(),
	}
}

func (ac *apeChecker) getRequestProps(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader,
	cont *containercore.Container, cnrID cid.ID,
) (map[string]string, error) {
	actor, pk, err := ac.getActorAndPublicKey(mh, vh, cnrID)
	if err != nil {
		return nil, err
	}
	role, err := ac.getRole(actor, pk, cont, cnrID)
	if err != nil {
		return nil, err
	}
	return map[string]string{
		nativeschema.PropertyKeyActorPublicKey: hex.EncodeToString(pk.Bytes()),
		nativeschema.PropertyKeyActorRole:      role,
	}, nil
}

func (ac *apeChecker) getRole(actor *user.ID, pk *keys.PublicKey, cont *containercore.Container, cnrID cid.ID) (string, error) {
	if cont.Value.Owner().Equals(*actor) {
		return nativeschema.PropertyValueContainerRoleOwner, nil
	}

	pkBytes := pk.Bytes()
	isIR, err := ac.isInnerRingKey(pkBytes)
	if err != nil {
		return "", err
	}
	if isIR {
		return nativeschema.PropertyValueContainerRoleIR, nil
	}

	isContainer, err := ac.isContainerKey(pkBytes, cnrID, cont)
	if err != nil {
		return "", err
	}
	if isContainer {
		return nativeschema.PropertyValueContainerRoleContainer, nil
	}

	return nativeschema.PropertyValueContainerRoleOthers, nil
}

func (ac *apeChecker) getActorAndPublicKey(mh *session.RequestMetaHeader, vh *session.RequestVerificationHeader, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) {
	st, err := ac.getSessionToken(mh)
	if err != nil {
		return nil, nil, err
	}

	if st != nil {
		return ac.getActorAndPKFromSessionToken(st, cnrID)
	}
	return ac.getActorAndPKFromSignature(vh)
}

func (ac *apeChecker) getActorAndPKFromSignature(vh *session.RequestVerificationHeader) (*user.ID, *keys.PublicKey, error) {
	for vh.GetOrigin() != nil {
		vh = vh.GetOrigin()
	}
	sig := vh.GetBodySignature()
	if sig == nil {
		return nil, nil, errEmptyBodySignature
	}
	key, err := keys.NewPublicKeyFromBytes(sig.GetKey(), elliptic.P256())
	if err != nil {
		return nil, nil, fmt.Errorf("invalid signature key: %w", err)
	}

	var userID user.ID
	user.IDFromKey(&userID, (ecdsa.PublicKey)(*key))

	return &userID, key, nil
}

func (ac *apeChecker) getSessionToken(mh *session.RequestMetaHeader) (*sessionSDK.Container, error) {
	for mh.GetOrigin() != nil {
		mh = mh.GetOrigin()
	}
	st := mh.GetSessionToken()
	if st == nil {
		return nil, nil
	}

	var tok sessionSDK.Container
	err := tok.ReadFromV2(*st)
	if err != nil {
		return nil, fmt.Errorf("invalid session token: %w", err)
	}

	return &tok, nil
}

func (ac *apeChecker) getActorAndPKFromSessionToken(st *sessionSDK.Container, cnrID cid.ID) (*user.ID, *keys.PublicKey, error) {
	if cnrID != undefinedContainerID && !st.AppliedTo(cnrID) {
		return nil, nil, errSessionContainerMissmatch
	}
	if !st.VerifySignature() {
		return nil, nil, errInvalidSessionTokenSignature
	}
	var tok session.Token
	st.WriteToV2(&tok)

	signaturePublicKey, err := keys.NewPublicKeyFromBytes(tok.GetSignature().GetKey(), elliptic.P256())
	if err != nil {
		return nil, nil, fmt.Errorf("invalid key in session token signature: %w", err)
	}

	tokenIssuer := st.Issuer()
	if !isOwnerFromKey(tokenIssuer, signaturePublicKey) {
		return nil, nil, errInvalidSessionTokenOwner
	}

	return &tokenIssuer, signaturePublicKey, nil
}

func isOwnerFromKey(id user.ID, key *keys.PublicKey) bool {
	if key == nil {
		return false
	}

	var id2 user.ID
	user.IDFromKey(&id2, (ecdsa.PublicKey)(*key))

	return id2.Equals(id)
}

func (ac *apeChecker) isInnerRingKey(pk []byte) (bool, error) {
	innerRingKeys, err := ac.ir.InnerRingKeys()
	if err != nil {
		return false, err
	}

	for i := range innerRingKeys {
		if bytes.Equal(innerRingKeys[i], pk) {
			return true, nil
		}
	}

	return false, nil
}

func (ac *apeChecker) isContainerKey(pk []byte, cnrID cid.ID, cont *containercore.Container) (bool, error) {
	binCnrID := make([]byte, sha256.Size)
	cnrID.Encode(binCnrID)

	nm, err := netmap.GetLatestNetworkMap(ac.nm)
	if err != nil {
		return false, err
	}

	in, err := isContainerNode(nm, pk, binCnrID, cont)
	if err != nil {
		return false, err
	} else if in {
		return true, nil
	}

	// then check previous netmap, this can happen in-between epoch change
	// when node migrates data from last epoch container
	nm, err = netmap.GetPreviousNetworkMap(ac.nm)
	if err != nil {
		return false, err
	}

	return isContainerNode(nm, pk, binCnrID, cont)
}

func isContainerNode(nm *netmapSDK.NetMap, pk, binCnrID []byte, cont *containercore.Container) (bool, error) {
	cnrVectors, err := nm.ContainerNodes(cont.Value.PlacementPolicy(), binCnrID)
	if err != nil {
		return false, err
	}

	for i := range cnrVectors {
		for j := range cnrVectors[i] {
			if bytes.Equal(cnrVectors[i][j].PublicKey(), pk) {
				return true, nil
			}
		}
	}

	return false, nil
}

func (ac *apeChecker) namespaceByOwner(owner *refs.OwnerID) (string, error) {
	var ownerSDK user.ID
	if owner == nil {
		return "", fmt.Errorf("owner id is not set")
	}
	if err := ownerSDK.ReadFromV2(*owner); err != nil {
		return "", err
	}
	addr, err := ownerSDK.ScriptHash()
	if err != nil {
		return "", err
	}

	namespace := ""
	subject, err := ac.frostFSIDClient.GetSubject(addr)
	if err == nil {
		namespace = subject.Namespace
	} else {
		if !strings.Contains(err.Error(), errSubjectNotFound.Error()) {
			return "", fmt.Errorf("get subject error: %w", err)
		}
	}
	return namespace, nil
}

// validateNamespace validates a namespace set in a container.
// If frostfs-id contract stores a namespace N1 for an owner ID and a container within a request
// is set with namespace N2 (via Zone() property), then N2 is invalid and the request is denied.
func validateNamespace(cnrV2 *container.Container, ownerIDNamespace string) error {
	if cnrV2 == nil {
		return nil
	}
	var cnr cnrSDK.Container
	if err := cnr.ReadFromV2(*cnrV2); err != nil {
		return err
	}
	cntNamespace, hasNamespace := strings.CutSuffix(cnrSDK.ReadDomain(cnr).Zone(), ".ns")
	if hasNamespace {
		if cntNamespace != ownerIDNamespace {
			if ownerIDNamespace == "" {
				return fmt.Errorf("invalid domain zone: no namespace is expected")
			}
			return fmt.Errorf("invalid domain zone: expected namespace %s, but got %s", ownerIDNamespace, cntNamespace)
		}
	} else if ownerIDNamespace != "" {
		return fmt.Errorf("invalid domain zone: expected namespace %s, but got invalid or empty", ownerIDNamespace)
	}
	return nil
}

// validateNamespace validates if a namespace of a request actor equals to owner's namespace.
// An actor's namespace is calculated by a public key.
func (ac *apeChecker) validateNamespaceByPublicKey(pk *keys.PublicKey, ownerIDNamespace string) error {
	var actor user.ID
	user.IDFromKey(&actor, (ecdsa.PublicKey)(*pk))
	actorOwnerID := new(refs.OwnerID)
	actor.WriteToV2(actorOwnerID)
	actorNamespace, err := ac.namespaceByOwner(actorOwnerID)
	if err != nil {
		return fmt.Errorf("could not get actor namespace: %w", err)
	}
	if actorNamespace != ownerIDNamespace {
		return fmt.Errorf("actor namespace %s differs from owner: %s", actorNamespace, ownerIDNamespace)
	}
	return nil
}