package oauth

import (
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/sirupsen/logrus"

	"github.com/tingly-dev/tingly-box/ai"
)

// SessionStatus represents the status of an OAuth session
type SessionStatus string

const (
	SessionStatusPending SessionStatus = "pending" // Authorization initiated
	SessionStatusSuccess SessionStatus = "success" // Provider created successfully
	SessionStatusFailed  SessionStatus = "failed"  // Authorization failed
)

// SessionState holds information about an OAuth session
type SessionState struct {
	SessionID    string        `json:"session_id"`
	Status       SessionStatus `json:"status"`
	Provider     ai.Issuer     `json:"provider"`
	UserID       string        `json:"user_id"`
	CreatedAt    time.Time     `json:"created_at"`
	ExpiresAt    time.Time     `json:"expires_at"`
	ProviderUUID string        `json:"provider_uuid,omitempty"` // Set when success
	Error        string        `json:"error,omitempty"`         // Set when failed
	ProxyURL     string        `json:"proxy_url,omitempty"`     // Proxy URL used for this session
}

// Manager handles OAuth flows
type Manager struct {
	config         *Config
	registry       *Registry
	tokenStorage   TokenStorage
	stateStorage   StateStorage
	sessionStorage SessionStorage
	Debug          bool
}

// StateData holds information about an OAuth state
type StateData struct {
	State         string
	UserID        string
	Provider      ai.Issuer
	ExpiresAt     time.Time
	Timestamp     int64  // Unix timestamp when state was created
	ExpiresAtUnix int64  // Unix timestamp when state expires
	RedirectTo    string // Optional redirect URL after successful auth
	Name          string // Optional custom provider name
	CodeVerifier  string // PKCE code verifier (for PKCE flow)
	RedirectURI   string // Actual redirect_uri used in auth request (must match in token request)
	SessionID     string // Session ID for status tracking
}

// NewManager creates a new OAuth manager
func NewManager(config *Config, registry *Registry) *Manager {
	if config == nil {
		config = DefaultConfig()
	}
	if registry == nil {
		registry = DefaultRegistry()
	}

	// Use storage from config, or create default memory storage
	tokenStorage := config.TokenStorage
	if tokenStorage == nil {
		tokenStorage = NewMemoryTokenStorage()
	}

	stateStorage := config.StateStorage
	if stateStorage == nil {
		stateStorage = NewMemoryStateStorage()
	}

	sessionStorage := config.SessionStorage
	if sessionStorage == nil {
		sessionStorage = NewMemorySessionStorage()
	}

	m := &Manager{
		config:         config,
		registry:       registry,
		tokenStorage:   tokenStorage,
		stateStorage:   stateStorage,
		sessionStorage: sessionStorage,
	}

	// Start cleanup goroutine
	go m.cleanupPeriodically()

	return m
}

// generateState generates a secure random state parameter
func (m *Manager) generateState(encoding StateEncoding) (string, error) {
	var size int
	switch encoding {
	case StateEncodingBase64URL32:
		size = 32 // 32 bytes -> 43 chars in base64url (matches OpenAI Codex)
	case StateEncodingBase64URL:
		size = 16 // 16 bytes -> 22 chars in base64url
	default:
		size = 16 // 16 bytes -> 32 chars in hex
	}
	b := make([]byte, size)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("failed to generate random bytes: %w", err)
	}
	switch encoding {
	case StateEncodingBase64URL, StateEncodingBase64URL32:
		return base64.RawURLEncoding.EncodeToString(b), nil
	default:
		return hex.EncodeToString(b), nil
	}
}

// generateCodeVerifier generates a PKCE code verifier
// 96 random bytes → 128 base64url chars
func (m *Manager) generateCodeVerifier() (string, error) {
	// Generate 96 random bytes (matches OpenAI Codex implementation)
	bytes := make([]byte, 96)
	if _, err := rand.Read(bytes); err != nil {
		return "", fmt.Errorf("failed to generate code verifier: %w", err)
	}
	verifier := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(bytes)
	return verifier, nil
}

// generateCodeChallenge creates a PKCE code challenge from the verifier
// Uses SHA256 hash + base64url encoding
func (m *Manager) generateCodeChallenge(verifier string) string {
	hash := sha256.Sum256([]byte(verifier))
	challenge := base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(hash[:])
	return challenge
}

// generateStateKey generates a key for storing state data
func (m *Manager) stateKey(state string) string {
	return state
}

// saveState saves state data with expiration
func (m *Manager) saveState(data *StateData) error {
	// Set expiration time based on config
	now := time.Now()
	data.Timestamp = now.Unix()
	data.ExpiresAt = now.Add(m.config.StateExpiry)
	data.ExpiresAtUnix = data.ExpiresAt.Unix()

	return m.stateStorage.SaveState(data.State, data)
}

