// Copyright 2024 The Casdoor Authors. 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 object

import (
	"fmt"
	"strings"
	"time"

	"github.com/casdoor/casdoor/idp"
	"github.com/casdoor/casdoor/util"
)

func GetOAuthToken(grantType string, clientId string, clientSecret string, code string, verifier string, scope string, nonce string, username string, password string, host string, refreshToken string, tag string, avatar string, lang string, subjectToken string, subjectTokenType string, actorToken string, actorTokenType string, assertion string, clientAssertion string, clientAssertionType string, audience string, resource string, dpopProof string) (interface{}, error) {
	var (
		application *Application
		err         error
		ok          bool
	)

	if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
		ok, application, err = ValidateClientAssertion(clientAssertion, host)
		if err != nil {
			return nil, err
		}

		if !ok || application == nil {
			return &TokenError{
				Error:            InvalidClient,
				ErrorDescription: "client_assertion is invalid",
			}, nil
		}

		clientSecret = application.ClientSecret
		clientId = application.ClientId
	} else {
		application, err = GetApplicationByClientId(clientId)
		if err != nil {
			return nil, err
		}
	}

	if application == nil {
		return &TokenError{
			Error:            InvalidClient,
			ErrorDescription: "client_id is invalid",
		}, nil
	}

	// Handle WeChat Mini Program flow separately — it does not use standard OAuth grant types
	if tag == "wechat_miniprogram" {
		token, tokenError, err := GetWechatMiniProgramToken(application, code, host, username, avatar, lang)
		if err != nil {
			return nil, err
		}
		if tokenError != nil {
			return tokenError, nil
		}
		return token, nil
	}

	// Check if grantType is allowed in the current application
	if !IsGrantTypeValid(grantType, application.GrantTypes) {
		return &TokenError{
			Error:            UnsupportedGrantType,
			ErrorDescription: fmt.Sprintf("grant_type: %s is not supported in this application", grantType),
		}, nil
	}

	var token *Token
	var tokenError *TokenError
	switch grantType {
	case "authorization_code": // Authorization Code Grant
		token, tokenError, err = GetAuthorizationCodeToken(application, clientSecret, code, verifier, resource)
	case "password": // Resource Owner Password Credentials Grant
		token, tokenError, err = GetPasswordToken(application, username, password, scope, host)
	case "client_credentials": // Client Credentials Grant
		token, tokenError, err = GetClientCredentialsToken(application, clientSecret, scope, host)
	case "token", "id_token": // Implicit Grant
		token, tokenError, err = GetImplicitToken(application, username, password, scope, nonce, host)
	case "urn:ietf:params:oauth:grant-type:jwt-bearer":
		token, tokenError, err = GetJwtBearerToken(application, assertion, scope, nonce, host)
	case "urn:ietf:params:oauth:grant-type:device_code":
		// The user has already authenticated via browser in the device flow,
		// so we skip password verification and mint a token directly.
		token, tokenError, err = mintImplicitToken(application, username, scope, nonce, host)
	case "urn:ietf:params:oauth:grant-type:token-exchange": // Token Exchange Grant (RFC 8693)
		token, tokenError, err = GetTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, actorToken, actorTokenType, audience, scope, host)
	case "refresh_token":
		refreshToken2, err := RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host, dpopProof)
		if err != nil {
			return nil, err
		}
		return refreshToken2, nil
	}

	if err != nil {
		return nil, err
	}

	if tokenError != nil {
		return tokenError, nil
	}

	var deviceSecret string
	if grantType == "authorization_code" {
		deviceSecret = addNativeSsoDeviceSecret(token)
	}

	// Apply DPoP binding (RFC 9449) if a DPoP proof was supplied by the client.
	if dpopProof != "" {
		dpopHtu := GetDPoPHtu(host, "/api/login/oauth/access_token")
		jkt, dpopErr := ValidateDPoPProof(dpopProof, "POST", dpopHtu, "")
		if dpopErr != nil {
			return &TokenError{
				Error:            "invalid_dpop_proof",
				ErrorDescription: dpopErr.Error(),
			}, nil
		}
		token.TokenType = "DPoP"
		token.DPoPJkt = jkt
		if err = updateTokenDPoP(token); err != nil {
			return nil, err
		}
	}

	token.CodeIsUsed = true

	_, err = updateUsedByCode(token)
	if err != nil {
		return nil, err
	}

	tokenWrapper := &TokenWrapper{
		AccessToken:  token.AccessToken,
		IdToken:      token.AccessToken,
		RefreshToken: token.RefreshToken,
		TokenType:    token.TokenType,
		ExpiresIn:    token.ExpiresIn,
		Scope:        token.Scope,
		DeviceSecret: deviceSecret,
	}

	return tokenWrapper, nil
}

