// Copyright 2019 Sorint.lab
//
// 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 action

import (
	"context"
	"encoding/json"
	"time"

	"github.com/sorintlab/agola/internal/datamanager"
	"github.com/sorintlab/agola/internal/db"
	"github.com/sorintlab/agola/internal/services/configstore/readdb"
	"github.com/sorintlab/agola/internal/services/types"
	"github.com/sorintlab/agola/internal/util"

	"github.com/pkg/errors"
	uuid "github.com/satori/go.uuid"
)

type CreateUserRequest struct {
	UserName string

	CreateUserLARequest *CreateUserLARequest
}

func (h *ActionHandler) CreateUser(ctx context.Context, req *CreateUserRequest) (*types.User, error) {
	if req.UserName == "" {
		return nil, util.NewErrBadRequest(errors.Errorf("user name required"))
	}
	if !util.ValidateName(req.UserName) {
		return nil, util.NewErrBadRequest(errors.Errorf("invalid user name %q", req.UserName))
	}

	var cgt *datamanager.ChangeGroupsUpdateToken
	// changegroup is the username (and in future the email) to ensure no
	// concurrent user creation/modification using the same name
	cgNames := []string{util.EncodeSha256Hex("username-" + req.UserName)}
	var rs *types.RemoteSource

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		// check duplicate user name
		u, err := h.readDB.GetUserByName(tx, req.UserName)
		if err != nil {
			return err
		}
		if u != nil {
			return util.NewErrBadRequest(errors.Errorf("user with name %q already exists", u.Name))
		}

		if req.CreateUserLARequest != nil {
			rs, err = h.readDB.GetRemoteSourceByName(tx, req.CreateUserLARequest.RemoteSourceName)
			if err != nil {
				return err
			}
			if rs == nil {
				return util.NewErrBadRequest(errors.Errorf("remote source %q doesn't exist", req.CreateUserLARequest.RemoteSourceName))
			}
			user, err := h.readDB.GetUserByLinkedAccountRemoteUserIDandSource(tx, req.CreateUserLARequest.RemoteUserID, rs.ID)
			if err != nil {
				return errors.Wrapf(err, "failed to get user for remote user id %q and remote source %q", req.CreateUserLARequest.RemoteUserID, rs.ID)
			}
			if user != nil {
				return util.NewErrBadRequest(errors.Errorf("user for remote user id %q for remote source %q already exists", req.CreateUserLARequest.RemoteUserID, req.CreateUserLARequest.RemoteSourceName))
			}
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	user := &types.User{
		ID:   uuid.NewV4().String(),
		Name: req.UserName,
	}
	if req.CreateUserLARequest != nil {
		if user.LinkedAccounts == nil {
			user.LinkedAccounts = make(map[string]*types.LinkedAccount)
		}

		la := &types.LinkedAccount{
			ID:                         uuid.NewV4().String(),
			RemoteSourceID:             rs.ID,
			RemoteUserID:               req.CreateUserLARequest.RemoteUserID,
			RemoteUserName:             req.CreateUserLARequest.RemoteUserName,
			UserAccessToken:            req.CreateUserLARequest.UserAccessToken,
			Oauth2AccessToken:          req.CreateUserLARequest.Oauth2AccessToken,
			Oauth2RefreshToken:         req.CreateUserLARequest.Oauth2RefreshToken,
			Oauth2AccessTokenExpiresAt: req.CreateUserLARequest.Oauth2AccessTokenExpiresAt,
		}

		user.LinkedAccounts[la.ID] = la
	}

	userj, err := json.Marshal(user)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to marshal user")
	}

	pg := &types.ProjectGroup{
		ID: uuid.NewV4().String(),
		Parent: types.Parent{
			Type: types.ConfigTypeUser,
			ID:   user.ID,
		},
	}
	pgj, err := json.Marshal(pg)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to marshal project group")
	}

	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeProjectGroup),
			ID:         pg.ID,
			Data:       pgj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return user, err
}

func (h *ActionHandler) DeleteUser(ctx context.Context, userRef string) error {
	var user *types.User

	var cgt *datamanager.ChangeGroupsUpdateToken
	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error

		// check user existance
		user, err = h.readDB.GetUser(tx, userRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", userRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return err
	}

	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypeDelete,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return err
}

type UpdateUserRequest struct {
	UserRef string

	UserName string
}

