// Copyright 2021 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 controllers

import (
	"encoding/json"
	"fmt"
	"time"

	"github.com/beego/beego/v2/core/utils/pagination"
	"github.com/casdoor/casdoor/object"
	"github.com/casdoor/casdoor/util"
)

// GetTokens
// @Title GetTokens
// @Tag Token API
// @Description get tokens
// @Param   owner     query    string  true        "The organization name (e.g., built-in)"
// @Param   pageSize     query    string  true        "The size of each page"
// @Param   p     query    string  true        "The number of the page"
// @Success 200 {array} object.Token The Response object
// @router /get-tokens [get]
func (c *ApiController) GetTokens() {
	owner := c.Ctx.Input.Query("owner")
	limit := c.Ctx.Input.Query("pageSize")
	page := c.Ctx.Input.Query("p")
	field := c.Ctx.Input.Query("field")
	value := c.Ctx.Input.Query("value")
	sortField := c.Ctx.Input.Query("sortField")
	sortOrder := c.Ctx.Input.Query("sortOrder")
	organization := c.Ctx.Input.Query("organization")
	if limit == "" || page == "" {
		token, err := object.GetTokens(owner, organization)
		if err != nil {
			c.ResponseError(err.Error())
			return
		}

		c.ResponseOk(token)
	} else {
		limit := util.ParseInt(limit)
		count, err := object.GetTokenCount(owner, organization, field, value)
		if err != nil {
			c.ResponseError(err.Error())
			return
		}

		paginator := pagination.NewPaginator(c.Ctx.Request, limit, count)
		tokens, err := object.GetPaginationTokens(owner, organization, paginator.Offset(), limit, field, value, sortField, sortOrder)
		if err != nil {
			c.ResponseError(err.Error())
			return
		}

		c.ResponseOk(tokens, paginator.Nums())
	}
}

// GetToken
// @Title GetToken
// @Tag Token API
// @Description get token
// @Param   id     query    string  true        "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
// @Success 200 {object} object.Token The Response object
// @router /get-token [get]
func (c *ApiController) GetToken() {
	id := c.Ctx.Input.Query("id")
	organization := c.Ctx.Input.Query("organization")
	token, err := object.GetToken(id)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	if token == nil {
		c.ResponseError(fmt.Sprintf(c.T("general:The token: %s does not exist"), id))
		return
	}

	isGlobalAdmin, _ := c.isGlobalAdmin()
	if token.Organization != organization && !isGlobalAdmin {
		c.ResponseError(c.T("auth:Unauthorized operation"))
		return
	}

	c.ResponseOk(token)
}

// UpdateToken
// @Title UpdateToken
// @Tag Token API
// @Description update token
// @Param   id     query    string  true        "The token ID in format: organization/token-name (e.g., built-in/token-123456)"
// @Param   body    body   object.Token  true        "Details of the token"
// @Success 200 {object} controllers.Response The Response object
// @router /update-token [post]
func (c *ApiController) UpdateToken() {
	id := c.Ctx.Input.Query("id")

	var token object.Token
	err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	c.Data["json"] = wrapActionResponse(object.UpdateToken(id, &token, c.IsGlobalAdmin()))
	c.ServeJSON()
}

// AddToken
// @Title AddToken
// @Tag Token API
// @Description add token
// @Param   body    body   object.Token  true        "Details of the token"
// @Success 200 {object} controllers.Response The Response object
// @router /add-token [post]
func (c *ApiController) AddToken() {
	var token object.Token
	err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	c.Data["json"] = wrapActionResponse(object.AddToken(&token))
	c.ServeJSON()
}

// DeleteToken
// @Tag Token API
// @Title DeleteToken
// @Description delete token
// @Param   body    body   object.Token  true        "Details of the token"
// @Success 200 {object} controllers.Response The Response object
// @router /delete-token [post]
func (c *ApiController) DeleteToken() {
	var token object.Token
	err := json.Unmarshal(c.Ctx.Input.RequestBody, &token)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	c.Data["json"] = wrapActionResponse(object.DeleteToken(&token))
	c.ServeJSON()
}

