// Copyright 2026 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

package extauth

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"slices"
	"strings"

	"github.com/modelcontextprotocol/go-sdk/auth"
	"github.com/modelcontextprotocol/go-sdk/oauthex"
	"golang.org/x/oauth2"
	"golang.org/x/oauth2/clientcredentials"
)

// ClientCredentialsHandlerConfig is the configuration for [ClientCredentialsHandler].
type ClientCredentialsHandlerConfig struct {
	// Credentials contains the pre-registered client ID and secret.
	// REQUIRED. Both ClientID and ClientSecretAuth must be set, since the
	// client credentials grant requires a confidential client.
	Credentials *oauthex.ClientCredentials

	// HTTPClient is an optional HTTP client for customization.
	// If nil, http.DefaultClient is used.
	// OPTIONAL.
	HTTPClient *http.Client
}

// ClientCredentialsHandler is an implementation of [auth.OAuthHandler] that
// uses the OAuth 2.0 Client Credentials grant (RFC 6749 Section 4.4) to
// obtain access tokens.
//
// This handler is intended for service-to-service authentication where the
// client has pre-registered credentials (client ID and secret) and does not
// require user interaction. It bypasses both dynamic client registration and
// the authorization code flow.
//
// The token endpoint and scopes are discovered automatically via Protected
// Resource Metadata (RFC 9728) and Authorization Server Metadata (RFC 8414),
// following the ext-auth specification SEP-1046.
type ClientCredentialsHandler struct {
	config      *ClientCredentialsHandlerConfig
	tokenSource oauth2.TokenSource
}

// Compile-time check that ClientCredentialsHandler implements auth.OAuthHandler.
var _ auth.OAuthHandler = (*ClientCredentialsHandler)(nil)

// NewClientCredentialsHandler creates a new ClientCredentialsHandler.
// It validates the configuration and returns an error if invalid.
func NewClientCredentialsHandler(config *ClientCredentialsHandlerConfig) (*ClientCredentialsHandler, error) {
	if config == nil {
		return nil, fmt.Errorf("config must be provided")
	}
	if config.Credentials == nil {
		return nil, fmt.Errorf("credentials are required")
	}
	if err := config.Credentials.Validate(); err != nil {
		return nil, fmt.Errorf("invalid credentials: %w", err)
	}
	if config.Credentials.ClientSecretAuth == nil {
		return nil, fmt.Errorf("clientSecretAuth is required for client credentials grant")
	}
	return &ClientCredentialsHandler{config: config}, nil
}

// TokenSource returns the token source for outgoing requests.
// Returns nil if authorization has not been performed yet.
func (h *ClientCredentialsHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) {
	return h.tokenSource, nil
}

// Authorize performs the Client Credentials grant to obtain an access token.
// It is called when a request fails with 401 or 403.
//
// The flow follows the ext-auth specification SEP-1046:
//  1. Discover Protected Resource Metadata from the request URL
//  2. Discover Authorization Server Metadata from PRM
//  3. Exchange client credentials for an access token at the token endpoint
func (h *ClientCredentialsHandler) Authorize(ctx context.Context, req *http.Request, resp *http.Response) error {
	defer resp.Body.Close()
	defer io.Copy(io.Discard, resp.Body)

	httpClient := h.config.HTTPClient
	if httpClient == nil {
		httpClient = http.DefaultClient
	}

	// Step 1: Discover Protected Resource Metadata.
	wwwChallenges, err := oauthex.ParseWWWAuthenticate(resp.Header[http.CanonicalHeaderKey("WWW-Authenticate")])
	if err != nil {
		return fmt.Errorf("failed to parse WWW-Authenticate header: %v", err)
	}

	prm, err := getProtectedResourceMetadata(ctx, wwwChallenges, req.URL.String(), httpClient)
	if err != nil {
		return err
	}

	if len(prm.AuthorizationServers) == 0 {
		return fmt.Errorf("protected resource metadata has no authorization servers specified")
	}

	// Step 2: Discover Authorization Server Metadata.
	asm, err := auth.GetAuthServerMetadata(ctx, prm.AuthorizationServers[0], httpClient)
	if err != nil {
		return fmt.Errorf("failed to get authorization server metadata: %w", err)
	}
	if asm == nil {
		// Fallback to 2025-03-26 spec: predefined endpoints.
		authServerURL := prm.AuthorizationServers[0]
		asm = &oauthex.AuthServerMeta{
			Issuer:        authServerURL,
			TokenEndpoint: authServerURL + "/token",
		}
	}

	// Determine scopes: use PRM's scopes_supported if available.
	scopes := scopesFromChallenges(wwwChallenges)
	if len(scopes) == 0 && len(prm.ScopesSupported) > 0 {
		scopes = prm.ScopesSupported
	}

	// Step 3: Exchange client credentials for an access token.
	creds := h.config.Credentials
	cfg := &clientcredentials.Config{
		ClientID:     creds.ClientID,
		ClientSecret: creds.ClientSecretAuth.ClientSecret,
		TokenURL:     asm.TokenEndpoint,
		Scopes:       scopes,
		AuthStyle:    selectTokenAuthMethod(asm.TokenEndpointAuthMethodsSupported),
	}

	ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, httpClient)
	h.tokenSource = cfg.TokenSource(ctxWithClient)

	// Eagerly fetch a token to surface errors immediately.
	if _, err := h.tokenSource.Token(); err != nil {
		h.tokenSource = nil
		return fmt.Errorf("client credentials token request failed: %w", err)
	}
	return nil
}

