package api

import (
	"sort"
	"time"

	"github.com/mayswind/ezbookkeeping/pkg/avatars"
	"github.com/mayswind/ezbookkeeping/pkg/core"
	"github.com/mayswind/ezbookkeeping/pkg/errs"
	"github.com/mayswind/ezbookkeeping/pkg/log"
	"github.com/mayswind/ezbookkeeping/pkg/models"
	"github.com/mayswind/ezbookkeeping/pkg/services"
	"github.com/mayswind/ezbookkeeping/pkg/settings"
	"github.com/mayswind/ezbookkeeping/pkg/utils"
)

// TokensApi represents token api
type TokensApi struct {
	ApiUsingConfig
	ApiWithUserInfo
	tokens               *services.TokenService
	users                *services.UserService
	userAppCloudSettings *services.UserApplicationCloudSettingsService
}

// Initialize a token api singleton instance
var (
	Tokens = &TokensApi{
		ApiUsingConfig: ApiUsingConfig{
			container: settings.Container,
		},
		ApiWithUserInfo: ApiWithUserInfo{
			ApiUsingConfig: ApiUsingConfig{
				container: settings.Container,
			},
			ApiUsingAvatarProvider: ApiUsingAvatarProvider{
				container: avatars.Container,
			},
		},
		tokens:               services.Tokens,
		users:                services.Users,
		userAppCloudSettings: services.UserApplicationCloudSettings,
	}
)

