// 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 client

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/url"
)

type Role struct {
	Role        string       `json:"role"`
	Permissions Permissions  `json:"permissions"`
	Grant       *Permissions `json:"grant,omitempty"`
	Revoke      *Permissions `json:"revoke,omitempty"`
}

type Permissions struct {
	KV rwPermission `json:"kv"`
}

type rwPermission struct {
	Read  []string `json:"read"`
	Write []string `json:"write"`
}

type PermissionType int

const (
	ReadPermission PermissionType = iota
	WritePermission
	ReadWritePermission
)

// NewAuthRoleAPI constructs a new AuthRoleAPI that uses HTTP to
// interact with etcd's role creation and modification features.
func NewAuthRoleAPI(c Client) AuthRoleAPI {
	return &httpAuthRoleAPI{
		client: c,
	}
}

type AuthRoleAPI interface {
	// AddRole adds a role.
	AddRole(ctx context.Context, role string) error

	// RemoveRole removes a role.
	RemoveRole(ctx context.Context, role string) error

	// GetRole retrieves role details.
	GetRole(ctx context.Context, role string) (*Role, error)

	// GrantRoleKV grants a role some permission prefixes for the KV store.
	GrantRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)

	// RevokeRoleKV revokes some permission prefixes for a role on the KV store.
	RevokeRoleKV(ctx context.Context, role string, prefixes []string, permType PermissionType) (*Role, error)

	// ListRoles lists roles.
	ListRoles(ctx context.Context) ([]string, error)
}

type httpAuthRoleAPI struct {
	client httpClient
}

type authRoleAPIAction struct {
	verb string
	name string
	role *Role
}

type authRoleAPIList struct{}

func (list *authRoleAPIList) HTTPRequest(ep url.URL) *http.Request {
	u := v2AuthURL(ep, "roles", "")
	req, _ := http.NewRequest(http.MethodGet, u.String(), nil)
	req.Header.Set("Content-Type", "application/json")
	return req
}

func (l *authRoleAPIAction) HTTPRequest(ep url.URL) *http.Request {
	u := v2AuthURL(ep, "roles", l.name)
	if l.role == nil {
		req, _ := http.NewRequest(l.verb, u.String(), nil)
		return req
	}
	b, err := json.Marshal(l.role)
	if err != nil {
		panic(err)
	}
	body := bytes.NewReader(b)
	req, _ := http.NewRequest(l.verb, u.String(), body)
	req.Header.Set("Content-Type", "application/json")
	return req
}

func (r *httpAuthRoleAPI) ListRoles(ctx context.Context) ([]string, error) {
	resp, body, err := r.client.Do(ctx, &authRoleAPIList{})
	if err != nil {
		return nil, err
	}
	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
		return nil, err
	}
	var roleList struct {
		Roles []Role `json:"roles"`
	}
	if err = json.Unmarshal(body, &roleList); err != nil {
		return nil, err
	}
	ret := make([]string, 0, len(roleList.Roles))
	for _, r := range roleList.Roles {
		ret = append(ret, r.Role)
	}
	return ret, nil
}

func (r *httpAuthRoleAPI) AddRole(ctx context.Context, rolename string) error {
	role := &Role{
		Role: rolename,
	}
	return r.addRemoveRole(ctx, &authRoleAPIAction{
		verb: http.MethodPut,
		name: rolename,
		role: role,
	})
}

func (r *httpAuthRoleAPI) RemoveRole(ctx context.Context, rolename string) error {
	return r.addRemoveRole(ctx, &authRoleAPIAction{
		verb: http.MethodDelete,
		name: rolename,
	})
}

func (r *httpAuthRoleAPI) addRemoveRole(ctx context.Context, req *authRoleAPIAction) error {
	resp, body, err := r.client.Do(ctx, req)
	if err != nil {
		return err
	}
	if err := assertStatusCode(resp.StatusCode, http.StatusOK, http.StatusCreated); err != nil {
		var sec authError
		err := json.Unmarshal(body, &sec)
		if err != nil {
			return err
		}
		return sec
	}
	return nil
}

func (r *httpAuthRoleAPI) GetRole(ctx context.Context, rolename string) (*Role, error) {
	return r.modRole(ctx, &authRoleAPIAction{
		verb: http.MethodGet,
		name: rolename,
	})
}

func buildRWPermission(prefixes []string, permType PermissionType) rwPermission {
	var out rwPermission
	switch permType {
	case ReadPermission:
		out.Read = prefixes
	case WritePermission:
		out.Write = prefixes
	case ReadWritePermission:
		out.Read = prefixes
		out.Write = prefixes
	}
	return out
}

func (r *httpAuthRoleAPI) GrantRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
	rwp := buildRWPermission(prefixes, permType)
	role := &Role{
		Role: rolename,
		Grant: &Permissions{
			KV: rwp,
		},
	}
	return r.modRole(ctx, &authRoleAPIAction{
		verb: http.MethodPut,
		name: rolename,
		role: role,
	})
}

func (r *httpAuthRoleAPI) RevokeRoleKV(ctx context.Context, rolename string, prefixes []string, permType PermissionType) (*Role, error) {
	rwp := buildRWPermission(prefixes, permType)
	role := &Role{
		Role: rolename,
		Revoke: &Permissions{
			KV: rwp,
		},
	}
	return r.modRole(ctx, &authRoleAPIAction{
		verb: http.MethodPut,
		name: rolename,
		role: role,
	})
}

func (r *httpAuthRoleAPI) modRole(ctx context.Context, req *authRoleAPIAction) (*Role, error) {
	resp, body, err := r.client.Do(ctx, req)
	if err != nil {
		return nil, err
	}
	if err = assertStatusCode(resp.StatusCode, http.StatusOK); err != nil {
		var sec authError
		err = json.Unmarshal(body, &sec)
		if err != nil {
			return nil, err
		}
		return nil, sec
	}
	var role Role
	if err = json.Unmarshal(body, &role); err != nil {
		return nil, err
	}
	return &role, nil
}
