package handlers

import (
	"net/http"
	"strings"

	"golang.org/x/oauth2"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	log "github.com/sirupsen/logrus"

	"github.com/authorizerdev/authorizer/server/constants"
	"github.com/authorizerdev/authorizer/server/memorystore"
	"github.com/authorizerdev/authorizer/server/oauth"
	"github.com/authorizerdev/authorizer/server/parsers"
	"github.com/authorizerdev/authorizer/server/utils"
	"github.com/authorizerdev/authorizer/server/validators"
)

// OAuthLoginHandler set host in the oauth state that is useful for redirecting to oauth_callback
func OAuthLoginHandler() gin.HandlerFunc {
	return func(c *gin.Context) {
		hostname := parsers.GetHost(c)
		// deprecating redirectURL instead use redirect_uri
		redirectURI := strings.TrimSpace(c.Query("redirectURL"))
		if redirectURI == "" {
			redirectURI = strings.TrimSpace(c.Query("redirect_uri"))
		}
		roles := strings.TrimSpace(c.Query("roles"))
		state := strings.TrimSpace(c.Query("state"))
		scopeString := strings.TrimSpace(c.Query("scope"))

		if redirectURI == "" {
			log.Debug("redirect_uri is empty")
			c.JSON(400, gin.H{
				"error": "invalid redirect uri",
			})
			return
		}

		if state == "" {
			log.Debug("state is empty. creating a new state")
			state = uuid.New().String()
		}

		var scope []string
		if scopeString == "" {
			scope = []string{"openid", "profile", "email"}
		} else {
			scope = strings.Split(scopeString, " ")
		}

		if roles != "" {
			// validate role
			rolesSplit := strings.Split(roles, ",")

			// use protected roles verification for admin login only.
			// though if not associated with user, it will be rejected from oauth_callback
			rolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyRoles)
			roles := []string{}
			if err != nil {
				log.Debug("Error getting roles: ", err)
				rolesString = ""
			} else {
				roles = strings.Split(rolesString, ",")
			}

			protectedRolesString, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyProtectedRoles)
			protectedRoles := []string{}
			if err != nil {
				log.Debug("Error getting protected roles: ", err)
				protectedRolesString = ""
			} else {
				protectedRoles = strings.Split(protectedRolesString, ",")
			}

			if !validators.IsValidRoles(rolesSplit, append([]string{}, append(roles, protectedRoles...)...)) {
				log.Debug("Invalid roles: ", roles)
				c.JSON(400, gin.H{
					"error": "invalid role",
				})
				return
			}
		} else {
			defaultRoles, err := memorystore.Provider.GetStringStoreEnvVariable(constants.EnvKeyDefaultRoles)
			if err != nil {
				log.Debug("Error getting default roles: ", err)
				c.JSON(400, gin.H{
					"error": "invalid role",
				})
				return
			}
			roles = defaultRoles

		}

		oauthStateString := state + "___" + redirectURI + "___" + roles + "___" + strings.Join(scope, " ")

		provider := c.Param("oauth_provider")
		isProviderConfigured := true
		switch provider {
		case constants.AuthRecipeMethodGoogle:
			if oauth.OAuthProviders.GoogleConfig == nil {
				log.Debug("Google OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodGoogle)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			// during the init of OAuthProvider authorizer url might be empty
			oauth.OAuthProviders.GoogleConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodGoogle
			url := oauth.OAuthProviders.GoogleConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodGithub:
			if oauth.OAuthProviders.GithubConfig == nil {
				log.Debug("Github OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodGithub)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.GithubConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodGithub
			url := oauth.OAuthProviders.GithubConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodFacebook:
			if oauth.OAuthProviders.FacebookConfig == nil {
				log.Debug("Facebook OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodFacebook)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.FacebookConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodFacebook
			url := oauth.OAuthProviders.FacebookConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodLinkedIn:
			if oauth.OAuthProviders.LinkedInConfig == nil {
				log.Debug("Linkedin OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodLinkedIn)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.LinkedInConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodLinkedIn
			url := oauth.OAuthProviders.LinkedInConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodTwitter:
			if oauth.OAuthProviders.TwitterConfig == nil {
				log.Debug("Twitter OAuth provider is not configured")
				isProviderConfigured = false
				break
			}

			verifier, challenge := utils.GenerateCodeChallenge()

			err := memorystore.Provider.SetState(oauthStateString, verifier)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.TwitterConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitter
			url := oauth.OAuthProviders.TwitterConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"))
			c.Redirect(http.StatusTemporaryRedirect, url)

		case constants.AuthRecipeMethodDiscord:
			if oauth.OAuthProviders.DiscordConfig == nil {
				log.Debug("Discord OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodDiscord)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.DiscordConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodDiscord
			url := oauth.OAuthProviders.DiscordConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodApple:
			if oauth.OAuthProviders.AppleConfig == nil {
				log.Debug("Apple OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodApple)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			oauth.OAuthProviders.AppleConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodApple
			// there is scope encoding issue with oauth2 and how apple expects, hence added scope manually
			// check: https://github.com/golang/oauth2/issues/449
			url := oauth.OAuthProviders.AppleConfig.AuthCodeURL(oauthStateString, oauth2.SetAuthURLParam("response_mode", "form_post")) + "&scope=name email"
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodMicrosoft:
			if oauth.OAuthProviders.MicrosoftConfig == nil {
				log.Debug("Microsoft OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodMicrosoft)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			// during the init of OAuthProvider authorizer url might be empty
			oauth.OAuthProviders.MicrosoftConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodMicrosoft
			url := oauth.OAuthProviders.MicrosoftConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodTwitch:
			if oauth.OAuthProviders.TwitchConfig == nil {
				log.Debug("Twitch OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodTwitch)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			// during the init of OAuthProvider authorizer url might be empty
			oauth.OAuthProviders.TwitchConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodTwitch
			url := oauth.OAuthProviders.TwitchConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		case constants.AuthRecipeMethodRoblox:
			if oauth.OAuthProviders.RobloxConfig == nil {
				log.Debug("RobloxConfig OAuth provider is not configured")
				isProviderConfigured = false
				break
			}
			err := memorystore.Provider.SetState(oauthStateString, constants.AuthRecipeMethodRoblox)
			if err != nil {
				log.Debug("Error setting state: ", err)
				c.JSON(500, gin.H{
					"error": "internal server error",
				})
				return
			}
			// during the init of OAuthProvider authorizer url might be empty
			oauth.OAuthProviders.RobloxConfig.RedirectURL = hostname + "/oauth_callback/" + constants.AuthRecipeMethodRoblox
			url := oauth.OAuthProviders.RobloxConfig.AuthCodeURL(oauthStateString)
			c.Redirect(http.StatusTemporaryRedirect, url)
		default:
			log.Debug("Invalid oauth provider: ", provider)
			c.JSON(422, gin.H{
				"message": "Invalid oauth provider",
			})
		}

		if !isProviderConfigured {
			c.JSON(422, gin.H{
				"message": provider + " not configured",
			})
		}
	}
}
