package rest

import (
	"context"
	"crypto/subtle"
	"encoding/base64"
	"encoding/json"
	stdErrors "errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/internal/application/services"
	"github.com/nexuscrm/backend/internal/domain/ports"
	"github.com/nexuscrm/backend/internal/features/auth"
	"github.com/nexuscrm/backend/internal/features/permissions"
	"github.com/nexuscrm/backend/internal/interfaces/middleware"
	origincfg "github.com/nexuscrm/backend/internal/interfaces/origin"
	"github.com/nexuscrm/backend/pkg/errors"
	"github.com/nexuscrm/shared/pkg/constants"
	"github.com/nexuscrm/shared/pkg/models"
	"github.com/rs/zerolog/log"
)

type OAuthHandler struct {
	svcMgr    *services.ServiceManager
	permCache PermCacheWarmer
}

// PermCacheWarmer is the interface for pre-warming the permission cache on login.
type PermCacheWarmer interface {
	WarmUser(ctx context.Context, userID, profileID string, permSvc *permissions.PermissionService)
}

func NewOAuthHandler(svcMgr *services.ServiceManager) *OAuthHandler {
	return &OAuthHandler{svcMgr: svcMgr}
}

func mapConnectedAppLookupError(err error) (int, gin.H) {
	if err == nil {
		return http.StatusInternalServerError, gin.H{
			"error":             "server_error",
			"error_description": "failed to load connected app",
		}
	}

	var notFoundErr *errors.NotFoundError
	if stdErrors.As(err, &notFoundErr) {
		return http.StatusBadRequest, gin.H{
			"error":             "invalid_client",
			"error_description": "unknown client_id",
		}
	}

	var forbiddenErr *errors.ForbiddenError
	if stdErrors.As(err, &forbiddenErr) {
		return http.StatusBadRequest, gin.H{
			"error":             "invalid_client",
			"error_description": forbiddenErr.Error(),
		}
	}

	return http.StatusInternalServerError, gin.H{
		"error":             "server_error",
		"error_description": "failed to load connected app",
	}
}

func mapClientCredentialsSessionError(err error) (int, gin.H) {
	if err == nil {
		return http.StatusBadRequest, gin.H{
			"error":             "server_error",
			"error_description": "failed to generate token",
		}
	}

	var unauthorizedErr *errors.UnauthorizedError
	if stdErrors.As(err, &unauthorizedErr) {
		return http.StatusUnauthorized, gin.H{
			"error":             "invalid_grant",
			"error_description": unauthorizedErr.Reason,
		}
	}

	return http.StatusBadRequest, gin.H{
		"error":             "server_error",
		"error_description": "failed to generate token",
	}
}

// SetPermCache attaches a permission cache for login-time warming.
func (h *OAuthHandler) SetPermCache(cache PermCacheWarmer) {
	h.permCache = cache
}

// warmPermissionsAsync fires a background goroutine to pre-load all permission
// types for the given user so subsequent API requests hit memory, not DB.
func (h *OAuthHandler) warmPermissionsAsync(userID, profileID string) {
	if h.permCache == nil {
		return
	}
	go h.permCache.WarmUser(context.Background(), userID, profileID, h.svcMgr.Permissions)
}

// Authorize handles GET /services/oauth2/authorize
func (h *OAuthHandler) Authorize(c *gin.Context) {
	// Standard OAuth params
	clientID := c.Query("client_id")
	redirectURI := c.Query("redirect_uri")
	responseType := c.Query("response_type") // Expect "code"
	scope := c.Query("scope")

	// Require Authorization header (production flow would use a dedicated login/consent UI page).

	user := MustGetUser(c)

	if responseType != "code" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_response_type"})
		return
	}

	// Generate Code
	code, err := h.svcMgr.OAuth.GenerateAuthCode(c.Request.Context(), clientID, redirectURI, scope, user.ID)
	if err != nil {
		RespondAppError(c, errors.NewValidationError("oauth", err.Error()))
		return
	}

	// Redirect
	finalURL := redirectURI
	if strings.Contains(finalURL, "?") {
		finalURL += "&code=" + code
	} else {
		finalURL += "?code=" + code
	}

	c.Redirect(http.StatusFound, finalURL)
}

