package frostfsid

import (
	"context"
	"encoding/hex"
	"fmt"
	"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/handler"
	"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/authmate"
	frostfsutil "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
)

type FrostFSID struct {
	cli *client.Client
}

type Config struct {
	// RPCAddress is an endpoint to connect to neo rpc.
	RPCAddress string

	// Contract is hash of contract or its name in NNS.
	Contract string

	// ProxyContract is hash of proxy contract or its name in NNS to interact with frostfsid.
	ProxyContract string

	// Key is used to interact with frostfsid contract.
	// If this is nil than random key will be generated.
	Key *keys.PrivateKey
}

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

// New creates new FrostfsID contract wrapper that implements auth.FrostFSID interface.
func New(ctx context.Context, cfg Config) (*FrostFSID, error) {
	contractHash, err := frostfsutil.ResolveContractHash(cfg.Contract, cfg.RPCAddress)
	if err != nil {
		return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
	}

	key := cfg.Key
	if key == nil {
		if key, err = keys.NewPrivateKey(); err != nil {
			return nil, fmt.Errorf("generate anon private key for frostfsid: %w", err)
		}
	}

	rpcCli, err := rpcclient.New(ctx, cfg.RPCAddress, rpcclient.Options{})
	if err != nil {
		return nil, fmt.Errorf("init rpc client: %w", err)
	}

	var opt client.Options
	opt.ProxyContract, err = frostfsutil.ResolveContractHash(cfg.ProxyContract, cfg.RPCAddress)
	if err != nil {
		return nil, fmt.Errorf("resolve frostfs contract hash: %w", err)
	}

	cli, err := client.New(rpcCli, wallet.NewAccountFromPrivateKey(key), contractHash, opt)
	if err != nil {
		return nil, fmt.Errorf("init frostfsid client: %w", err)
	}

	return &FrostFSID{
		cli: cli,
	}, nil
}

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

func (f *FrostFSID) RegisterPublicKey(ns string, key *keys.PublicKey) error {
	_, err := f.cli.Wait(f.cli.CreateSubject(ns, key))
	if err != nil && !strings.Contains(err.Error(), "subject already exists") {
		return err
	}

	return nil
}

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

	return key.Address(), nil
}

func (f *FrostFSID) GetUserKey(account, name string) (string, error) {
	key, err := f.cli.GetSubjectKeyByName(account, name)
	if err != nil {
		return "", err
	}

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

func (f *FrostFSID) GetUserGroupIDs(userHash util.Uint160) ([]string, error) {
	subjExt, err := f.cli.GetSubjectExtended(userHash)
	if err != nil {
		if strings.Contains(err.Error(), "not found") {
			return nil, nil
		}
		return nil, err
	}

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

	return res, nil
}