package core

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net/url"
	"os"
	"runtime"
	"strings"
	"time"

	"github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/diagnostics"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/oauth"
	"github.com/smart-mcp-proxy/mcpproxy-go/internal/transport"

	"github.com/mark3labs/mcp-go/client"
	uptransport "github.com/mark3labs/mcp-go/client/transport"
	"go.uber.org/zap"
)

// OAuthParameterError represents a missing or invalid OAuth parameter
type OAuthParameterError struct {
	Parameter   string
	Location    string // "authorization_url" or "token_request"
	Message     string
	OriginalErr error
}

func (e *OAuthParameterError) Error() string {
	return fmt.Sprintf("OAuth provider requires '%s' parameter: %s", e.Parameter, e.Message)
}

func (e *OAuthParameterError) Unwrap() error {
	return e.OriginalErr
}

// ErrOAuthPending represents a deferred OAuth authentication requirement.
// This error indicates that OAuth is required but has been intentionally deferred
// (e.g., for user action via tray UI or CLI) rather than being a connection failure.
type ErrOAuthPending struct {
	ServerName string
	ServerURL  string
	Message    string
}

func (e *ErrOAuthPending) Error() string {
	if e.Message != "" {
		return fmt.Sprintf("OAuth authentication required for %s: %s", e.ServerName, e.Message)
	}
	return fmt.Sprintf("OAuth authentication required for %s - use 'mcpproxy auth login --server=%s' or tray menu", e.ServerName, e.ServerName)
}

// Code attributes a stable diagnostic code so diagnostics.Classify's typed
// fast-path resolves this to an actionable OAuth user-state instead of
// MCPX_UNKNOWN_UNCLASSIFIED. A first-time sign-in maps to OAuthLoginRequired;
// the "stored token broke" variant (connection_oauth.go server-5xx path) maps
// to OAuthReauthRequired. The distinction is carried in the message text so the
// stringified error classifies the same way downstream (e.g. health). MCP-1820.
func (e *ErrOAuthPending) Code() diagnostics.Code {
	lower := strings.ToLower(e.Error())
	if strings.Contains(lower, "re-login available") ||
		strings.Contains(lower, "re-authentication required") ||
		strings.Contains(lower, "server error with stored token") {
		return diagnostics.OAuthReauthRequired
	}
	return diagnostics.OAuthLoginRequired
}

// IsOAuthPending checks if an error is an ErrOAuthPending
func IsOAuthPending(err error) bool {
	_, ok := err.(*ErrOAuthPending)
	return ok
}

// OAuthStartResult contains the result of initiating an OAuth flow.
// Used by Phase 3 (Spec 020) to return auth URL and browser status synchronously.
type OAuthStartResult struct {
	AuthURL       string // The authorization URL for manual use
	BrowserOpened bool   // Whether the browser was successfully opened
	BrowserError  string // Error message if browser opening failed
	CorrelationID string // Unique ID for tracking this OAuth flow
}

// parseOAuthError extracts structured error information from OAuth provider responses
func parseOAuthError(err error, responseBody []byte) error {
	// Try to parse as FastAPI validation error (Runlayer format)
	var fapiErr struct {
		Detail []struct {
			Type  string   `json:"type"`
			Loc   []string `json:"loc"`
			Msg   string   `json:"msg"`
			Input any      `json:"input"`
		} `json:"detail"`
	}

	if json.Unmarshal(responseBody, &fapiErr) == nil && len(fapiErr.Detail) > 0 {
		for _, detail := range fapiErr.Detail {
			if detail.Type == "missing" && len(detail.Loc) >= 2 {
				if detail.Loc[0] == "query" {
					paramName := detail.Loc[1]
					return &OAuthParameterError{
						Parameter:   paramName,
						Location:    "authorization_url",
						Message:     detail.Msg,
						OriginalErr: err,
					}
				}
			}
		}
	}

	// Try to parse as RFC 6749 OAuth error response
	var oauthErr struct {
		Error            string `json:"error"`
		ErrorDescription string `json:"error_description"`
		ErrorURI         string `json:"error_uri"`
	}

	if json.Unmarshal(responseBody, &oauthErr) == nil && oauthErr.Error != "" {
		return fmt.Errorf("OAuth error: %s - %s", oauthErr.Error, oauthErr.ErrorDescription)
	}

	// Fallback to original error
	return err
}

