// Copyright © 2023 OpenIM open source community. All rights reserved.
//
// 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 service

import (
	"errors"
	"fmt"
	"net"

	"github.com/gin-gonic/gin"

	"github.com/openimsdk/openkf/server/internal/common"
	"github.com/openimsdk/openkf/server/internal/config"
	"github.com/openimsdk/openkf/server/internal/dal/dao"
	"github.com/openimsdk/openkf/server/internal/models/base"
	systemroles "github.com/openimsdk/openkf/server/internal/models/system_roles"
	requestparams "github.com/openimsdk/openkf/server/internal/params/request"
	responseparams "github.com/openimsdk/openkf/server/internal/params/response"
	internal_utils "github.com/openimsdk/openkf/server/internal/utils"
	"github.com/openimsdk/openkf/server/pkg/openim/param/request"
	"github.com/openimsdk/openkf/server/pkg/openim/param/response"
	"github.com/openimsdk/openkf/server/pkg/openim/sdk/auth"
	"github.com/openimsdk/openkf/server/pkg/openim/sdk/user"
	"github.com/openimsdk/openkf/server/pkg/utils"
)

// UserService user service.
type UserService struct {
	Service

	SysUserDao      *dao.SysUserDao
	SysCommunityDao *dao.SysCommunityDao
}

// NewUserService return new service with gin context.
func NewUserService(c *gin.Context) *UserService {
	return &UserService{
		Service: Service{
			ctx: c,
		},
		SysUserDao:      dao.NewSysUserDao(),
		SysCommunityDao: dao.NewSysCommunityDao(),
	}
}

// CreateAdmin create admin user.
func (svc *UserService) CreateAdmin(user *requestparams.RegisterAdminParams) (string, uint, error) {
	// Check code
	mService := NewMailService((svc.ctx).(*gin.Context))
	if isExist := mService.CheckCode(user.UserInfo.Email, user.Code); !isExist {
		return "", 0, errors.New("code is not valid")
	}

	// Create community
	communityParam := user.CommunityInfo
	cService := NewCommunityService((svc.ctx).(*gin.Context))
	_, cid, err := cService.Create(&communityParam)
	if err != nil {
		return "", 0, err
	}

	// Create admin
	uuid := utils.GenUUIDWithoutHyphen()
	adminParam := user.UserInfo
	admin := &systemroles.SysUser{
		UserBase: base.UserBase{
			UUID:     uuid,
			Email:    adminParam.Email,
			Nickname: adminParam.Nickname,
			Avatar:   *adminParam.Avatar,
			IsEnable: true,
		},
		IsAdmin:     true,
		Password:    utils.EncryptPassword(adminParam.Password),
		CommunityId: cid,
	}
	if err = svc.SysUserDao.Create(admin); err != nil {
		return uuid, 0, err
	}

	u, _ := svc.SysUserDao.FindFirstByUUID(uuid)

	// TODO: set pipline to tx.
	param := &request.RegisterUserParams{
		Secret: config.Config.OpenIM.Secret,
		Users: []request.User{
			{
				UserID:   uuid,
				Nickname: adminParam.Nickname,
				FaceURL:  "", // Use OpenKF avatar
			},
		},
	}
	ok, err := registerUserToOpenIM(param)
	if err != nil || !ok {
		// Assume that the user has been created/deleted successfully
		_ = svc.SysUserDao.Delete(u)

		return uuid, u.Id, err
	}

	return uuid, u.Id, nil
}

// CreateStaff create staff user.
func (svc *UserService) CreateStaff(cid string, user *requestparams.RegisterStaffParams) (string, uint, error) {
	// Create staff
	uid := utils.GenUUIDWithoutHyphen()

	communityInfo, err := svc.SysCommunityDao.FindFirstByUUID(cid)
	if err != nil {
		return "", 0, err
	}

	staffParam := user.UserInfo
	staff := &systemroles.SysUser{
		UserBase: base.UserBase{
			UUID:     uid,
			Email:    staffParam.Email,
			Nickname: staffParam.Nickname,
			Avatar:   *staffParam.Avatar,
			IsEnable: true,
		},
		IsAdmin:     false,
		Password:    utils.EncryptPassword(staffParam.Password),
		CommunityId: communityInfo.Id,
	}
	if err = svc.SysUserDao.Create(staff); err != nil {
		return uid, 0, err
	}

	// TODO: Send email to staff

	u, _ := svc.SysUserDao.FindFirstByUUID(uid)

	// TODO: set pipline to tx.
	param := &request.RegisterUserParams{
		Secret: config.Config.OpenIM.Secret,
		Users: []request.User{
			{
				UserID:   uid,
				Nickname: staffParam.Nickname,
				FaceURL:  "", // Use OpenKF avatar
			},
		},
	}
	ok, err := registerUserToOpenIM(param)
	if err != nil || !ok {
		// Assume that the user has been created/deleted successfully
		_ = svc.SysUserDao.Delete(u)

		return uid, u.Id, err
	}

	return uid, u.Id, nil
}