// GetAuthorizationCodeToken handles the Authorization Code Grant flow.
func GetAuthorizationCodeToken(application *Application, clientSecret string, code string, verifier string, resource string) (*Token, *TokenError, error) {
	if code == "" {
		return nil, &TokenError{
			Error:            InvalidRequest,
			ErrorDescription: "authorization code should not be empty",
		}, nil
	}

	// Handle guest user creation
	if code == "guest-user" {
		if application.Organization == "built-in" {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "guest signin is not allowed for built-in organization",
			}, nil
		}
		if !application.EnableGuestSignin {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "guest signin is not enabled for this application",
			}, nil
		}
		if !application.EnableSignUp {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "sign up is not enabled for this application",
			}, nil
		}
		return createGuestUserToken(application, clientSecret, verifier)
	}

	token, err := getTokenByCode(code)
	if err != nil {
		return nil, nil, err
	}

	if token == nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("authorization code: [%s] is invalid", code),
		}, nil
	}

	if token.CodeIsUsed {
		// anti replay attacks
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("authorization code has been used for token: [%s]", token.GetId()),
		}, nil
	}

	if token.CodeChallenge != "" {
		challengeAnswer := pkceChallenge(verifier)
		if challengeAnswer != token.CodeChallenge {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: fmt.Sprintf("verifier is invalid, challengeAnswer: [%s], token.CodeChallenge: [%s]", challengeAnswer, token.CodeChallenge),
			}, nil
		}
	}

	if application.ClientSecret != clientSecret {
		// when using PKCE, the Client Secret can be empty,
		// but if it is provided, it must be accurate.
		if token.CodeChallenge == "" {
			return nil, &TokenError{
				Error:            InvalidClient,
				ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: empty", application.GetId()),
			}, nil
		} else {
			if clientSecret != "" {
				return nil, &TokenError{
					Error:            InvalidClient,
					ErrorDescription: fmt.Sprintf("client_secret is invalid for application: [%s], token.CodeChallenge: [%s]", application.GetId(), token.CodeChallenge),
				}, nil
			}
		}
	}

	if application.Name != token.Application {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("the token is for wrong application (client_id), application.Name: [%s], token.Application: [%s]", application.Name, token.Application),
		}, nil
	}

	// RFC 8707: Validate resource parameter matches the one in the authorization request
	if resource != token.Resource {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("resource parameter does not match authorization request, expected: [%s], got: [%s]", token.Resource, resource),
		}, nil
	}

	nowUnix := time.Now().Unix()
	if nowUnix > token.CodeExpireIn {
		// code must be used within 5 minutes
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("authorization code has expired, nowUnix: [%s], token.CodeExpireIn: [%s]", time.Unix(nowUnix, 0).Format(time.RFC3339), time.Unix(token.CodeExpireIn, 0).Format(time.RFC3339)),
		}, nil
	}
	return token, nil, nil
}

// GetPasswordToken handles the Resource Owner Password Credentials Grant flow.
func GetPasswordToken(application *Application, username string, password string, scope string, host string) (*Token, *TokenError, error) {
	expandedScope, ok := IsScopeValidAndExpand(scope, application)
	if !ok {
		return nil, &TokenError{
			Error:            InvalidScope,
			ErrorDescription: "the requested scope is invalid or not defined in the application",
		}, nil
	}
	scope = expandedScope

	user, err := GetUserByFieldsForSharedApp(application, application.Organization, username)
	if err != nil {
		return nil, nil, err
	}
	if user == nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: "the user does not exist",
		}, nil
	}

	if user.Ldap != "" {
		err = CheckLdapUserPassword(user, password, "en")
	} else {
		// For OAuth users who don't have a password set, they cannot use password grant type
		if user.Password == "" {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "OAuth users cannot use password grant type, please use authorization code flow",
			}, nil
		}
		err = CheckPassword(user, password, "en")
	}
	if err != nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
		}, nil
	}

	if user.IsForbidden {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
		}, nil
	}

	err = ExtendUserWithRolesAndPermissions(user)
	if err != nil {
		return nil, nil, err
	}

	accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
	if err != nil {
		return nil, &TokenError{
			Error:            EndpointError,
			ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
		}, nil
	}
	token := &Token{
		Owner:        application.Owner,
		Name:         tokenName,
		CreatedTime:  util.GetCurrentTime(),
		Application:  application.Name,
		Organization: user.Owner,
		User:         user.Name,
		Code:         util.GenerateClientId(),
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(application.ExpireInHours * float64(hourSeconds)),
		Scope:        scope,
		TokenType:    "Bearer",
		CodeIsUsed:   true,
	}
	_, err = AddToken(token)
	if err != nil {
		return nil, nil, err
	}

	return token, nil, nil
}