// tryOAuthAuth attempts OAuth authentication
func (c *Client) tryOAuthAuth(ctx context.Context) error {
	// Use the global OAuth flow coordinator to prevent race conditions
	coordinator := oauth.GetGlobalCoordinator()

	// Try to start a new OAuth flow for this server
	flowCtx, err := coordinator.StartFlow(c.config.Name)
	if err != nil {
		if err == oauth.ErrFlowInProgress {
			// Another flow is already in progress for this server
			// Wait for it to complete instead of starting a new one
			c.logger.Info("⏳ OAuth flow already in progress for this server, waiting for completion",
				zap.String("server", c.config.Name))

			waitErr := coordinator.WaitForFlow(ctx, c.config.Name, oauth.DefaultFlowTimeout)
			if waitErr != nil {
				return fmt.Errorf("waiting for OAuth flow failed: %w", waitErr)
			}

			// Flow completed, try to connect with the new tokens
			c.logger.Info("✅ OAuth flow completed by another goroutine, retrying connection",
				zap.String("server", c.config.Name))
			return nil // The caller will retry the connection
		}
		return fmt.Errorf("failed to start OAuth flow: %w", err)
	}

	// We own this OAuth flow, make sure to end it when done
	// Use named return to capture final error state
	var oauthErr error
	defer func() {
		success := oauthErr == nil
		coordinator.EndFlow(c.config.Name, success, oauthErr)
	}()

	// Update the flow context with the one from the coordinator
	ctx = oauth.WithFlowContext(ctx, flowCtx)
	logger := oauth.CorrelationLoggerWithFlow(ctx, c.logger)

	oauth.LogOAuthFlowStart(logger, c.config.Name, flowCtx.CorrelationID)

	logger.Debug("🚨 OAUTH AUTH FUNCTION CALLED - START")

	// Check if OAuth was recently completed by another client (e.g., tray OAuth, CLI)
	// If so, we should skip the browser flow and try to use the existing tokens
	// This handles cross-process OAuth completion (e.g., CLI completed OAuth, daemon needs to reconnect)
	tokenManager := oauth.GetTokenStoreManager()
	skipBrowserFlow := false

	// First check for valid persisted token directly (handles cross-process OAuth)
	hasTokenPrecheck, hasRefreshPrecheck, tokenExpiredPrecheck := oauth.HasPersistedToken(c.config.Name, c.config.URL, c.storage)
	if hasTokenPrecheck && !tokenExpiredPrecheck {
		logger.Info("🔄 Valid OAuth token found in persistent storage - will skip browser flow if OAuth error occurs",
			zap.String("server", c.config.Name),
			zap.Bool("has_refresh_token", hasRefreshPrecheck))
		skipBrowserFlow = true
	} else if tokenManager.HasRecentOAuthCompletion(c.config.Name) {
		// Also check in-memory completion flag (same-process OAuth)
		logger.Info("🔄 OAuth was recently completed in this process but token may be stale",
			zap.String("server", c.config.Name),
			zap.Bool("has_token", hasTokenPrecheck),
			zap.Bool("token_expired", tokenExpiredPrecheck))
	}

	logger.Debug("🔐 Attempting OAuth authentication",
		zap.String("url", c.config.URL))

	// Mark OAuth as in progress (local state, coordinator handles cross-goroutine coordination)
	c.markOAuthInProgress()

	// Check if tokens already exist for this server (both in-memory and persisted)
	hasInMemoryTokens := tokenManager.HasTokenStore(c.config.Name)
	hasPersistedToken, hasRefreshToken, isExpired := oauth.HasPersistedToken(c.config.Name, c.config.URL, c.storage)
	logger.Info("🔍 HTTP OAuth strategy token status",
		zap.Bool("has_in_memory_token_store", hasInMemoryTokens),
		zap.Bool("has_persisted_token", hasPersistedToken),
		zap.Bool("has_refresh_token", hasRefreshToken),
		zap.Bool("token_expired", isExpired),
		zap.String("strategy", "HTTP OAuth"))

	// If we have a persisted token with refresh_token but it's expired,
	// mcp-go should automatically try to refresh it when we call Start().
	// Log this scenario for debugging.
	if hasPersistedToken && hasRefreshToken && isExpired {
		logger.Info("🔄 Token expired but refresh_token available - mcp-go will attempt automatic refresh",
			zap.String("strategy", "HTTP OAuth"))
	}

	logger.Debug("🔧 Creating OAuth config with resource auto-detection")

	// Create OAuth config with auto-detected extra params (RFC 8707 resource)
	oauthConfig, extraParams := oauth.CreateOAuthConfigWithExtraParams(ctx, c.config, c.storage)

	c.logger.Debug("OAuth config created",
		zap.Bool("config_nil", oauthConfig == nil),
		zap.Int("extra_params_count", len(extraParams)))

	if oauthConfig == nil {
		c.logger.Error("🚨 OAUTH CONFIG IS NIL - RETURNING ERROR")
		oauthErr = fmt.Errorf("failed to create OAuth config")
		return oauthErr
	}

	c.logger.Info("🌟 Starting OAuth authentication flow",
		zap.String("server", c.config.Name),
		zap.String("redirect_uri", oauthConfig.RedirectURI),
		zap.Strings("scopes", oauthConfig.Scopes),
		zap.Bool("pkce_enabled", oauthConfig.PKCEEnabled))

	// Create HTTP transport config with OAuth
	c.logger.Debug("🛠️ Creating HTTP transport config for OAuth")
	httpConfig := transport.CreateHTTPTransportConfig(c.config, oauthConfig)

	c.logger.Debug("🔨 Calling transport.CreateHTTPClient with OAuth config")
	httpClient, err := transport.CreateHTTPClient(httpConfig)
	if err != nil {
		c.logger.Error("💥 Failed to create OAuth HTTP client in transport layer",
			zap.Error(err))
		return fmt.Errorf("failed to create OAuth HTTP client: %w", err)
	}

	c.logger.Debug("✅ HTTP client created, storing in c.client")

	c.logger.Debug("🔗 OAuth HTTP client created, starting connection")
	c.client = httpClient

	// Add detailed logging before starting the OAuth client
	c.logger.Info("🚀 Starting OAuth client - this should trigger browser opening",
		zap.String("server", c.config.Name),
		zap.String("callback_uri", oauthConfig.RedirectURI))

	// Add debug logging to check environment and system capabilities
	c.logger.Debug("🔍 OAuth environment diagnostics",
		zap.String("DISPLAY", os.Getenv("DISPLAY")),
		zap.String("PATH", os.Getenv("PATH")),
		zap.String("GOOS", runtime.GOOS),
		zap.Bool("has_open_command", hasCommand("open")),
		zap.Bool("has_xdg_open", hasCommand("xdg-open")),
		zap.String("BROWSER", os.Getenv("BROWSER")),
		zap.String("XDG_SESSION_TYPE", os.Getenv("XDG_SESSION_TYPE")),
		zap.String("WAYLAND_DISPLAY", os.Getenv("WAYLAND_DISPLAY")),
		zap.Bool("CI", os.Getenv("CI") != ""),
		zap.Bool("HEADLESS", os.Getenv("HEADLESS") != ""),
		zap.Bool("NO_BROWSER", os.Getenv("NO_BROWSER") != ""),
		zap.String("SSH_CLIENT", os.Getenv("SSH_CLIENT")),
		zap.String("SSH_TTY", os.Getenv("SSH_TTY")))

	// Check for conditions that might prevent browser opening
	browserBlockingConditions := []string{}
	if os.Getenv("CI") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "CI=true")
	}
	if os.Getenv("HEADLESS") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "HEADLESS=true")
	}
	if os.Getenv("NO_BROWSER") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "NO_BROWSER=true")
	}
	if os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "SSH_session")
	}
	if runtime.GOOS == osLinux && os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
		browserBlockingConditions = append(browserBlockingConditions, "no_GUI_on_linux")
	}

	if len(browserBlockingConditions) > 0 {
		c.logger.Warn("⚠️ Detected conditions that may prevent browser opening",
			zap.String("server", c.config.Name),
			zap.Strings("blocking_conditions", browserBlockingConditions),
			zap.String("recommendation", "You may need to manually open the OAuth URL when prompted"))

		// For remote scenarios, log additional guidance
		if os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" {
			c.logger.Info("📡 Remote SSH session detected - extended OAuth timeout enabled",
				zap.String("server", c.config.Name),
				zap.Duration("timeout", 120*time.Second),
				zap.String("note", "OAuth URL will be displayed for manual opening"))
		}
	}

	// Start the OAuth client and handle OAuth authorization errors properly
	c.logger.Info("🚀 Starting OAuth client - using proper mcp-go OAuth error handling",
		zap.String("server", c.config.Name),
		zap.Duration("callback_timeout", 120*time.Second))

	// Token refresh retry configuration
	refreshConfig := oauth.DefaultRefreshConfig()
	var lastErr error

	// If we have a refresh token, retry Start() with exponential backoff
	// to give mcp-go's automatic token refresh a chance to succeed
	if hasRefreshToken {
		backoff := refreshConfig.InitialBackoff
		for attempt := 1; attempt <= refreshConfig.MaxAttempts; attempt++ {
			oauth.LogClientConnectionAttempt(c.logger, attempt, refreshConfig.MaxAttempts)

			err = c.client.Start(ctx)
			if err == nil {
				oauth.LogClientConnectionSuccess(c.logger, time.Duration(attempt)*backoff)
				lastErr = nil
				break
			}

			// If not an OAuth error, don't retry for token refresh
			if !client.IsOAuthAuthorizationRequiredError(err) {
				c.logger.Error("❌ OAuth client start failed with non-OAuth error",
					zap.String("server", c.config.Name),
					zap.Error(err))
				oauthErr = fmt.Errorf("OAuth client start failed: %w", err)
				return oauthErr
			}

			oauth.LogClientConnectionFailure(c.logger, attempt, err)
			lastErr = err

			// Don't sleep on the last attempt
			if attempt < refreshConfig.MaxAttempts {
				c.logger.Debug("⏳ Waiting before retry",
					zap.String("server", c.config.Name),
					zap.Duration("backoff", backoff),
					zap.Int("attempt", attempt))
				time.Sleep(backoff)
				backoff = min(backoff*2, refreshConfig.MaxBackoff)
			}
		}
	} else {
		// No refresh token, single attempt
		err = c.client.Start(ctx)
		lastErr = err
	}

	// If we still have an error after retries, proceed with browser OAuth flow
	if lastErr != nil {
		// Check if this is an OAuth authorization error that we need to handle manually
		if client.IsOAuthAuthorizationRequiredError(lastErr) {
			// CRITICAL FIX: If we have a valid persisted token (e.g., from CLI OAuth),
			// skip browser flow and return retriable error. The token exists but
			// the mcp-go client needs to pick it up on retry.
			if skipBrowserFlow {
				c.logger.Info("🔄 OAuth authorization error but valid token exists - skipping browser flow",
					zap.String("server", c.config.Name),
					zap.String("tip", "Token should be used on next connection attempt"))

				// Clear in-progress state so retry can proceed
				c.clearOAuthState()

				// Return a retriable error - the managed client will retry and pick up the token
				return fmt.Errorf("OAuth token exists in storage, retry connection to use it: %w", lastErr)
			}

			c.logger.Info("🎯 OAuth authorization required after connection attempts - starting manual OAuth flow",
				zap.String("server", c.config.Name),
				zap.Bool("had_refresh_token", hasRefreshToken))

			// Handle OAuth authorization manually using the example pattern
			if handleErr := c.handleOAuthAuthorization(ctx, lastErr, oauthConfig, extraParams); handleErr != nil {
				c.clearOAuthState() // Clear state on OAuth failure
				oauthErr = fmt.Errorf("OAuth authorization failed: %w", handleErr)
				return oauthErr
			}

			// Retry starting the client after OAuth is complete
			c.logger.Info("🔄 Retrying client start after OAuth authorization",
				zap.String("server", c.config.Name))

			err = c.client.Start(ctx)
			if err != nil {
				c.logger.Error("❌ OAuth client start failed after authorization",
					zap.String("server", c.config.Name),
					zap.Error(err))
				oauthErr = fmt.Errorf("OAuth client start failed after authorization: %w", err)
				return oauthErr
			}

			c.logger.Info("✅ OAuth client start successful after authorization",
				zap.String("server", c.config.Name))
		} else {
			c.logger.Error("❌ OAuth client start failed with non-OAuth error",
				zap.String("server", c.config.Name),
				zap.Error(lastErr))
			oauthErr = fmt.Errorf("OAuth client start failed: %w", lastErr)
			return oauthErr
		}
	}

	c.logger.Info("✅ OAuth client started successfully",
		zap.String("server", c.config.Name))

	c.logger.Info("✅ OAuth setup complete - using proper mcp-go OAuth error handling pattern",
		zap.String("server", c.config.Name))

	// CRITICAL FIX: Test initialize() to verify connection and set serverInfo
	// This ensures consistency with other auth strategies and sets c.serverInfo for ListTools
	c.logger.Debug("🔍 Starting MCP initialization after OAuth setup",
		zap.String("server", c.config.Name))

	if err := c.initialize(ctx); err != nil {
		c.logger.Error("❌ MCP initialization failed after OAuth setup",
			zap.String("server", c.config.Name),
			zap.Error(err))

		// Check if this is a deprecated endpoint error (HTTP 410 Gone)
		// This indicates the server has migrated to a new endpoint URL
		if c.isDeprecatedEndpointError(err) {
			correlationID := ""
			if flowCtx != nil {
				correlationID = flowCtx.CorrelationID
			}
			c.logger.Error("⚠️ ENDPOINT DEPRECATED: Server has migrated to a new URL",
				zap.String("server", c.config.Name),
				zap.String("current_url", c.config.URL),
				zap.String("correlation_id", correlationID),
				zap.String("action", "Update the server URL in your configuration"),
				zap.String("hint", "Check the server's documentation or try removing /sse from the URL"),
				zap.Error(err))

			return transport.NewEndpointDeprecatedError(
				c.config.URL,
				fmt.Sprintf("Server '%s' endpoint has been deprecated or removed", c.config.Name),
				"", // migration guide - extracted from error if available
				"", // new endpoint - would need to parse from server response
			)
		}

		// Check if this is an OAuth authorization error that we need to handle manually
		if client.IsOAuthAuthorizationRequiredError(err) {
			c.logger.Info("🎯 OAuth authorization required during MCP init - deferring OAuth for background processing",
				zap.String("server", c.config.Name))

			// For daemon mode, defer OAuth to prevent UI blocking
			// The connection will be retried by the managed client retry logic
			// which will eventually complete OAuth in the background
			if c.isDeferOAuthForTray(ctx) {
				c.logger.Info("⏳ Deferring OAuth to prevent UI blocking - will retry in background",
					zap.String("server", c.config.Name))

				// Log a user-friendly message about OAuth login options
				c.logger.Info("💡 OAuth login available via Web UI, system tray menu, or CLI command",
					zap.String("server", c.config.Name))

				return &ErrOAuthPending{
					ServerName: c.config.Name,
					ServerURL:  c.config.URL,
					Message:    "login available via Web UI, system tray menu, or 'mcpproxy auth login' CLI command",
				}
			}

			// CRITICAL FIX: If we have a valid persisted token, skip browser flow
			if skipBrowserFlow {
				c.logger.Info("🔄 OAuth authorization error during MCP init but valid token exists - skipping browser flow",
					zap.String("server", c.config.Name),
					zap.String("tip", "Token should be used on next connection attempt"))

				// Clear in-progress state so retry can proceed
				c.clearOAuthState()

				// Return a retriable error - the managed client will retry and pick up the token
				return fmt.Errorf("OAuth token exists in storage, retry connection to use it: %w", err)
			}

			// Clear OAuth state before starting manual flow to prevent "already in progress" errors
			c.clearOAuthState()

			// Handle OAuth authorization manually using the example pattern
			if handleErr := c.handleOAuthAuthorization(ctx, err, oauthConfig, extraParams); handleErr != nil {
				c.clearOAuthState() // Clear state on OAuth failure
				oauthErr = fmt.Errorf("OAuth authorization during MCP init failed: %w", handleErr)
				return oauthErr
			}

			// Retry MCP initialization after OAuth is complete
			c.logger.Info("🔄 Retrying MCP initialization after OAuth authorization",
				zap.String("server", c.config.Name))

			if retryErr := c.initialize(ctx); retryErr != nil {
				c.logger.Error("❌ MCP initialization failed after OAuth authorization",
					zap.String("server", c.config.Name),
					zap.Error(retryErr))
				oauthErr = fmt.Errorf("MCP initialize failed after OAuth authorization: %w", retryErr)
				return oauthErr
			}

			c.logger.Info("✅ MCP initialization successful after OAuth authorization",
				zap.String("server", c.config.Name))
		} else if c.isServerSideError(err) && !skipBrowserFlow {
			// Server returned 5xx (e.g., 500) during MCP initialize.
			// Some servers (e.g., Cloudflare Workers) crash with 500 instead of returning
			// 401 when they receive an invalid/revoked/stale token. Clear the stored token
			// and attempt a fresh browser OAuth flow.
			c.logger.Warn("⚠️ Server returned 5xx during MCP init - stored token may be invalid, attempting fresh OAuth",
				zap.String("server", c.config.Name),
				zap.Error(err))

			// Clear the stored token so a fresh one can be obtained
			if tokenStore, ok := oauthConfig.TokenStore.(interface{ ClearToken() error }); ok {
				if clearErr := tokenStore.ClearToken(); clearErr != nil {
					c.logger.Warn("Failed to clear stored token",
						zap.String("server", c.config.Name),
						zap.Error(clearErr))
				}
			}

			c.clearOAuthState()

			// For daemon mode, defer to background retry
			if c.isDeferOAuthForTray(ctx) {
				c.logger.Info("⏳ Deferring fresh OAuth to background after server 5xx error",
					zap.String("server", c.config.Name))
				return &ErrOAuthPending{
					ServerName: c.config.Name,
					ServerURL:  c.config.URL,
					Message:    "server error with stored token - re-login available via Web UI, system tray menu, or 'mcpproxy auth login' CLI command",
				}
			}

			// Handle fresh OAuth authorization
			if handleErr := c.handleOAuthAuthorization(ctx, err, oauthConfig, extraParams); handleErr != nil {
				c.clearOAuthState()
				oauthErr = fmt.Errorf("OAuth re-authorization after server 5xx failed: %w", handleErr)
				return oauthErr
			}

			// Retry MCP initialization with fresh token
			if retryErr := c.initialize(ctx); retryErr != nil {
				c.logger.Error("❌ MCP initialization failed after fresh OAuth (server may be down)",
					zap.String("server", c.config.Name),
					zap.Error(retryErr))
				oauthErr = fmt.Errorf("MCP initialize failed after fresh OAuth: %w", retryErr)
				return oauthErr
			}

			c.logger.Info("✅ MCP initialization successful after fresh OAuth re-authorization",
				zap.String("server", c.config.Name))
		} else {
			oauthErr = fmt.Errorf("MCP initialize failed during OAuth strategy: %w", err)
			return oauthErr
		}
	}

	c.logger.Info("✅ MCP initialization completed successfully after OAuth",
		zap.String("server", c.config.Name))

	return nil
}