func (h *ActionHandler) UpdateUser(ctx context.Context, req *UpdateUserRequest) (*types.User, error) {
	var cgt *datamanager.ChangeGroupsUpdateToken

	cgNames := []string{}
	var user *types.User

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, req.UserRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", req.UserRef))
		}

		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		if req.UserName != "" {
			// check duplicate user name
			u, err := h.readDB.GetUserByName(tx, req.UserName)
			if err != nil {
				return err
			}
			if u != nil {
				return util.NewErrBadRequest(errors.Errorf("user with name %q already exists", u.Name))
			}
			// changegroup is the username (and in future the email) to ensure no
			// concurrent user creation/modification using the same name
			cgNames = append(cgNames, util.EncodeSha256Hex("username-"+req.UserName))
		}

		return nil
	})
	if err != nil {
		return nil, err
	}

	if req.UserName != "" {
		user.Name = req.UserName
	}

	userj, err := json.Marshal(user)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to marshal user")
	}

	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return user, err
}

type CreateUserLARequest struct {
	UserRef string

	RemoteSourceName           string
	RemoteUserID               string
	RemoteUserName             string
	UserAccessToken            string
	Oauth2AccessToken          string
	Oauth2RefreshToken         string
	Oauth2AccessTokenExpiresAt time.Time
}

func (h *ActionHandler) CreateUserLA(ctx context.Context, req *CreateUserLARequest) (*types.LinkedAccount, error) {
	if req.UserRef == "" {
		return nil, util.NewErrBadRequest(errors.Errorf("user ref required"))
	}
	if req.RemoteSourceName == "" {
		return nil, util.NewErrBadRequest(errors.Errorf("remote source name required"))
	}

	var user *types.User
	var rs *types.RemoteSource

	var cgt *datamanager.ChangeGroupsUpdateToken

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, req.UserRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", req.UserRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		rs, err = h.readDB.GetRemoteSourceByName(tx, req.RemoteSourceName)
		if err != nil {
			return err
		}
		if rs == nil {
			return util.NewErrBadRequest(errors.Errorf("remote source %q doesn't exist", req.RemoteSourceName))
		}

		user, err := h.readDB.GetUserByLinkedAccountRemoteUserIDandSource(tx, req.RemoteUserID, rs.ID)
		if err != nil {
			return errors.Wrapf(err, "failed to get user for remote user id %q and remote source %q", req.RemoteUserID, rs.ID)
		}
		if user != nil {
			return util.NewErrBadRequest(errors.Errorf("user for remote user id %q for remote source %q already exists", req.RemoteUserID, req.RemoteSourceName))
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	if user.LinkedAccounts == nil {
		user.LinkedAccounts = make(map[string]*types.LinkedAccount)
	}

	la := &types.LinkedAccount{
		ID:                         uuid.NewV4().String(),
		RemoteSourceID:             rs.ID,
		RemoteUserID:               req.RemoteUserID,
		RemoteUserName:             req.RemoteUserName,
		UserAccessToken:            req.UserAccessToken,
		Oauth2AccessToken:          req.Oauth2AccessToken,
		Oauth2RefreshToken:         req.Oauth2RefreshToken,
		Oauth2AccessTokenExpiresAt: req.Oauth2AccessTokenExpiresAt,
	}

	user.LinkedAccounts[la.ID] = la

	userj, err := json.Marshal(user)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to marshal user")
	}
	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return la, err
}

func (h *ActionHandler) DeleteUserLA(ctx context.Context, userRef, laID string) error {
	if userRef == "" {
		return util.NewErrBadRequest(errors.Errorf("user ref  required"))
	}
	if laID == "" {
		return util.NewErrBadRequest(errors.Errorf("user linked account id required"))
	}

	var user *types.User

	var cgt *datamanager.ChangeGroupsUpdateToken

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, userRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", userRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return err
	}

	_, ok := user.LinkedAccounts[laID]
	if !ok {
		return util.NewErrBadRequest(errors.Errorf("linked account id %q for user %q doesn't exist", laID, userRef))
	}

	delete(user.LinkedAccounts, laID)

	userj, err := json.Marshal(user)
	if err != nil {
		return errors.Wrapf(err, "failed to marshal user")
	}
	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return err
}

type UpdateUserLARequest struct {
	UserRef string

	LinkedAccountID            string
	RemoteUserID               string
	RemoteUserName             string
	UserAccessToken            string
	Oauth2AccessToken          string
	Oauth2RefreshToken         string
	Oauth2AccessTokenExpiresAt time.Time
}