// GetClientCredentialsToken handles the Client Credentials Grant flow.
func GetClientCredentialsToken(application *Application, clientSecret string, scope string, host string) (*Token, *TokenError, error) {
	if application.ClientSecret != clientSecret {
		return nil, &TokenError{
			Error:            InvalidClient,
			ErrorDescription: "client_secret is invalid",
		}, nil
	}
	expandedScope, ok := IsScopeValidAndExpand(scope, application)
	if !ok {
		return nil, &TokenError{
			Error:            InvalidScope,
			ErrorDescription: "the requested scope is invalid or not defined in the application",
		}, nil
	}
	scope = expandedScope
	nullUser := &User{
		Owner: application.Owner,
		Id:    application.GetId(),
		Name:  application.Name,
		Type:  "application",
	}

	accessToken, _, tokenName, err := generateJwtToken(application, nullUser, "", "", "", scope, "", host)
	if err != nil {
		return nil, &TokenError{
			Error:            EndpointError,
			ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
		}, nil
	}
	token := &Token{
		Owner:        application.Owner,
		Name:         tokenName,
		CreatedTime:  util.GetCurrentTime(),
		Application:  application.Name,
		Organization: application.Organization,
		User:         nullUser.Name,
		Code:         util.GenerateClientId(),
		AccessToken:  accessToken,
		ExpiresIn:    int(application.ExpireInHours * float64(hourSeconds)),
		Scope:        scope,
		TokenType:    "Bearer",
		CodeIsUsed:   true,
	}
	_, err = AddToken(token)
	if err != nil {
		return nil, nil, err
	}

	return token, nil, nil
}

// GetImplicitToken handles the Implicit Grant flow (requires password verification).
func GetImplicitToken(application *Application, username string, password string, scope string, nonce string, host string) (*Token, *TokenError, error) {
	user, err := GetUserByFieldsForSharedApp(application, application.Organization, username)
	if err != nil {
		return nil, nil, err
	}
	if user == nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: "the user does not exist",
		}, nil
	}

	if user.Ldap != "" {
		err = CheckLdapUserPassword(user, password, "en")
	} else {
		if user.Password == "" {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "OAuth users cannot use implicit grant type, please use authorization code flow",
			}, nil
		}
		err = CheckPassword(user, password, "en")
	}
	if err != nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("invalid username or password: %s", err.Error()),
		}, nil
	}

	return mintImplicitToken(application, username, scope, nonce, host)
}

// GetJwtBearerToken handles the JWT Bearer Grant flow (RFC 7523).
func GetJwtBearerToken(application *Application, assertion string, scope string, nonce string, host string) (*Token, *TokenError, error) {
	ok, claims, err := ValidateJwtAssertion(assertion, application, host)
	if err != nil || !ok {
		if err != nil {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: err.Error(),
			}, err
		}

		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("assertion (JWT) is invalid for application: [%s]", application.GetId()),
		}, nil
	}

	// JWT assertion has already been validated above; skip password re-verification
	return mintImplicitToken(application, claims.Subject, scope, nonce, host)
}