// trySSEOAuthAuth attempts SSE OAuth authentication
func (c *Client) trySSEOAuthAuth(ctx context.Context) error {
	// Use the global OAuth flow coordinator to prevent race conditions
	coordinator := oauth.GetGlobalCoordinator()

	// Try to start a new OAuth flow for this server
	flowCtx, err := coordinator.StartFlow(c.config.Name)
	if err != nil {
		if err == oauth.ErrFlowInProgress {
			// Another flow is already in progress for this server
			// Wait for it to complete instead of starting a new one
			c.logger.Info("⏳ SSE OAuth flow already in progress for this server, waiting for completion",
				zap.String("server", c.config.Name))

			waitErr := coordinator.WaitForFlow(ctx, c.config.Name, oauth.DefaultFlowTimeout)
			if waitErr != nil {
				return fmt.Errorf("waiting for SSE OAuth flow failed: %w", waitErr)
			}

			// Flow completed, try to connect with the new tokens
			c.logger.Info("✅ SSE OAuth flow completed by another goroutine, retrying connection",
				zap.String("server", c.config.Name))
			return nil // The caller will retry the connection
		}
		return fmt.Errorf("failed to start SSE OAuth flow: %w", err)
	}

	// We own this OAuth flow, make sure to end it when done
	// Use named return to capture final error state
	var oauthErr error
	defer func() {
		success := oauthErr == nil
		coordinator.EndFlow(c.config.Name, success, oauthErr)
	}()

	// Update the flow context with the one from the coordinator
	ctx = oauth.WithFlowContext(ctx, flowCtx)
	logger := oauth.CorrelationLoggerWithFlow(ctx, c.logger)

	oauth.LogOAuthFlowStart(logger, c.config.Name, flowCtx.CorrelationID)

	// Check if OAuth was recently completed by another client (e.g., tray OAuth, CLI)
	// This handles cross-process OAuth completion (e.g., CLI completed OAuth, daemon needs to reconnect)
	tokenManager := oauth.GetTokenStoreManager()
	skipBrowserFlow := false

	// First check for valid persisted token directly (handles cross-process OAuth)
	hasTokenPrecheck, hasRefreshPrecheck, tokenExpiredPrecheck := oauth.HasPersistedToken(c.config.Name, c.config.URL, c.storage)
	if hasTokenPrecheck && !tokenExpiredPrecheck {
		logger.Info("🔄 Valid OAuth token found in persistent storage - will skip browser flow if OAuth error occurs",
			zap.String("server", c.config.Name),
			zap.Bool("has_refresh_token", hasRefreshPrecheck))
		skipBrowserFlow = true
	} else if tokenManager.HasRecentOAuthCompletion(c.config.Name) {
		// Also check in-memory completion flag (same-process OAuth)
		logger.Info("🔄 SSE OAuth was recently completed in this process but token may be stale",
			zap.String("server", c.config.Name),
			zap.Bool("has_token", hasTokenPrecheck),
			zap.Bool("token_expired", tokenExpiredPrecheck))
	}

	logger.Debug("🔐 Attempting SSE OAuth authentication",
		zap.String("url", c.config.URL))

	// Mark OAuth as in progress
	c.markOAuthInProgress()

	// Check if tokens already exist for this server (both in-memory and persisted)
	hasInMemoryTokens := tokenManager.HasTokenStore(c.config.Name)
	hasPersistedToken, hasRefreshToken, isExpired := oauth.HasPersistedToken(c.config.Name, c.config.URL, c.storage)
	logger.Info("🔍 SSE OAuth strategy token status",
		zap.Bool("has_in_memory_token_store", hasInMemoryTokens),
		zap.Bool("has_persisted_token", hasPersistedToken),
		zap.Bool("has_refresh_token", hasRefreshToken),
		zap.Bool("token_expired", isExpired),
		zap.String("strategy", "SSE OAuth"))

	// If we have a persisted token with refresh_token but it's expired,
	// mcp-go should automatically try to refresh it when we call Start().
	// Log this scenario for debugging.
	if hasPersistedToken && hasRefreshToken && isExpired {
		logger.Info("🔄 Token expired but refresh_token available - mcp-go will attempt automatic refresh",
			zap.String("strategy", "SSE OAuth"))
	}

	// Create OAuth config with auto-detected extra params (RFC 8707 resource)
	oauthConfig, extraParams := oauth.CreateOAuthConfigWithExtraParams(ctx, c.config, c.storage)
	if oauthConfig == nil {
		oauthErr = fmt.Errorf("failed to create OAuth config")
		return oauthErr
	}

	c.logger.Info("🌟 Starting SSE OAuth authentication flow",
		zap.String("server", c.config.Name),
		zap.String("redirect_uri", oauthConfig.RedirectURI),
		zap.Strings("scopes", oauthConfig.Scopes),
		zap.Bool("pkce_enabled", oauthConfig.PKCEEnabled))

	// Create SSE transport config with OAuth
	c.logger.Debug("🛠️ Creating SSE transport config for OAuth")
	httpConfig := transport.CreateHTTPTransportConfig(c.config, oauthConfig)

	c.logger.Debug("🔨 Calling transport.CreateSSEClient with OAuth config")
	sseClient, err := transport.CreateSSEClient(httpConfig)
	if err != nil {
		c.logger.Error("💥 Failed to create OAuth SSE client in transport layer",
			zap.Error(err))
		return fmt.Errorf("failed to create OAuth SSE client: %w", err)
	}

	c.logger.Debug("✅ SSE client created, storing in c.client")

	c.logger.Debug("🔗 OAuth SSE client created, starting connection")
	c.client = sseClient

	// Register connection lost handler for SSE transport to detect GOAWAY/disconnects
	c.client.OnConnectionLost(func(err error) {
		c.logger.Warn("⚠️ SSE OAuth connection lost detected",
			zap.String("server", c.config.Name),
			zap.Error(err),
			zap.String("transport", "sse-oauth"),
			zap.String("note", "Connection dropped by server or network - will attempt reconnection"))
	})

	// Add detailed logging before starting the OAuth client
	c.logger.Info("🚀 Starting OAuth SSE client - this should trigger browser opening",
		zap.String("server", c.config.Name),
		zap.String("callback_uri", oauthConfig.RedirectURI))

	// Add debug logging to check environment and system capabilities
	c.logger.Debug("🔍 SSE OAuth environment diagnostics",
		zap.String("DISPLAY", os.Getenv("DISPLAY")),
		zap.String("PATH", os.Getenv("PATH")),
		zap.String("GOOS", runtime.GOOS),
		zap.Bool("has_open_command", hasCommand("open")),
		zap.Bool("has_xdg_open", hasCommand("xdg-open")),
		zap.String("BROWSER", os.Getenv("BROWSER")),
		zap.String("XDG_SESSION_TYPE", os.Getenv("XDG_SESSION_TYPE")),
		zap.String("WAYLAND_DISPLAY", os.Getenv("WAYLAND_DISPLAY")),
		zap.Bool("CI", os.Getenv("CI") != ""),
		zap.Bool("HEADLESS", os.Getenv("HEADLESS") != ""))

	// Detect conditions that might prevent browser opening
	var browserBlockingConditions []string
	if os.Getenv("CI") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "CI=true")
	}
	if os.Getenv("HEADLESS") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "HEADLESS=true")
	}
	if os.Getenv("NO_BROWSER") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "NO_BROWSER=true")
	}
	if os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" {
		browserBlockingConditions = append(browserBlockingConditions, "SSH_session")
	}
	if runtime.GOOS == osLinux && os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
		browserBlockingConditions = append(browserBlockingConditions, "no_GUI_on_linux")
	}

	if len(browserBlockingConditions) > 0 {
		c.logger.Warn("⚠️ Detected conditions that may prevent browser opening for SSE OAuth",
			zap.String("server", c.config.Name),
			zap.Strings("blocking_conditions", browserBlockingConditions),
			zap.String("recommendation", "You may need to manually open the OAuth URL when prompted"))

		// For remote scenarios, log additional guidance
		if os.Getenv("SSH_CLIENT") != "" || os.Getenv("SSH_TTY") != "" {
			c.logger.Info("📡 Remote SSH session detected - extended OAuth timeout enabled",
				zap.String("server", c.config.Name),
				zap.Duration("timeout", 120*time.Second),
				zap.String("note", "OAuth URL will be displayed for manual opening"))
		}
	}

	// Start the OAuth client and handle OAuth authorization errors properly
	c.logger.Info("🚀 Starting SSE OAuth client - using proper mcp-go OAuth error handling",
		zap.String("server", c.config.Name),
		zap.Duration("callback_timeout", 120*time.Second))

	// Start the client with persistent context so SSE stream keeps running
	// even if the connect context is short-lived (same as stdio transport).
	// SSE stream runs in a background goroutine and needs context to stay alive.
	persistentCtx := context.Background()
	c.logger.Debug("🔍 Starting SSE OAuth client with persistent context",
		zap.String("server", c.config.Name))

	// Token refresh retry configuration
	refreshConfig := oauth.DefaultRefreshConfig()
	var lastErr error

	// If we have a refresh token, retry Start() with exponential backoff
	// to give mcp-go's automatic token refresh a chance to succeed
	if hasRefreshToken {
		backoff := refreshConfig.InitialBackoff
		for attempt := 1; attempt <= refreshConfig.MaxAttempts; attempt++ {
			oauth.LogClientConnectionAttempt(c.logger, attempt, refreshConfig.MaxAttempts)

			err = c.client.Start(persistentCtx)
			if err == nil {
				oauth.LogClientConnectionSuccess(c.logger, time.Duration(attempt)*backoff)
				lastErr = nil
				break
			}

			// If not an OAuth error, don't retry for token refresh
			if !client.IsOAuthAuthorizationRequiredError(err) {
				c.logger.Error("❌ SSE OAuth client start failed with non-OAuth error",
					zap.String("server", c.config.Name),
					zap.Error(err))
				oauthErr = fmt.Errorf("SSE OAuth client start failed: %w", err)
				return oauthErr
			}

			oauth.LogClientConnectionFailure(c.logger, attempt, err)
			lastErr = err

			// Don't sleep on the last attempt
			if attempt < refreshConfig.MaxAttempts {
				c.logger.Debug("⏳ Waiting before retry",
					zap.String("server", c.config.Name),
					zap.Duration("backoff", backoff),
					zap.Int("attempt", attempt))
				time.Sleep(backoff)
				backoff = min(backoff*2, refreshConfig.MaxBackoff)
			}
		}
	} else {
		// No refresh token, single attempt
		err = c.client.Start(persistentCtx)
		lastErr = err
	}

	// If we still have an error after retries, proceed with browser OAuth flow
	if lastErr != nil {
		// Check if this is an OAuth authorization error that we need to handle manually
		if client.IsOAuthAuthorizationRequiredError(lastErr) {
			// CRITICAL FIX: If we have a valid persisted token (e.g., from CLI OAuth),
			// skip browser flow and return retriable error. The token exists but
			// the mcp-go client needs to pick it up on retry.
			if skipBrowserFlow {
				c.logger.Info("🔄 SSE OAuth authorization error but valid token exists - skipping browser flow",
					zap.String("server", c.config.Name),
					zap.String("tip", "Token should be used on next connection attempt"))

				// Clear in-progress state so retry can proceed
				c.clearOAuthState()

				// Return a retriable error - the managed client will retry and pick up the token
				return fmt.Errorf("OAuth token exists in storage, retry connection to use it: %w", lastErr)
			}

			c.logger.Info("🎯 SSE OAuth authorization required after connection attempts - starting manual OAuth flow",
				zap.String("server", c.config.Name),
				zap.Bool("had_refresh_token", hasRefreshToken))

			// Handle OAuth authorization manually using the example pattern
			if handleErr := c.handleOAuthAuthorization(ctx, lastErr, oauthConfig, extraParams); handleErr != nil {
				c.clearOAuthState() // Clear state on OAuth failure
				oauthErr = fmt.Errorf("SSE OAuth authorization failed: %w", handleErr)
				return oauthErr
			}

			// Retry starting the client after OAuth is complete
			c.logger.Info("🔄 Retrying SSE client start after OAuth authorization",
				zap.String("server", c.config.Name))

			err = c.client.Start(persistentCtx)
			if err != nil {
				c.logger.Error("❌ SSE OAuth client start failed after authorization",
					zap.String("server", c.config.Name),
					zap.Error(err))
				oauthErr = fmt.Errorf("SSE OAuth client start failed after authorization: %w", err)
				return oauthErr
			}

			c.logger.Info("✅ SSE OAuth client start successful after authorization",
				zap.String("server", c.config.Name))
		} else {
			c.logger.Error("❌ SSE OAuth client start failed with non-OAuth error",
				zap.String("server", c.config.Name),
				zap.Error(lastErr))
			oauthErr = fmt.Errorf("SSE OAuth client start failed: %w", lastErr)
			return oauthErr
		}
	}

	c.logger.Info("✅ SSE OAuth client started successfully",
		zap.String("server", c.config.Name))

	// CRITICAL FIX: Test initialize() to detect OAuth errors during auth strategy phase
	// This ensures OAuth strategy will be tried if SSE OAuth fails during MCP initialization
	// Use caller's context for initialize() to respect timeouts
	if err := c.initialize(ctx); err != nil {
		oauthErr = fmt.Errorf("MCP initialize failed during SSE OAuth strategy: %w", err)
		return oauthErr
	}

	return nil
}

