package main

import (
	"context"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"os"
	"time"

	sdk "github.com/nspcc-dev/cdn-sdk"
	"github.com/nspcc-dev/cdn-sdk/creds/hcs"
	"github.com/nspcc-dev/cdn-sdk/creds/neofs"
	"github.com/nspcc-dev/cdn-sdk/grace"
	"github.com/nspcc-dev/cdn-sdk/pool"
	"github.com/nspcc-dev/neofs-api-go/pkg/container"
	"github.com/nspcc-dev/neofs-s3-gw/authmate"
	"github.com/urfave/cli/v2"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

type gateKey struct {
	PrivateKey string `json:"private_key"`
	PublicKey  string `json:"public_key"`
}

const (
	poolConnectTimeout = 5 * time.Second
	poolRequestTimeout = 5 * time.Second
)

var (
	// Version of the program.
	Version = "dev"
)

var (
	neoFSKeyPathFlag      string
	peerAddressFlag       string
	eaclRulesFlag         string
	gatePrivateKeyFlag    string
	secretAddressFlag     string
	ownerPrivateKeyFlag   string
	containerIDFlag       string
	containerFriendlyName string
	gatesPublicKeysFlag   cli.StringSlice
	gatesKeysCountFlag    int
	logEnabledFlag        bool
	logDebugEnabledFlag   bool
)

var zapConfig = zap.Config{
	Development: true,
	Encoding:    "console",
	Level:       zap.NewAtomicLevelAt(zapcore.FatalLevel),
	OutputPaths: []string{"stdout"},
	EncoderConfig: zapcore.EncoderConfig{
		MessageKey:   "message",
		LevelKey:     "level",
		EncodeLevel:  zapcore.CapitalLevelEncoder,
		TimeKey:      "time",
		EncodeTime:   zapcore.ISO8601TimeEncoder,
		CallerKey:    "caller",
		EncodeCaller: zapcore.ShortCallerEncoder,
	},
}

func prepare() (context.Context, *zap.Logger) {
	var (
		err error
		log = zap.NewNop()
	)

	if !logEnabledFlag {
		return grace.Context(log), log
	} else if logDebugEnabledFlag {
		zapConfig.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel)
	}

	if log, err = zapConfig.Build(); err != nil {
		panic(err)
	}

	return grace.Context(log), log
}

func main() {
	app := &cli.App{
		Name:     "NeoFS gate authentication manager",
		Usage:    "Helps manage delegated access via gates to data stored in NeoFS network",
		Version:  Version,
		Flags:    appFlags(),
		Commands: appCommands(),
	}

	if err := app.Run(os.Args); err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "error: %s\n", err)
		os.Exit(100)
	}
}

func appFlags() []cli.Flag {
	return []cli.Flag{
		&cli.BoolFlag{
			Name:        "with-log",
			Usage:       "Enable logger",
			Destination: &logEnabledFlag,
		},
		&cli.BoolFlag{
			Name:        "debug",
			Usage:       "Enable debug logger level",
			Destination: &logDebugEnabledFlag,
		},
	}
}

func appCommands() []*cli.Command {
	return []*cli.Command{
		issueSecret(),
		obtainSecret(),
		generateKeys(),
	}
}

func generateGatesKeys(count int) ([]hcs.Credentials, error) {
	var (
		err error
		res = make([]hcs.Credentials, count)
	)

	for i := 0; i < count; i++ {
		if res[i], err = hcs.Generate(rand.Reader); err != nil {
			return nil, err
		}
	}

	return res, nil
}

func generateKeys() *cli.Command {
	return &cli.Command{
		Name:  "generate-keys",
		Usage: "Generate key pairs for gates",
		Flags: []cli.Flag{
			&cli.IntFlag{
				Name:        "count",
				Usage:       "number of x25519 key pairs to generate",
				Value:       1,
				Destination: &gatesKeysCountFlag,
			},
		},
		Action: func(c *cli.Context) error {
			_, log := prepare()

			log.Info("start generating x25519 keys")

			csl, err := generateGatesKeys(gatesKeysCountFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to create key pairs of gates: %s", err), 1)
			}

			log.Info("generated x25519 keys")

			gatesKeys := make([]gateKey, len(csl))
			for i, cs := range csl {
				privateKey, publicKey := cs.PrivateKey().String(), cs.PublicKey().String()
				gatesKeys[i] = gateKey{PrivateKey: privateKey, PublicKey: publicKey}
			}

			keys, err := json.MarshalIndent(gatesKeys, "", "  ")
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to marshal key pairs of gates: %s", err), 2)
			}

			fmt.Println(string(keys))
			return nil
		},
	}
}