// Token handles POST /services/oauth2/token
// Supports grant_type: authorization_code, password
func (h *OAuthHandler) Token(c *gin.Context) {
	// Support both JSON body and Form Data (Salesforce uses Form Data often)
	var req struct {
		GrantType     string `json:"grant_type" form:"grant_type"`
		ClientID      string `json:"client_id" form:"client_id"`
		ClientSecret  string `json:"client_secret" form:"client_secret"`
		Code          string `json:"code" form:"code"`
		RedirectURI   string `json:"redirect_uri" form:"redirect_uri"`
		Username      string `json:"username" form:"username"`
		Password      string `json:"password" form:"password"`
		Scope         string `json:"scope" form:"scope"`                   // Optional OAuth scope request
		SecurityToken string `json:"security_token" form:"security_token"` // Optional, ignored but accepted
	}

	if err := c.ShouldBind(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request"})
		return
	}

	switch req.GrantType {
	case "authorization_code":
		// Exchange authorization code for token
		token, err := h.svcMgr.OAuth.ExchangeToken(c.Request.Context(), req.ClientID, req.ClientSecret, req.Code, req.RedirectURI)
		if err != nil {
			log.Warn().Err(err).Str("client_id", req.ClientID).Msg("authorization_code grant: ExchangeToken failed")
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": err.Error()})
			return
		}
		c.JSON(http.StatusOK, token)

	case "password":
		// Username-Password OAuth flow (RFC 6749 Section 4.3)
		// This enables go-salesforce and other Salesforce client libraries
		if req.Username == "" || req.Password == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "username and password are required"})
			return
		}

		// client_id is required for password grant — every API session must be governed
		// by a Connected App. Direct user authentication without a Connected App is only
		// available through UI login (browser session) and Google SSO.
		if req.ClientID == "" {
			c.JSON(http.StatusBadRequest, gin.H{
				"error":             "invalid_request",
				"error_description": "client_id is required for password grant",
			})
			return
		}

		// Validate Connected App (client_id + client_secret)
		var passwordGrantApp *models.SystemConnectedApp
		if req.ClientID != "" {
			var appErr error
			passwordGrantApp, appErr = h.svcMgr.OAuth.GetConnectedApp(c.Request.Context(), req.ClientID)
			if appErr != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client", "error_description": "unknown client_id"})
				return
			}
			// Verify client secret (timing-safe) — required, same check as client_credentials flow
			if subtle.ConstantTimeCompare([]byte(passwordGrantApp.ConsumerSecret), []byte(req.ClientSecret)) != 1 {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client", "error_description": "invalid client_secret"})
				return
			}
		}

		// Authenticate user via existing auth service
		// Salesforce appends security_token to password, but we ignore it (combine if provided)
		password := req.Password
		if req.SecurityToken != "" {
			password = req.Password + req.SecurityToken
		}

		// Get IP and User-Agent for login tracking
		ip := c.ClientIP()
		userAgent := c.GetHeader("User-Agent")
		if userAgent == "" {
			userAgent = "OAuth2-PasswordGrant"
		}

		// Compute scope opts before login so session is stamped correctly
		var passwordGrantOpts *auth.SessionOptions
		if passwordGrantApp != nil {
			effectiveScope, scopeErr := auth.ComputeEffectiveScope(passwordGrantApp.Scopes, req.Scope)
			if scopeErr != nil {
				c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_scope", "error_description": scopeErr.Error()})
				return
			}
			passwordGrantOpts = &auth.SessionOptions{
				ConnectedAppId: passwordGrantApp.ID,
				Scope:          effectiveScope,
				GrantType:      "password",
			}
		}

		result, err := h.svcMgr.Auth.Login(c.Request.Context(), req.Username, password, ip, userAgent, passwordGrantOpts)
		if err != nil {
			// Log the real cause before masking: Login classifies failures
			// into UnauthorizedError (user not found / wrong password /
			// account locked) and InternalError (DB / schema / infra). SF
			// parity keeps the client-facing message generic, but the
			// operator-facing log must carry enough signal to diagnose
			// (today's "user not found" vs tomorrow's "column missing"
			// are indistinguishable without this).
			log.Warn().Err(err).Str("username", req.Username).Msg("password grant: Login failed")
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "authentication failure"})
			return
		}

		// Check AdminApproved access policy for Connected App
		if passwordGrantApp != nil {
			permitted, permErr := h.svcMgr.OAuth.IsUserPermittedForApp(c.Request.Context(), passwordGrantApp, result.User.ID)
			if permErr != nil {
				log.Error().Err(permErr).Msg("password grant: failed to check app access policy")
				c.JSON(http.StatusForbidden, gin.H{"error": "access_denied", "error_description": "User is not authorized to use this Connected App"})
				return
			}
			if !permitted {
				c.JSON(http.StatusForbidden, gin.H{"error": "access_denied", "error_description": "User is not authorized to use this Connected App"})
				return
			}
		}

		// Pre-warm permission cache in background so first API calls are fast
		h.warmPermissionsAsync(result.User.ID, result.User.ProfileId)

		// Return Salesforce-compatible response
		// This format is expected by go-salesforce and other SF client libraries
		instanceURL := resolveInstanceURLFromRequest(c)

		middleware.SetCSRFCookie(c)

		c.JSON(http.StatusOK, gin.H{
			"access_token": result.Token,
			"token_type":   "Bearer",
			"instance_url": instanceURL,
			"id":           instanceURL + "/id/" + constants.DefaultOrgID + "/" + result.User.ID,
			"issued_at":    fmt.Sprintf("%d", time.Now().UnixMilli()),
		})

	case "client_credentials":
		// OAuth 2.0 Client Credentials Flow (RFC 6749 Section 4.4)
		// For service-to-service authentication using Connected App
		if req.ClientID == "" || req.ClientSecret == "" {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "client_id and client_secret are required"})
			return
		}

		// Validate Connected App and get associated user
		app, err := h.svcMgr.OAuth.GetConnectedApp(c.Request.Context(), req.ClientID)
		if err != nil {
			status, payload := mapConnectedAppLookupError(err)
			c.JSON(status, payload)
			return
		}

		// Verify client secret (timing-safe comparison to prevent timing attacks)
		if subtle.ConstantTimeCompare([]byte(app.ConsumerSecret), []byte(req.ClientSecret)) != 1 {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client", "error_description": "invalid client_secret"})
			return
		}

		// Resolve the user for client_credentials flow.
		// Salesforce's client_credentials flow runs as the Connected App's configured RunAsUserID.
		// RunAsUserID is required — without it the flow is rejected.
		adminUserID := ""
		if app.RunAsUserID != nil {
			adminUserID = *app.RunAsUserID
		}
		if adminUserID == "" {
			log.Error().Str("app", app.Name).Msg("client_credentials: RunAsUserId not configured, rejecting request")
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_client", "error_description": "Connected App must have a Run As User configured for the client_credentials flow"})
			return
		}

		// Check AdminApproved access policy
		permitted, permErr := h.svcMgr.OAuth.IsUserPermittedForApp(c.Request.Context(), app, adminUserID)
		if permErr != nil {
			log.Error().Err(permErr).Msg("client_credentials: failed to check app access policy")
			c.JSON(http.StatusForbidden, gin.H{"error": "access_denied", "error_description": "User is not authorized to use this Connected App"})
			return
		}
		if !permitted {
			c.JSON(http.StatusForbidden, gin.H{"error": "access_denied", "error_description": "User is not authorized to use this Connected App"})
			return
		}

		ip := c.ClientIP()
		userAgent := c.GetHeader("User-Agent")
		if userAgent == "" {
			userAgent = "OAuth2-ClientCredentials"
		}

		// Compute effective scope for the client_credentials session
		effectiveScope, scopeErr := auth.ComputeEffectiveScope(app.Scopes, "")
		if scopeErr != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_scope", "error_description": scopeErr.Error()})
			return
		}

		// Create session for the resolved admin user (no password needed)
		token, err := h.svcMgr.Auth.CreateSessionForUser(c.Request.Context(), adminUserID, ip, userAgent, &auth.SessionOptions{
			ConnectedAppId: app.ID,
			Scope:          effectiveScope,
			GrantType:      "client_credentials",
		})
		if err != nil {
			log.Error().Err(err).Msg("client_credentials: failed to create session")
			status, payload := mapClientCredentialsSessionError(err)
			c.JSON(status, payload)
			return
		}

		instanceURL := resolveInstanceURLFromRequest(c)

		c.JSON(http.StatusOK, gin.H{
			"access_token": token.AccessToken,
			"token_type":   "Bearer",
			"instance_url": instanceURL,
			"id":           token.ID,
			"issued_at":    fmt.Sprintf("%d", time.Now().UnixMilli()),
		})

	default:
		c.JSON(http.StatusBadRequest, gin.H{"error": "unsupported_grant_type", "error_description": "Supported: authorization_code, password, client_credentials"})
	}
}

