// Copyright 2015 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package command

import (
	"fmt"
	"os"
	"reflect"
	"strings"

	"github.com/urfave/cli"
	"go.etcd.io/etcd/client/pkg/v3/pathutil"
	"go.etcd.io/etcd/client/v2"
)

func NewRoleCommands() cli.Command {
	return cli.Command{
		Name:  "role",
		Usage: "role add, grant and revoke subcommands",
		Subcommands: []cli.Command{
			{
				Name:      "add",
				Usage:     "add a new role for the etcd cluster",
				ArgsUsage: "<role> ",
				Action:    actionRoleAdd,
			},
			{
				Name:      "get",
				Usage:     "get details for a role",
				ArgsUsage: "<role>",
				Action:    actionRoleGet,
			},
			{
				Name:      "list",
				Usage:     "list all roles",
				ArgsUsage: " ",
				Action:    actionRoleList,
			},
			{
				Name:      "remove",
				Usage:     "remove a role from the etcd cluster",
				ArgsUsage: "<role>",
				Action:    actionRoleRemove,
			},
			{
				Name:      "grant",
				Usage:     "grant path matches to an etcd role",
				ArgsUsage: "<role>",
				Flags: []cli.Flag{
					cli.StringFlag{Name: "path", Value: "", Usage: "Path granted for the role to access"},
					cli.BoolFlag{Name: "read", Usage: "Grant read-only access"},
					cli.BoolFlag{Name: "write", Usage: "Grant write-only access"},
					cli.BoolFlag{Name: "readwrite, rw", Usage: "Grant read-write access"},
				},
				Action: actionRoleGrant,
			},
			{
				Name:      "revoke",
				Usage:     "revoke path matches for an etcd role",
				ArgsUsage: "<role>",
				Flags: []cli.Flag{
					cli.StringFlag{Name: "path", Value: "", Usage: "Path revoked for the role to access"},
					cli.BoolFlag{Name: "read", Usage: "Revoke read access"},
					cli.BoolFlag{Name: "write", Usage: "Revoke write access"},
					cli.BoolFlag{Name: "readwrite, rw", Usage: "Revoke read-write access"},
				},
				Action: actionRoleRevoke,
			},
		},
	}
}

func mustNewAuthRoleAPI(c *cli.Context) client.AuthRoleAPI {
	hc := mustNewClient(c)

	if c.GlobalBool("debug") {
		fmt.Fprintf(os.Stderr, "Cluster-Endpoints: %s\n", strings.Join(hc.Endpoints(), ", "))
	}

	return client.NewAuthRoleAPI(hc)
}

func actionRoleList(c *cli.Context) error {
	if len(c.Args()) != 0 {
		fmt.Fprintln(os.Stderr, "No arguments accepted")
		os.Exit(1)
	}
	r := mustNewAuthRoleAPI(c)
	ctx, cancel := contextWithTotalTimeout(c)
	roles, err := r.ListRoles(ctx)
	cancel()
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}

	for _, role := range roles {
		fmt.Printf("%s\n", role)
	}

	return nil
}

func actionRoleAdd(c *cli.Context) error {
	api, role := mustRoleAPIAndName(c)
	ctx, cancel := contextWithTotalTimeout(c)
	defer cancel()
	currentRole, _ := api.GetRole(ctx, role)
	if currentRole != nil {
		fmt.Fprintf(os.Stderr, "Role %s already exists\n", role)
		os.Exit(1)
	}

	err := api.AddRole(ctx, role)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}

	fmt.Printf("Role %s created\n", role)
	return nil
}

func actionRoleRemove(c *cli.Context) error {
	api, role := mustRoleAPIAndName(c)
	ctx, cancel := contextWithTotalTimeout(c)
	err := api.RemoveRole(ctx, role)
	cancel()
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}

	fmt.Printf("Role %s removed\n", role)
	return nil
}

func actionRoleGrant(c *cli.Context) error {
	roleGrantRevoke(c, true)
	return nil
}

func actionRoleRevoke(c *cli.Context) error {
	roleGrantRevoke(c, false)
	return nil
}

func roleGrantRevoke(c *cli.Context, grant bool) {
	path := c.String("path")
	if path == "" {
		fmt.Fprintln(os.Stderr, "No path specified; please use `--path`")
		os.Exit(1)
	}
	if pathutil.CanonicalURLPath(path) != path {
		fmt.Fprintf(os.Stderr, "Not canonical path; please use `--path=%s`\n", pathutil.CanonicalURLPath(path))
		os.Exit(1)
	}

	read := c.Bool("read")
	write := c.Bool("write")
	rw := c.Bool("readwrite")
	permcount := 0
	for _, v := range []bool{read, write, rw} {
		if v {
			permcount++
		}
	}
	if permcount != 1 {
		fmt.Fprintln(os.Stderr, "Please specify exactly one of --read, --write or --readwrite")
		os.Exit(1)
	}
	var permType client.PermissionType
	switch {
	case read:
		permType = client.ReadPermission
	case write:
		permType = client.WritePermission
	case rw:
		permType = client.ReadWritePermission
	}

	api, role := mustRoleAPIAndName(c)
	ctx, cancel := contextWithTotalTimeout(c)
	defer cancel()
	currentRole, err := api.GetRole(ctx, role)
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
	var newRole *client.Role
	if grant {
		newRole, err = api.GrantRoleKV(ctx, role, []string{path}, permType)
	} else {
		newRole, err = api.RevokeRoleKV(ctx, role, []string{path}, permType)
	}
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
	if reflect.DeepEqual(newRole, currentRole) {
		if grant {
			fmt.Printf("Role unchanged; already granted")
		} else {
			fmt.Printf("Role unchanged; already revoked")
		}
	}

	fmt.Printf("Role %s updated\n", role)
}

func actionRoleGet(c *cli.Context) error {
	api, rolename := mustRoleAPIAndName(c)

	ctx, cancel := contextWithTotalTimeout(c)
	role, err := api.GetRole(ctx, rolename)
	cancel()
	if err != nil {
		fmt.Fprintln(os.Stderr, err.Error())
		os.Exit(1)
	}
	fmt.Printf("Role: %s\n", role.Role)
	fmt.Printf("KV Read:\n")
	for _, v := range role.Permissions.KV.Read {
		fmt.Printf("\t%s\n", v)
	}
	fmt.Printf("KV Write:\n")
	for _, v := range role.Permissions.KV.Write {
		fmt.Printf("\t%s\n", v)
	}
	return nil
}

func mustRoleAPIAndName(c *cli.Context) (client.AuthRoleAPI, string) {
	args := c.Args()
	if len(args) != 1 {
		fmt.Fprintln(os.Stderr, "Please provide a role name")
		os.Exit(1)
	}

	name := args[0]
	api := mustNewAuthRoleAPI(c)
	return api, name
}
