package server

import (
	"context"
	"encoding/base64"
	"fmt"
	"time"

	cpb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/conflict"
	apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/conflict"
	rbacInterfaces "code.fbi.h-da.de/danet/gosdn/controller/interfaces/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/metrics"
	"code.fbi.h-da.de/danet/gosdn/controller/rbac"
	"code.fbi.h-da.de/danet/gosdn/controller/store"
	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/sethvargo/go-password/password"
	log "github.com/sirupsen/logrus"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"golang.org/x/crypto/argon2"
)

// UserServer holds a JWTManager and represents a UserServiceServer.
type UserServer struct {
	apb.UnimplementedUserServiceServer
	jwtManager  *rbac.JWTManager
	userService rbacInterfaces.UserService
}

// NewUserServer receives a JWTManager and a UserService and returns a new UserServer.
func NewUserServer(jwtManager *rbac.JWTManager, userService rbacInterfaces.UserService) *UserServer {
	return &UserServer{
		jwtManager:  jwtManager,
		userService: userService,
	}
}

// CreateUsers creates new users, can be 1 or more.
func (u UserServer) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest) (*apb.CreateUsersResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, user := range request.User {
		roles := map[string]string{}
		for key, elem := range user.Roles {
			_, err := uuid.Parse(key)
			if err != nil {
				return nil, handleRPCError(labels, err)
			}
			roles[key] = elem
		}

		// Generate a salt that is 16 characters long with 3 digits, 0 symbols,
		// allowing upper and lower case letters, disallowing repeat characters.
		salt, err := password.Generate(16, 3, 0, true, false)
		if err != nil {
			log.Error(err)
			return nil, status.Errorf(codes.Aborted, "%v", err)
		}

		hashedPassword := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(user.Password), []byte(salt), 1, 64*1024, 4, 32))

		user := rbac.NewUser(uuid.New(), user.Name, roles, string(hashedPassword), user.Token, salt, conflict.Metadata{ResourceVersion: 0})
		err = u.userService.Add(user)
		if err != nil {
			log.Error(err)
			return nil, status.Errorf(codes.Aborted, "%v", err)
		}
	}

	return &apb.CreateUsersResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}

// GetUser returns one user by name.
func (u UserServer) GetUser(ctx context.Context, request *apb.GetUserRequest) (*apb.GetUserResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "get"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	userID, err := uuid.Parse(request.Id)
	if err != nil {
		return nil, fmt.Errorf("could not parse user uuid")
	}

	userData, err := u.userService.Get(store.Query{Name: request.Name, ID: userID})
	if err != nil {
		return nil, err
	}

	user := &apb.User{
		Id:    userData.ID().String(),
		Name:  userData.Name(),
		Roles: userData.GetRoles(),
		Metadata: &cpb.Metadata{
			ResourceVersion: int64(userData.GetMetadata().ResourceVersion),
		},
	}

	return &apb.GetUserResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
		User:      user,
	}, nil
}

// GetUsers returns all availbale users.
func (u UserServer) GetUsers(ctx context.Context, request *apb.GetUsersRequest) (*apb.GetUsersResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "get"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	userList, err := u.userService.GetAll()
	if err != nil {
		return nil, err
	}

	users := []*apb.User{}
	for _, u := range userList {
		users = append(users, &apb.User{
			Id:    u.ID().String(),
			Name:  u.Name(),
			Roles: u.GetRoles(),
			Metadata: &cpb.Metadata{
				ResourceVersion: int64(u.GetMetadata().ResourceVersion),
			},
		})
	}

	return &apb.GetUsersResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
		User:      users,
	}, nil
}

// UpdateUsers updates the user data of one or more users provided in the request.
func (u UserServer) UpdateUsers(ctx context.Context, request *apb.UpdateUsersRequest) (*apb.UpdateUsersResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "post"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, user := range request.User {
		uid, err := uuid.Parse(user.Id)
		if err != nil {
			return nil, handleRPCError(labels, err)
		}

		storedUser, err := u.userService.Get(store.Query{ID: uid})
		if err != nil {
			return nil, status.Errorf(codes.Canceled, "user not found %v", err)
		}

		hashedPassword := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(user.Password), []byte(storedUser.GetSalt()), 1, 64*1024, 4, 32))

		userToUpdate := rbac.NewUser(uid, user.Name, user.Roles, string(hashedPassword), user.Token, storedUser.GetSalt(), conflict.Metadata{
			ResourceVersion: int(user.Metadata.ResourceVersion)})

		usr, _ := userToUpdate.(*rbac.User)
		usr.Metadata.ResourceVersion = int(user.Metadata.ResourceVersion)

		err = u.userService.Update(usr)
		if err != nil {
			return nil, status.Errorf(codes.Aborted, "could not update user %v", err)
		}
	}

	return &apb.UpdateUsersResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}

// DeleteUsers deletes one or more users provided in the request.
func (u UserServer) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest) (*apb.DeleteUsersResponse, error) {
	labels := prometheus.Labels{"service": "auth", "rpc": "delete"}
	start := metrics.StartHook(labels, grpcRequestsTotal)
	defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)

	for _, user := range request.Username {
		userToDelete, err := u.userService.Get(store.Query{Name: user})
		if err != nil {
			return nil, status.Errorf(codes.Canceled, "user not found %v", err)
		}

		err = u.userService.Delete(userToDelete)
		if err != nil {
			return nil, status.Errorf(codes.Aborted, "error deleting user %v", err)
		}
	}
	return &apb.DeleteUsersResponse{
		Timestamp: time.Now().UnixNano(),
		Status:    apb.Status_STATUS_OK,
	}, nil
}