// Introspect handles POST /services/oauth2/introspect (RFC 7662)
// Requires authentication — the caller must present a valid Bearer token.
func (h *OAuthHandler) Introspect(c *gin.Context) {
	tokenString := c.PostForm("token")
	if tokenString == "" {
		c.JSON(http.StatusOK, gin.H{"active": false})
		return
	}

	claims, err := h.svcMgr.Auth.ValidateSession(c.Request.Context(), tokenString)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{"active": false})
		return
	}

	response := gin.H{
		"active":     true,
		"token_type": "Bearer",
		"sub":        claims.User.ID,
		"username":   claims.User.Email,
		"exp":        claims.ExpiresAt.Unix(),
		"iat":        claims.IssuedAt.Unix(),
	}

	if claims.User.Scope != "" {
		response["scope"] = claims.User.Scope
	}
	if claims.User.ConnectedAppId != "" {
		response["client_id"] = claims.User.ConnectedAppId
	}

	c.JSON(http.StatusOK, response)
}

// Revoke handles POST /services/oauth2/revoke
func (h *OAuthHandler) Revoke(c *gin.Context) {
	token := c.PostForm("token")
	if token == "" {
		token = c.Query("token")
	}

	if token != "" {
		if err := h.svcMgr.Auth.Logout(c.Request.Context(), token); err != nil {
			log.Warn().Err(err).Msg("Failed to logout during token revocation")
		}
	}

	c.Status(http.StatusOK)
}

