package frostfsid

import (
	"encoding/hex"
	"errors"
	"strconv"
	"strings"

	"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/handler"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/frostfsid/contract"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"go.uber.org/zap"
)

type FrostFSID struct {
	frostfsid *contract.FrostFSID
	cache     *cache.FrostfsIDCache
	log       *zap.Logger
}

type Config struct {
	Cache     *cache.FrostfsIDCache
	FrostFSID *contract.FrostFSID
	Logger    *zap.Logger
}

var (
	_ api.FrostFSID     = (*FrostFSID)(nil)
	_ handler.FrostFSID = (*FrostFSID)(nil)
)

// NewFrostFSID creates new FrostfsID wrapper.
func NewFrostFSID(cfg Config) (*FrostFSID, error) {
	switch {
	case cfg.FrostFSID == nil:
		return nil, errors.New("missing frostfsid client")
	case cfg.Cache == nil:
		return nil, errors.New("missing frostfsid cache")
	case cfg.Logger == nil:
		return nil, errors.New("missing frostfsid logger")
	}

	return &FrostFSID{
		frostfsid: cfg.FrostFSID,
		cache:     cfg.Cache,
		log:       cfg.Logger,
	}, nil
}

func (f *FrostFSID) ValidatePublicKey(key *keys.PublicKey) error {
	_, err := f.getSubject(key.GetScriptHash())
	return err
}

func (f *FrostFSID) GetUserGroupIDsAndClaims(userHash util.Uint160) ([]string, map[string]string, error) {
	subj, err := f.getSubject(userHash)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			f.log.Debug(logs.UserGroupsListIsEmpty, zap.Error(err))
			return nil, nil, nil
		}
		return nil, nil, err
	}

	res := make([]string, len(subj.Groups))
	for i, group := range subj.Groups {
		res[i] = strconv.FormatInt(group.ID, 10)
	}

	return res, subj.KV, nil
}

func (f *FrostFSID) getSubject(addr util.Uint160) (*client.SubjectExtended, error) {
	if subj := f.cache.GetSubject(addr); subj != nil {
		return subj, nil
	}

	subj, err := f.frostfsid.GetSubjectExtended(addr)
	if err != nil {
		return nil, err
	}

	if err = f.cache.PutSubject(addr, subj); err != nil {
		f.log.Warn(logs.CouldntCacheSubject, zap.Error(err))
	}

	return subj, nil
}

func (f *FrostFSID) GetUserAddress(namespace, name string) (string, error) {
	userKey, err := f.getUserKey(namespace, name)
	if err != nil {
		return "", err
	}

	return userKey.Address(), nil
}

func (f *FrostFSID) GetUserKey(namespace, name string) (string, error) {
	userKey, err := f.getUserKey(namespace, name)
	if err != nil {
		return "", err
	}

	return hex.EncodeToString(userKey.Bytes()), nil
}

func (f *FrostFSID) getUserKey(namespace, name string) (*keys.PublicKey, error) {
	if userKey := f.cache.GetUserKey(namespace, name); userKey != nil {
		return userKey, nil
	}

	userKey, err := f.frostfsid.GetSubjectKeyByName(namespace, name)
	if err != nil {
		return nil, err
	}

	if err = f.cache.PutUserKey(namespace, name, userKey); err != nil {
		f.log.Warn(logs.CouldntCacheUserKey, zap.Error(err))
	}

	return userKey, nil
}