// GetTokenByUser mints a token for the given user (Implicit flow helper).
func GetTokenByUser(application *Application, user *User, scope string, nonce string, host string) (*Token, error) {
	err := ExtendUserWithRolesAndPermissions(user)
	if err != nil {
		return nil, err
	}

	accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", nonce, scope, "", host)
	if err != nil {
		return nil, err
	}

	token := &Token{
		Owner:        application.Owner,
		Name:         tokenName,
		CreatedTime:  util.GetCurrentTime(),
		Application:  application.Name,
		Organization: user.Owner,
		User:         user.Name,
		Code:         util.GenerateClientId(),
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(application.ExpireInHours * float64(hourSeconds)),
		Scope:        scope,
		TokenType:    "Bearer",
		CodeIsUsed:   true,
	}
	_, err = AddToken(token)
	if err != nil {
		return nil, err
	}

	return token, nil
}

// GetWechatMiniProgramToken handles the WeChat Mini Program flow.
func GetWechatMiniProgramToken(application *Application, code string, host string, username string, avatar string, lang string) (*Token, *TokenError, error) {
	mpProvider := GetWechatMiniProgramProvider(application)
	if mpProvider == nil {
		return nil, &TokenError{
			Error:            InvalidClient,
			ErrorDescription: "the application does not support wechat mini program",
		}, nil
	}
	provider, err := GetProvider(util.GetId("admin", mpProvider.Name))
	if err != nil {
		return nil, nil, err
	}

	mpIdp := idp.NewWeChatMiniProgramIdProvider(provider.ClientId, provider.ClientSecret)
	session, err := mpIdp.GetSessionByCode(code)
	if err != nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("get wechat mini program session error: %s", err.Error()),
		}, nil
	}

	openId, unionId := session.Openid, session.Unionid
	if openId == "" && unionId == "" {
		return nil, &TokenError{
			Error:            InvalidRequest,
			ErrorDescription: "the wechat mini program session is invalid",
		}, nil
	}
	user, err := getUserByWechatId(application.Organization, openId, unionId)
	if err != nil {
		return nil, nil, err
	}

	if user == nil {
		if !application.EnableSignUp {
			return nil, &TokenError{
				Error:            InvalidGrant,
				ErrorDescription: "the application does not allow to sign up new account",
			}, nil
		}
		// Add new user
		var name string
		if CheckUsername(username, lang) == "" {
			name = username
		} else {
			name = fmt.Sprintf("wechat-%s", openId)
		}

		// Generate a unique user ID within the confines of the application
		newUserId, idErr := GenerateIdForNewUser(application)
		if idErr != nil {
			// If we fail to generate a unique user ID, we can fallback to a random ID
			newUserId = util.GenerateId()
		}

		user = &User{
			Owner:             application.Organization,
			Id:                newUserId,
			Name:              name,
			Avatar:            avatar,
			SignupApplication: application.Name,
			WeChat:            openId,
			Type:              "normal-user",
			CreatedTime:       util.GetCurrentTime(),
			IsAdmin:           false,
			IsForbidden:       false,
			IsDeleted:         false,
			Properties: map[string]string{
				UserPropertiesWechatOpenId:  openId,
				UserPropertiesWechatUnionId: unionId,
			},
		}
		_, err = AddUser(user, "en")
		if err != nil {
			return nil, nil, err
		}
	}

	err = ExtendUserWithRolesAndPermissions(user)
	if err != nil {
		return nil, nil, err
	}

	accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", "", "", host)
	if err != nil {
		return nil, &TokenError{
			Error:            EndpointError,
			ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
		}, nil
	}

	token := &Token{
		Owner:        application.Owner,
		Name:         tokenName,
		CreatedTime:  util.GetCurrentTime(),
		Application:  application.Name,
		Organization: user.Owner,
		User:         user.Name,
		Code:         session.SessionKey, // a trick, because miniprogram does not use the code, so use the code field to save the session_key
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(application.ExpireInHours * float64(hourSeconds)),
		Scope:        "",
		TokenType:    "Bearer",
		CodeIsUsed:   true,
	}
	_, err = AddToken(token)
	if err != nil {
		return nil, nil, err
	}
	return token, nil, nil
}