// UserInfo handles GET /services/oauth2/userinfo
// Returns OIDC compatible user information
func (h *OAuthHandler) UserInfo(c *gin.Context) {
	user := MustGetUser(c)

	instanceURL := resolveInstanceURLFromRequest(c)

	// Resolve the user's CURRENT profile from the database rather than
	// relying on the JWT claim which is baked at session-creation time.
	// Without this, profile changes made after login are invisible until
	// the user creates a new session (re-logs in).
	profileID := user.ProfileID
	if h.svcMgr != nil && h.svcMgr.Permissions != nil {
		if currentProfile, err := h.svcMgr.Permissions.GetUserProfileID(c.Request.Context(), user.ID); err == nil && currentProfile != "" {
			profileID = currentProfile
		}
	}

	// Resolve the user's CURRENT role from the database for the same reason.
	var roleID *string
	var roleName string
	if h.svcMgr != nil && h.svcMgr.UserRepo != nil {
		if rid, err := h.svcMgr.UserRepo.GetUserRoleID(c.Request.Context(), user.ID); err == nil && rid != nil {
			roleID = rid
			if rname, err := h.svcMgr.UserRepo.GetRoleNameByID(c.Request.Context(), *rid); err == nil {
				roleName = rname
			}
		}
	}

	// Always include effective system permissions so the frontend has them
	// at session start for UI gating (Setup, Flows, etc.).
	perms := make(map[string]bool)
	if h.svcMgr != nil && h.svcMgr.Permissions != nil {
		resolvedPerms, _ := h.svcMgr.Permissions.GetEffectiveSystemPermissions(c.Request.Context(), user.ID, profileID)
		if resolvedPerms != nil {
			perms = resolvedPerms
		}
	}

	// Allow short-lived caching — userinfo rarely changes within a session.
	// private: only browser cache, no shared/CDN caching of auth data.
	c.Header("Cache-Control", "private, max-age=60")
	c.Header("Vary", "Authorization")

	// Standard OIDC Claims + Salesforce specific fields
	// https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5
	c.JSON(http.StatusOK, gin.H{
		"sub":                user.ID,
		"user_id":            user.ID,
		"organization_id":    constants.DefaultOrgID,
		"preferred_username": user.Email,
		"nickname":           user.Name,
		"name":               user.Name,
		"email":              user.Email,
		"email_verified":     true,
		"given_name":         splitName(user.Name, true),
		"family_name":        splitName(user.Name, false),
		"profile":            instanceURL + "/" + user.ID,
		"picture":            "", // Add when avatar support is ready
		"address":            gin.H{},
		"phone_number":       "",
		"user_type":          "STANDARD",
		"language":           "en_US",
		// Custom Claims
		"profile_id":  profileID,
		"role_id":     roleID,
		"role_name":   roleName,
		"is_active":   true,
		"permissions": perms,
	})
}