// isOAuthError checks if the error is OAuth-related (actual authentication failure)
func (c *Client) isOAuthError(err error) bool {
	if err == nil {
		return false
	}

	errStr := err.Error()
	oauthErrors := []string{
		"invalid_token",
		"invalid_grant",
		"access_denied",
		"unauthorized",
		"401", // HTTP 401 Unauthorized
		"Missing or invalid access token",
		"OAuth authentication failed",
		"oauth timeout",
		"oauth error",
		"no valid token available", // Transport layer token check
		"authorization required",   // Generic authorization needed
	}

	for _, oauthErr := range oauthErrors {
		if containsString(errStr, oauthErr) {
			return true
		}
	}

	return false
}

// handleOAuthAuthorization handles the manual OAuth flow.
func (c *Client) handleOAuthAuthorization(ctx context.Context, authErr error, oauthConfig *client.OAuthConfig, extraParams map[string]string) error {
	// Check if OAuth is already in progress to prevent duplicate flows (CRITICAL FIX for Phase 1)
	if c.isOAuthInProgress() {
		c.logger.Warn("⚠️ OAuth authorization already in progress, skipping duplicate attempt",
			zap.String("server", c.config.Name))
		return fmt.Errorf("OAuth authorization already in progress for %s", c.config.Name)
	}

	// Mark OAuth as in progress to prevent concurrent attempts
	c.markOAuthInProgress()
	defer func() {
		// Clear OAuth progress state on exit (success or failure)
		c.oauthMu.Lock()
		c.oauthInProgress = false
		c.oauthMu.Unlock()
	}()

	c.logger.Info("🔐 Starting manual OAuth authorization flow",
		zap.String("server", c.config.Name))

	// Phase 2 (Spec 020): Pre-flight OAuth metadata validation
	// Validate metadata BEFORE starting the full OAuth flow to fail fast with clear errors
	if c.config.URL != "" {
		_, validationErr := oauth.ValidateOAuthMetadata(c.config.URL, c.config.Name, 5*time.Second)
		if validationErr != nil {
			// Convert OAuthMetadataError to OAuthFlowError for consistent error handling
			if metadataErr, ok := validationErr.(*oauth.OAuthMetadataError); ok {
				c.logger.Warn("⚠️ OAuth metadata validation failed",
					zap.String("server", c.config.Name),
					zap.String("error_type", metadataErr.ErrorType),
					zap.String("message", metadataErr.Message))

				return &contracts.OAuthFlowError{
					Success:    false,
					ErrorType:  metadataErr.ErrorType,
					ErrorCode:  metadataErr.ErrorCode,
					ServerName: c.config.Name,
					Message:    metadataErr.Message,
					Details: &contracts.OAuthErrorDetails{
						ServerURL: c.config.URL,
						ProtectedResourceMetadata: func() *contracts.MetadataStatus {
							if metadataErr.Details.ProtectedResourceMetadata != nil {
								return &contracts.MetadataStatus{
									Found:                metadataErr.Details.ProtectedResourceMetadata.Found,
									URLChecked:           metadataErr.Details.ProtectedResourceMetadata.URLChecked,
									Error:                metadataErr.Details.ProtectedResourceMetadata.Error,
									AuthorizationServers: metadataErr.Details.ProtectedResourceMetadata.AuthorizationServers,
								}
							}
							return nil
						}(),
						AuthorizationServerMetadata: func() *contracts.MetadataStatus {
							if metadataErr.Details.AuthorizationServerMetadata != nil {
								return &contracts.MetadataStatus{
									Found:      metadataErr.Details.AuthorizationServerMetadata.Found,
									URLChecked: metadataErr.Details.AuthorizationServerMetadata.URLChecked,
									Error:      metadataErr.Details.AuthorizationServerMetadata.Error,
								}
							}
							return nil
						}(),
					},
					Suggestion: metadataErr.Suggestion,
					DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
				}
			}
			// For non-metadata errors, log and continue (don't block OAuth flow)
			c.logger.Debug("OAuth metadata validation returned non-metadata error, continuing with flow",
				zap.String("server", c.config.Name),
				zap.Error(validationErr))
		}
	}

	// Get the OAuth handler from the error (as shown in the example)
	oauthHandler := client.GetOAuthHandler(authErr)
	if oauthHandler == nil {
		return fmt.Errorf("failed to get OAuth handler from error")
	}

	c.logger.Info("✅ OAuth handler obtained from error",
		zap.String("server", c.config.Name))

	// Generate PKCE code verifier and challenge
	codeVerifier, err := client.GenerateCodeVerifier()
	if err != nil {
		return fmt.Errorf("failed to generate code verifier: %w", err)
	}
	codeChallenge := client.GenerateCodeChallenge(codeVerifier)

	// Generate state parameter
	state, err := client.GenerateState()
	if err != nil {
		return fmt.Errorf("failed to generate state: %w", err)
	}

	c.logger.Info("🔑 Generated PKCE and state parameters",
		zap.String("server", c.config.Name),
		zap.String("state", state))

	// Check if OAuth credentials are available (either from config or persisted DCR)
	// oauthConfig.ClientID may contain persisted DCR credentials loaded by CreateOAuthConfig()
	hasStaticCredentials := c.config.OAuth != nil && c.config.OAuth.ClientID != ""
	hasPersistedCredentials := oauthConfig.ClientID != ""

	// Determine OAuth mode and attempt registration if needed
	var oauthMode string
	if hasStaticCredentials {
		// Skip DCR when static credentials are provided in config
		oauthMode = "static credentials"
		c.logger.Info("⏩ Skipping Dynamic Client Registration (static credentials provided)",
			zap.String("server", c.config.Name),
			zap.String("client_id", c.config.OAuth.ClientID))
	} else if hasPersistedCredentials {
		// Skip DCR when we have persisted DCR credentials from a previous OAuth flow
		oauthMode = "persisted DCR credentials"
		c.logger.Info("⏩ Skipping Dynamic Client Registration (using persisted DCR credentials)",
			zap.String("server", c.config.Name),
			zap.String("client_id", oauthConfig.ClientID))
	} else {
		// Attempt DCR for servers without static credentials
		c.logger.Info("📋 Attempting Dynamic Client Registration (optional)",
			zap.String("server", c.config.Name))
		var regErr error
		func() {
			defer func() {
				if r := recover(); r != nil {
					c.logger.Warn("OAuth RegisterClient panicked - server metadata missing or malformed",
						zap.String("server", c.config.Name),
						zap.Any("panic", r))
					regErr = fmt.Errorf("server does not support dynamic client registration: metadata missing")
				}
			}()
			regErr = oauthHandler.RegisterClient(ctx, "mcpproxy-go")
		}()

		if regErr != nil {
			// DCR failed - proceed with public client OAuth (PKCE without client_id)
			oauthMode = "public client (PKCE)"
			c.logger.Warn("⚠️ Dynamic Client Registration not supported - using public client OAuth with PKCE",
				zap.String("server", c.config.Name),
				zap.Error(regErr))
			c.logger.Info("💡 Proceeding with public client authentication (no client_id required)",
				zap.String("server", c.config.Name),
				zap.String("mode", "OAuth 2.1 public client with PKCE"),
				zap.Strings("scopes", oauthConfig.Scopes))
		} else {
			oauthMode = "dynamic client registration"

			// Persist DCR credentials and callback port for token refresh (Spec 022: OAuth Redirect URI Port Persistence)
			clientID := oauthHandler.GetClientID()
			clientSecret := oauthHandler.GetClientSecret()
			if c.storage != nil && clientID != "" {
				serverKey := oauth.GenerateServerKey(c.config.Name, c.config.URL)
				// Get the callback server port to persist alongside DCR credentials
				var callbackPort int
				if callbackServer, exists := oauth.GetCallbackServer(c.config.Name); exists {
					callbackPort = callbackServer.Port
				}
				if err := c.storage.UpdateOAuthClientCredentials(serverKey, clientID, clientSecret, callbackPort); err != nil {
					c.logger.Warn("Failed to persist DCR credentials - token refresh may fail later",
						zap.String("server", c.config.Name),
						zap.Error(err))
				} else {
					c.logger.Info("✅ DCR credentials persisted for token refresh",
						zap.String("server", c.config.Name),
						zap.String("client_id", clientID),
						zap.Int("callback_port", callbackPort))
				}
			}

			c.logger.Info("✅ Dynamic Client Registration successful",
				zap.String("server", c.config.Name),
				zap.String("client_id", clientID))
		}
	}

	// Continue with OAuth flow regardless of DCR result
	// Public client OAuth (RFC 8252) with PKCE doesn't require client_id
	// If server doesn't support this, it will reject the authorization request

	c.logger.Info("🌟 Starting OAuth authentication flow",
		zap.String("server", c.config.Name),
		zap.Strings("scopes", oauthConfig.Scopes),
		zap.Bool("pkce_enabled", true),
		zap.String("mode", oauthMode))

	// Get the authorization URL
	// Works with: static credentials, DCR, or public client OAuth (empty client_id + PKCE)
	var authURL string
	var authURLErr error
	func() {
		defer func() {
			if r := recover(); r != nil {
				c.logger.Error("GetAuthorizationURL panicked",
					zap.String("server", c.config.Name),
					zap.Any("panic", r))
				authURLErr = fmt.Errorf("internal error (panic recovered): %v", r)
			}
		}()
		authURL, authURLErr = oauthHandler.GetAuthorizationURL(ctx, state, codeChallenge)
	}()

	if authURLErr != nil {
		// Return structured error for Spec 020
		errType := contracts.OAuthErrorFlowFailed
		errCode := contracts.OAuthCodeFlowFailed
		suggestion := "Check server logs for details. The OAuth authorization server may not be properly configured."

		// Check for specific error patterns to provide better error classification
		errStr := authURLErr.Error()
		if strings.Contains(errStr, "metadata") || strings.Contains(errStr, "404") || strings.Contains(errStr, "not found") {
			errType = contracts.OAuthErrorMetadataMissing
			errCode = contracts.OAuthCodeNoMetadata
			suggestion = "The OAuth authorization server metadata is not available. Contact the server administrator."
		}

		return &contracts.OAuthFlowError{
			Success:    false,
			ErrorType:  errType,
			ErrorCode:  errCode,
			ServerName: c.config.Name,
			Message:    fmt.Sprintf("Failed to get authorization URL for '%s': %s", c.config.Name, authURLErr.Error()),
			Details: &contracts.OAuthErrorDetails{
				ServerURL: c.config.URL,
			},
			Suggestion: suggestion,
			DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
		}
	}

	// Append extra OAuth parameters to authorization URL (RFC 8707 resource, etc.)
	// extraParams contains both auto-detected values (from CreateOAuthConfigWithExtraParams) and manual config
	if len(extraParams) > 0 {
		parsedURL, err := url.Parse(authURL)
		if err == nil {
			query := parsedURL.Query()
			for key, value := range extraParams {
				query.Set(key, value)
				c.logger.Debug("Added extra OAuth parameter to authorization URL",
					zap.String("server", c.config.Name),
					zap.String("key", key),
					zap.String("value", value))
			}
			parsedURL.RawQuery = query.Encode()
			authURL = parsedURL.String()
			c.logger.Info("✅ Appended extra OAuth parameters to authorization URL",
				zap.String("server", c.config.Name),
				zap.Int("extra_params_count", len(extraParams)))
		} else {
			c.logger.Warn("Failed to parse authorization URL for extra params",
				zap.String("server", c.config.Name),
				zap.Error(err))
		}
	}

	// Check if we're attempting public client OAuth with empty client_id
	// Some servers (like Figma) advertise DCR but return 403, then reject empty client_id
	parsedAuthURL, parseErr := url.Parse(authURL)
	if parseErr == nil {
		clientIDParam := parsedAuthURL.Query().Get("client_id")
		if clientIDParam == "" && oauthMode == "public client (PKCE)" {
			c.logger.Error("❌ OAuth server requires client_id but DCR failed",
				zap.String("server", c.config.Name),
				zap.String("url", c.config.URL),
				zap.String("help", "Configure oauth.client_id in server config or contact the OAuth provider"))
			// Return structured error for Spec 020
			return &contracts.OAuthFlowError{
				Success:    false,
				ErrorType:  contracts.OAuthErrorClientIDRequired,
				ErrorCode:  contracts.OAuthCodeNoClientID,
				ServerName: c.config.Name,
				Message:    fmt.Sprintf("Server '%s' requires client_id but Dynamic Client Registration returned 403", c.config.Name),
				Details: &contracts.OAuthErrorDetails{
					ServerURL: c.config.URL,
					DCRStatus: &contracts.DCRStatus{
						Attempted:  true,
						Success:    false,
						StatusCode: 403,
						Error:      "Forbidden",
					},
				},
				Suggestion: "Register an OAuth app with the provider and configure oauth.client_id in server config.",
				DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
			}
		}
	}

	// Always log the computed authorization URL so users can copy/paste if auto-launch fails.
	c.logger.Info("OAuth authorization URL ready",
		zap.String("server", c.config.Name),
		zap.String("auth_url", authURL))
	fmt.Printf("OAuth login URL for %s:\n%s\n", c.config.Name, authURL)

	// Check if this is a manual OAuth flow using the proper context key
	isManualFlow := c.isManualOAuthFlow(ctx)

	// Rate limit browser opening to prevent spam (CRITICAL FIX for Phase 1)
	// Skip rate limiting for manual OAuth flows
	browserRateLimit := 5 * time.Minute
	c.oauthMu.RLock()
	timeSinceLastBrowser := time.Since(c.lastOAuthTimestamp)
	c.oauthMu.RUnlock()

	if !isManualFlow && timeSinceLastBrowser < browserRateLimit {
		c.logger.Warn("⏱️ Browser opening rate limited - OAuth attempt too soon after previous attempt",
			zap.String("server", c.config.Name),
			zap.Duration("time_since_last", timeSinceLastBrowser),
			zap.Duration("rate_limit", browserRateLimit),
			zap.String("auth_url", authURL))

		fmt.Printf("OAuth authorization required for %s, but browser opening is rate limited.\n", c.config.Name)
		fmt.Printf("Please open the following URL manually in your browser: %s\n", authURL)
	} else {
		if isManualFlow {
			c.logger.Info("🎯 Manual OAuth flow detected - bypassing rate limiting",
				zap.String("server", c.config.Name),
				zap.Duration("time_since_last", timeSinceLastBrowser))
		}

		// Open the browser to the authorization URL
		c.logger.Info("🌐 Opening browser for OAuth authorization",
			zap.String("server", c.config.Name),
			zap.String("auth_url", authURL))

		if err := c.openBrowser(authURL); err != nil {
			c.logger.Warn("Failed to open browser automatically, please open manually",
				zap.String("server", c.config.Name),
				zap.String("url", authURL),
				zap.Error(err))
			fmt.Printf("Please open the following URL in your browser: %s\n", authURL)
		}

		// Update the timestamp to track browser opening for rate limiting
		c.oauthMu.Lock()
		c.lastOAuthTimestamp = time.Now()
		c.oauthMu.Unlock()
	}

	// Wait for the callback using our callback server coordination system
	waitStartTime := time.Now()
	c.logger.Info("⏳ Waiting for OAuth authorization callback...",
		zap.String("server", c.config.Name),
		zap.Duration("timeout", 120*time.Second),
		zap.Time("wait_start", waitStartTime))

	// Get our callback server that was started in OAuth config creation
	callbackServer, exists := oauth.GetCallbackServer(c.config.Name)
	if !exists {
		return fmt.Errorf("callback server not found for %s", c.config.Name)
	}

	// Wait for the authorization code with extended timeout for remote/systemd scenarios
	select {
	case params := <-callbackServer.CallbackChan:
		waitDuration := time.Since(waitStartTime)
		c.logger.Info("🎯 OAuth callback received",
			zap.String("server", c.config.Name),
			zap.Duration("wait_duration", waitDuration),
			zap.String("note", fmt.Sprintf("User completed authorization in %.1f seconds", waitDuration.Seconds())))

		// Verify state parameter
		if params["state"] != state {
			return fmt.Errorf("state mismatch: expected %s, got %s", state, params["state"])
		}

		// Get authorization code
		code := params["code"]
		if code == "" {
			if params["error"] != "" {
				return fmt.Errorf("OAuth authorization failed: %s - %s", params["error"], params["error_description"])
			}
			return fmt.Errorf("no authorization code received")
		}

		// Exchange the authorization code for a token
		c.logger.Info("🔄 Exchanging authorization code for token",
			zap.String("server", c.config.Name),
			zap.String("code", code[:10]+"..."))

		err = oauthHandler.ProcessAuthorizationResponse(ctx, code, state, codeVerifier)
		if err != nil {
			c.logger.Error("❌ Failed to process authorization response",
				zap.String("server", c.config.Name),
				zap.Error(err))
			return fmt.Errorf("failed to process authorization response: %w", err)
		}

		c.logger.Info("✅ OAuth authorization successful - token obtained and processed",
			zap.String("server", c.config.Name))

		// Mark OAuth as complete to prevent retry loops
		c.markOAuthComplete()

		// Record OAuth completion in global token manager for other clients
		tokenManager := oauth.GetTokenStoreManager()
		tokenManager.MarkOAuthCompleted(c.config.Name)

		return nil

	case <-time.After(120 * time.Second):
		c.logger.Warn("⏱️ OAuth authorization timeout - user did not complete authorization within 120 seconds",
			zap.String("server", c.config.Name),
			zap.String("note", "Extended timeout for remote/systemd scenarios where manual browser opening may be needed"))
		return fmt.Errorf("OAuth authorization timeout - user did not complete authorization within 120 seconds (extended for remote access)")
	case <-ctx.Done():
		return ctx.Err()
	}
}

