// Package oauth provides OAuth 2.0 Protected Resource Metadata (RFC 9728) support
// for the GitHub MCP Server HTTP mode.
package oauth

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/github/github-mcp-server/pkg/http/headers"
	"github.com/github/github-mcp-server/pkg/utils"
	"github.com/go-chi/chi/v5"
	"github.com/modelcontextprotocol/go-sdk/auth"
	"github.com/modelcontextprotocol/go-sdk/oauthex"
)

const (
	// OAuthProtectedResourcePrefix is the well-known path prefix for OAuth protected resource metadata.
	OAuthProtectedResourcePrefix = "/.well-known/oauth-protected-resource"
)

// SupportedScopes lists every OAuth scope that an MCP tool may require. It is the
// source of truth in two places: HTTP mode advertises it as scopes_supported in
// the protected-resource metadata, and stdio OAuth login requests it by default
// and then filters the exposed tools to the granted scopes. A tool whose required
// scope is absent here is therefore hidden under default OAuth even though a PAT
// carrying that scope would expose it, so keep this list in sync with tool scope
// requirements when scopes change.
var SupportedScopes = []string{
	"repo",
	"read:org",
	"read:user",
	"user:email",
	"read:packages",
	"write:packages",
	"read:project",
	"project",
	"gist",
	"notifications",
	"workflow",
	"codespace",
}

// Config holds the OAuth configuration for the MCP server.
type Config struct {
	// BaseURL is the publicly accessible URL where this server is hosted.
	// This is used to construct the OAuth resource URL.
	BaseURL string

	// AuthorizationServer is the OAuth authorization server URL.
	// Defaults to GitHub's OAuth server if not specified.
	AuthorizationServer string

	// ResourcePath is the externally visible base path for the MCP server (e.g., "/mcp").
	// This is used to restore the original path when a proxy strips a base path before forwarding.
	// If empty, requests are treated as already using the external path.
	ResourcePath string

	// TrustProxyHeaders indicates whether X-Forwarded-Host and X-Forwarded-Proto
	// should be honored when deriving the effective host and scheme for OAuth
	// resource URLs. This must only be enabled when the server is deployed
	// behind a trusted proxy that sets these headers; otherwise an untrusted
	// client can influence the OAuth resource metadata URL advertised to MCP
	// clients. When BaseURL is set, it always takes precedence and these
	// headers are unused.
	TrustProxyHeaders bool
}

// AuthHandler handles OAuth-related HTTP endpoints.
type AuthHandler struct {
	cfg     *Config
	apiHost utils.APIHostResolver
}

// NewAuthHandler creates a new OAuth auth handler.
func NewAuthHandler(cfg *Config, apiHost utils.APIHostResolver) (*AuthHandler, error) {
	if cfg == nil {
		cfg = &Config{}
	}

	if apiHost == nil {
		var err error
		apiHost, err = utils.NewAPIHost("https://api.github.com")
		if err != nil {
			return nil, fmt.Errorf("failed to create default API host: %w", err)
		}
	}

	return &AuthHandler{
		cfg:     cfg,
		apiHost: apiHost,
	}, nil
}

// routePatterns defines the route patterns for OAuth protected resource metadata.
var routePatterns = []string{
	"",          // Root: /.well-known/oauth-protected-resource
	"/readonly", // Read-only mode
	"/insiders", // Insiders mode
	"/x/{toolset}",
	"/x/{toolset}/readonly",
}

// RegisterRoutes registers the OAuth protected resource metadata routes.
func (h *AuthHandler) RegisterRoutes(r chi.Router) {
	for _, pattern := range routePatterns {
		for _, route := range h.routesForPattern(pattern) {
			path := OAuthProtectedResourcePrefix + route
			r.Handle(path, h.metadataHandler())
		}
	}
}

func (h *AuthHandler) metadataHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := r.Context()
		resourcePath := resolveResourcePath(
			strings.TrimPrefix(r.URL.Path, OAuthProtectedResourcePrefix),
			h.cfg.ResourcePath,
		)
		resourceURL := h.buildResourceURL(r, resourcePath)

		var authorizationServerURL string
		if h.cfg.AuthorizationServer != "" {
			authorizationServerURL = h.cfg.AuthorizationServer
		} else {
			authURL, err := h.apiHost.AuthorizationServerURL(ctx)
			if err != nil {
				http.Error(w, fmt.Sprintf("failed to resolve authorization server URL: %v", err), http.StatusInternalServerError)
				return
			}
			authorizationServerURL = authURL.String()
		}

		metadata := &oauthex.ProtectedResourceMetadata{
			Resource:               resourceURL,
			AuthorizationServers:   []string{authorizationServerURL},
			ResourceName:           "GitHub MCP Server",
			ScopesSupported:        SupportedScopes,
			BearerMethodsSupported: []string{"header"},
		}

		auth.ProtectedResourceMetadataHandler(metadata).ServeHTTP(w, r)
	})
}

