package oauth

import (
	"context"
	"fmt"
	"strings"

	pkgoauth "github.com/giantswarm/muster/pkg/oauth"

	"github.com/giantswarm/muster/pkg/logging"
)

// softwareVersion is the version string reported in the Client ID Metadata Document.
// This is informational only and helps identify the muster version during OAuth debugging.
const softwareVersion = "1.0.0"

// Client handles OAuth 2.1 flows for remote MCP server authentication.
type Client struct {
	// Configuration
	clientID     string // The CIMD URL used as client_id
	publicURL    string // The public URL of the Muster Server
	callbackPath string // The path for OAuth callbacks (e.g., "/oauth/callback")
	cimdScopes   string // The OAuth scopes to advertise in the CIMD

	// Stores (interface-backed; defaults to in-memory, can be swapped to Valkey)
	tokenStore TokenStorer
	stateStore StateStorer

	// Shared OAuth client for protocol operations
	oauthClient *pkgoauth.Client
}

// ClientOption configures optional Client parameters.
type ClientOption func(*Client)

// WithTokenStorer sets a custom TokenStorer implementation (e.g., Valkey-backed).
func WithTokenStorer(ts TokenStorer) ClientOption {
	return func(c *Client) {
		c.tokenStore = ts
	}
}

// WithStateStorer sets a custom StateStorer implementation (e.g., Valkey-backed).
func WithStateStorer(ss StateStorer) ClientOption {
	return func(c *Client) {
		c.stateStore = ss
	}
}

// NewClient creates a new OAuth client with the given configuration.
// By default, in-memory stores are used. Use WithTokenStorer / WithStateStorer
// to inject Valkey-backed implementations.
func NewClient(clientID, publicURL, callbackPath, cimdScopes string, opts ...ClientOption) *Client {
	c := &Client{
		clientID:     clientID,
		publicURL:    publicURL,
		callbackPath: callbackPath,
		cimdScopes:   cimdScopes,
		tokenStore:   NewTokenStore(),
		stateStore:   NewStateStore(),
		oauthClient:  pkgoauth.NewClient(),
	}
	for _, opt := range opts {
		opt(c)
	}
	return c
}

// GetTokenStore returns the token store for external access.
func (c *Client) GetTokenStore() TokenStorer {
	return c.tokenStore
}

// GetStateStore returns the state store for external access.
func (c *Client) GetStateStore() StateStorer {
	return c.stateStore
}

// GetRedirectURI returns the full redirect URI for OAuth callbacks.
func (c *Client) GetRedirectURI() string {
	return strings.TrimSuffix(c.publicURL, "/") + c.callbackPath
}

// GetCIMDURL returns the URL where the Client ID Metadata Document is served.
// This is derived from the clientID which is expected to be the CIMD URL.
func (c *Client) GetCIMDURL() string {
	return c.clientID
}

// GetToken retrieves a valid token for the given session and issuer.
// Returns nil if no valid token exists.
func (c *Client) GetToken(sessionID, issuer, scope string) *pkgoauth.Token {
	key := TokenKey{
		SessionID: sessionID,
		Issuer:    issuer,
		Scope:     scope,
	}
	if token := c.tokenStore.Get(key); token != nil {
		return token
	}

	// Fall back to issuer-only match for SSO
	return c.tokenStore.GetByIssuer(sessionID, issuer)
}

// GenerateAuthURL creates an OAuth authorization URL for user authentication.
// Returns the URL. The code verifier is stored with the state for later retrieval.
func (c *Client) GenerateAuthURL(ctx context.Context, sessionID, userID, serverName, issuer, scope string) (string, error) {
	metadata, err := c.oauthClient.DiscoverMetadata(ctx, issuer)
	if err != nil {
		return "", fmt.Errorf("failed to fetch OAuth metadata: %w", err)
	}

	// MCP 2025-11-25 §"Authorization Code Protection" requires refusing the
	// flow if the AS doesn't advertise S256 PKCE. Fail closed: a server that
	// doesn't list it may silently ignore code_challenge_method=S256 and
	// produce confusing token-endpoint errors later.
	if !metadata.SupportsS256PKCE() {
		return "", fmt.Errorf("authorization server %q does not advertise S256 PKCE in code_challenge_methods_supported (MCP 2025-11-25 requires refusal)", issuer)
	}

	pkce, err := pkgoauth.GeneratePKCE()
	if err != nil {
		return "", fmt.Errorf("failed to generate PKCE: %w", err)
	}

	state, err := c.stateStore.GenerateState(sessionID, userID, serverName, issuer, pkce.CodeVerifier)
	if err != nil {
		return "", fmt.Errorf("failed to generate state: %w", err)
	}

	authURL, err := c.oauthClient.BuildAuthorizationURL(
		metadata.AuthorizationEndpoint,
		c.clientID,
		c.GetRedirectURI(),
		state,
		scope,
		pkce,
	)
	if err != nil {
		return "", fmt.Errorf("failed to build authorization URL: %w", err)
	}

	logging.Debug("OAuth", "Generated auth URL for session=%s server=%s issuer=%s",
		logging.TruncateIdentifier(sessionID), serverName, issuer)

	return authURL, nil
}

// ExchangeCode exchanges an authorization code for tokens.
func (c *Client) ExchangeCode(ctx context.Context, code, codeVerifier, issuer string) (*pkgoauth.Token, error) {
	// Fetch OAuth metadata using shared client
	metadata, err := c.oauthClient.DiscoverMetadata(ctx, issuer)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch OAuth metadata: %w", err)
	}

	// Exchange code using shared client
	token, err := c.oauthClient.ExchangeCode(
		ctx,
		metadata.TokenEndpoint,
		code,
		c.GetRedirectURI(),
		c.clientID,
		codeVerifier,
	)
	if err != nil {
		return nil, err
	}

	// Set issuer on the token
	token.Issuer = issuer

	logging.Debug("OAuth", "Successfully exchanged code for token (issuer=%s, expires_in=%d)",
		issuer, token.ExpiresIn)

	return token, nil
}

// StoreToken stores a token in the token store.
func (c *Client) StoreToken(sessionID, userID string, token *pkgoauth.Token) {
	key := TokenKey{
		SessionID: sessionID,
		Issuer:    token.Issuer,
		Scope:     token.Scope,
	}
	c.tokenStore.Store(key, token, userID)
}

// Stop stops background cleanup goroutines.
func (c *Client) Stop() {
	c.tokenStore.Stop()
	c.stateStore.Stop()
}

// GetClientMetadata returns the Client ID Metadata Document for this client.
func (c *Client) GetClientMetadata() *pkgoauth.ClientMetadata {
	return &pkgoauth.ClientMetadata{
		ClientID:                c.clientID,
		ClientName:              "Muster MCP Aggregator",
		ClientURI:               "https://github.com/giantswarm/muster",
		RedirectURIs:            []string{c.GetRedirectURI()},
		GrantTypes:              []string{"authorization_code", "refresh_token"},
		ResponseTypes:           []string{"code"},
		TokenEndpointAuthMethod: "none",
		Scope:                   c.cimdScopes,
		SoftwareID:              "giantswarm-muster",
		SoftwareVersion:         softwareVersion,
	}
}

// DiscoverMetadata fetches OAuth metadata for an issuer.
// This is exposed for external access to metadata discovery.
func (c *Client) DiscoverMetadata(ctx context.Context, issuer string) (*pkgoauth.Metadata, error) {
	return c.oauthClient.DiscoverMetadata(ctx, issuer)
}
