package ghmcp

import (
	"context"
	"crypto/rand"
	"fmt"
	"log/slog"

	"github.com/github/github-mcp-server/internal/oauth"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

// sessionPrompter adapts an MCP server session to oauth.Prompter, presenting
// authorization prompts to the user via elicitation. Keeping the prompt on the
// MCP control channel (rather than a tool result) keeps the authorization URL
// and any session-bound state out of the model's context.
type sessionPrompter struct {
	session *mcp.ServerSession
}

// elicitationCaps returns the client's declared elicitation capabilities, or nil
// if the client did not advertise any.
func (p *sessionPrompter) elicitationCaps() *mcp.ElicitationCapabilities {
	params := p.session.InitializeParams()
	if params == nil || params.Capabilities == nil {
		return nil
	}
	return params.Capabilities.Elicitation
}

// CanPromptURL reports whether the client supports URL-mode elicitation.
func (p *sessionPrompter) CanPromptURL() bool {
	caps := p.elicitationCaps()
	return caps != nil && caps.URL != nil
}

// PromptURL presents the authorization URL via URL-mode elicitation and blocks
// until the user acknowledges, declines, or ctx is done.
func (p *sessionPrompter) PromptURL(ctx context.Context, prompt oauth.Prompt) error {
	res, err := p.session.Elicit(ctx, &mcp.ElicitParams{
		Mode:          "url",
		Message:       prompt.Message,
		URL:           prompt.URL,
		ElicitationID: rand.Text(),
	})
	if err != nil {
		// The client advertised URL elicitation but the request itself failed:
		// classify it as undeliverable (not a user decision) so the flow can fall
		// back to a channel that needs no client capability.
		return fmt.Errorf("%w: %w", oauth.ErrPromptUnavailable, err)
	}
	if res.Action != "accept" {
		return oauth.ErrPromptDeclined
	}
	return nil
}

// CanPromptForm reports whether the client supports form-mode elicitation. The
// SDK treats a client that advertises neither form nor URL capabilities as
// supporting forms, for backward compatibility, so we mirror that here.
func (p *sessionPrompter) CanPromptForm() bool {
	caps := p.elicitationCaps()
	if caps == nil {
		return false
	}
	return caps.Form != nil || caps.URL == nil
}

// PromptForm presents a textual acknowledgement (used to display a device code
// when URL elicitation is unavailable) and blocks until the user responds.
func (p *sessionPrompter) PromptForm(ctx context.Context, prompt oauth.Prompt) error {
	res, err := p.session.Elicit(ctx, &mcp.ElicitParams{
		Mode:    "form",
		Message: prompt.Message,
	})
	if err != nil {
		// As with PromptURL, a delivery failure is undeliverable rather than a
		// decline, so the flow can fall back instead of aborting.
		return fmt.Errorf("%w: %w", oauth.ErrPromptUnavailable, err)
	}
	if res.Action != "accept" {
		return oauth.ErrPromptDeclined
	}
	return nil
}

// oauthAuthenticator is the subset of *oauth.Manager that the middleware needs.
// Depending on the interface (rather than the concrete manager) lets the
// middleware be exercised with a deterministic fake, since driving the real
// manager to its branches would require standing up live GitHub flows.
type oauthAuthenticator interface {
	HasToken() bool
	Authenticate(ctx context.Context, prompter oauth.Prompter) (*oauth.Outcome, error)
}

// createOAuthMiddleware returns receiving middleware that authorizes the session
// lazily, on the first tool call. Authorization is deferred until here (rather
// than at startup) because the prompts depend on an initialized session whose
// elicitation capabilities are known.
//
// When a token is already available the call proceeds untouched. Otherwise the
// flow runs: secure channels (browser, URL elicitation) block until the token
// arrives and then the call proceeds; the last-resort channel returns the
// instruction to the user as a tool result and asks them to retry.
func createOAuthMiddleware(mgr oauthAuthenticator, logger *slog.Logger) func(next mcp.MethodHandler) mcp.MethodHandler {
	return func(next mcp.MethodHandler) mcp.MethodHandler {
		return func(ctx context.Context, method string, request mcp.Request) (mcp.Result, error) {
			if method != "tools/call" || mgr.HasToken() {
				return next(ctx, method, request)
			}

			callReq, ok := request.(*mcp.CallToolRequest)
			if !ok {
				return next(ctx, method, request)
			}

			outcome, err := mgr.Authenticate(ctx, &sessionPrompter{session: callReq.Session})
			if err != nil {
				return nil, fmt.Errorf("github authorization failed: %w", err)
			}
			if outcome != nil && outcome.UserAction != nil {
				logger.Info("surfacing github authorization instructions to user")
				return &mcp.CallToolResult{
					Content: []mcp.Content{&mcp.TextContent{Text: outcome.UserAction.Message}},
				}, nil
			}
			return next(ctx, method, request)
		}
	}
}

// ensure sessionPrompter satisfies the Prompter contract.
var _ oauth.Prompter = (*sessionPrompter)(nil)