// getState retrieves and validates state data
func (m *Manager) getState(state string) (*StateData, error) {
	return m.stateStorage.GetState(state)
}

// GetStateData retrieves state data by state parameter (public method for external access)
func (m *Manager) GetStateData(state string) (*StateData, error) {
	return m.stateStorage.GetState(state)
}

// deleteState removes state data
func (m *Manager) deleteState(state string) {
	_ = m.stateStorage.DeleteState(state)
}

// cleanupExpiredStates is removed - now handled by cleanupPeriodically

// GetAuthURL generates the OAuth authorization URL for a provider
func (m *Manager) GetAuthURL(userID string, providerType ai.Issuer, redirectTo string, name string, sessionID string, opts ...Option) (string, string, error) {
	options := applyOptions(opts...)
	config, ok := m.registry.Get(providerType)
	if !ok {
		return "", "", fmt.Errorf("%w: %s", ErrInvalidProvider, providerType)
	}

	if config.ClientID == "" {
		return "", "", fmt.Errorf("%w: %s", ErrProviderNotConfigured, providerType)
	}

	// Generate state
	state, err := m.generateState(config.StateEncoding)
	if err != nil {
		return "", "", err
	}

	// Generate PKCE code verifier if provider uses PKCE
	var codeVerifier string
	if config.OAuthMethod == OAuthMethodPKCE {
		codeVerifier, err = m.generateCodeVerifier()
		if err != nil {
			return "", "", fmt.Errorf("failed to generate code verifier: %w", err)
		}
	}

	// Build authorization URL
	authURL, redirectURI, err := m.buildAuthURL(config, state, codeVerifier, options)
	if err != nil {
		m.deleteState(state)
		return "", "", err
	}

	// Update state with actual redirect_uri used
	if err := m.saveState(&StateData{
		State:        state,
		UserID:       userID,
		Provider:     providerType,
		RedirectTo:   redirectTo,
		Name:         name,
		CodeVerifier: codeVerifier,
		RedirectURI:  redirectURI,
		SessionID:    sessionID,
	}); err != nil {
		return "", "", err
	}

	return authURL, state, nil
}

// buildAuthURL builds the authorization URL with all required parameters
// Returns the auth URL and the actual redirect_uri used
func (m *Manager) buildAuthURL(config *ProviderConfig, state string, codeVerifier string, opts *Options) (string, string, error) {
	u, err := url.Parse(config.AuthURL)
	if err != nil {
		return "", "", err
	}

	// Validate port constraint if specified
	if len(config.CallbackPorts) > 0 {
		baseURL, err := url.Parse(m.callbackBaseURL(opts))
		if err == nil {
			port := baseURL.Port()
			if port == "" {
				// Default to port 80 for http, 443 for https
				if baseURL.Scheme == "https" {
					port = "443"
				} else {
					port = "80"
				}
			}
			portInt := 0
			if port != "" {
				_, err := fmt.Sscanf(port, "%d", &portInt)
				if err != nil {
					return "", "", fmt.Errorf("invalid port in BaseURL: %w", err)
				}
			}
			allowed := false
			for _, allowedPort := range config.CallbackPorts {
				if portInt == allowedPort {
					allowed = true
					break
				}
			}
			if !allowed {
				return "", "", fmt.Errorf("port %d is not allowed for provider %s (allowed ports: %v)", portInt, config.Type, config.CallbackPorts)
			}
		}
	}

	// Use hardcoded RedirectURL if provided (for providers requiring specific redirect URIs)
	callbackPath := config.Callback
	if callbackPath == "" {
		callbackPath = "/callback"
	}
	redirectURL := fmt.Sprintf("%s%s", m.callbackBaseURL(opts), callbackPath)

	query := u.Query()
	query.Set("client_id", config.ClientID)
	query.Set("redirect_uri", redirectURL)
	query.Set("response_type", "code")
	query.Set("state", state)
	if len(config.Scopes) > 0 {
		query.Set("scope", strings.Join(config.Scopes, " "))
	}

	// Add PKCE parameters if provider uses PKCE
	if config.OAuthMethod == OAuthMethodPKCE && codeVerifier != "" {
		challenge := m.generateCodeChallenge(codeVerifier)
		query.Set("code_challenge", challenge)
		query.Set("code_challenge_method", "S256")
	}

	// Call provider's auth hook if present
	if config.Hook != nil {
		// Convert query to map for hook
		params := make(map[string]string)
		for k, v := range query {
			if len(v) > 0 {
				params[k] = v[0]
			}
		}
		if err := config.Hook.BeforeAuth(params); err != nil {
			return "", "", err
		}
		// Convert back to query
		for k, v := range params {
			query.Set(k, v)
		}
	}

	u.RawQuery = query.Encode()

	return u.String(), redirectURL, nil
}