// getProtectedResourceMetadata discovers Protected Resource Metadata (RFC 9728)
// from the request URL. This mirrors the logic in AuthorizationCodeHandler.
func getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string, httpClient *http.Client) (*oauthex.ProtectedResourceMetadata, error) {
	// Use MCP server URL as the resource URI per
	// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri.
	for _, u := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) {
		prm, err := oauthex.GetProtectedResourceMetadata(ctx, u.url, u.resource, httpClient)
		if err != nil {
			continue
		}
		if prm == nil {
			continue
		}
		if len(prm.AuthorizationServers) == 0 {
			// If we found PRM, we enforce the 2025-11-25 spec and not search further.
			return nil, fmt.Errorf("protected resource metadata has no authorization servers specified")
		}
		return prm, nil
	}
	// Fallback to 2025-03-26 spec: MCP server root is the Authorization Server.
	// https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery
	u, err := url.Parse(mcpServerURL)
	if err != nil {
		return nil, fmt.Errorf("failed to parse MCP server URL: %v", err)
	}
	u.Path = ""
	return &oauthex.ProtectedResourceMetadata{
		AuthorizationServers: []string{u.String()},
		Resource:             mcpServerURL,
	}, nil
}

type prmURL struct {
	url      string
	resource string
}

// protectedResourceMetadataURLs returns URLs to try for PRM discovery.
// This mirrors the logic in AuthorizationCodeHandler.
func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL {
	var urls []prmURL
	if metadataURL != "" {
		urls = append(urls, prmURL{url: metadataURL, resource: resourceURL})
	}
	ru, err := url.Parse(resourceURL)
	if err != nil {
		return urls
	}
	mu := *ru
	// At the path of the server's MCP endpoint.
	mu.Path = "/.well-known/oauth-protected-resource/" + strings.TrimLeft(ru.Path, "/")
	urls = append(urls, prmURL{url: mu.String(), resource: resourceURL})
	// At the root.
	mu.Path = "/.well-known/oauth-protected-resource"
	ru.Path = ""
	urls = append(urls, prmURL{url: mu.String(), resource: ru.String()})
	return urls
}

// resourceMetadataURLFromChallenges returns a resource metadata URL from
// WWW-Authenticate challenges, or the empty string if there is none.
func resourceMetadataURLFromChallenges(cs []oauthex.Challenge) string {
	for _, c := range cs {
		if u := c.Params["resource_metadata"]; u != "" {
			return u
		}
	}
	return ""
}

// scopesFromChallenges returns scopes from WWW-Authenticate challenges.
// It only looks at challenges with the "Bearer" scheme.
func scopesFromChallenges(cs []oauthex.Challenge) []string {
	for _, c := range cs {
		if c.Scheme == "bearer" && c.Params["scope"] != "" {
			return strings.Fields(c.Params["scope"])
		}
	}
	return nil
}

// selectTokenAuthMethod selects the preferred token endpoint auth method based on
// the authorization server's supported methods. Prefers client_secret_post over
// client_secret_basic per the OAuth 2.1 draft.
func selectTokenAuthMethod(supported []string) oauth2.AuthStyle {
	prefOrder := []string{
		"client_secret_post",
		"client_secret_basic",
	}
	for _, method := range prefOrder {
		if slices.Contains(supported, method) {
			switch method {
			case "client_secret_post":
				return oauth2.AuthStyleInParams
			case "client_secret_basic":
				return oauth2.AuthStyleInHeader
			}
		}
	}
	return oauth2.AuthStyleAutoDetect
}