// handleOAuthAuthorizationWithResult handles the manual OAuth flow and returns the auth URL and browser status.
// This is used by Phase 3 (Spec 020) to return structured information about the OAuth flow start.
func (c *Client) handleOAuthAuthorizationWithResult(ctx context.Context, authErr error, oauthConfig *client.OAuthConfig, extraParams map[string]string) (*OAuthStartResult, error) {
	result := &OAuthStartResult{
		CorrelationID: fmt.Sprintf("oauth-%s-%d", c.config.Name, time.Now().UnixNano()),
	}

	// Check if OAuth is already in progress to prevent duplicate flows
	if c.isOAuthInProgress() {
		c.logger.Warn("⚠️ OAuth authorization already in progress, skipping duplicate attempt",
			zap.String("server", c.config.Name))
		return result, fmt.Errorf("OAuth authorization already in progress for %s", c.config.Name)
	}

	// Mark OAuth as in progress to prevent concurrent attempts
	c.markOAuthInProgress()
	defer func() {
		c.oauthMu.Lock()
		c.oauthInProgress = false
		c.oauthMu.Unlock()
	}()

	c.logger.Info("🔐 Starting manual OAuth authorization flow with result tracking",
		zap.String("server", c.config.Name),
		zap.String("correlation_id", result.CorrelationID))

	// Phase 2 (Spec 020): Pre-flight OAuth metadata validation
	if c.config.URL != "" {
		_, validationErr := oauth.ValidateOAuthMetadata(c.config.URL, c.config.Name, 5*time.Second)
		if validationErr != nil {
			if metadataErr, ok := validationErr.(*oauth.OAuthMetadataError); ok {
				c.logger.Warn("⚠️ OAuth metadata validation failed",
					zap.String("server", c.config.Name),
					zap.String("correlation_id", result.CorrelationID),
					zap.String("error_type", metadataErr.ErrorType),
					zap.String("message", metadataErr.Message))

				return result, &contracts.OAuthFlowError{
					Success:       false,
					ErrorType:     metadataErr.ErrorType,
					ErrorCode:     metadataErr.ErrorCode,
					ServerName:    c.config.Name,
					CorrelationID: result.CorrelationID,
					Message:       metadataErr.Message,
					Details: &contracts.OAuthErrorDetails{
						ServerURL: c.config.URL,
					},
					Suggestion: metadataErr.Suggestion,
					DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
				}
			}
			c.logger.Debug("OAuth metadata validation returned non-metadata error, continuing with flow",
				zap.String("server", c.config.Name),
				zap.Error(validationErr))
		}
	}

	// Get the OAuth handler from the error
	oauthHandler := client.GetOAuthHandler(authErr)
	if oauthHandler == nil {
		return result, fmt.Errorf("failed to get OAuth handler from error")
	}

	// Generate PKCE code verifier and challenge
	codeVerifier, err := client.GenerateCodeVerifier()
	if err != nil {
		return result, fmt.Errorf("failed to generate code verifier: %w", err)
	}
	codeChallenge := client.GenerateCodeChallenge(codeVerifier)

	// Generate state parameter
	state, err := client.GenerateState()
	if err != nil {
		return result, fmt.Errorf("failed to generate state: %w", err)
	}

	// Check for existing credentials or attempt DCR
	hasStaticCredentials := c.config.OAuth != nil && c.config.OAuth.ClientID != ""
	hasPersistedCredentials := oauthConfig.ClientID != ""

	if !hasStaticCredentials && !hasPersistedCredentials {
		c.logger.Info("📋 Attempting Dynamic Client Registration (optional)",
			zap.String("server", c.config.Name))

		// Note: DCR attempt is logged but we continue even if it fails
		var regErr error
		func() {
			defer func() {
				if r := recover(); r != nil {
					c.logger.Warn("OAuth RegisterClient panicked - server metadata missing or malformed",
						zap.String("server", c.config.Name),
						zap.Any("panic", r))
					regErr = fmt.Errorf("server does not support dynamic client registration: metadata missing")
				}
			}()
			regErr = oauthHandler.RegisterClient(ctx, "mcpproxy-go")
		}()

		if regErr != nil {
			c.logger.Info("ℹ️ DCR not available, continuing with public client OAuth",
				zap.String("server", c.config.Name),
				zap.Error(regErr))

			if strings.Contains(regErr.Error(), "403") {
				return result, &contracts.OAuthFlowError{
					Success:       false,
					ErrorType:     contracts.OAuthErrorClientIDRequired,
					ErrorCode:     contracts.OAuthCodeNoClientID,
					ServerName:    c.config.Name,
					CorrelationID: result.CorrelationID,
					Message:       fmt.Sprintf("Server '%s' requires client_id but Dynamic Client Registration returned 403", c.config.Name),
					Details: &contracts.OAuthErrorDetails{
						ServerURL: c.config.URL,
						DCRStatus: &contracts.DCRStatus{
							Attempted:  true,
							Success:    false,
							StatusCode: 403,
							Error:      "Forbidden",
						},
					},
					Suggestion: "Register an OAuth app with the provider and configure oauth.client_id in server config.",
					DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
				}
			}
		} else {
			clientID := oauthHandler.GetClientID()
			clientSecret := oauthHandler.GetClientSecret()
			c.logger.Info("✅ DCR successful",
				zap.String("server", c.config.Name),
				zap.String("client_id", clientID))
			// Persist DCR credentials and callback port for future use (Spec 022)
			if c.storage != nil && clientID != "" {
				serverKey := oauth.GenerateServerKey(c.config.Name, c.config.URL)
				var callbackPort int
				if callbackServer, exists := oauth.GetCallbackServer(c.config.Name); exists {
					callbackPort = callbackServer.Port
				}
				if saveErr := c.storage.UpdateOAuthClientCredentials(serverKey, clientID, clientSecret, callbackPort); saveErr != nil {
					c.logger.Warn("Failed to persist DCR credentials",
						zap.String("server", c.config.Name),
						zap.Error(saveErr))
				}
			}
		}
	}

	// Build and get the authorization URL
	var authURL string
	var authURLErr error
	func() {
		defer func() {
			if r := recover(); r != nil {
				c.logger.Error("GetAuthorizationURL panicked",
					zap.String("server", c.config.Name),
					zap.Any("panic", r))
				authURLErr = fmt.Errorf("internal error (panic recovered): %v", r)
			}
		}()
		authURL, authURLErr = oauthHandler.GetAuthorizationURL(ctx, state, codeChallenge)
	}()

	if authURLErr != nil {
		c.logger.Error("❌ Failed to get authorization URL",
			zap.String("server", c.config.Name),
			zap.Error(authURLErr))
		return result, &contracts.OAuthFlowError{
			Success:       false,
			ErrorType:     contracts.OAuthErrorFlowFailed,
			ErrorCode:     contracts.OAuthCodeFlowFailed,
			ServerName:    c.config.Name,
			CorrelationID: result.CorrelationID,
			Message:       fmt.Sprintf("Failed to get authorization URL: %v", authURLErr),
			Details: &contracts.OAuthErrorDetails{
				ServerURL: c.config.URL,
			},
			Suggestion: "Check server OAuth configuration and try again.",
			DebugHint:  fmt.Sprintf("For logs: mcpproxy upstream logs %s", c.config.Name),
		}
	}

	// Append extra OAuth parameters to authorization URL (RFC 8707 resource, etc.)
	// extraParams contains both auto-detected values (from CreateOAuthConfigWithExtraParams) and manual config
	// This is the same injection done in handleOAuthAuthorization() - fixes issue #271
	if len(extraParams) > 0 {
		parsedURL, err := url.Parse(authURL)
		if err == nil {
			query := parsedURL.Query()
			for key, value := range extraParams {
				query.Set(key, value)
				c.logger.Debug("Added extra OAuth parameter to authorization URL",
					zap.String("server", c.config.Name),
					zap.String("key", key),
					zap.String("value", value))
			}
			parsedURL.RawQuery = query.Encode()
			authURL = parsedURL.String()
			c.logger.Info("✅ Appended extra OAuth parameters to authorization URL",
				zap.String("server", c.config.Name),
				zap.Int("extra_params_count", len(extraParams)))
		} else {
			c.logger.Warn("Failed to parse authorization URL for extra params",
				zap.String("server", c.config.Name),
				zap.Error(err))
		}
	}

	// Store the auth URL in the result
	result.AuthURL = authURL
	c.logger.Info("🌐 Authorization URL obtained",
		zap.String("server", c.config.Name),
		zap.String("auth_url", authURL),
		zap.String("correlation_id", result.CorrelationID))

	// Open the browser
	if err := c.openBrowser(authURL); err != nil {
		c.logger.Warn("Failed to open browser automatically, please open manually",
			zap.String("server", c.config.Name),
			zap.String("url", authURL),
			zap.Error(err))
		result.BrowserOpened = false
		result.BrowserError = err.Error()
		fmt.Printf("Please open the following URL in your browser: %s\n", authURL)
	} else {
		result.BrowserOpened = true
	}

	// Update the timestamp
	c.oauthMu.Lock()
	c.lastOAuthTimestamp = time.Now()
	c.oauthMu.Unlock()

	// Wait for the callback
	callbackServer, exists := oauth.GetCallbackServer(c.config.Name)
	if !exists {
		return result, fmt.Errorf("callback server not found for %s", c.config.Name)
	}

	select {
	case params := <-callbackServer.CallbackChan:
		c.logger.Info("🎯 OAuth callback received",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))

		// Verify state parameter
		if params["state"] != state {
			return result, fmt.Errorf("state mismatch: expected %s, got %s", state, params["state"])
		}

		// Get authorization code
		code := params["code"]
		if code == "" {
			if params["error"] != "" {
				return result, fmt.Errorf("OAuth authorization failed: %s - %s", params["error"], params["error_description"])
			}
			return result, fmt.Errorf("no authorization code received")
		}

		// Exchange the authorization code for a token
		err = oauthHandler.ProcessAuthorizationResponse(ctx, code, state, codeVerifier)
		if err != nil {
			return result, fmt.Errorf("failed to process authorization response: %w", err)
		}

		c.logger.Info("✅ OAuth authorization successful",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))

		// Mark OAuth as complete
		c.markOAuthComplete()
		tokenManager := oauth.GetTokenStoreManager()
		tokenManager.MarkOAuthCompleted(c.config.Name)

		return result, nil

	case <-time.After(120 * time.Second):
		c.logger.Warn("⏱️ OAuth authorization timeout",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))
		return result, fmt.Errorf("OAuth authorization timeout - user did not complete authorization within 120 seconds")
	case <-ctx.Done():
		return result, ctx.Err()
	}
}