// splitName splits a full display name into given/family name.
// If getFirst is true, returns the given (first) name; otherwise the family (last) name.
func splitName(fullName string, getFirst bool) string {
	parts := strings.SplitN(strings.TrimSpace(fullName), " ", 2)
	if getFirst {
		return parts[0]
	}
	if len(parts) > 1 {
		return parts[1]
	}
	return ""
}

// googleHTTPClient is a dedicated HTTP client for Google API calls with a timeout.
var googleHTTPClient = &http.Client{Timeout: 10 * time.Second}

// GoogleToken handles POST /services/oauth2/google/token
// Exchanges a Google authorization code for a NexusCRM session.
// Flow: Google auth code → Google token → extract email from ID token → match email → NexusCRM JWT
func (h *OAuthHandler) GoogleToken(c *gin.Context) {
	var req struct {
		Code        string `json:"code" form:"code"`
		RedirectURI string `json:"redirect_uri" form:"redirect_uri"`
	}
	if err := c.ShouldBind(&req); err != nil || req.Code == "" || req.RedirectURI == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "code and redirect_uri are required"})
		return
	}

	// Validate redirect_uri against allowed hosts
	if !origincfg.IsAllowedRedirectURI(req.RedirectURI) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_request", "error_description": "redirect_uri is not allowed"})
		return
	}

	clientID := os.Getenv("GOOGLE_CLIENT_ID")
	clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
	if clientID == "" || clientSecret == "" {
		log.Error().Msg("GOOGLE_CLIENT_ID or GOOGLE_CLIENT_SECRET not configured")
		c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error", "error_description": "Google SSO not configured"})
		return
	}

	// 1. Exchange authorization code for tokens with Google
	tokenResp, err := exchangeGoogleCode(req.Code, clientID, clientSecret, req.RedirectURI)
	if err != nil {
		log.Error().Err(err).Msg("Google token exchange failed")
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": err.Error()})
		return
	}

	// 2. Extract email from ID token (avoids extra network call to userinfo endpoint)
	//    Also enforces email_verified = true to prevent unverified email attacks.
	email := extractEmailFromIDToken(tokenResp.IDToken)
	if email == "" {
		// Fallback: fetch from userinfo endpoint if ID token parsing fails
		googleUser, err := getGoogleUserInfo(tokenResp.AccessToken)
		if err != nil {
			log.Error().Err(err).Msg("Google userinfo fetch failed")
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "failed to retrieve Google user info"})
			return
		}
		if !googleUser.EmailVerified {
			c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "Google email is not verified"})
			return
		}
		email = googleUser.Email
	}

	if email == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "Google account has no verified email"})
		return
	}

	log.Debug().Str("email", email).Msg("Google SSO: looking up user by email")

	// 3. Find NexusCRM user by email
	userWithPwd, err := h.svcMgr.UserRepo.FindUserByEmailWithPassword(c.Request.Context(), email)
	if err != nil || userWithPwd == nil {
		log.Warn().Str("email", email).Msg("Google SSO: no matching account")
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "No matching account found. Please contact your administrator."})
		return
	}
	if !userWithPwd.IsActive {
		c.JSON(http.StatusBadRequest, gin.H{"error": "invalid_grant", "error_description": "User account is inactive"})
		return
	}

	// 4. Create NexusCRM session (no password check — Google already authenticated)
	ip := c.ClientIP()
	userAgent := c.GetHeader("User-Agent")
	if userAgent == "" {
		userAgent = "Google-SSO"
	}

	sessionToken, err := h.svcMgr.Auth.CreateSessionForUser(c.Request.Context(), userWithPwd.ID, ip, userAgent, nil)
	if err != nil {
		log.Error().Err(err).Str("userId", userWithPwd.ID).Msg("Google SSO: failed to create session")
		c.JSON(http.StatusInternalServerError, gin.H{"error": "server_error", "error_description": "failed to create session"})
		return
	}

	// Pre-warm permission cache in background so first API calls are fast
	h.warmPermissionsAsync(userWithPwd.ID, userWithPwd.ProfileID)

	instanceURL := resolveInstanceURLFromRequest(c)
	middleware.SetCSRFCookie(c)

	c.JSON(http.StatusOK, gin.H{
		"access_token": sessionToken.AccessToken,
		"token_type":   "Bearer",
		"instance_url": instanceURL,
		"id":           instanceURL + "/id/" + constants.DefaultOrgID + "/" + userWithPwd.ID,
		"issued_at":    fmt.Sprintf("%d", time.Now().UnixMilli()),
	})
}