// routesForPattern generates route variants for a given pattern.
// GitHub strips the /mcp prefix before forwarding, so we register both variants:
// - With /mcp prefix: for direct access or when GitHub doesn't strip
// - Without /mcp prefix: for when GitHub has stripped the prefix
func (h *AuthHandler) routesForPattern(pattern string) []string {
	basePaths := []string{""}
	if basePath := normalizeBasePath(h.cfg.ResourcePath); basePath != "" {
		basePaths = append(basePaths, basePath)
	} else {
		basePaths = append(basePaths, "/mcp")
	}

	routes := make([]string, 0, len(basePaths)*2)
	for _, basePath := range basePaths {
		routes = append(routes, joinRoute(basePath, pattern))
		routes = append(routes, joinRoute(basePath, pattern)+"/")
	}

	return routes
}

// resolveResourcePath returns the externally visible resource path,
// restoring the configured base path when proxies strip it before forwarding.
func resolveResourcePath(path, basePath string) string {
	if path == "" {
		path = "/"
	}
	base := normalizeBasePath(basePath)
	if base == "" {
		return path
	}
	if path == "/" {
		return base
	}
	if path == base || strings.HasPrefix(path, base+"/") {
		return path
	}
	return base + path
}

// ResolveResourcePath returns the externally visible resource path for a request.
// Exported for use by middleware.
func ResolveResourcePath(r *http.Request, cfg *Config) string {
	basePath := ""
	if cfg != nil {
		basePath = cfg.ResourcePath
	}
	return resolveResourcePath(r.URL.Path, basePath)
}

// buildResourceURL constructs the full resource URL for OAuth metadata.
func (h *AuthHandler) buildResourceURL(r *http.Request, resourcePath string) string {
	host, scheme := GetEffectiveHostAndScheme(r, h.cfg)
	baseURL := fmt.Sprintf("%s://%s", scheme, host)
	if h.cfg.BaseURL != "" {
		baseURL = strings.TrimSuffix(h.cfg.BaseURL, "/")
	}
	if resourcePath == "" {
		resourcePath = "/"
	}
	if !strings.HasPrefix(resourcePath, "/") {
		resourcePath = "/" + resourcePath
	}
	return baseURL + resourcePath
}

// GetEffectiveHostAndScheme returns the effective host and scheme for a request.
//
// X-Forwarded-Host and X-Forwarded-Proto are only honored when cfg.TrustProxyHeaders
// is true. Without that opt-in, an untrusted client could otherwise influence the
// OAuth resource metadata URL advertised to MCP clients.
func GetEffectiveHostAndScheme(r *http.Request, cfg *Config) (host, scheme string) { //nolint:revive
	trustProxy := cfg != nil && cfg.TrustProxyHeaders

	if trustProxy {
		if fh := r.Header.Get(headers.ForwardedHostHeader); fh != "" {
			host = fh
		}
	}
	if host == "" {
		host = r.Host
	}
	if host == "" {
		host = "localhost"
	}

	if trustProxy {
		if fp := r.Header.Get(headers.ForwardedProtoHeader); fp != "" {
			scheme = strings.ToLower(fp)
		}
	}
	if scheme == "" {
		if r.TLS != nil {
			scheme = "https"
		} else {
			scheme = "http"
		}
	}
	return
}

// BuildResourceMetadataURL constructs the full URL to the OAuth protected resource metadata endpoint.
func BuildResourceMetadataURL(r *http.Request, cfg *Config, resourcePath string) string {
	host, scheme := GetEffectiveHostAndScheme(r, cfg)
	suffix := ""
	if resourcePath != "" && resourcePath != "/" {
		if !strings.HasPrefix(resourcePath, "/") {
			suffix = "/" + resourcePath
		} else {
			suffix = resourcePath
		}
	}
	if cfg != nil && cfg.BaseURL != "" {
		return strings.TrimSuffix(cfg.BaseURL, "/") + OAuthProtectedResourcePrefix + suffix
	}
	return fmt.Sprintf("%s://%s%s%s", scheme, host, OAuthProtectedResourcePrefix, suffix)
}

func normalizeBasePath(path string) string {
	trimmed := strings.TrimSpace(path)
	if trimmed == "" || trimmed == "/" {
		return ""
	}
	if !strings.HasPrefix(trimmed, "/") {
		trimmed = "/" + trimmed
	}
	return strings.TrimSuffix(trimmed, "/")
}

func joinRoute(basePath, pattern string) string {
	if basePath == "" {
		return pattern
	}
	if pattern == "" {
		return basePath
	}
	if before, ok := strings.CutSuffix(basePath, "/"); ok {
		return before + pattern
	}
	return basePath + pattern
}