// callbackBaseURL returns the per-request callback base URL or the manager default.
func (m *Manager) callbackBaseURL(opts *Options) string {
	if opts != nil && opts.BaseURL != "" {
		return opts.BaseURL
	}
	return m.config.BaseURL
}

// getHTTPClient returns appropriate HTTP client based on options and config
func (m *Manager) getHTTPClient(opts *Options) *http.Client {
	// 1. Use explicit HTTPClient from options if provided
	if opts.HTTPClient != nil {
		return opts.HTTPClient
	}

	// 2. Use proxy from options if provided
	if opts.ProxyURL != nil {
		transport := &http.Transport{
			Proxy: http.ProxyURL(opts.ProxyURL),
		}
		return &http.Client{
			Transport: transport,
			Timeout:   30 * time.Second,
		}
	}

	// 3. Fall back to config's HTTP client (which may have proxy)
	return m.config.GetHTTPClient()
}

// HandleCallback handles the OAuth callback request
func (m *Manager) HandleCallback(ctx context.Context, r *http.Request, opts ...Option) (*Token, error) {
	options := applyOptions(opts...)

	// Parse callback parameters
	code := r.URL.Query().Get("code")
	state := r.URL.Query().Get("state")
	errorParam := r.URL.Query().Get("error")

	if errorParam != "" {
		return nil, fmt.Errorf("oauth error: %s", errorParam)
	}

	if code == "" {
		return nil, ErrInvalidCode
	}

	// Validate state
	stateData, err := m.getState(state)
	if err != nil {
		return nil, err
	}
	defer m.deleteState(state)

	// Get provider config
	config, ok := m.registry.Get(stateData.Provider)
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrInvalidProvider, stateData.Provider)
	}

	// Exchange code for token
	// For PKCE providers, include the code verifier; for standard OAuth, omit it
	var codeVerifier string
	if config.OAuthMethod == OAuthMethodPKCE {
		codeVerifier = stateData.CodeVerifier
	}

	token, err := m.exchangeCodeForToken(ctx, config, state, code, codeVerifier, stateData.RedirectURI, options)
	if err != nil {
		return nil, err
	}
	token.Provider = stateData.Provider
	token.RedirectTo = stateData.RedirectTo
	token.Name = stateData.Name
	token.SessionID = stateData.SessionID

	// Save token
	if err := m.config.TokenStorage.SaveToken(stateData.UserID, stateData.Provider, token); err != nil {
		return nil, err
	}

	return token, nil
}