func issueSecret() *cli.Command {
	return &cli.Command{
		Name:  "issue-secret",
		Usage: "Issue a secret in NeoFS network",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:        "neofs-key",
				Value:       "",
				Usage:       "path to owner's neofs private ecdsa key",
				Required:    true,
				Destination: &neoFSKeyPathFlag,
			},
			&cli.StringFlag{
				Name:        "peer",
				Value:       "",
				Usage:       "address of a neofs peer to connect to",
				Required:    true,
				Destination: &peerAddressFlag,
			},
			&cli.StringFlag{
				Name:        "rules",
				Usage:       "eacl rules as plain json string",
				Required:    false,
				Destination: &eaclRulesFlag,
			},
			&cli.StringSliceFlag{
				Name:        "gate-public-key",
				Usage:       "public x25519 key of a gate (use flags repeatedly for multiple gates)",
				Required:    true,
				Destination: &gatesPublicKeysFlag,
			},
			&cli.StringFlag{
				Name:        "owner-private-key",
				Usage:       "owner's private x25519 key",
				Required:    false,
				Destination: &ownerPrivateKeyFlag,
			},
			&cli.StringFlag{
				Name:        "container-id",
				Usage:       "auth container id to put the secret into",
				Required:    false,
				Destination: &containerIDFlag,
			},
			&cli.StringFlag{
				Name:        "container-friendly-name",
				Usage:       "friendly name of auth container to put the secret into",
				Required:    false,
				Destination: &containerFriendlyName,
				Value:       "auth-container",
			},
		},
		Action: func(c *cli.Context) error {
			ctx, log := prepare()

			neofsCreds, err := neofs.New(neoFSKeyPathFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to load neofs private key: %s", err), 1)
			}

			ctx, cancel := context.WithCancel(ctx)
			defer cancel()

			client, err := createSDKClient(ctx, log, neofsCreds, peerAddressFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to create sdk client: %s", err), 2)
			}

			agent := authmate.New(log, client)
			var cid *container.ID
			if len(containerIDFlag) > 0 {
				cid = container.NewID()
				if err := cid.Parse(containerIDFlag); err != nil {
					return cli.Exit(fmt.Sprintf("failed to parse auth container id: %s", err), 3)
				}
			}

			var owner hcs.Credentials
			if owner, err = fetchHCSCredentials(ownerPrivateKeyFlag); err != nil {
				return cli.Exit(fmt.Sprintf("failed to create owner's private key: %s", err), 4)
			}

			var gatesPublicKeys []hcs.PublicKey
			for _, key := range gatesPublicKeysFlag.Value() {
				gpk, err := hcs.LoadPublicKey(key)
				if err != nil {
					return cli.Exit(fmt.Sprintf("failed to load gate's public key: %s", err), 5)
				}
				gatesPublicKeys = append(gatesPublicKeys, gpk)
			}

			issueSecretOptions := &authmate.IssueSecretOptions{
				ContainerID:           cid,
				ContainerFriendlyName: containerFriendlyName,
				NEOFSCreds:            neofsCreds,
				OwnerPrivateKey:       owner.PrivateKey(),
				GatesPublicKeys:       gatesPublicKeys,
				EACLRules:             []byte(eaclRulesFlag),
			}

			if err = agent.IssueSecret(ctx, os.Stdout, issueSecretOptions); err != nil {
				return cli.Exit(fmt.Sprintf("failed to issue secret: %s", err), 6)
			}

			return nil
		},
	}
}

func obtainSecret() *cli.Command {
	command := &cli.Command{
		Name:  "obtain-secret",
		Usage: "Obtain a secret from NeoFS network",
		Flags: []cli.Flag{
			&cli.StringFlag{
				Name:        "neofs-key",
				Value:       "",
				Usage:       "path to owner's neofs private ecdsa key",
				Required:    true,
				Destination: &neoFSKeyPathFlag,
			},
			&cli.StringFlag{
				Name:        "peer",
				Value:       "",
				Usage:       "address of neofs peer to connect to",
				Required:    true,
				Destination: &peerAddressFlag,
			},
			&cli.StringFlag{
				Name:        "gate-private-key",
				Usage:       "gate's private x25519 key",
				Required:    true,
				Destination: &gatePrivateKeyFlag,
			},
			&cli.StringFlag{
				Name:        "secret-address",
				Usage:       "address of a secret (i.e. access key id for s3)",
				Required:    true,
				Destination: &secretAddressFlag,
			},
		},
		Action: func(c *cli.Context) error {
			ctx, log := prepare()

			neofsCreds, err := neofs.New(neoFSKeyPathFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to load neofs private key: %s", err), 1)
			}

			ctx, cancel := context.WithCancel(ctx)
			defer cancel()

			client, err := createSDKClient(ctx, log, neofsCreds, peerAddressFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to create sdk client: %s", err), 2)
			}

			agent := authmate.New(log, client)

			var _ = agent

			gateCreds, err := hcs.NewCredentials(gatePrivateKeyFlag)
			if err != nil {
				return cli.Exit(fmt.Sprintf("failed to create owner's private key: %s", err), 4)
			}

			obtainSecretOptions := &authmate.ObtainSecretOptions{
				SecretAddress:  secretAddressFlag,
				GatePrivateKey: gateCreds.PrivateKey(),
			}

			if err = agent.ObtainSecret(ctx, os.Stdout, obtainSecretOptions); err != nil {
				return cli.Exit(fmt.Sprintf("failed to obtain secret: %s", err), 5)
			}

			return nil
		},
	}
	return command
}

func fetchHCSCredentials(val string) (hcs.Credentials, error) {
	if val == "" {
		return hcs.Generate(rand.Reader)
	}

	return hcs.NewCredentials(val)
}

func createSDKClient(ctx context.Context, log *zap.Logger, neofsCreds neofs.Credentials, peerAddress string) (sdk.Client, error) {
	log.Debug("prepare connection pool")

	p, err := pool.New(ctx,
		pool.WithLogger(log),
		pool.WithAddress(peerAddress),
		pool.WithCredentials(neofsCreds),
		pool.WithAPIPreparer(sdk.APIPreparer),
		pool.WithConnectTimeout(poolConnectTimeout),
		pool.WithRequestTimeout(poolRequestTimeout))
	if err != nil {
		return nil, fmt.Errorf("failed to create connection pool: %w", err)
	}

	log.Debug("prepare sdk client")

	return sdk.New(ctx,
		sdk.WithLogger(log),
		sdk.WithCredentials(neofsCreds),
		sdk.WithConnectionPool(p),
		sdk.WithAPIPreparer(sdk.APIPreparer))
}