package api

import (
	"context"
	"crypto/sha256"
	"fmt"
	"net/http"

	"github.com/coreos/go-oidc/v3/oidc"
	"github.com/supabase/auth/internal/api/provider"
	"github.com/supabase/auth/internal/conf"
	"github.com/supabase/auth/internal/models"
	"github.com/supabase/auth/internal/observability"
	"github.com/supabase/auth/internal/storage"
)

// IdTokenGrantParams are the parameters the IdTokenGrant method accepts
type IdTokenGrantParams struct {
	IdToken     string `json:"id_token"`
	AccessToken string `json:"access_token"`
	Nonce       string `json:"nonce"`
	Provider    string `json:"provider"`
	ClientID    string `json:"client_id"`
	Issuer      string `json:"issuer"`
}

func (p *IdTokenGrantParams) getProvider(ctx context.Context, config *conf.GlobalConfiguration, r *http.Request) (*oidc.Provider, bool, string, []string, error) {
	log := observability.GetLogEntry(r).Entry

	var cfg *conf.OAuthProviderConfiguration
	var issuer string
	var providerType string
	var acceptableClientIDs []string

	switch true {
	case p.Provider == "apple" || p.Issuer == provider.IssuerApple:
		cfg = &config.External.Apple
		providerType = "apple"
		issuer = provider.IssuerApple
		acceptableClientIDs = append(acceptableClientIDs, config.External.Apple.ClientID...)

		if config.External.IosBundleId != "" {
			acceptableClientIDs = append(acceptableClientIDs, config.External.IosBundleId)
		}

	case p.Provider == "google" || p.Issuer == provider.IssuerGoogle:
		cfg = &config.External.Google
		providerType = "google"
		issuer = provider.IssuerGoogle
		acceptableClientIDs = append(acceptableClientIDs, config.External.Google.ClientID...)

	case p.Provider == "azure" || provider.IsAzureIssuer(p.Issuer):
		issuer = p.Issuer
		if issuer == "" || !provider.IsAzureIssuer(issuer) {
			detectedIssuer, err := provider.DetectAzureIDTokenIssuer(ctx, p.IdToken)
			if err != nil {
				return nil, false, "", nil, badRequestError(ErrorCodeValidationFailed, "Unable to detect issuer in ID token for Azure provider").WithInternalError(err)
			}
			issuer = detectedIssuer
		}
		cfg = &config.External.Azure
		providerType = "azure"
		acceptableClientIDs = append(acceptableClientIDs, config.External.Azure.ClientID...)

	case p.Provider == "facebook" || p.Issuer == provider.IssuerFacebook:
		cfg = &config.External.Facebook
		providerType = "facebook"
		issuer = provider.IssuerFacebook
		acceptableClientIDs = append(acceptableClientIDs, config.External.Facebook.ClientID...)

	case p.Provider == "keycloak" || (config.External.Keycloak.Enabled && config.External.Keycloak.URL != "" && p.Issuer == config.External.Keycloak.URL):
		cfg = &config.External.Keycloak
		providerType = "keycloak"
		issuer = config.External.Keycloak.URL
		acceptableClientIDs = append(acceptableClientIDs, config.External.Keycloak.ClientID...)

	case p.Provider == "kakao" || p.Issuer == provider.IssuerKakao:
		cfg = &config.External.Kakao
		providerType = "kakao"
		issuer = provider.IssuerKakao
		acceptableClientIDs = append(acceptableClientIDs, config.External.Kakao.ClientID...)

	case p.Provider == "vercel_marketplace" || p.Issuer == provider.IssuerVercelMarketplace:
		cfg = &config.External.VercelMarketplace
		providerType = "vercel_marketplace"
		issuer = provider.IssuerVercelMarketplace
		acceptableClientIDs = append(acceptableClientIDs, config.External.VercelMarketplace.ClientID...)

	default:
		log.WithField("issuer", p.Issuer).WithField("client_id", p.ClientID).Warn("Use of POST /token with arbitrary issuer and client_id is deprecated for security reasons. Please switch to using the API with provider only!")

		allowed := false
		for _, allowedIssuer := range config.External.AllowedIdTokenIssuers {
			if p.Issuer == allowedIssuer {
				allowed = true
				providerType = allowedIssuer
				acceptableClientIDs = []string{p.ClientID}
				issuer = allowedIssuer
				break
			}
		}

		if !allowed {
			return nil, false, "", nil, badRequestError(ErrorCodeValidationFailed, fmt.Sprintf("Custom OIDC provider %q not allowed", p.Provider))
		}

		cfg = &conf.OAuthProviderConfiguration{
			Enabled:        true,
			SkipNonceCheck: false,
		}
	}

	if !cfg.Enabled {
		return nil, false, "", nil, badRequestError(ErrorCodeProviderDisabled, fmt.Sprintf("Provider (issuer %q) is not enabled", issuer))
	}

	oidcProvider, err := oidc.NewProvider(ctx, issuer)
	if err != nil {
		return nil, false, "", nil, err
	}

	return oidcProvider, cfg.SkipNonceCheck, providerType, acceptableClientIDs, nil
}

