package apemanager

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

	apeV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/ape"
	apemanagerV2 "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/apemanager"
	"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
	session "git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/session"
	ape_contract "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/ape/contract_storage"
	containercore "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
	apemanager_errors "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/apemanager/errors"
	"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/util/logger"
	cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
	"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
	apechain "git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
	policy_engine "git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
	"github.com/mr-tron/base58/base58"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"go.uber.org/zap"
)

var errEmptyBodySignature = errors.New("malformed request: empty body signature")

type cfg struct {
	log *logger.Logger
}

type Service struct {
	cfg

	cnrSrc containercore.Source

	contractStorage ape_contract.ProxyAdaptedContractStorage
}

type Option func(*cfg)

func New(cnrSrc containercore.Source, contractStorage ape_contract.ProxyAdaptedContractStorage, opts ...Option) *Service {
	s := &Service{
		cnrSrc: cnrSrc,

		contractStorage: contractStorage,
	}

	for i := range opts {
		opts[i](&s.cfg)
	}

	if s.log == nil {
		s.log = &logger.Logger{Logger: zap.NewNop()}
	}

	return s
}

func WithLogger(log *logger.Logger) Option {
	return func(c *cfg) {
		c.log = log
	}
}

var _ Server = (*Service)(nil)

// validateContainerTargetRequest validates request for the container target.
// It checks if request actor is the owner of the container, otherwise it denies the request.
func (s *Service) validateContainerTargetRequest(cid string, pubKey *keys.PublicKey) error {
	var cidSDK cidSDK.ID
	if err := cidSDK.DecodeString(cid); err != nil {
		return fmt.Errorf("invalid CID format: %w", err)
	}
	isOwner, err := s.isActorContainerOwner(cidSDK, pubKey)
	if err != nil {
		return fmt.Errorf("failed to check owner: %w", err)
	}
	if !isOwner {
		return apemanager_errors.ErrAPEManagerAccessDenied("actor must be container owner")
	}
	return nil
}

func (s *Service) AddChain(_ context.Context, req *apemanagerV2.AddChainRequest) (*apemanagerV2.AddChainResponse, error) {
	pub, err := getSignaturePublicKey(req.GetVerificationHeader())
	if err != nil {
		return nil, err
	}

	chain, err := decodeAndValidateChain(req.GetBody().GetChain().GetKind().(*apeV2.ChainRaw).GetRaw())
	if err != nil {
		return nil, err
	}
	if len(chain.ID) == 0 {
		const randomIDLength = 10
		randID, err := base58Str(randomIDLength)
		if err != nil {
			return nil, fmt.Errorf("randomize chain ID error: %w", err)
		}
		chain.ID = apechain.ID(randID)
	}

	var target policy_engine.Target

	switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType {
	case apeV2.TargetTypeContainer:
		reqCID := req.GetBody().GetTarget().GetName()
		if err = s.validateContainerTargetRequest(reqCID, pub); err != nil {
			return nil, err
		}
		target = policy_engine.ContainerTarget(reqCID)
	default:
		return nil, fmt.Errorf("unsupported target type: %s", targetType)
	}

	if _, _, err = s.contractStorage.AddMorphRuleChain(apechain.Ingress, target, &chain); err != nil {
		return nil, err
	}

	body := new(apemanagerV2.AddChainResponseBody)
	body.SetChainID(chain.ID)

	resp := new(apemanagerV2.AddChainResponse)
	resp.SetBody(body)

	return resp, nil
}

func (s *Service) RemoveChain(_ context.Context, req *apemanagerV2.RemoveChainRequest) (*apemanagerV2.RemoveChainResponse, error) {
	pub, err := getSignaturePublicKey(req.GetVerificationHeader())
	if err != nil {
		return nil, err
	}

	var target policy_engine.Target

	switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType {
	case apeV2.TargetTypeContainer:
		reqCID := req.GetBody().GetTarget().GetName()
		if err = s.validateContainerTargetRequest(reqCID, pub); err != nil {
			return nil, err
		}
		target = policy_engine.ContainerTarget(reqCID)
	default:
		return nil, fmt.Errorf("unsupported target type: %s", targetType)
	}

	if _, _, err = s.contractStorage.RemoveMorphRuleChain(apechain.Ingress, target, req.GetBody().GetChainID()); err != nil {
		return nil, err
	}

	body := new(apemanagerV2.RemoveChainResponseBody)

	resp := new(apemanagerV2.RemoveChainResponse)
	resp.SetBody(body)

	return resp, nil
}

func (s *Service) ListChains(_ context.Context, req *apemanagerV2.ListChainsRequest) (*apemanagerV2.ListChainsResponse, error) {
	pub, err := getSignaturePublicKey(req.GetVerificationHeader())
	if err != nil {
		return nil, err
	}

	var target policy_engine.Target

	switch targetType := req.GetBody().GetTarget().GetTargetType(); targetType {
	case apeV2.TargetTypeContainer:
		reqCID := req.GetBody().GetTarget().GetName()
		if err = s.validateContainerTargetRequest(reqCID, pub); err != nil {
			return nil, err
		}
		target = policy_engine.ContainerTarget(reqCID)
	default:
		return nil, fmt.Errorf("unsupported target type: %s", targetType)
	}

	chs, err := s.contractStorage.ListMorphRuleChains(apechain.Ingress, target)
	if err != nil {
		return nil, err
	}

	res := make([]*apeV2.Chain, 0, len(chs))
	for _, ch := range chs {
		v2chraw := new(apeV2.ChainRaw)
		v2chraw.SetRaw(ch.Bytes())

		v2ch := new(apeV2.Chain)
		v2ch.SetKind(v2chraw)

		res = append(res, v2ch)
	}

	body := new(apemanagerV2.ListChainsResponseBody)
	body.SetChains(res)

	resp := new(apemanagerV2.ListChainsResponse)
	resp.SetBody(body)

	return resp, nil
}

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

	return key, nil
}

func (s *Service) isActorContainerOwner(cid cidSDK.ID, pk *keys.PublicKey) (bool, error) {
	var actor user.ID
	user.IDFromKey(&actor, (ecdsa.PublicKey)(*pk))
	actorOwnerID := new(refs.OwnerID)
	actor.WriteToV2(actorOwnerID)

	cnr, err := s.cnrSrc.Get(cid)
	if err != nil {
		return false, fmt.Errorf("get container error: %w", err)
	}
	return cnr.Value.Owner().Equals(actor), nil
}

// base58Str generates base58 string.
func base58Str(n int) (string, error) {
	b := make([]byte, n)
	_, err := rand.Read(b)
	if err != nil {
		return "", err
	}
	return base58.FastBase58Encoding(b), nil
}