// googleTokenResponse represents the token response from Google OAuth2.
type googleTokenResponse struct {
	AccessToken string `json:"access_token"`
	IDToken     string `json:"id_token"`
	ExpiresIn   int    `json:"expires_in"`
	TokenType   string `json:"token_type"`
	Scope       string `json:"scope"`
}

// googleUserInfo represents the userinfo response from Google.
type googleUserInfo struct {
	Sub           string `json:"sub"`
	Email         string `json:"email"`
	EmailVerified bool   `json:"email_verified"`
	Name          string `json:"name"`
	Picture       string `json:"picture"`
	GivenName     string `json:"given_name"`
	FamilyName    string `json:"family_name"`
}

// extractEmailFromIDToken decodes the email from a Google ID token (JWT).
// The ID token is trusted because it was received directly from Google's token
// endpoint over HTTPS — no signature verification needed here.
func extractEmailFromIDToken(idToken string) string {
	if idToken == "" {
		return ""
	}
	parts := strings.SplitN(idToken, ".", 3)
	if len(parts) < 2 {
		return ""
	}
	decoded, err := base64.RawURLEncoding.DecodeString(parts[1])
	if err != nil {
		return ""
	}
	var claims struct {
		Email         string `json:"email"`
		EmailVerified bool   `json:"email_verified"`
	}
	if err := json.Unmarshal(decoded, &claims); err != nil {
		return ""
	}
	if !claims.EmailVerified {
		return ""
	}
	return claims.Email
}