// isOAuthInProgress checks if OAuth is in progress
func (c *Client) isOAuthInProgress() bool {
	c.oauthMu.RLock()
	defer c.oauthMu.RUnlock()
	return c.oauthInProgress
}

// markOAuthInProgress marks OAuth as in progress
func (c *Client) markOAuthInProgress() {
	c.oauthMu.Lock()
	defer c.oauthMu.Unlock()
	c.oauthInProgress = true
	c.lastOAuthTimestamp = time.Now()
}

// markOAuthComplete marks OAuth as complete and cleans up callback server
func (c *Client) markOAuthComplete() {
	c.oauthMu.Lock()
	defer c.oauthMu.Unlock()

	c.oauthInProgress = false
	c.oauthCompleted = true
	c.lastOAuthTimestamp = time.Now()

	c.logger.Info("✅ OAuth marked as complete",
		zap.String("server", c.config.Name),
		zap.Time("completion_time", c.lastOAuthTimestamp))

	// Persist DCR credentials from handler to storage for proactive token refresh
	// This is necessary because mcp-go stores ClientID in-memory during DCR,
	// but we need it persisted to refresh tokens without re-authenticating
	c.persistDCRCredentials()

	// Notify global token manager so the running process (daemon) can trigger
	// an immediate reconnect. Also persist a DB event when possible so other
	// processes can detect completion without polling.
	tm := oauth.GetTokenStoreManager()
	if c.storage != nil {
		if err := tm.MarkOAuthCompletedWithDB(c.config.Name, c.storage); err != nil {
			c.logger.Warn("Failed to persist OAuth completion event to DB; using in-memory notification",
				zap.String("server", c.config.Name),
				zap.Error(err))
			tm.MarkOAuthCompleted(c.config.Name)
		} else {
			c.logger.Info("📢 OAuth completion recorded to DB for cross-process notification",
				zap.String("server", c.config.Name))
		}
	} else {
		tm.MarkOAuthCompleted(c.config.Name)
		c.logger.Info("📢 OAuth completion recorded in-memory (no DB available)",
			zap.String("server", c.config.Name))
	}

	// Clean up the callback server to free the port
	if manager := oauth.GetGlobalCallbackManager(); manager != nil {
		if err := manager.StopCallbackServer(c.config.Name); err != nil {
			c.logger.Warn("Failed to stop OAuth callback server",
				zap.String("server", c.config.Name),
				zap.Error(err))
		}
	}
}

// persistDCRCredentials saves the ClientID and ClientSecret from the OAuth handler
// to persistent storage. This enables proactive token refresh to use stored credentials
// when the handler's config is not populated (common with DCR flows).
// Uses UpdateOAuthClientCredentials (the canonical DCR persistence path) rather than
// SaveOAuthToken to keep a single code path for credential updates.
//
// IMPORTANT: This is called from markOAuthComplete() which runs inside Connect() while
// c.mu is held. We must NOT call GetOAuthHandler() which acquires c.mu.RLock() — that
// would deadlock (Go's RWMutex is not reentrant). Instead, access c.client directly.
func (c *Client) persistDCRCredentials() {
	if c.storage == nil {
		return
	}

	handler := c.getOAuthHandlerLocked()
	if handler == nil {
		return
	}

	clientID := handler.GetClientID()
	clientSecret := handler.GetClientSecret()

	if clientID == "" {
		c.logger.Debug("No ClientID in OAuth handler to persist",
			zap.String("server", c.config.Name))
		return
	}

	serverKey := oauth.GenerateServerKey(c.config.Name, c.config.URL)

	// Preserve existing callbackPort from the record (set during the DCR flow)
	callbackPort := 0
	if record, err := c.storage.GetOAuthToken(serverKey); err == nil {
		callbackPort = record.CallbackPort
	}

	if err := c.storage.UpdateOAuthClientCredentials(serverKey, clientID, clientSecret, callbackPort); err != nil {
		c.logger.Error("Failed to persist DCR credentials",
			zap.String("server", c.config.Name),
			zap.Error(err))
		return
	}

	c.logger.Info("DCR credentials persisted for proactive token refresh",
		zap.String("server", c.config.Name),
		zap.String("client_id_prefix", clientID[:min(8, len(clientID))]+"..."),
		zap.Bool("has_client_secret", clientSecret != ""))
}