// IdTokenGrant implements the id_token grant type flow
func (a *API) IdTokenGrant(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
	log := observability.GetLogEntry(r).Entry

	db := a.db.WithContext(ctx)
	config := a.config

	params := &IdTokenGrantParams{}
	if err := retrieveRequestParams(r, params); err != nil {
		return err
	}

	if params.IdToken == "" {
		return oauthError("invalid request", "id_token required")
	}

	if params.Provider == "" && (params.ClientID == "" || params.Issuer == "") {
		return oauthError("invalid request", "provider or client_id and issuer required")
	}

	oidcProvider, skipNonceCheck, providerType, acceptableClientIDs, err := params.getProvider(ctx, config, r)
	if err != nil {
		return err
	}

	idToken, userData, err := provider.ParseIDToken(ctx, oidcProvider, nil, params.IdToken, provider.ParseIDTokenOptions{
		SkipAccessTokenCheck: params.AccessToken == "",
		AccessToken:          params.AccessToken,
	})
	if err != nil {
		return oauthError("invalid request", "Bad ID token").WithInternalError(err)
	}

	userData.Metadata.EmailVerified = false
	for _, email := range userData.Emails {
		if email.Primary {
			userData.Metadata.Email = email.Email
			userData.Metadata.EmailVerified = email.Verified
			break
		} else {
			userData.Metadata.Email = email.Email
			userData.Metadata.EmailVerified = email.Verified
		}
	}

	if idToken.Subject == "" {
		return oauthError("invalid request", "Missing sub claim in id_token")
	}

	correctAudience := false
	for _, clientID := range acceptableClientIDs {
		if clientID == "" {
			continue
		}

		for _, aud := range idToken.Audience {
			if aud == clientID {
				correctAudience = true
				break
			}
		}

		if correctAudience {
			break
		}
	}

	if !correctAudience {
		return oauthError("invalid request", fmt.Sprintf("Unacceptable audience in id_token: %v", idToken.Audience))
	}

	if !skipNonceCheck {
		tokenHasNonce := idToken.Nonce != ""
		paramsHasNonce := params.Nonce != ""

		if tokenHasNonce != paramsHasNonce {
			return oauthError("invalid request", "Passed nonce and nonce in id_token should either both exist or not.")
		} else if tokenHasNonce && paramsHasNonce {
			// verify nonce to mitigate replay attacks
			hash := fmt.Sprintf("%x", sha256.Sum256([]byte(params.Nonce)))
			if hash != idToken.Nonce {
				return oauthError("invalid nonce", "Nonces mismatch")
			}
		}
	}

	if params.AccessToken == "" {
		if idToken.AccessTokenHash != "" {
			log.Warn("ID token has a at_hash claim, but no access_token parameter was provided. In future versions, access_token will be mandatory as it's security best practice.")
		}
	} else {
		if idToken.AccessTokenHash == "" {
			log.Info("ID token does not have a at_hash claim, access_token parameter is unused.")
		}
	}

	var token *AccessTokenResponse
	var grantParams models.GrantParams

	grantParams.FillGrantParams(r)

	if err := db.Transaction(func(tx *storage.Connection) error {
		var user *models.User
		var terr error

		user, terr = a.createAccountFromExternalIdentity(tx, r, userData, providerType)
		if terr != nil {
			return terr
		}

		token, terr = a.issueRefreshToken(r, tx, user, models.OAuth, grantParams)
		if terr != nil {
			return terr
		}

		return nil
	}); err != nil {
		switch err.(type) {
		case *storage.CommitWithError:
			return err
		case *HTTPError:
			return err
		default:
			return oauthError("server_error", "Internal Server Error").WithInternalError(err)
		}
	}

	return sendJSON(w, http.StatusOK, token)
}