// exchangeGoogleCode exchanges an authorization code with Google's token endpoint.
func exchangeGoogleCode(code, clientID, clientSecret, redirectURI string) (*googleTokenResponse, error) {
	data := url.Values{
		"code":          {code},
		"client_id":     {clientID},
		"client_secret": {clientSecret},
		"redirect_uri":  {redirectURI},
		"grant_type":    {"authorization_code"},
	}

	resp, err := googleHTTPClient.PostForm("https://oauth2.googleapis.com/token", data)
	if err != nil {
		return nil, fmt.Errorf("request to Google token endpoint failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) // 1 MB max
	if err != nil {
		return nil, fmt.Errorf("failed to read Google token response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		var errResp struct {
			Error       string `json:"error"`
			Description string `json:"error_description"`
		}
		_ = json.Unmarshal(body, &errResp)
		return nil, fmt.Errorf("Google token exchange failed: %s - %s", errResp.Error, errResp.Description)
	}

	var tokenResp googleTokenResponse
	if err := json.Unmarshal(body, &tokenResp); err != nil {
		return nil, fmt.Errorf("failed to parse Google token response: %w", err)
	}
	return &tokenResp, nil
}

// getGoogleUserInfo fetches user profile from Google's userinfo endpoint.
func getGoogleUserInfo(accessToken string) (*googleUserInfo, error) {
	req, err := http.NewRequest("GET", "https://www.googleapis.com/oauth2/v3/userinfo", nil)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Authorization", "Bearer "+accessToken)

	resp, err := googleHTTPClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("Google userinfo request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("Google userinfo returned status %d", resp.StatusCode)
	}

	var userInfo googleUserInfo
	if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
		return nil, fmt.Errorf("failed to parse Google userinfo: %w", err)
	}
	return &userInfo, nil
}

// ChangePassword handles POST /services/oauth2/change-password
// Allows authenticated users to change their own password.
func (h *OAuthHandler) ChangePassword(c *gin.Context) {
	userSession := MustGetUser(c)

	var req struct {
		CurrentPassword string `json:"current_password"`
		NewPassword     string `json:"new_password"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || req.CurrentPassword == "" || req.NewPassword == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "current_password and new_password are required"})
		return
	}

	if err := h.svcMgr.Auth.ChangePassword(c.Request.Context(), userSession.ID, req.CurrentPassword, req.NewPassword); err != nil {
		log.Warn().Err(err).Str("userID", userSession.ID).Msg("Change password failed")
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Password changed successfully"})
}

// ForgotPassword handles POST /services/oauth2/forgot-password
// Sends a password reset email to system admin users.
func (h *OAuthHandler) ForgotPassword(c *gin.Context) {
	var req struct {
		Email string `json:"email" form:"email"`
	}
	if err := c.ShouldBind(&req); err != nil || req.Email == "" {
		// Always return success to prevent email enumeration
		c.JSON(http.StatusOK, gin.H{"message": "If an admin account exists with that email, a reset link has been sent."})
		return
	}

	user, err := h.svcMgr.UserRepo.FindUserByEmailWithPassword(c.Request.Context(), req.Email)
	if err != nil || user == nil || !user.IsActive {
		// Don't reveal whether user exists
		c.JSON(http.StatusOK, gin.H{"message": "If an admin account exists with that email, a reset link has been sent."})
		return
	}

	// Only allow password reset for system admins
	if !constants.IsSuperUser(user.ProfileID) {
		c.JSON(http.StatusOK, gin.H{"message": "If an admin account exists with that email, a reset link has been sent."})
		return
	}

	token, expiry, err := h.svcMgr.Auth.CreatePasswordResetToken(c.Request.Context(), user.ID)
	if err != nil {
		log.Error().Err(err).Str("email", req.Email).Msg("Failed to create password reset token")
		c.JSON(http.StatusOK, gin.H{"message": "If an admin account exists with that email, a reset link has been sent."})
		return
	}

	// Build reset link from FRONTEND_URL or request origin
	origin := os.Getenv("FRONTEND_URL")
	if origin == "" {
		scheme := "http"
		if c.Request.TLS != nil {
			scheme = "https"
		}
		origin = scheme + "://" + c.Request.Host
	}
	resetLink := origin + "/reset-password?token=" + token

	// Send email
	emailMsg := &ports.EmailMessage{
		To:      []string{req.Email},
		Subject: "NexusCRM Password Reset",
		HTML: fmt.Sprintf(
			`<p>You requested a password reset for your NexusCRM admin account.</p>
			<p><a href="%s">Click here to reset your password</a></p>
			<p>This link expires at %s.</p>
			<p>If you did not request this, please ignore this email.</p>`,
			resetLink, expiry.Format(time.RFC822),
		),
	}
	if err := h.svcMgr.EmailSender.Send(c.Request.Context(), emailMsg); err != nil {
		log.Error().Err(err).Str("email", req.Email).Msg("Failed to send password reset email")
	}

	c.JSON(http.StatusOK, gin.H{"message": "If an admin account exists with that email, a reset link has been sent."})
}

// ResetPassword handles POST /services/oauth2/reset-password
// Validates a reset token and sets a new password.
func (h *OAuthHandler) ResetPassword(c *gin.Context) {
	var req struct {
		Token       string `json:"token"`
		NewPassword string `json:"new_password"`
	}
	if err := c.ShouldBindJSON(&req); err != nil || req.Token == "" || req.NewPassword == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "token and new_password are required"})
		return
	}

	if err := h.svcMgr.Auth.ResetPasswordWithToken(c.Request.Context(), req.Token, req.NewPassword); err != nil {
		log.Warn().Err(err).Msg("Password reset failed")
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusOK, gin.H{"message": "Password has been reset successfully"})
}