// registerUserToOpenIM register user to openim.
func registerUserToOpenIM(param *request.RegisterUserParams) (bool, error) {
	// TODO: Add get operationID

	// Default not use tls/ssl
	host := fmt.Sprintf("http://%s", net.JoinHostPort(config.Config.OpenIM.Ip, fmt.Sprintf("%d", config.Config.OpenIM.ApiPort)))
	resp, err := user.RegisterUser(param, "registerUserToOpenIM", host)
	if err != nil {
		return false, err
	}

	if resp.ErrCode != 0 {
		return false, errors.New(resp.ErrMsg)
	}

	return true, nil
}

// DeleteStaff delete staff user.
func (svc *UserService) DeleteStaff(uid string) error {
	if uid == "" {
		return common.NewError(common.I_INVALID_PARAM)
	}

	u, err := svc.SysUserDao.FindFirstByUUID(uid)
	if err != nil {
		return err
	}

	err = svc.SysUserDao.Delete(u)
	if err != nil {
		return err
	}

	return nil
}

// LoginWithAccount login with account.
func (svc *UserService) LoginWithAccount(param *requestparams.LoginParamsWithAccount) (*responseparams.UserTokenResponse, error) {
	resp := &responseparams.UserTokenResponse{}

	// Check user
	u, err := svc.SysUserDao.FindFirstByEmail(param.Email)
	if err != nil {
		return resp, err
	}

	// Check password
	if !utils.ComparePassword(param.Password, u.Password) {
		return resp, errors.New("password is not correct")
	}

	// Get community id
	cService := NewCommunityService((svc.ctx).(*gin.Context))
	c, err := cService.GetCommunityInfoById(u.CommunityId)
	if err != nil {
		return resp, err
	}

	// Generate KF token
	kfToken, kfExpireTimeSeconds, err := internal_utils.GenerateJwtToken(u.UUID, c.UUID)
	if err != nil {
		return resp, err
	}

	// Get IM token
	imParam := &request.UserTokenParams{
		Secret:     config.Config.OpenIM.Secret,
		UserID:     u.UUID,
		PlatformID: uint(config.Config.OpenIM.PlatformID),
	}
	imResp, err := getUserIMToken(imParam)
	if err != nil {
		return resp, err
	}

	// Fill response data
	resp.UUID = u.UUID
	resp.KFToken = &responseparams.TokenResponse{
		Token:             kfToken,
		ExpireTimeSeconds: kfExpireTimeSeconds,
	}
	resp.IMToken = &responseparams.TokenResponse{
		Token:             imResp.Token,
		ExpireTimeSeconds: imResp.ExpireTimeSeconds,
	}

	// TODO: Set Online in OpenIM or do this in js-sdk

	return resp, nil
}

// getUserIMToken get user im token.
func getUserIMToken(param *request.UserTokenParams) (*response.TokenData, error) {
	// TODO: Add operationID

	// Default not use tls/ssl
	host := fmt.Sprintf("http://%s", net.JoinHostPort(config.Config.OpenIM.Ip, fmt.Sprintf("%d", config.Config.OpenIM.ApiPort)))
	resp, err := auth.GetUserToken(param, "getUserIMToken", host)
	if err != nil {
		return &response.TokenData{}, err
	}

	if resp.ErrCode != 0 {
		return &response.TokenData{}, errors.New(resp.ErrMsg)
	}

	return &resp.Data, nil
}

// GetAdminToken get admin token.
func (svc *UserService) GetAdminToken() (*response.TokenData, error) {
	params := &request.UserTokenParams{
		Secret:     config.Config.OpenIM.Secret,
		PlatformID: uint(config.Config.OpenIM.PlatformID),
		UserID:     config.Config.OpenIM.AdminID,
	}

	// TODO: Get cache from redis

	return getUserIMToken(params)
}