// TokenListHandler returns available token list of current user
func (a *TokensApi) TokenListHandler(c *core.WebContext) (any, *errs.Error) {
	uid := c.GetCurrentUid()
	tokens, err := a.tokens.GetAllUnexpiredNormalAndMCPTokensByUid(c, uid)

	if err != nil {
		log.Errorf(c, "[tokens.TokenListHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	tokenResps := make(models.TokenInfoResponseSlice, len(tokens))
	claims := c.GetTokenClaims()

	for i := 0; i < len(tokens); i++ {
		token := tokens[i]
		tokenResp := &models.TokenInfoResponse{
			TokenId:   a.tokens.GenerateTokenId(token),
			TokenType: token.TokenType,
			UserAgent: token.UserAgent,
			LastSeen:  token.LastSeenUnixTime,
		}

		if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
			tokenResp.IsCurrent = true
		}

		if token.TokenType == core.USER_TOKEN_TYPE_API && token.UserAgent != core.TokenUserAgentCreatedViaCli {
			tokenResp.UserAgent = core.TokenUserAgentForAPI
		} else if token.TokenType == core.USER_TOKEN_TYPE_MCP && token.UserAgent != core.TokenUserAgentCreatedViaCli {
			tokenResp.UserAgent = core.TokenUserAgentForMCP
		}

		tokenResps[i] = tokenResp
	}

	sort.Sort(tokenResps)

	return tokenResps, nil
}

// TokenGenerateAPIHandler generates a new API token for current user
func (a *TokensApi) TokenGenerateAPIHandler(c *core.WebContext) (any, *errs.Error) {
	if !a.CurrentConfig().EnableAPIToken {
		return nil, errs.ErrAPITokenNotEnabled
	}

	var generateAPITokenReq models.TokenGenerateAPIRequest
	err := c.ShouldBindJSON(&generateAPITokenReq)

	if err != nil {
		log.Warnf(c, "[tokens.TokenGenerateAPIHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	uid := c.GetCurrentUid()
	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		log.Warnf(c, "[tokens.TokenGenerateAPIHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_GENERATE_API_TOKEN) {
		return false, errs.ErrNotPermittedToPerformThisAction
	}

	if !a.users.IsPasswordEqualsUserPassword(generateAPITokenReq.Password, user) {
		return nil, errs.ErrUserPasswordWrong
	}

	token, claims, err := a.tokens.CreateAPIToken(c, user, generateAPITokenReq.ExpiredInSeconds)

	if err != nil {
		log.Errorf(c, "[tokens.TokenGenerateAPIHandler] failed to create api token for user \"uid:%d\", because %s", user.Uid, err.Error())
		return nil, errs.Or(err, errs.ErrTokenGenerating)
	}

	log.Infof(c, "[tokens.TokenGenerateAPIHandler] user \"uid:%d\" has generated api token, new token will be expired at %d", user.Uid, claims.ExpiresAt)

	generateAPITokenResp := &models.TokenGenerateAPIResponse{
		Token:      token,
		APIBaseUrl: a.CurrentConfig().RootUrl + "api",
	}

	return generateAPITokenResp, nil
}

// TokenGenerateMCPHandler generates a new MCP token for current user
func (a *TokensApi) TokenGenerateMCPHandler(c *core.WebContext) (any, *errs.Error) {
	if !a.CurrentConfig().EnableMCPServer {
		return nil, errs.ErrMCPServerNotEnabled
	}

	var generateMCPTokenReq models.TokenGenerateMCPRequest
	err := c.ShouldBindJSON(&generateMCPTokenReq)

	if err != nil {
		log.Warnf(c, "[tokens.TokenGenerateMCPHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	uid := c.GetCurrentUid()
	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		log.Warnf(c, "[tokens.TokenGenerateMCPHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_MCP_ACCESS) {
		return false, errs.ErrNotPermittedToPerformThisAction
	}

	if !a.users.IsPasswordEqualsUserPassword(generateMCPTokenReq.Password, user) {
		return nil, errs.ErrUserPasswordWrong
	}

	token, claims, err := a.tokens.CreateMCPToken(c, user, generateMCPTokenReq.ExpiredInSeconds)

	if err != nil {
		log.Errorf(c, "[tokens.TokenGenerateMCPHandler] failed to create mcp token for user \"uid:%d\", because %s", user.Uid, err.Error())
		return nil, errs.Or(err, errs.ErrTokenGenerating)
	}

	log.Infof(c, "[tokens.TokenGenerateMCPHandler] user \"uid:%d\" has generated mcp token, new token will be expired at %d", user.Uid, claims.ExpiresAt)

	generateMCPTokenResp := &models.TokenGenerateMCPResponse{
		Token:  token,
		MCPUrl: a.CurrentConfig().RootUrl + "mcp",
	}

	return generateMCPTokenResp, nil
}

// TokenRevokeCurrentHandler revokes current token of current user
func (a *TokensApi) TokenRevokeCurrentHandler(c *core.WebContext) (any, *errs.Error) {
	tokenString := c.GetTokenStringFromHeader()

	if tokenString == "" {
		return false, errs.ErrTokenIsEmpty
	}

	_, claims, _, err := a.tokens.ParseToken(c, tokenString)

	if err != nil {
		return nil, errs.Or(err, errs.NewIncompleteOrIncorrectSubmissionError(err))
	}

	userTokenId, err := utils.StringToInt64(claims.UserTokenId)

	if err != nil {
		log.Warnf(c, "[tokens.TokenRevokeCurrentHandler] parse user token id failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	tokenRecord := &models.TokenRecord{
		Uid:             claims.Uid,
		UserTokenId:     userTokenId,
		CreatedUnixTime: claims.IssuedAt,
	}

	tokenId := a.tokens.GenerateTokenId(tokenRecord)
	err = a.tokens.DeleteToken(c, tokenRecord)

	if err != nil {
		log.Errorf(c, "[tokens.TokenRevokeCurrentHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenId, claims.Uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	log.Infof(c, "[tokens.TokenRevokeCurrentHandler] user \"uid:%d\" has revoked token \"id:%s\"", claims.Uid, tokenId)
	return true, nil
}

// TokenRevokeHandler revokes specific token of current user
func (a *TokensApi) TokenRevokeHandler(c *core.WebContext) (any, *errs.Error) {
	var tokenRevokeReq models.TokenRevokeRequest
	err := c.ShouldBindJSON(&tokenRevokeReq)

	if err != nil {
		log.Warnf(c, "[tokens.TokenRevokeHandler] parse request failed, because %s", err.Error())
		return nil, errs.NewIncompleteOrIncorrectSubmissionError(err)
	}

	tokenRecord, err := a.tokens.ParseFromTokenId(tokenRevokeReq.TokenId)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Errorf(c, "[tokens.TokenRevokeHandler] failed to parse token \"id:%s\", because %s", tokenRevokeReq.TokenId, err.Error())
		}

		return nil, errs.Or(err, errs.ErrInvalidTokenId)
	}

	uid := c.GetCurrentUid()

	if tokenRecord.Uid != uid {
		log.Warnf(c, "[tokens.TokenRevokeHandler] token \"id:%s\" is not owned by user \"uid:%d\"", tokenRevokeReq.TokenId, uid)
		return nil, errs.ErrInvalidTokenId
	}

	if utils.Int64ToString(tokenRecord.UserTokenId) != c.GetTokenClaims().UserTokenId || tokenRecord.CreatedUnixTime != c.GetTokenClaims().IssuedAt {
		user, err := a.users.GetUserById(c, uid)

		if err != nil {
			if !errs.IsCustomError(err) {
				log.Errorf(c, "[tokens.TokenRevokeHandler] failed to get user, because %s", err.Error())
			}

			return nil, errs.ErrUserNotFound
		}

		if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
			return nil, errs.ErrNotPermittedToPerformThisAction
		}
	}

	err = a.tokens.DeleteToken(c, tokenRecord)

	if err != nil {
		log.Errorf(c, "[tokens.TokenRevokeHandler] failed to revoke token \"id:%s\" for user \"uid:%d\", because %s", tokenRevokeReq.TokenId, uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	log.Infof(c, "[tokens.TokenRevokeHandler] user \"uid:%d\" has revoked token \"id:%s\"", uid, tokenRevokeReq.TokenId)
	return true, nil
}

// TokenRevokeAllHandler revokes all tokens of current user except current token
func (a *TokensApi) TokenRevokeAllHandler(c *core.WebContext) (any, *errs.Error) {
	uid := c.GetCurrentUid()
	tokens, err := a.tokens.GetAllTokensByUid(c, uid)

	if err != nil {
		log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get all tokens for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	claims := c.GetTokenClaims()
	currentTokenIndex := 0

	for i := 0; i < len(tokens); i++ {
		token := tokens[i]

		if token.Uid == claims.Uid && utils.Int64ToString(token.UserTokenId) == claims.UserTokenId && token.CreatedUnixTime == claims.IssuedAt {
			currentTokenIndex = i
			break
		}
	}

	tokens = append(tokens[:currentTokenIndex], tokens[currentTokenIndex+1:]...)

	if len(tokens) < 1 {
		return nil, errs.ErrTokenRecordNotFound
	}

	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		if !errs.IsCustomError(err) {
			log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to get user, because %s", err.Error())
		}

		return nil, errs.ErrUserNotFound
	}

	if user.FeatureRestriction.Contains(core.USER_FEATURE_RESTRICTION_TYPE_REVOKE_OTHER_SESSION) {
		return nil, errs.ErrNotPermittedToPerformThisAction
	}

	err = a.tokens.DeleteTokens(c, uid, tokens)

	if err != nil {
		log.Errorf(c, "[tokens.TokenRevokeAllHandler] failed to revoke all tokens for user \"uid:%d\", because %s", uid, err.Error())
		return nil, errs.Or(err, errs.ErrOperationFailed)
	}

	log.Infof(c, "[tokens.TokenRevokeAllHandler] user \"uid:%d\" has revoked all tokens", uid)
	return true, nil
}

// TokenRefreshHandler refresh current token of current user
func (a *TokensApi) TokenRefreshHandler(c *core.WebContext) (any, *errs.Error) {
	uid := c.GetCurrentUid()
	user, err := a.users.GetUserById(c, uid)

	if err != nil {
		log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get user \"uid:%d\" info, because %s", uid, err.Error())
		return nil, errs.ErrUserNotFound
	}

	now := time.Now().Unix()
	oldTokenClaims := c.GetTokenClaims()

	if now-oldTokenClaims.IssuedAt < int64(a.CurrentConfig().TokenMinRefreshInterval) {
		log.Infof(c, "[tokens.TokenRefreshHandler] token of user \"uid:%d\" does not need to be refreshed", uid)

		userTokenId, err := utils.StringToInt64(oldTokenClaims.UserTokenId)

		if err != nil {
			log.Warnf(c, "[tokens.TokenRefreshHandler] parse user token id failed, because %s", err.Error())
		} else {
			tokenRecord := &models.TokenRecord{
				Uid:             oldTokenClaims.Uid,
				UserTokenId:     userTokenId,
				CreatedUnixTime: oldTokenClaims.IssuedAt,
			}

			tokenId := a.tokens.GenerateTokenId(tokenRecord)

			err = a.tokens.UpdateTokenLastSeen(c, tokenRecord)

			if err != nil {
				log.Warnf(c, "[tokens.TokenRefreshHandler] failed to update last seen of token \"id:%s\" for user \"uid:%d\", because %s", tokenId, uid, err.Error())
			}
		}

		userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
		var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil

		if err != nil {
			log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
		} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
			applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
		}

		refreshResp := &models.TokenRefreshResponse{
			User:                     a.GetUserBasicInfo(user),
			ApplicationCloudSettings: applicationCloudSettingSlice,
			NotificationContent:      a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
		}

		return refreshResp, nil
	}

	token, claims, err := a.tokens.CreateToken(c, user)

	if err != nil {
		log.Errorf(c, "[tokens.TokenRefreshHandler] failed to create token for user \"uid:%d\", because %s", user.Uid, err.Error())
		return nil, errs.Or(err, errs.ErrTokenGenerating)
	}

	oldUserTokenId, _ := utils.StringToInt64(oldTokenClaims.UserTokenId)
	oldTokenRecord := &models.TokenRecord{
		Uid:             uid,
		UserTokenId:     oldUserTokenId,
		CreatedUnixTime: oldTokenClaims.IssuedAt,
	}

	c.SetTextualToken(token)
	c.SetTokenClaims(claims)
	c.SetTokenContext("")

	userApplicationCloudSettings, err := a.userAppCloudSettings.GetUserApplicationCloudSettingsByUid(c, user.Uid)
	var applicationCloudSettingSlice *models.ApplicationCloudSettingSlice = nil

	if err != nil {
		log.Warnf(c, "[tokens.TokenRefreshHandler] failed to get latest user application cloud settings for user \"uid:%d\", because %s", user.Uid, err.Error())
	} else if userApplicationCloudSettings != nil && len(userApplicationCloudSettings.Settings) > 0 {
		applicationCloudSettingSlice = &userApplicationCloudSettings.Settings
	}

	log.Infof(c, "[tokens.TokenRefreshHandler] user \"uid:%d\" token refreshed, new token will be expired at %d", user.Uid, claims.ExpiresAt)

	refreshResp := &models.TokenRefreshResponse{
		NewToken:                 token,
		OldTokenId:               a.tokens.GenerateTokenId(oldTokenRecord),
		User:                     a.GetUserBasicInfo(user),
		ApplicationCloudSettings: applicationCloudSettingSlice,
		NotificationContent:      a.GetAfterOpenNotificationContent(user.Language, c.GetClientLocale()),
	}

	return refreshResp, nil
}