// GetOAuthToken
// @Title GetOAuthToken
// @Tag Token API
// @Description get OAuth access token
// @Param   grant_type     query    string  true        "OAuth grant type"
// @Param   client_id     query    string  true        "OAuth client id"
// @Param   client_secret     query    string  true        "OAuth client secret"
// @Param   code     query    string  true        "OAuth code"
// @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/access_token [post]
func (c *ApiController) GetOAuthToken() {
	clientId := c.Ctx.Input.Query("client_id")
	clientSecret := c.Ctx.Input.Query("client_secret")
	assertion := c.Ctx.Input.Query("assertion")
	clientAssertion := c.Ctx.Input.Query("client_assertion")
	clientAssertionType := c.Ctx.Input.Query("client_assertion_type")
	grantType := c.Ctx.Input.Query("grant_type")
	code := c.Ctx.Input.Query("code")
	verifier := c.Ctx.Input.Query("code_verifier")
	scope := c.Ctx.Input.Query("scope")
	nonce := c.Ctx.Input.Query("nonce")
	username := c.Ctx.Input.Query("username")
	password := c.Ctx.Input.Query("password")
	tag := c.Ctx.Input.Query("tag")
	avatar := c.Ctx.Input.Query("avatar")
	refreshToken := c.Ctx.Input.Query("refresh_token")
	deviceCode := c.Ctx.Input.Query("device_code")
	subjectToken := c.Ctx.Input.Query("subject_token")
	subjectTokenType := c.Ctx.Input.Query("subject_token_type")
	actorToken := c.Ctx.Input.Query("actor_token")
	actorTokenType := c.Ctx.Input.Query("actor_token_type")
	audience := c.Ctx.Input.Query("audience")
	resource := c.Ctx.Input.Query("resource")

	if clientId == "" && clientSecret == "" {
		clientId, clientSecret, _ = c.Ctx.Request.BasicAuth()
	}

	if len(c.Ctx.Input.RequestBody) != 0 && grantType != "urn:ietf:params:oauth:grant-type:device_code" {
		// If clientId is empty, try to read data from RequestBody
		var tokenRequest TokenRequest
		err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest)
		if err == nil {
			if clientId == "" {
				clientId = tokenRequest.ClientId
			}
			if clientSecret == "" {
				clientSecret = tokenRequest.ClientSecret
			}
			if clientAssertion == "" {
				clientAssertion = tokenRequest.ClientAssertion
			}
			if clientAssertionType == "" {
				clientAssertionType = tokenRequest.ClientAssertionType
			}
			if grantType == "" {
				grantType = tokenRequest.GrantType
			}
			if code == "" {
				code = tokenRequest.Code
			}
			if verifier == "" {
				verifier = tokenRequest.Verifier
			}
			if scope == "" {
				scope = tokenRequest.Scope
			}
			if nonce == "" {
				nonce = tokenRequest.Nonce
			}
			if username == "" {
				username = tokenRequest.Username
			}
			if password == "" {
				password = tokenRequest.Password
			}
			if tag == "" {
				tag = tokenRequest.Tag
			}
			if avatar == "" {
				avatar = tokenRequest.Avatar
			}
			if refreshToken == "" {
				refreshToken = tokenRequest.RefreshToken
			}
			if subjectToken == "" {
				subjectToken = tokenRequest.SubjectToken
			}
			if subjectTokenType == "" {
				subjectTokenType = tokenRequest.SubjectTokenType
			}
			if actorToken == "" {
				actorToken = tokenRequest.ActorToken
			}
			if actorTokenType == "" {
				actorTokenType = tokenRequest.ActorTokenType
			}
			if audience == "" {
				audience = tokenRequest.Audience
			}
			if resource == "" {
				resource = tokenRequest.Resource
			}
			if assertion == "" {
				assertion = tokenRequest.Assertion
			}
		}
	}

	// Extract DPoP proof header (RFC 9449). Empty string when DPoP is not used.
	dpopProof := c.Ctx.Request.Header.Get("DPoP")

	host := c.Ctx.Request.Host
	var pendingDeviceCode string
	var pendingDeviceAuthCache object.DeviceAuthCache

	if deviceCode != "" {
		deviceAuthCache, ok := object.DeviceAuthMap.Load(deviceCode)
		if !ok {
			c.Data["json"] = &object.TokenError{
				Error:            "expired_token",
				ErrorDescription: "token is expired",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}

		deviceAuthCacheCast := deviceAuthCache.(object.DeviceAuthCache)

		if deviceAuthCacheCast.RequestAt.Add(time.Second * object.DeviceAuthExpiresIn).Before(time.Now()) {
			object.DeviceAuthMap.Delete(deviceCode)
			c.Data["json"] = &object.TokenError{
				Error:            "expired_token",
				ErrorDescription: "token is expired",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}

		if deviceAuthCacheCast.Status == object.DeviceAuthStatusDenied {
			c.Data["json"] = &object.TokenError{
				Error:            "access_denied",
				ErrorDescription: "device login was denied",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}

		if deviceAuthCacheCast.Status == object.DeviceAuthStatusTokenIssued {
			c.Data["json"] = &object.TokenError{
				Error:            "access_denied",
				ErrorDescription: "device_code has already been used",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}

		if !deviceAuthCacheCast.UserSignIn {
			c.Data["json"] = &object.TokenError{
				Error:            "authorization_pending",
				ErrorDescription: "authorization pending",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}
		// Bind client_id to the application from the original device auth request.
		if deviceAuthCacheCast.ClientId != "" && deviceAuthCacheCast.ClientId != clientId {
			c.Data["json"] = &object.TokenError{
				Error:            object.InvalidClient,
				ErrorDescription: "client_id does not match the device authorization request",
			}
			c.SetTokenErrorHttpStatus()
			c.ServeJSON()
			return
		}
		username = deviceAuthCacheCast.UserName
		scope = deviceAuthCacheCast.Scope
		pendingDeviceCode = deviceCode
		pendingDeviceAuthCache = deviceAuthCacheCast
	} else if grantType == "urn:ietf:params:oauth:grant-type:device_code" {
		c.Data["json"] = &object.TokenError{
			Error:            "invalid_request",
			ErrorDescription: "device_code parameter is required for this grant type",
		}
		c.SetTokenErrorHttpStatus()
		c.ServeJSON()
		return
	}

	token, err := object.GetOAuthToken(grantType, clientId, clientSecret, code, verifier, scope, nonce, username, password, host, refreshToken, tag, avatar, c.GetAcceptLanguage(), subjectToken, subjectTokenType, actorToken, actorTokenType, assertion, clientAssertion, clientAssertionType, audience, resource, dpopProof)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	if pendingDeviceCode != "" {
		if _, isTokenError := token.(*object.TokenError); !isTokenError {
			pendingDeviceAuthCache.Status = object.DeviceAuthStatusTokenIssued
			object.DeviceAuthMap.Store(pendingDeviceCode, pendingDeviceAuthCache)
		}
	}

	c.Data["json"] = token
	c.SetTokenErrorHttpStatus()
	c.ServeJSON()
}

// RefreshToken
// @Title RefreshToken
// @Tag Token API
// @Description refresh OAuth access token
// @Param   grant_type     query    string  true        "OAuth grant type"
// @Param	refresh_token	query	string	true		"OAuth refresh token"
// @Param   scope     query    string  true        "OAuth scope"
// @Param   client_id     query    string  true        "OAuth client id"
// @Param   client_secret     query    string  false        "OAuth client secret"
// @Success 200 {object} object.TokenWrapper The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/refresh_token [post]
func (c *ApiController) RefreshToken() {
	grantType := c.Ctx.Input.Query("grant_type")
	refreshToken := c.Ctx.Input.Query("refresh_token")
	scope := c.Ctx.Input.Query("scope")
	clientId := c.Ctx.Input.Query("client_id")
	clientSecret := c.Ctx.Input.Query("client_secret")
	host := c.Ctx.Request.Host

	if clientId == "" {
		// If clientID is empty, try to read data from RequestBody
		var tokenRequest TokenRequest
		if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
			clientId = tokenRequest.ClientId
			clientSecret = tokenRequest.ClientSecret
			grantType = tokenRequest.GrantType
			scope = tokenRequest.Scope
			refreshToken = tokenRequest.RefreshToken
		}
	}

	ok, application, clientId, _, err := c.ValidateOAuth(true)
	if err != nil || !ok {
		return
	}

	dpopProof := c.Ctx.Request.Header.Get("DPoP")
	refreshToken2, err := object.RefreshToken(application, grantType, refreshToken, scope, clientId, clientSecret, host, dpopProof)
	if err != nil {
		c.ResponseError(err.Error())
		return
	}

	c.Data["json"] = refreshToken2
	c.SetTokenErrorHttpStatus()
	c.ServeJSON()
}

func (c *ApiController) ResponseTokenError(errorMsg string, errorDescription string) {
	c.Data["json"] = &object.TokenError{
		Error:            errorMsg,
		ErrorDescription: errorDescription,
	}
	c.SetTokenErrorHttpStatus()
	c.ServeJSON()
}

func (c *ApiController) ValidateOAuth(ignoreValidSecret bool) (ok bool, application *object.Application, clientId, clientSecret string, err error) {
	reqClientId := c.Ctx.Input.Query("client_id")
	reqClientSecret := c.Ctx.Input.Query("client_secret")
	clientAssertion := c.Ctx.Input.Query("client_assertion")
	clientAssertionType := c.Ctx.Input.Query("client_assertion_type")

	if reqClientId == "" && clientAssertionType == "" {
		var tokenRequest TokenRequest
		if err := json.Unmarshal(c.Ctx.Input.RequestBody, &tokenRequest); err == nil {
			reqClientId = tokenRequest.ClientId
			reqClientSecret = tokenRequest.ClientSecret
			clientAssertion = tokenRequest.ClientAssertion
			clientAssertionType = tokenRequest.ClientAssertionType
		}
	}

	if clientAssertionType == "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" {
		ok, application, err = object.ValidateClientAssertion(clientAssertion, c.Ctx.Request.Host)
		if err != nil {
			c.ResponseTokenError(object.InvalidClient, err.Error())
			return
		}

		if !ok || application == nil {
			c.ResponseTokenError(object.InvalidClient, "client_assertion is invalid")
			return
		}

		clientSecret = application.ClientSecret
		clientId = application.ClientId
		ok = true
		return
	}

	if reqClientId == "" && reqClientSecret == "" {
		clientId, clientSecret, ok = c.Ctx.Request.BasicAuth()
		if !ok {
			clientId = c.Ctx.Input.Query("client_id")
			clientSecret = c.Ctx.Input.Query("client_secret")
			if clientId == "" || clientSecret == "" {
				c.ResponseTokenError(object.InvalidRequest, "")
				return
			}
		}
	} else {
		clientId = reqClientId
		clientSecret = reqClientSecret
	}

	application, err = object.GetApplicationByClientId(clientId)
	if err != nil {
		c.ResponseTokenError(object.InvalidClient, err.Error())
		return
	}

	if application == nil || (application.ClientSecret != clientSecret && !ignoreValidSecret) {
		c.ResponseTokenError(object.InvalidClient, c.T("token:Invalid application or wrong clientSecret"))
		return
	}

	ok = true
	return
}

// IntrospectToken
// @Title IntrospectToken
// @Tag Login API
// @Description The introspection endpoint is an OAuth 2.0 endpoint that takes a
// parameter representing an OAuth 2.0 token and returns a JSON document
// representing the meta information surrounding the
// token, including whether this token is currently active.
// This endpoint support Basic Authorization and authorization defined in RFC 7523.
//
// @Param token formData string true "access_token's value or refresh_token's value"
// @Param token_type_hint formData string true "the token type access_token or refresh_token"
// @Success 200 {object} object.IntrospectionResponse The Response object
// @Success 400 {object} object.TokenError The Response object
// @Success 401 {object} object.TokenError The Response object
// @router /login/oauth/introspect [post]
func (c *ApiController) IntrospectToken() {
	tokenValue := c.Ctx.Input.Query("token")

	ok, application, clientId, _, err := c.ValidateOAuth(false)
	if err != nil || !ok {
		return
	}

	respondWithInactiveToken := func() {
		c.Data["json"] = &object.IntrospectionResponse{Active: false}
		c.ServeJSON()
	}

	tokenTypeHint := c.Ctx.Input.Query("token_type_hint")
	var token *object.Token
	if tokenTypeHint != "" {
		token, err = object.GetTokenByTokenValue(tokenValue, tokenTypeHint)
		if err != nil {
			c.ResponseTokenError(object.InvalidRequest, err.Error())
			return
		}
		if token == nil || token.ExpiresIn <= 0 {
			respondWithInactiveToken()
			return
		}

		if token.ExpiresIn <= 0 {
			c.Data["json"] = &object.IntrospectionResponse{Active: false}
			c.ServeJSON()
			return
		}
	}

	var introspectionResponse object.IntrospectionResponse

	if application.TokenFormat == "JWT-Standard" {
		jwtToken, err := object.ParseStandardJwtTokenByApplication(tokenValue, application)
		if err != nil {
			// and token revoked case. but we not implement
			// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
			// refs: https://tools.ietf.org/html/rfc7009
			respondWithInactiveToken()
			return
		}

		introspectionResponse = object.IntrospectionResponse{
			Active:    true,
			Scope:     jwtToken.Scope,
			ClientId:  clientId,
			Username:  jwtToken.Name,
			TokenType: jwtToken.TokenType,
			Exp:       jwtToken.ExpiresAt.Unix(),
			Iat:       jwtToken.IssuedAt.Unix(),
			Nbf:       jwtToken.NotBefore.Unix(),
			Sub:       jwtToken.Subject,
			Aud:       jwtToken.Audience,
			Iss:       jwtToken.Issuer,
			Jti:       jwtToken.ID,
		}
	} else {
		jwtToken, err := object.ParseJwtTokenByApplication(tokenValue, application)
		if err != nil {
			// and token revoked case. but we not implement
			// TODO: 2022-03-03 add token revoked check, when we implemented the Token Revocation(rfc7009) Specs.
			// refs: https://tools.ietf.org/html/rfc7009
			respondWithInactiveToken()
			return
		}

		introspectionResponse = object.IntrospectionResponse{
			Active:   true,
			ClientId: clientId,
			Exp:      jwtToken.ExpiresAt.Unix(),
			Iat:      jwtToken.IssuedAt.Unix(),
			Nbf:      jwtToken.NotBefore.Unix(),
			Sub:      jwtToken.Subject,
			Aud:      jwtToken.Audience,
			Iss:      jwtToken.Issuer,
			Jti:      jwtToken.ID,
		}

		if jwtToken.Scope != "" {
			introspectionResponse.Scope = jwtToken.Scope
		}
		if jwtToken.Name != "" {
			introspectionResponse.Username = jwtToken.Name
		}
		if jwtToken.TokenType != "" {
			introspectionResponse.TokenType = jwtToken.TokenType
		}
	}

	if tokenTypeHint == "" {
		token, err = object.GetTokenByTokenValue(tokenValue, introspectionResponse.TokenType)
		if err != nil {
			c.ResponseTokenError(object.InvalidRequest, err.Error())
			return
		}
		if token == nil || token.ExpiresIn <= 0 {
			respondWithInactiveToken()
			return
		}
	}

	if token != nil {
		application, err = object.GetApplication(fmt.Sprintf("%s/%s", token.Owner, token.Application))
		if err != nil {
			c.ResponseTokenError(object.InvalidClient, err.Error())
			return
		}
		if application == nil {
			c.ResponseError(fmt.Sprintf(c.T("auth:The application: %s does not exist"), token.Application))
			return
		}

		introspectionResponse.TokenType = token.TokenType
		introspectionResponse.ClientId = application.ClientId

		// Expose DPoP key binding in the introspection response (RFC 9449 §8).
		if token.DPoPJkt != "" {
			introspectionResponse.Cnf = &object.DPoPConfirmation{JKT: token.DPoPJkt}
		}
	}

	c.Data["json"] = introspectionResponse
	c.ServeJSON()
}