// exchangeCodeForToken exchanges the authorization code for an access token
func (m *Manager) exchangeCodeForToken(ctx context.Context, config *ProviderConfig, state string, code string, codeVerifier string, redirectURI string, opts *Options) (*Token, error) {
	// Build common parameters
	params := map[string]string{
		"grant_type":   "authorization_code",
		"client_id":    config.ClientID,
		"code":         code,
		"redirect_uri": redirectURI,
	}

	// Add client_secret if possible

	switch config.Type {
	case ai.IssuerCodex:
		// ignore client secret for codex
	case ai.IssuerClaudeCode:
		// require state for claude code
		params["state"] = state
		if config.ClientSecret != "" {
			params["client_secret"] = config.ClientSecret
		}
	default:
		if config.ClientSecret != "" {
			params["client_secret"] = config.ClientSecret
		}
	}

	// Add code_verifier for PKCE
	if config.OAuthMethod == OAuthMethodPKCE && codeVerifier != "" {
		params["code_verifier"] = codeVerifier
	}

	logrus.WithFields(logrus.Fields{
		"state":                state,
		"provider":             config.Type,
		"code_verifier_length": len(codeVerifier),
		"redirect_uri":         redirectURI,
		"grant_type":           params["grant_type"],
		"has_client_secret":    config.ClientSecret != "",
		"token_url":            config.TokenURL,
		"request_format":       map[TokenRequestFormat]string{TokenRequestFormatJSON: "JSON", TokenRequestFormatForm: "Form"}[config.TokenRequestFormat],
	}).Info("[OAuth] PKCE code_verifier added to token request")

	reqBody, contentType, err := buildRequestBody(params, config.TokenRequestFormat)
	if err != nil {
		return nil, fmt.Errorf("failed to build request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", config.TokenURL, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Accept", "application/json")

	// Call provider's token hook if present
	if config.Hook != nil {
		if err := config.Hook.BeforeToken(params, req.Header); err != nil {
			return nil, err
		}
		req.Body = io.NopCloser(reqBody)
	}

	// Debug: print request details
	m.debugRequest(req, config.TokenRequestFormat)

	// Send request with optional proxy support
	// Uses OAUTH_PROXY_URL, HTTP_PROXY, or HTTPS_PROXY environment variables
	client := m.getHTTPClient(opts)
	client.Timeout = 60 * time.Second

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("client error: %w: %v", ErrTokenExchangeFailed, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		bodyStr := string(body)
		// Log the body for debugging (truncate if too long)
		if len(bodyStr) > 500 {
			logrus.Debugf("token exchange failed: status %d, body: %s...", resp.StatusCode, bodyStr[:500])
		} else {
			logrus.Debugf("token exchange failed: status %d, body: %s", resp.StatusCode, bodyStr)
		}
		return nil, fmt.Errorf("token exchange failed: status %d, body: %s", resp.StatusCode, bodyStr)
	}

	// Parse response directly into Token
	token := &Token{}
	if err := json.NewDecoder(resp.Body).Decode(token); err != nil {
		return nil, fmt.Errorf("data decode: %w: %v", ErrTokenExchangeFailed, err)
	}

	// Convert ExpiresIn to Expiry
	if token.ExpiresIn > 0 {
		token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
	}

	// For Codex provider, parse ID token to extract user info
	if config.Type == ai.IssuerCodex && token.IDToken != "" {
		if claims := parseIDToken(token.IDToken); claims != nil {
			if token.Metadata == nil {
				token.Metadata = make(map[string]any)
			}
			if claims.Email != "" {
				token.Metadata["email"] = claims.Email
			}
			if accountID := claims.GetAccountID(); accountID != "" {
				token.Metadata["account_id"] = accountID
			}
			if claims.Name != "" {
				token.Metadata["name"] = claims.Name
			}
		} else {
			logrus.Warnf("[OAuth] Failed to parse ID token for Codex provider")
		}
	} else if config.Type == ai.IssuerCodex {
		logrus.Warnf("[OAuth] Codex provider token has no ID token (id_token field is empty)")
	}

	// Call provider's after-token hook to fetch additional metadata
	if config.Hook != nil && token.AccessToken != "" {
		metadata, err := config.Hook.AfterToken(ctx, token.AccessToken, client)
		if err != nil {
			fmt.Printf("[OAuth] AfterToken hook failed: %v\n", err)
			// Continue even if AfterToken fails, as we already have the token
		}
		if metadata != nil {
			if token.Metadata == nil {
				token.Metadata = metadata
			} else {
				// Merge metadata
				for k, v := range metadata {
					token.Metadata[k] = v
				}
			}
		}
	}

	return token, nil
}

// GetToken retrieves a token for a user and provider, refreshing if necessary
func (m *Manager) GetToken(ctx context.Context, userID string, providerType ai.Issuer, opts ...Option) (*Token, error) {
	options := applyOptions(opts...)
	token, err := m.config.TokenStorage.GetToken(userID, providerType)
	if err != nil {
		return nil, err
	}

	// Check if token needs refresh
	if token.ExpiredIn(m.config.TokenExpiryBuffer) {
		if token.RefreshToken != "" {
			refreshed, err := m.refreshToken(ctx, providerType, token.RefreshToken, options)
			if err == nil {
				refreshed.Provider = providerType
				// Preserve old refresh token if new one is not returned
				if refreshed.RefreshToken == "" {
					refreshed.RefreshToken = token.RefreshToken
				}
				if err := m.config.TokenStorage.SaveToken(userID, providerType, refreshed); err == nil {
					return refreshed, nil
				}
			}
		}
		// If refresh failed, return the existing token if still valid
		if token.Valid() {
			return token, nil
		}
		return nil, fmt.Errorf("token expired and refresh failed")
	}

	return token, nil
}

// refreshToken refreshes an access token using a refresh token
func (m *Manager) refreshToken(ctx context.Context, providerType ai.Issuer, refreshToken string, opts *Options) (*Token, error) {
	config, ok := m.registry.Get(providerType)
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrInvalidProvider, providerType)
	}

	// Build common parameters
	params := map[string]string{
		"grant_type":    "refresh_token",
		"refresh_token": refreshToken,
		"client_id":     config.ClientID,
	}

	// ref: https://github.com/openai/codex/blob/d807d44a/codex-rs/core/tests/suite/auth_refresh.rs#L35-L94
	// codex DO NOT require client_secret
	if providerType != ai.IssuerCodex {
		params["client_secret"] = config.ClientSecret
	}

	// Build request body first
	reqBody, contentType, err := buildRequestBody(params, config.TokenRequestFormat)
	if err != nil {
		return nil, fmt.Errorf("failed to build request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", config.TokenURL, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Accept", "application/json")

	// Call provider's token hook if present (may modify params and headers)
	if config.Hook != nil {
		if err := config.Hook.BeforeToken(params, req.Header); err != nil {
			return nil, err
		}
		// Rebuild body in case hook modified params
		reqBody, contentType, err = buildRequestBody(params, config.TokenRequestFormat)
		if err != nil {
			return nil, fmt.Errorf("failed to rebuild request body: %w", err)
		}
		req.Body = io.NopCloser(reqBody)
	}

	// Set Content-Type after hook (hook may have modified it)
	req.Header.Set("Content-Type", contentType)

	// Debug: print request details
	m.debugRequest(req, config.TokenRequestFormat)

	// Send request with optional proxy support
	// Uses OAUTH_PROXY_URL, HTTP_PROXY, or HTTPS_PROXY environment variables
	client := m.getHTTPClient(opts)
	client.Timeout = 30 * time.Second

	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("refresh token: %w: %v", ErrTokenExchangeFailed, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("refresh token failed: status %d, body: %d", resp.StatusCode, len(string(body)))
	}

	// Parse response directly into Token
	token := &Token{}
	if err := json.NewDecoder(resp.Body).Decode(token); err != nil {
		return nil, fmt.Errorf("decode error: %w: %v", ErrTokenExchangeFailed, err)
	}

	// Convert ExpiresIn to Expiry
	if token.ExpiresIn > 0 {
		token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
	}

	// For Codex provider, parse ID token to extract user info
	if providerType == ai.IssuerCodex && token.IDToken != "" {
		if claims := parseIDToken(token.IDToken); claims != nil {
			if token.Metadata == nil {
				token.Metadata = make(map[string]any)
			}
			if claims.Email != "" {
				token.Metadata["email"] = claims.Email
			}
			if accountID := claims.GetAccountID(); accountID != "" {
				token.Metadata["account_id"] = accountID
			}
			if claims.Name != "" {
				token.Metadata["name"] = claims.Name
			}
		}
	}

	return token, nil
}

// RefreshToken refreshes an access token using a refresh token
// This is a public method that can be called from HTTP handlers
func (m *Manager) RefreshToken(ctx context.Context, userID string, providerType ai.Issuer, refreshToken string, opts ...Option) (*Token, error) {
	options := applyOptions(opts...)
	// Refresh the token
	token, err := m.refreshToken(ctx, providerType, refreshToken, options)
	if err != nil {
		return nil, err
	}

	token.Provider = providerType

	// Preserve old refresh token if new one is not returned
	// Some OAuth providers don't return a new refresh token on each refresh
	if token.RefreshToken == "" {
		token.RefreshToken = refreshToken
	}

	// Save the refreshed token
	if err := m.config.TokenStorage.SaveToken(userID, providerType, token); err != nil {
		return nil, fmt.Errorf("failed to save refreshed token: %w", err)
	}

	return token, nil
}

// RevokeToken removes a token for a user and provider
func (m *Manager) RevokeToken(userID string, providerType ai.Issuer) error {
	return m.config.TokenStorage.DeleteToken(userID, providerType)
}

// ListProviders returns all providers that have valid tokens for the user
func (m *Manager) ListProviders(userID string) ([]ai.Issuer, error) {
	return m.config.TokenStorage.ListProviders(userID)
}

// GetRegistry returns the provider registry
func (m *Manager) GetRegistry() *Registry {
	return m.registry
}

// GetConfig returns the OAuth configuration
func (m *Manager) GetConfig() *Config {
	return m.config
}

// SetBaseURL updates the BaseURL in the OAuth configuration
// This is used when starting a dynamic callback server on a specific port
func (m *Manager) SetBaseURL(baseURL string) {
	m.config.BaseURL = baseURL
}

// SetProxyURL updates the ProxyURL in the OAuth configuration
// This is used to temporarily set a proxy for a specific OAuth flow
func (m *Manager) SetProxyURL(proxyURL *url.URL) {
	m.config.ProxyURL = proxyURL
	if proxyURL != nil {
		logrus.Infof("[OAuth] Set proxy URL: %s", proxyURL.String())
	}
}

// ResetProxyURL clears the ProxyURL in the OAuth configuration
// This should be called after OAuth flow completes
func (m *Manager) ResetProxyURL() {
	m.config.ProxyURL = nil
	logrus.Info("[OAuth] Reset proxy URL")
}

// InitiateDeviceCodeFlow initiates the Device Code flow and returns device code data
// RFC 8628: OAuth 2.0 Device Authorization Grant
func (m *Manager) InitiateDeviceCodeFlow(ctx context.Context, userID string, providerType ai.Issuer, redirectTo string, name string, opts ...Option) (*DeviceCodeData, error) {
	options := applyOptions(opts...)
	config, ok := m.registry.Get(providerType)
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrInvalidProvider, providerType)
	}

	if config.ClientID == "" {
		return nil, fmt.Errorf("%w: %s", ErrProviderNotConfigured, providerType)
	}

	if config.DeviceCodeURL == "" {
		return nil, fmt.Errorf("provider %s does not support device code flow", providerType)
	}

	// Generate PKCE code verifier if provider uses Device Code PKCE
	var codeVerifier string
	var codeChallenge string
	if config.OAuthMethod == OAuthMethodDeviceCodePKCE {
		var err error
		codeVerifier, err = m.generateCodeVerifier()
		if err != nil {
			return nil, fmt.Errorf("failed to generate code verifier: %w", err)
		}
		codeChallenge = m.generateCodeChallenge(codeVerifier)
	}

	// Build device authorization request
	// Build common parameters
	params := map[string]string{
		"client_id": config.ClientID,
		"scope":     strings.Join(config.Scopes, " "),
	}
	// Add PKCE parameters for Device Code PKCE flow
	if config.OAuthMethod == OAuthMethodDeviceCodePKCE {
		params["code_challenge"] = codeChallenge
		params["code_challenge_method"] = "S256"
	}

	reqBody, contentType, err := buildRequestBody(params, config.TokenRequestFormat)
	if err != nil {
		return nil, fmt.Errorf("failed to build request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", config.DeviceCodeURL, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Accept", "application/json")

	// Call provider's token hook if present
	if config.Hook != nil {
		if err := config.Hook.BeforeToken(params, req.Header); err != nil {
			return nil, err
		}
		// Rebuild body in case hook modified params
		reqBody, contentType, err = buildRequestBody(params, config.TokenRequestFormat)
		if err != nil {
			return nil, fmt.Errorf("failed to rebuild request body: %w", err)
		}
		req.Body = io.NopCloser(reqBody)
		req.Header.Set("Content-Type", contentType)
	}

	client := m.getHTTPClient(options)
	client.Timeout = 30 * time.Second
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("device code request failed: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return nil, fmt.Errorf("device code request failed: status %d, body: %d", resp.StatusCode, len(string(body)))
	}

	// Parse device code response
	var deviceResp DeviceCodeResponse
	if err := json.NewDecoder(resp.Body).Decode(&deviceResp); err != nil {
		return nil, fmt.Errorf("failed to decode device code response: %w", err)
	}

	// Create device code data
	now := time.Now()
	data := &DeviceCodeData{
		DeviceCodeResponse: &deviceResp,
		Provider:           providerType,
		UserID:             userID,
		RedirectTo:         redirectTo,
		Name:               name,
		InitiatedAt:        now,
		ExpiresAt:          now.Add(time.Duration(deviceResp.ExpiresIn) * time.Second),
		CodeVerifier:       codeVerifier, // Store PKCE verifier for token polling
	}

	return data, nil
}

// PollForToken polls the token endpoint until the user completes authentication
// or the device code expires.
func (m *Manager) PollForToken(ctx context.Context, data *DeviceCodeData, callback func(*Token), opts ...Option) (*Token, error) {
	options := applyOptions(opts...)
	config, ok := m.registry.Get(data.Provider)
	if !ok {
		return nil, fmt.Errorf("%w: %s", ErrInvalidProvider, data.Provider)
	}

	// Default interval is 5 seconds if not specified
	interval := time.Duration(data.Interval) * time.Second
	if interval == 0 {
		interval = 2 * time.Second
	}

	ticker := time.NewTicker(interval)
	defer ticker.Stop()

	// Poll until the provider's device code expires, falling back to the
	// standard OAuth session expiry when the provider did not return one.
	pollDeadline := data.ExpiresAt
	if pollDeadline.IsZero() {
		pollDeadline = time.Now().Add(DefaultSessionExpiry)
	}
	timeoutCtx, cancel := context.WithDeadline(ctx, pollDeadline)
	defer cancel()

	fmt.Printf("[OAuth] Device code polling started for %s, expires at: %s\n", data.Provider, pollDeadline.Format(time.RFC3339))

	for {
		select {
		case <-timeoutCtx.Done():
			return nil, fmt.Errorf("authentication timed out at %s", pollDeadline.Format(time.RFC3339))
		case <-ctx.Done():
			return nil, ctx.Err()
		case <-ticker.C:
			fmt.Printf("[OAuth] Polling token endpoint for %s...\n", data.Provider)
			token, err := m.pollTokenRequest(ctx, config, data.DeviceCode, data.CodeVerifier, options)
			if err != nil {
				// Check if error is a transient error that we should retry
				if isTransientDeviceCodeError(err) {
					fmt.Printf("[OAuth] Authorization pending for %s, continuing poll...\n", data.Provider)
					time.Sleep(interval)
					continue
				}
				fmt.Printf("[OAuth] Polling error for %s: %v\n", data.Provider, err)
				return nil, err
			}

			fmt.Printf("[OAuth] Successfully obtained token for %s\n", data.Provider)
			// Successfully got token
			token.Provider = data.Provider
			token.RedirectTo = data.RedirectTo
			token.Name = data.Name

			// Save token
			if err := m.config.TokenStorage.SaveToken(data.UserID, data.Provider, token); err != nil {
				return nil, fmt.Errorf("failed to save token: %w", err)
			}

			// Call callback if provided
			if callback != nil {
				callback(token)
			}

			return token, nil
		}
	}
}

// pollTokenRequest makes a single token polling request
func (m *Manager) pollTokenRequest(ctx context.Context, config *ProviderConfig, deviceCode string, codeVerifier string, opts *Options) (*Token, error) {
	// Build common parameters
	params := map[string]string{
		"grant_type":  config.GrantType,
		"client_id":   config.ClientID,
		"device_code": deviceCode,
	}
	if config.ClientSecret != "" {
		params["client_secret"] = config.ClientSecret
	}
	// Add PKCE code_verifier for Device Code PKCE flow
	if config.OAuthMethod == OAuthMethodDeviceCodePKCE && codeVerifier != "" {
		params["code_verifier"] = codeVerifier
	}

	reqBody, contentType, err := buildRequestBody(params, config.TokenRequestFormat)
	if err != nil {
		return nil, fmt.Errorf("failed to build request body: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", config.TokenURL, reqBody)
	if err != nil {
		return nil, err
	}

	req.Header.Set("Content-Type", contentType)
	req.Header.Set("Accept", "application/json")

	// Call provider's token hook if present
	if config.Hook != nil {
		if err := config.Hook.BeforeToken(params, req.Header); err != nil {
			return nil, err
		}
		reqBody, _, err = buildRequestBody(params, config.TokenRequestFormat)
		if err != nil {
			return nil, fmt.Errorf("failed to rebuild request body: %w", err)
		}
		req.Body = io.NopCloser(reqBody)
	}

	client := m.getHTTPClient(opts)
	client.Timeout = 30 * time.Second
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("token poll request failed: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	// Check for authorization pending (should retry)
	if resp.StatusCode == http.StatusBadRequest {
		var errResp struct {
			Error string `json:"error"`
		}
		if json.Unmarshal(body, &errResp) == nil {
			switch errResp.Error {
			case "authorization_pending", "slow_down":
				return nil, &DeviceCodePendingError{Message: errResp.Error}
			case "access_denied", "expired_token":
				return nil, fmt.Errorf("device code error: %s", errResp.Error)
			}
			// Unknown error in 400 response
			return nil, fmt.Errorf("device code error (400): %s - body: %s", errResp.Error, string(body))
		}
		// 400 but no valid error response
		return nil, fmt.Errorf("device code error (400): body: %s", string(body))
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token poll failed: status %d, body: %s", resp.StatusCode, string(body))
	}

	// Parse token response directly into Token
	token := &Token{}
	if err := json.Unmarshal(body, token); err != nil {
		return nil, fmt.Errorf("failed to decode token response: %w", err)
	}

	// Convert ExpiresIn to Expiry
	if token.ExpiresIn > 0 {
		token.Expiry = time.Now().Add(time.Duration(token.ExpiresIn) * time.Second)
	}

	return token, nil
}

// DeviceCodePendingError represents a pending device code authorization
type DeviceCodePendingError struct {
	Message string
}

func (e *DeviceCodePendingError) Error() string {
	return e.Message
}

// isTransientDeviceCodeError checks if an error is a transient device code error
func isTransientDeviceCodeError(err error) bool {
	if _, ok := err.(*DeviceCodePendingError); ok {
		return true
	}
	return false
}

// debugRequest prints HTTP request details for debugging
func (m *Manager) debugRequest(req *http.Request, format TokenRequestFormat) {
	if !m.Debug {
		return
	}
	logrus.Debug("=== OAuth Debug: HTTP Request ===")
	logrus.Debugf("Method: %s", req.Method)
	logrus.Debugf("URL: %s", req.URL.String())
	logrus.Debug("Headers:")
	for key, values := range req.Header {
		for _, value := range values {
			// Mask sensitive headers
			if strings.EqualFold(key, "Authorization") {
				value = "***REDACTED***"
			}
			logrus.Debugf("  %s: %s", key, value)
		}
	}

	if req.Body != nil && req.Body != http.NoBody {
		logrus.Debug("Body:")
		// Read body to print it (but we need to restore it for the actual request)
		bodyBytes, err := io.ReadAll(req.Body)
		if err == nil {
			// Try to format JSON for readability
			switch format {
			case TokenRequestFormatJSON:
				var formatted any
				if json.Unmarshal(bodyBytes, &formatted) == nil {
					if pretty, err := json.MarshalIndent(formatted, "", "  "); err == nil {
						logrus.Debugf("%s", string(pretty))
					} else {
						logrus.Debugf("%s", string(bodyBytes))
					}
				} else {
					logrus.Debugf("%s", string(bodyBytes))
				}
			default:
				logrus.Debugf("%s", string(bodyBytes))
			}
			// Restore body for actual request
			req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
		}
	}
	logrus.Debug("================================")
}

// =============================================
// Session Management for OAuth Status Tracking
// =============================================

// generateSessionID generates a unique session ID
func (m *Manager) generateSessionID() (string, error) {
	b := make([]byte, 16)
	if _, err := rand.Read(b); err != nil {
		return "", fmt.Errorf("failed to generate session ID: %w", err)
	}
	return hex.EncodeToString(b), nil
}

// CreateSession creates a new OAuth session with pending status
func (m *Manager) CreateSession(userID string, provider ai.Issuer) (*SessionState, error) {
	sessionID, err := m.generateSessionID()
	if err != nil {
		return nil, err
	}

	now := time.Now()
	session := &SessionState{
		SessionID: sessionID,
		Status:    SessionStatusPending,
		Provider:  provider,
		UserID:    userID,
		CreatedAt: now,
		ExpiresAt: now.Add(10 * time.Minute), // Session expires after 10 minutes
	}

	if err := m.sessionStorage.SaveSession(sessionID, session); err != nil {
		return nil, err
	}

	logrus.WithFields(logrus.Fields{
		"session_id": sessionID,
		"provider":   provider,
		"user_id":    userID,
		"status":     SessionStatusPending,
	}).Info("[OAuth] Session created")

	return session, nil
}

// GetSession retrieves a session by ID
func (m *Manager) GetSession(sessionID string) (*SessionState, error) {
	session, err := m.sessionStorage.GetSession(sessionID)
	if err != nil {
		return nil, err
	}

	// Check expiration
	if !session.ExpiresAt.IsZero() && time.Now().After(session.ExpiresAt) {
		return nil, ErrSessionNotFound
	}

	return session, nil
}

// StoreSession stores or updates a session
func (m *Manager) StoreSession(session *SessionState) {
	_ = m.sessionStorage.SaveSession(session.SessionID, session)
}

// UpdateSessionStatus updates the status of a session
func (m *Manager) UpdateSessionStatus(sessionID string, status SessionStatus, providerUUID string, errMsg string) error {
	// First get the session to log provider info
	session, err := m.sessionStorage.GetSession(sessionID)
	if err != nil {
		logrus.WithField("session_id", sessionID).Warn("[OAuth] Failed to update session: not found")
		return err
	}

	// Update the status
	if err := m.sessionStorage.UpdateSessionStatus(sessionID, status, providerUUID, errMsg); err != nil {
		return err
	}

	// Log session status change
	logEntry := logrus.WithFields(logrus.Fields{
		"session_id":    sessionID,
		"provider":      session.Provider,
		"new_status":    status,
		"provider_uuid": providerUUID,
	})

	if status == SessionStatusSuccess {
		logEntry.Info("[OAuth] Session completed successfully")
	} else if status == SessionStatusFailed {
		logEntry.WithField("error", errMsg).Error("[OAuth] Session failed")
	} else {
		logEntry.Debug("[OAuth] Session status updated")
	}

	return nil
}

// cleanupExpiredSessions is removed - now handled by cleanupPeriodically

// cleanupPeriodically removes expired states, sessions, and tokens
func (m *Manager) cleanupPeriodically() {
	ticker := time.NewTicker(time.Minute)
	defer ticker.Stop()

	for range ticker.C {
		m.stateStorage.CleanupExpired()
		m.sessionStorage.CleanupExpired()
		m.tokenStorage.CleanupExpired()
	}
}