// GetTokenExchangeToken handles the Token Exchange Grant flow (RFC 8693).
// Exchanges a subject token for a new token with different audience or scope.
func GetTokenExchangeToken(application *Application, clientSecret string, subjectToken string, subjectTokenType string, actorToken string, actorTokenType string, audience string, scope string, host string) (*Token, *TokenError, error) {
	if actorTokenType == "urn:openid:params:token-type:device-secret" {
		return getNativeSsoTokenExchangeToken(application, clientSecret, subjectToken, subjectTokenType, actorToken, actorTokenType, scope, host)
	}

	if application.ClientSecret != clientSecret {
		return nil, &TokenError{
			Error:            InvalidClient,
			ErrorDescription: "client_secret is invalid",
		}, nil
	}

	if subjectToken == "" {
		return nil, &TokenError{
			Error:            InvalidRequest,
			ErrorDescription: "subject_token is required",
		}, nil
	}

	// RFC 8693 defines standard token type identifiers
	if subjectTokenType == "" {
		subjectTokenType = "urn:ietf:params:oauth:token-type:access_token" // Default to access_token
	}

	supportedTokenTypes := []string{
		"urn:ietf:params:oauth:token-type:access_token",
		"urn:ietf:params:oauth:token-type:jwt",
		"urn:ietf:params:oauth:token-type:id_token",
	}

	isValidTokenType := false
	for _, tokenType := range supportedTokenTypes {
		if subjectTokenType == tokenType {
			isValidTokenType = true
			break
		}
	}

	if !isValidTokenType {
		return nil, &TokenError{
			Error:            InvalidRequest,
			ErrorDescription: fmt.Sprintf("unsupported subject_token_type: %s", subjectTokenType),
		}, nil
	}

	subjectOwner, subjectName, subjectScope, tokenError, err := parseAndValidateSubjectToken(subjectToken, application.ClientId)
	if err != nil {
		return nil, nil, err
	}
	if tokenError != nil {
		return nil, tokenError, nil
	}

	user, err := getUser(subjectOwner, subjectName)
	if err != nil {
		return nil, nil, err
	}
	if user == nil {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: fmt.Sprintf("user from subject_token does not exist: %s", util.GetId(subjectOwner, subjectName)),
		}, nil
	}

	if user.IsForbidden {
		return nil, &TokenError{
			Error:            InvalidGrant,
			ErrorDescription: "the user is forbidden to sign in, please contact the administrator",
		}, nil
	}

	// If scope is not provided, use the scope from the subject token.
	// If scope is provided, it should be a subset of the subject token's scope (downscoping).
	if scope == "" {
		scope = subjectScope
	} else {
		if subjectScope != "" {
			subjectScopes := strings.Split(subjectScope, " ")
			requestedScopes := strings.Split(scope, " ")
			for _, requestedScope := range requestedScopes {
				if requestedScope == "" {
					continue
				}
				found := false
				for _, existingScope := range subjectScopes {
					if existingScope != "" && requestedScope == existingScope {
						found = true
						break
					}
				}
				if !found {
					return nil, &TokenError{
						Error:            InvalidScope,
						ErrorDescription: fmt.Sprintf("requested scope '%s' is not in subject token's scope", requestedScope),
					}, nil
				}
			}
		}
	}

	err = ExtendUserWithRolesAndPermissions(user)
	if err != nil {
		return nil, nil, err
	}

	accessToken, refreshToken, tokenName, err := generateJwtToken(application, user, "", "", "", scope, "", host)
	if err != nil {
		return nil, &TokenError{
			Error:            EndpointError,
			ErrorDescription: fmt.Sprintf("generate jwt token error: %s", err.Error()),
		}, nil
	}

	token := &Token{
		Owner:        application.Owner,
		Name:         tokenName,
		CreatedTime:  util.GetCurrentTime(),
		Application:  application.Name,
		Organization: user.Owner,
		User:         user.Name,
		Code:         util.GenerateClientId(),
		AccessToken:  accessToken,
		RefreshToken: refreshToken,
		ExpiresIn:    int(application.ExpireInHours * float64(hourSeconds)),
		Scope:        scope,
		TokenType:    "Bearer",
		CodeIsUsed:   true,
	}

	_, err = AddToken(token)
	if err != nil {
		return nil, nil, err
	}

	return token, nil, nil
}

func GetAccessTokenByUser(user *User, host string) (string, error) {
	application, err := GetApplicationByUser(user)
	if err != nil {
		return "", err
	}
	if application == nil {
		return "", fmt.Errorf("the application for user %s is not found", user.Id)
	}

	token, err := GetTokenByUser(application, user, "profile", "", host)
	if err != nil {
		return "", err
	}

	return token.AccessToken, nil
}