// wasOAuthRecentlyCompleted checks if OAuth was completed recently to prevent retry loops
func (c *Client) wasOAuthRecentlyCompleted() bool {
	c.oauthMu.RLock()
	defer c.oauthMu.RUnlock()

	// Consider OAuth "recently completed" if it finished within the last 10 seconds
	return c.oauthCompleted && time.Since(c.lastOAuthTimestamp) < 10*time.Second
}

// ClearOAuthState clears OAuth state (public API for manual OAuth flows)
func (c *Client) ClearOAuthState() {
	c.clearOAuthState()
}

// ForceOAuthFlow forces an OAuth authentication flow, bypassing rate limiting (for manual auth)
func (c *Client) ForceOAuthFlow(ctx context.Context) error {
	_, err := c.ForceOAuthFlowWithResult(ctx)
	return err
}

// StartOAuthFlowQuick starts the OAuth flow and returns browser status immediately.
// Unlike ForceOAuthFlowWithResult which blocks until OAuth completes, this function:
// 1. Gets authorization URL synchronously (quick operation)
// 2. Checks HEADLESS environment variable
// 3. Attempts browser open and captures result
// 4. Returns OAuthStartResult immediately
// 5. Continues OAuth callback handling in a goroutine
//
// This is used by the login API endpoint to return accurate browser_opened status
// without blocking the HTTP response for the full OAuth flow.
func (c *Client) StartOAuthFlowQuick(ctx context.Context) (*OAuthStartResult, error) {
	// Generate correlation ID first so all logs can use it
	result := &OAuthStartResult{
		CorrelationID: fmt.Sprintf("oauth-%s-%d", c.config.Name, time.Now().UnixNano()),
	}

	c.logger.Info("🔐 Starting quick OAuth flow",
		zap.String("server", c.config.Name),
		zap.String("correlation_id", result.CorrelationID))

	// Fast-fail if OAuth is clearly not applicable for this server
	if !oauth.ShouldUseOAuth(c.config) {
		c.logger.Warn("⚠️ OAuth not applicable for server",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))
		return result, fmt.Errorf("OAuth is not supported or not applicable for server '%s'", c.config.Name)
	}

	// Check if OAuth is already in progress
	if c.isOAuthInProgress() {
		c.logger.Warn("⚠️ OAuth authorization already in progress",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))
		return result, fmt.Errorf("OAuth authorization already in progress for %s", c.config.Name)
	}

	// Clear any existing OAuth state
	c.clearOAuthState()

	// Ensure transport type is determined
	if c.transportType == "" {
		c.transportType = transport.DetermineTransportType(c.config)
	}

	// Create OAuth config
	oauthConfig, extraParams := oauth.CreateOAuthConfigWithExtraParams(ctx, c.config, c.storage)
	if oauthConfig == nil {
		c.logger.Error("❌ Failed to create OAuth config",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID))
		return result, fmt.Errorf("failed to create OAuth config - server may not support OAuth")
	}

	// Phase 2 (Spec 020): Pre-flight OAuth metadata validation
	if c.config.URL != "" {
		_, validationErr := oauth.ValidateOAuthMetadata(c.config.URL, c.config.Name, 5*time.Second)
		if validationErr != nil {
			if metadataErr, ok := validationErr.(*oauth.OAuthMetadataError); ok {
				c.logger.Warn("⚠️ OAuth metadata validation failed",
					zap.String("server", c.config.Name),
					zap.String("correlation_id", result.CorrelationID),
					zap.String("error_type", metadataErr.ErrorType))
				return result, &contracts.OAuthFlowError{
					Success:       false,
					ErrorType:     metadataErr.ErrorType,
					ErrorCode:     metadataErr.ErrorCode,
					ServerName:    c.config.Name,
					CorrelationID: result.CorrelationID,
					Message:       metadataErr.Message,
					Suggestion:    metadataErr.Suggestion,
				}
			}
		}
	}

	// Get authorization URL - this is the key synchronous operation
	authURL, oauthHandler, codeVerifier, state, err := c.getAuthorizationURLQuick(ctx, oauthConfig, extraParams, result.CorrelationID)
	if err != nil {
		c.logger.Error("❌ Failed to get authorization URL",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", result.CorrelationID),
			zap.Error(err))

		// Add correlation_id to structured errors for tracing
		var flowErr *contracts.OAuthFlowError
		if errors.As(err, &flowErr) {
			flowErr.CorrelationID = result.CorrelationID
		}
		return result, err
	}

	result.AuthURL = authURL
	c.logger.Info("🌐 Authorization URL obtained",
		zap.String("server", c.config.Name),
		zap.String("correlation_id", result.CorrelationID))

	// Check HEADLESS mode - skip browser if set
	if os.Getenv("HEADLESS") != "" {
		c.logger.Info("📵 HEADLESS mode detected - skipping browser open",
			zap.String("server", c.config.Name),
			zap.String("auth_url", authURL))
		result.BrowserOpened = false
		result.BrowserError = "HEADLESS mode - browser not opened. Please open the auth_url manually."

		// Start OAuth callback handling in background
		go c.waitForOAuthCallbackAsync(ctx, oauthHandler, codeVerifier, state, result.CorrelationID)

		return result, nil
	}

	// Attempt to open browser
	if err := c.openBrowser(authURL); err != nil {
		c.logger.Warn("Failed to open browser automatically",
			zap.String("server", c.config.Name),
			zap.String("url", authURL),
			zap.Error(err))
		result.BrowserOpened = false
		result.BrowserError = err.Error()
	} else {
		result.BrowserOpened = true
		c.logger.Info("✅ Browser opened successfully",
			zap.String("server", c.config.Name))
	}

	// Start OAuth callback handling in background
	go c.waitForOAuthCallbackAsync(ctx, oauthHandler, codeVerifier, state, result.CorrelationID)

	return result, nil
}

