package auth

import (
	"log/slog"
	"net/http"
	"strings"
	"time"

	"github.com/gin-gonic/gin"

	"github.com/tidbcloud/AgentTeam/internal/api"
	"github.com/tidbcloud/AgentTeam/internal/api/dto"
	"github.com/tidbcloud/AgentTeam/internal/core/authidentity"
	"github.com/tidbcloud/AgentTeam/internal/oidc"
	"github.com/tidbcloud/AgentTeam/internal/service/authflow"
)

// OIDCHandler owns PingCAP User Center OIDC auth routes.
type OIDCHandler struct {
	store                     Store
	authFlow                  AuthFlow
	logger                    *slog.Logger
	sessionTTL                time.Duration
	pingCAPAuthConfigProvider func() PingCAPAuthConfig
	httpClient                *http.Client
}

// Discovery handles GET /api/v1/auth/oidc/discovery.
// Returns the PingCAP User Center OIDC discovery information.
func (handler *OIDCHandler) Discovery(ctx *gin.Context) {
	if !pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider).Enabled {
		api.Error(ctx, http.StatusServiceUnavailable, "PingCAP User Center is not enabled")
		return
	}

	config := pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider)
	provider := handler.newOIDCProvider(config.RedirectURI, config.webOIDCClient())

	discovery, err := provider.Discover(ctx.Request.Context())
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc discovery failed", slog.String("error_class", "provider_discovery_failed"))
		api.Error(ctx, http.StatusServiceUnavailable, "Failed to fetch PingCAP User Center configuration")
		return
	}

	ctx.JSON(http.StatusOK, gin.H{
		"issuer":                 discovery.Issuer,
		"authorization_endpoint": discovery.AuthorizationEndpoint,
		"token_endpoint":         discovery.TokenEndpoint,
		"userinfo_endpoint":      discovery.UserInfoEndpoint,
		"jwks_uri":               discovery.JWKSURI,
		"scopes_supported":       discovery.ScopesSupported,
	})
}

// AuthorizationURL handles POST /api/v1/auth/oidc/authorize-url.
// Generates an authorization URL for PingCAP User Center OIDC login.
func (handler *OIDCHandler) AuthorizationURL(ctx *gin.Context) {
	if !pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider).Enabled {
		api.Error(ctx, http.StatusServiceUnavailable, "PingCAP User Center is not enabled")
		return
	}

	var request dto.OIDCAuthorizationURLRequest
	if err := ctx.ShouldBindJSON(&request); err != nil {
		api.Error(ctx, http.StatusBadRequest, "Invalid request body")
		return
	}

	// Use the provided redirect URI or fall back to the configured one
	redirectURI := strings.TrimSpace(request.RedirectURI)
	if redirectURI == "" {
		redirectURI = pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider).RedirectURI
	}

	provider, err := handler.newOIDCProviderForRedirectURI(redirectURI)
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc client unavailable", slog.String("error_class", "client_not_configured"))
		api.Error(ctx, http.StatusServiceUnavailable, "PingCAP User Center client is not configured")
		return
	}

	// Generate state and nonce
	state, err := oidc.GenerateRandomState()
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc authorization state generation failed", slog.String("error_class", "state_generation_failed"))
		api.Error(ctx, http.StatusInternalServerError, "Failed to generate authorization URL")
		return
	}

	nonce, err := oidc.GenerateRandomNonce()
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc authorization nonce generation failed", slog.String("error_class", "nonce_generation_failed"))
		api.Error(ctx, http.StatusInternalServerError, "Failed to generate authorization URL")
		return
	}

	// Generate PKCE code verifier and challenge
	codeVerifier, err := oidc.GenerateCodeVerifier()
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc code verifier generation failed", slog.String("error_class", "code_verifier_generation_failed"))
		api.Error(ctx, http.StatusInternalServerError, "Failed to generate authorization URL")
		return
	}
	codeChallenge := oidc.GenerateCodeChallenge(codeVerifier)

	authURL, err := provider.BuildAuthorizationURLContext(ctx.Request.Context(), state, nonce, codeChallenge)
	if err != nil {
		handler.logger.ErrorContext(ctx.Request.Context(), "oidc authorization URL generation failed", slog.String("error_class", "authorization_url_generation_failed"))
		api.Error(ctx, http.StatusInternalServerError, "Failed to generate authorization URL")
		return
	}

	// Store code verifier and state in session (simplified - in production use proper session storage)
	// For now, we'll return them in the response and the frontend will send them back
	ctx.JSON(http.StatusOK, dto.OIDCAuthorizationURLResponse{
		AuthorizationURL: authURL,
		State:            state,
		Nonce:            nonce,
		CodeVerifier:     codeVerifier,
	})
}