func (h *ActionHandler) UpdateUserLA(ctx context.Context, req *UpdateUserLARequest) (*types.LinkedAccount, error) {
	if req.UserRef == "" {
		return nil, util.NewErrBadRequest(errors.Errorf("user ref required"))
	}

	var user *types.User
	var rs *types.RemoteSource

	var cgt *datamanager.ChangeGroupsUpdateToken

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, req.UserRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", req.UserRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		la, ok := user.LinkedAccounts[req.LinkedAccountID]
		if !ok {
			return util.NewErrBadRequest(errors.Errorf("linked account id %q for user %q doesn't exist", req.LinkedAccountID, user.Name))
		}

		rs, err = h.readDB.GetRemoteSource(tx, la.RemoteSourceID)
		if err != nil {
			return err
		}
		if rs == nil {
			return util.NewErrBadRequest(errors.Errorf("remote source with id %q doesn't exist", la.RemoteSourceID))
		}
		return nil
	})
	if err != nil {
		return nil, err
	}

	la := user.LinkedAccounts[req.LinkedAccountID]

	la.RemoteUserID = req.RemoteUserID
	la.RemoteUserName = req.RemoteUserName
	la.UserAccessToken = req.UserAccessToken
	la.Oauth2AccessToken = req.Oauth2AccessToken
	la.Oauth2RefreshToken = req.Oauth2RefreshToken
	la.Oauth2AccessTokenExpiresAt = req.Oauth2AccessTokenExpiresAt

	userj, err := json.Marshal(user)
	if err != nil {
		return nil, errors.Wrapf(err, "failed to marshal user")
	}
	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return la, err
}

func (h *ActionHandler) CreateUserToken(ctx context.Context, userRef, tokenName string) (string, error) {
	if userRef == "" {
		return "", util.NewErrBadRequest(errors.Errorf("user ref required"))
	}
	if tokenName == "" {
		return "", util.NewErrBadRequest(errors.Errorf("token name required"))
	}

	var user *types.User

	var cgt *datamanager.ChangeGroupsUpdateToken

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, userRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", userRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return "", err
	}
	if user.Tokens != nil {
		if _, ok := user.Tokens[tokenName]; ok {
			return "", util.NewErrBadRequest(errors.Errorf("token %q for user %q already exists", tokenName, userRef))
		}
	}

	if user.Tokens == nil {
		user.Tokens = make(map[string]string)
	}

	token := util.EncodeSha1Hex(uuid.NewV4().String())
	user.Tokens[tokenName] = token

	userj, err := json.Marshal(user)
	if err != nil {
		return "", errors.Wrapf(err, "failed to marshal user")
	}
	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return token, err
}

func (h *ActionHandler) DeleteUserToken(ctx context.Context, userRef, tokenName string) error {
	if userRef == "" {
		return util.NewErrBadRequest(errors.Errorf("user ref required"))
	}
	if tokenName == "" {
		return util.NewErrBadRequest(errors.Errorf("token name required"))
	}

	var user *types.User

	var cgt *datamanager.ChangeGroupsUpdateToken

	// must do all the checks in a single transaction to avoid concurrent changes
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err = h.readDB.GetUser(tx, userRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrBadRequest(errors.Errorf("user %q doesn't exist", userRef))
		}

		// changegroup is the userid
		cgNames := []string{util.EncodeSha256Hex("userid-" + user.ID)}
		cgt, err = h.readDB.GetChangeGroupsUpdateTokens(tx, cgNames)
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return err
	}

	_, ok := user.Tokens[tokenName]
	if !ok {
		return util.NewErrBadRequest(errors.Errorf("token %q for user %q doesn't exist", tokenName, userRef))
	}

	delete(user.Tokens, tokenName)

	userj, err := json.Marshal(user)
	if err != nil {
		return errors.Wrapf(err, "failed to marshal user")
	}
	actions := []*datamanager.Action{
		{
			ActionType: datamanager.ActionTypePut,
			DataType:   string(types.ConfigTypeUser),
			ID:         user.ID,
			Data:       userj,
		},
	}

	_, err = h.dm.WriteWal(ctx, actions, cgt)
	return err
}

type UserOrgsResponse struct {
	Organization *types.Organization
	Role         types.MemberRole
}

func userOrgsResponse(userOrg *readdb.UserOrg) *UserOrgsResponse {
	return &UserOrgsResponse{
		Organization: userOrg.Organization,
		Role:         userOrg.Role,
	}
}

func (h *ActionHandler) GetUserOrgs(ctx context.Context, userRef string) ([]*UserOrgsResponse, error) {
	var userOrgs []*readdb.UserOrg
	err := h.readDB.Do(func(tx *db.Tx) error {
		var err error
		user, err := h.readDB.GetUser(tx, userRef)
		if err != nil {
			return err
		}
		if user == nil {
			return util.NewErrNotFound(errors.Errorf("user %q doesn't exist", userRef))
		}

		userOrgs, err = h.readDB.GetUserOrgs(tx, user.ID)
		return err
	})
	if err != nil {
		return nil, err
	}

	res := make([]*UserOrgsResponse, len(userOrgs))
	for i, userOrg := range userOrgs {
		res[i] = userOrgsResponse(userOrg)
	}

	return res, nil
}