// getAuthorizationURLQuick gets the authorization URL without starting the full OAuth flow.
// Returns the URL, OAuth handler, code verifier, and state for later use.
func (c *Client) getAuthorizationURLQuick(ctx context.Context, oauthConfig *client.OAuthConfig, extraParams map[string]string, correlationID string) (string, *uptransport.OAuthHandler, string, string, error) {
	// Create transport config with OAuth
	httpConfig := transport.CreateHTTPTransportConfig(c.config, oauthConfig)

	// Create OAuth-enabled HTTP client
	httpClient, err := transport.CreateHTTPClient(httpConfig)
	if err != nil {
		return "", nil, "", "", fmt.Errorf("failed to create OAuth HTTP client: %w", err)
	}

	// Store the client
	c.client = httpClient

	// Start the client
	if err := c.client.Start(ctx); err != nil {
		return "", nil, "", "", fmt.Errorf("failed to start OAuth client: %w", err)
	}

	// Try to initialize - this will trigger OAuth authorization requirement
	err = c.initialize(ctx)
	if err == nil {
		// No OAuth needed - server connected without auth
		return "", nil, "", "", fmt.Errorf("server connected without OAuth - no authentication required")
	}

	// Check if this is an OAuth authorization error
	if !client.IsOAuthAuthorizationRequiredError(err) && !c.isOAuthError(err) {
		return "", nil, "", "", fmt.Errorf("initialization failed with non-OAuth error: %w", err)
	}

	// Get the OAuth handler from the error
	oauthHandler := client.GetOAuthHandler(err)
	if oauthHandler == nil {
		return "", nil, "", "", fmt.Errorf("failed to get OAuth handler from error")
	}

	// Generate PKCE code verifier and challenge
	codeVerifier, err := client.GenerateCodeVerifier()
	if err != nil {
		return "", nil, "", "", fmt.Errorf("failed to generate code verifier: %w", err)
	}
	codeChallenge := client.GenerateCodeChallenge(codeVerifier)

	// Generate state parameter
	state, err := client.GenerateState()
	if err != nil {
		return "", nil, "", "", fmt.Errorf("failed to generate state: %w", err)
	}

	// Check for existing credentials or attempt DCR
	hasStaticCredentials := c.config.OAuth != nil && c.config.OAuth.ClientID != ""
	hasPersistedCredentials := oauthConfig.ClientID != ""

	if !hasStaticCredentials && !hasPersistedCredentials {
		c.logger.Info("📋 Attempting Dynamic Client Registration (DCR)",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", correlationID))

		var regErr error
		func() {
			defer func() {
				if r := recover(); r != nil {
					regErr = fmt.Errorf("DCR panicked: %v", r)
				}
			}()
			regErr = oauthHandler.RegisterClient(ctx, "mcpproxy-go")
		}()

		if regErr != nil {
			c.logger.Warn("⚠️ DCR failed",
				zap.String("server", c.config.Name),
				zap.String("correlation_id", correlationID),
				zap.Error(regErr))

			if strings.Contains(regErr.Error(), "403") {
				c.logger.Error("❌ DCR returned 403 - client_id required",
					zap.String("server", c.config.Name),
					zap.String("correlation_id", correlationID),
					zap.String("suggestion", "Register an OAuth app with the provider"))
				return "", nil, "", "", &contracts.OAuthFlowError{
					Success:       false,
					ErrorType:     contracts.OAuthErrorClientIDRequired,
					ErrorCode:     contracts.OAuthCodeNoClientID,
					ServerName:    c.config.Name,
					CorrelationID: correlationID,
					Message:       fmt.Sprintf("Server '%s' requires client_id but DCR returned 403", c.config.Name),
					Suggestion:    "Register an OAuth app with the provider and configure oauth.client_id in server config.",
				}
			}
		} else {
			c.logger.Info("✅ DCR succeeded",
				zap.String("server", c.config.Name),
				zap.String("correlation_id", correlationID))
			// Persist DCR credentials and callback port (Spec 022)
			clientID := oauthHandler.GetClientID()
			clientSecret := oauthHandler.GetClientSecret()
			if c.storage != nil && clientID != "" {
				serverKey := oauth.GenerateServerKey(c.config.Name, c.config.URL)
				var callbackPort int
				if callbackServer, exists := oauth.GetCallbackServer(c.config.Name); exists {
					callbackPort = callbackServer.Port
				}
				_ = c.storage.UpdateOAuthClientCredentials(serverKey, clientID, clientSecret, callbackPort)
			}
		}
	}

	// Build and get the authorization URL
	var authURL string
	var authURLErr error
	func() {
		defer func() {
			if r := recover(); r != nil {
				authURLErr = fmt.Errorf("GetAuthorizationURL panicked: %v", r)
			}
		}()
		authURL, authURLErr = oauthHandler.GetAuthorizationURL(ctx, state, codeChallenge)
	}()

	if authURLErr != nil {
		return "", nil, "", "", &contracts.OAuthFlowError{
			Success:    false,
			ErrorType:  contracts.OAuthErrorFlowFailed,
			ErrorCode:  contracts.OAuthCodeFlowFailed,
			ServerName: c.config.Name,
			Message:    fmt.Sprintf("Failed to get authorization URL: %v", authURLErr),
			Suggestion: "Check server OAuth configuration and try again.",
		}
	}

	// Append extra OAuth parameters to authorization URL (RFC 8707 resource, etc.)
	// extraParams contains both auto-detected values (from CreateOAuthConfigWithExtraParams) and manual config
	// This is the same injection done in handleOAuthAuthorization() - fixes issue #271
	if len(extraParams) > 0 {
		parsedURL, err := url.Parse(authURL)
		if err == nil {
			query := parsedURL.Query()
			for key, value := range extraParams {
				query.Set(key, value)
				c.logger.Debug("Added extra OAuth parameter to authorization URL",
					zap.String("server", c.config.Name),
					zap.String("key", key),
					zap.String("value", value))
			}
			parsedURL.RawQuery = query.Encode()
			authURL = parsedURL.String()
			c.logger.Info("✅ Appended extra OAuth parameters to authorization URL",
				zap.String("server", c.config.Name),
				zap.Int("extra_params_count", len(extraParams)))
		} else {
			c.logger.Warn("Failed to parse authorization URL for extra params",
				zap.String("server", c.config.Name),
				zap.Error(err))
		}
	}

	return authURL, oauthHandler, codeVerifier, state, nil
}

// waitForOAuthCallbackAsync waits for OAuth callback and handles token exchange in background.
func (c *Client) waitForOAuthCallbackAsync(ctx context.Context, oauthHandler *uptransport.OAuthHandler, codeVerifier, state, correlationID string) {
	c.markOAuthInProgress()
	defer func() {
		c.oauthMu.Lock()
		c.oauthInProgress = false
		c.oauthMu.Unlock()
	}()

	c.logger.Info("⏳ Waiting for OAuth callback in background",
		zap.String("server", c.config.Name),
		zap.String("correlation_id", correlationID))

	// Get or create callback server
	callbackServer, exists := oauth.GetCallbackServer(c.config.Name)
	if !exists {
		c.logger.Error("❌ Callback server not found",
			zap.String("server", c.config.Name))
		return
	}

	select {
	case params := <-callbackServer.CallbackChan:
		c.logger.Info("🎯 OAuth callback received",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", correlationID))

		// Verify state parameter
		if params["state"] != state {
			c.logger.Error("❌ State mismatch in OAuth callback",
				zap.String("server", c.config.Name),
				zap.String("expected", state),
				zap.String("got", params["state"]))
			return
		}

		// Get authorization code
		code := params["code"]
		if code == "" {
			if params["error"] != "" {
				c.logger.Error("❌ OAuth authorization failed",
					zap.String("server", c.config.Name),
					zap.String("error", params["error"]),
					zap.String("description", params["error_description"]))
			}
			return
		}

		// Exchange the authorization code for a token
		if err := oauthHandler.ProcessAuthorizationResponse(ctx, code, state, codeVerifier); err != nil {
			c.logger.Error("❌ Failed to exchange authorization code",
				zap.String("server", c.config.Name),
				zap.Error(err))
			return
		}

		c.logger.Info("✅ OAuth authorization successful",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", correlationID))

		// Mark OAuth as complete
		c.markOAuthComplete()
		tokenManager := oauth.GetTokenStoreManager()
		tokenManager.MarkOAuthCompleted(c.config.Name)

	case <-time.After(120 * time.Second):
		c.logger.Warn("⏱️ OAuth authorization timeout",
			zap.String("server", c.config.Name),
			zap.String("correlation_id", correlationID))

	case <-ctx.Done():
		c.logger.Info("OAuth flow cancelled",
			zap.String("server", c.config.Name))
	}
}

// ForceOAuthFlowWithResult forces an OAuth authentication flow and returns the auth URL and browser status.
// This is used by Phase 3 (Spec 020) to provide the auth URL to clients even when browser opens successfully.
func (c *Client) ForceOAuthFlowWithResult(ctx context.Context) (*OAuthStartResult, error) {
	c.logger.Info("🔐 Starting forced OAuth authentication flow",
		zap.String("server", c.config.Name))

	// Fast‑fail if OAuth is clearly not applicable for this server
	if !oauth.ShouldUseOAuth(c.config) {
		return nil, fmt.Errorf("OAuth is not supported or not applicable for server '%s'", c.config.Name)
	}

	// Clear any existing OAuth state
	c.clearOAuthState()

	// Ensure transport type is determined if not already set
	if c.transportType == "" {
		c.transportType = transport.DetermineTransportType(c.config)
		c.logger.Info("Transport type determined for OAuth flow",
			zap.String("server", c.config.Name),
			zap.String("transport_type", c.transportType))
	}

	// Mark context as manual OAuth flow to bypass rate limiting
	manualCtx := context.WithValue(ctx, manualOAuthKey, true)

	// Try to create an OAuth-enabled client that will trigger the OAuth flow
	switch c.transportType {
	case transportHTTP, transportHTTPStreamable:
		return c.forceHTTPOAuthFlowWithResult(manualCtx)
	case transportSSE:
		return c.forceSSEOAuthFlowWithResult(manualCtx)
	default:
		return nil, fmt.Errorf("OAuth not supported for transport type: %s", c.transportType)
	}
}

// forceHTTPOAuthFlowWithResult forces OAuth flow for HTTP transport and returns auth URL/browser status.
func (c *Client) forceHTTPOAuthFlowWithResult(ctx context.Context) (*OAuthStartResult, error) {
	// Create OAuth config with auto-detected extra params (RFC 8707 resource)
	oauthConfig, extraParams := oauth.CreateOAuthConfigWithExtraParams(ctx, c.config, c.storage)
	if oauthConfig == nil {
		return nil, fmt.Errorf("failed to create OAuth config - server may not support OAuth")
	}

	c.logger.Info("🌐 Starting manual HTTP OAuth flow with result tracking...",
		zap.String("server", c.config.Name),
		zap.Int("extra_params_count", len(extraParams)))

	// Create HTTP transport config with OAuth
	httpConfig := transport.CreateHTTPTransportConfig(c.config, oauthConfig)

	// Create OAuth-enabled HTTP client using transport layer
	httpClient, err := transport.CreateHTTPClient(httpConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to create OAuth HTTP client: %w", err)
	}

	// Store the client temporarily
	c.client = httpClient

	c.logger.Info("🚀 Starting OAuth HTTP client and triggering initialization to force authorization...")

	// Start the client first
	err = c.client.Start(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to start OAuth client: %w", err)
	}

	// Now try to initialize - this will trigger OAuth authorization requirement
	c.logger.Info("🎯 Attempting initialize to trigger OAuth authorization requirement...")
	err = c.initialize(ctx)
	if err != nil {
		// Check if this is an OAuth authorization error that we need to handle manually
		if client.IsOAuthAuthorizationRequiredError(err) || c.isOAuthError(err) {
			c.logger.Info("✅ OAuth authorization requirement triggered - starting manual OAuth flow",
				zap.String("error_type", fmt.Sprintf("%T", err)),
				zap.String("error", err.Error()))

			// Handle OAuth authorization manually and get result
			result, oauthErr := c.handleOAuthAuthorizationWithResult(ctx, err, oauthConfig, extraParams)
			if oauthErr != nil {
				return result, fmt.Errorf("OAuth authorization failed: %w", oauthErr)
			}

			// Retry initialization after OAuth is complete
			c.logger.Info("🔄 Retrying initialization after OAuth authorization")
			err = c.initialize(ctx)
			if err != nil {
				return result, fmt.Errorf("initialization failed after OAuth authorization: %w", err)
			}

			c.logger.Info("✅ Manual HTTP OAuth authentication completed successfully")
			return result, nil
		}
		return nil, fmt.Errorf("initialization failed with non-OAuth error: %w", err)
	}

	c.logger.Info("✅ Manual HTTP OAuth authentication completed successfully (no OAuth needed)")
	return &OAuthStartResult{BrowserOpened: false}, nil
}

// forceSSEOAuthFlowWithResult forces OAuth flow for SSE transport and returns auth URL/browser status.
func (c *Client) forceSSEOAuthFlowWithResult(ctx context.Context) (*OAuthStartResult, error) {
	// Create OAuth config with auto-detected extra params (RFC 8707 resource)
	oauthConfig, extraParams := oauth.CreateOAuthConfigWithExtraParams(ctx, c.config, c.storage)
	if oauthConfig == nil {
		return nil, fmt.Errorf("failed to create OAuth config - server may not support OAuth")
	}

	c.logger.Info("🌐 Starting manual SSE OAuth flow with result tracking...",
		zap.String("server", c.config.Name),
		zap.Int("extra_params_count", len(extraParams)))

	// Create SSE transport config with OAuth
	httpConfig := transport.CreateHTTPTransportConfig(c.config, oauthConfig)

	// Create OAuth-enabled SSE client using transport layer
	sseClient, err := transport.CreateSSEClient(httpConfig)
	if err != nil {
		return nil, fmt.Errorf("failed to create OAuth SSE client: %w", err)
	}

	// Store the client temporarily
	c.client = sseClient

	c.logger.Info("🚀 Starting OAuth SSE client and triggering authorization...")

	// Start the client first - this may fail with authorization required for SSE
	var result *OAuthStartResult
	err = c.client.Start(ctx)
	if err != nil {
		// Check if this is an OAuth authorization error from Start()
		if c.isOAuthError(err) || strings.Contains(err.Error(), "authorization required") || strings.Contains(err.Error(), "no valid token") {
			c.logger.Info("✅ OAuth authorization required from SSE Start() - triggering manual OAuth flow")

			// Handle OAuth authorization manually and get result
			result, oauthErr := c.handleOAuthAuthorizationWithResult(ctx, err, oauthConfig, extraParams)
			if oauthErr != nil {
				return result, fmt.Errorf("OAuth authorization failed: %w", oauthErr)
			}

			// Retry starting the client after OAuth is complete
			c.logger.Info("🔄 Retrying SSE client start after OAuth authorization")
			err = c.client.Start(ctx)
			if err != nil {
				return result, fmt.Errorf("SSE client start failed after OAuth authorization: %w", err)
			}
		} else {
			return nil, fmt.Errorf("failed to start OAuth client: %w", err)
		}
	}

	// Now try to initialize to ensure connection is working
	c.logger.Info("🎯 Attempting initialize to verify connection...")
	err = c.initialize(ctx)
	if err != nil {
		// Check if this is an OAuth authorization error that we need to handle manually
		if client.IsOAuthAuthorizationRequiredError(err) || c.isOAuthError(err) {
			c.logger.Info("✅ OAuth authorization requirement from initialize - starting manual OAuth flow")

			// Handle OAuth authorization manually and get result
			result, oauthErr := c.handleOAuthAuthorizationWithResult(ctx, err, oauthConfig, extraParams)
			if oauthErr != nil {
				return result, fmt.Errorf("OAuth authorization failed: %w", oauthErr)
			}

			// Retry initialization after OAuth is complete
			c.logger.Info("🔄 Retrying initialization after OAuth authorization")
			err = c.initialize(ctx)
			if err != nil {
				return result, fmt.Errorf("initialization failed after OAuth authorization: %w", err)
			}
		} else {
			return nil, fmt.Errorf("initialization failed with non-OAuth error: %w", err)
		}
	}

	c.logger.Info("✅ Manual SSE OAuth authentication completed successfully")
	if result == nil {
		result = &OAuthStartResult{BrowserOpened: false}
	}
	return result, nil
}

// isManualOAuthFlow checks if this is a manual OAuth flow
func (c *Client) isManualOAuthFlow(ctx context.Context) bool {
	// Check if context has manual OAuth marker
	if ctx != nil {
		if value := ctx.Value(manualOAuthKey); value != nil {
			if manual, ok := value.(bool); ok && manual {
				return true
			}
		}
	}
	return false
}

// clearOAuthState clears OAuth state (for cleaning up stale state)
func (c *Client) clearOAuthState() {
	c.oauthMu.Lock()
	defer c.oauthMu.Unlock()

	c.logger.Info("🧹 Clearing OAuth state",
		zap.String("server", c.config.Name),
		zap.Bool("was_in_progress", c.oauthInProgress),
		zap.Bool("was_completed", c.oauthCompleted))

	c.oauthInProgress = false
	c.oauthCompleted = false
	c.lastOAuthTimestamp = time.Time{}
}