// Callback handles POST /api/v1/auth/oidc/callback.
// Handles the OAuth callback from PingCAP User Center.
func (handler *OIDCHandler) Callback(c *gin.Context) {
	if !pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider).Enabled {
		api.Error(c, http.StatusServiceUnavailable, "PingCAP User Center is not enabled")
		return
	}

	// Support both GET (OAuth redirect) and POST (frontend API) requests
	var req dto.OIDCCallbackRequest
	if c.Request.Method == "GET" {
		// OAuth callback via GET redirect from PingCAP Auth
		req.Code = c.Query("code")
		req.State = c.Query("state")
		req.RedirectURI = ""
		req.Nonce = ""
		req.CodeVerifier = ""
	} else {
		// Frontend API call via POST
		if err := c.ShouldBindJSON(&req); err != nil {
			api.Error(c, http.StatusBadRequest, "Invalid request body")
			return
		}
	}

	if req.Code == "" {
		api.Error(c, http.StatusBadRequest, "code is required")
		return
	}
	if req.CodeVerifier == "" {
		api.Error(c, http.StatusBadRequest, "code_verifier is required")
		return
	}

	// Use the provided redirect URI or fall back to the configured one
	redirectURI := strings.TrimSpace(req.RedirectURI)
	if redirectURI == "" {
		redirectURI = pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider).RedirectURI
	}

	provider, err := handler.newOIDCProviderForRedirectURI(redirectURI)
	if err != nil {
		handler.logger.ErrorContext(c.Request.Context(), "oidc client unavailable", slog.String("error_class", "client_not_configured"))
		api.Error(c, http.StatusServiceUnavailable, "PingCAP User Center client is not configured")
		return
	}

	// Exchange code for tokens
	tokenResp, err := provider.ExchangeCodeForToken(c.Request.Context(), req.Code, req.CodeVerifier)
	if err != nil {
		handler.logger.ErrorContext(c.Request.Context(), "oidc token exchange failed", slog.String("error_class", "token_exchange_failed"))
		api.Error(c, http.StatusUnauthorized, "Failed to exchange authorization code")
		return
	}

	// Get user info
	userInfo, err := provider.GetUserInfo(c.Request.Context(), tokenResp.AccessToken)
	if err != nil {
		handler.logger.ErrorContext(c.Request.Context(), "oidc userinfo fetch failed", slog.String("error_class", "userinfo_fetch_failed"))
		api.Error(c, http.StatusUnauthorized, "Failed to fetch user information")
		return
	}

	// Find or create user by PingCAP Auth identity
	user, created, err := handler.store.FindOrCreateUserByPingCAPAuth(c.Request.Context(), pingcapAuthUserInfoFromOIDC(userInfo))
	if err != nil {
		handler.logger.ErrorContext(c.Request.Context(), "oidc user upsert failed", slog.String("error_class", "user_upsert_failed"))
		api.StoreError(c, err)
		return
	}

	if created {
		handler.logger.InfoContext(c.Request.Context(), "oidc user created", slog.String("user_id", user.ID))
	}

	loginIdentifierKind := authidentity.KindEmail
	if !user.HasVerifiedEmail() && user.HasVerifiedMobile() {
		loginIdentifierKind = authidentity.KindMobile
	}
	tryAutoJoinOrganizationsAfterVerifiedLogin(c, handler.authFlow, handler.logger, user, loginIdentifierKind)

	// Create session tokens
	authResponse, err := createAuthTokensResponse(c, handler.authFlow, handler.store, handler.sessionTTL, user, authflow.DefaultWorkspaceOptions{
		ProvisionOnboardingTrial: created && desktopOnboardingProvisioningEligible(
			user,
			req.DesktopOnboardingEnabled,
			req.DesktopOnboardingRegistrationCutoff,
		),
	})
	if err != nil {
		handler.logger.ErrorContext(c.Request.Context(), "oidc session creation failed", slog.String("error_class", "session_creation_failed"))
		api.StoreError(c, err)
		return
	}

	c.JSON(http.StatusOK, authResponse)
}

func (handler *OIDCHandler) newOIDCProviderForRedirectURI(redirectURI string) (*oidc.Provider, error) {
	config := pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider)
	client := config.oidcClientForRedirectURI(redirectURI)
	if client.clientID == "" {
		return nil, errPingCAPAuthClientNotConfigured
	}
	return handler.newOIDCProvider(redirectURI, client), nil
}

func (handler *OIDCHandler) newOIDCProvider(redirectURI string, client pingCAPOIDCClient) *oidc.Provider {
	config := pingCAPAuthConfigValue(handler.pingCAPAuthConfigProvider)
	return oidc.NewProvider(
		config.Issuer,
		client.clientID,
		client.clientSecret,
		redirectURI,
		config.Scopes,
		oidc.WithHTTPClient(handler.httpClient),
	)
}