// GetUserInfoByUUID get user info by uuid.
func (svc *UserService) GetUserInfoByUUID(uid string) (*responseparams.UserInfoResponse, error) {
	resp := &responseparams.UserInfoResponse{}

	if uid == "" {
		return resp, common.NewError(common.I_INVALID_PARAM)
	}

	u, err := svc.SysUserDao.FindFirstByUUID(uid)
	if err != nil {
		return resp, err
	}

	resp.UUID = u.UUID
	resp.Email = u.Email
	resp.Nickname = u.Nickname
	resp.Avatar = u.Avatar
	resp.Description = u.Description
	resp.IsAdmin = u.IsAdmin
	resp.IsEnable = u.IsEnable
	resp.CreatedAt = u.CreatedAt.Format("2006-01-02 15:04:05")

	return resp, nil
}

// UpdateUserInfo update user info.
func (svc *UserService) UpdateUserInfo(uid string, params *requestparams.UpdateUserInfoParams) (*responseparams.UserInfoResponse, error) {
	resp := &responseparams.UserInfoResponse{}

	if uid == "" {
		return resp, common.NewError(common.I_INVALID_PARAM)
	}

	u, err := svc.SysUserDao.FindFirstByUUID(uid)
	if err != nil {
		return resp, err
	}

	// Update user info
	if params.Nickname != nil {
		u.Nickname = *params.Nickname
	}
	if params.Description != nil {
		u.Description = *params.Description
	}
	if params.Avatar != nil {
		u.Avatar = *params.Avatar
	}
	if params.Email != nil {
		u.Email = *params.Email
	}

	// TODO: Check if the user is admin
	if params.IsEnable != nil {
		u.IsEnable = *params.IsEnable
	}
	if params.IsAdmin != nil {
		u.IsAdmin = *params.IsAdmin
	}

	// Update user info
	if err = svc.SysUserDao.Update(u); err != nil {
		return resp, err
	}

	// Get new info
	u, _ = svc.SysUserDao.FindFirstByUUID(uid)
	resp.UUID = u.UUID
	resp.Email = u.Email
	resp.Nickname = u.Nickname
	resp.Avatar = u.Avatar
	resp.Description = u.Description
	resp.IsAdmin = u.IsAdmin
	resp.IsEnable = u.IsEnable
	resp.CreatedAt = u.CreatedAt.Format("2006-01-02 15:04:05")

	return resp, nil
}

// UpdateUserPassword update user password.
func (svc *UserService) UpdateUserPassword(uid string, params *requestparams.UpdateUserPasswordParams) error {
	if uid == "" {
		return common.NewError(common.I_INVALID_PARAM)
	}

	u, err := svc.SysUserDao.FindFirstByUUID(uid)
	if err != nil {
		return err
	}

	// Update user info
	if params.Password != "" && params.RepeatPassword != "" &&
		params.Password == params.RepeatPassword {
		u.Password = utils.EncryptPassword(params.Password)
	}

	// Update user info
	if err = svc.SysUserDao.Update(u); err != nil {
		return err
	}

	return nil
}

// GetCommunityUserList get community user list.
func (svc *UserService) GetCommunityUserList(cid string, params *requestparams.ListPageParams) (*responseparams.ListPageResponse, error) {
	resp := &responseparams.ListPageResponse{}
	userInfos := make([]*responseparams.UserInfoResponse, 0)

	csvc := NewCommunityService((svc.ctx).(*gin.Context))
	info, err := csvc.GetCommunityInfoByUUIDV2(cid)
	if err != nil {
		return resp, err
	}

	// Get user list
	users, total, err := svc.SysUserDao.FindByCommunityIdPage(info.Id, (params.Page-1)*params.PageSize, params.PageSize)
	if err != nil {
		return resp, err
	}

	// Fill response data
	for _, u := range users {
		userInfos = append(userInfos, &responseparams.UserInfoResponse{
			UUID:        u.UUID,
			Email:       u.Email,
			Nickname:    u.Nickname,
			Avatar:      u.Avatar,
			Description: u.Description,
			IsAdmin:     u.IsAdmin,
			IsEnable:    u.IsEnable,
			CreatedAt:   u.CreatedAt.Format("2006-01-02 15:04:05"),
		})
	}

	resp.List = userInfos
	resp.Page = params.Page
	resp.PageSize = params.PageSize
	resp.Total = int(total)

	return resp, nil
}
