// Package transport owns stateless MCP HTTP framing. Product tool catalogs and the
// Runtime-owned authorization decision are injected; no token is verified here.
package transport

import (
	"context"
	"encoding/json"
	"errors"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"unicode/utf8"
)

const maxRequestBytes = 1 << 20

// AdmissionError communicates only a safe failure class, never owner diagnostics.
type AdmissionError uint8

const (
	Unauthenticated AdmissionError = iota + 1
	Forbidden
	RateLimited
	Unavailable
)

func (AdmissionError) Error() string { return "MCP admission failed" }

type Tool struct {
	Name        string          `json:"name"`
	Description string          `json:"description"`
	InputSchema json.RawMessage `json:"inputSchema"`
}

// Config contains trusted composition inputs, never values derived from Host or
// forwarded request headers. Authorize must admit every POST through the
// Runtime user identity authority; its returned context holds request-local state.
type Config struct {
	PublicOrigin  string
	ResourceURL   string
	ServerName    string
	ServerVersion string
	AllowedOrigin func(string) bool
	Authorize     func(context.Context, string, string) (context.Context, error)
	Tools         []Tool
	// Authorize receives the registered tool name, or empty for protocol methods.
	// Execute returns an already projected MCP tool result. Errors are sanitized.
	Execute func(context.Context, string, json.RawMessage) (any, error)
}
type Handler struct {
	config Config
	names  map[string]struct{}
}

func NewHandler(config Config) (*Handler, error) {
	u, err := url.Parse(config.PublicOrigin)
	if err != nil || u.Host == "" || u.User != nil || u.Path != "" || u.RawQuery != "" || u.Fragment != "" || u.Opaque != "" {
		return nil, errors.New("MCP requires a canonical configured public origin")
	}
	loopback := strings.EqualFold(u.Hostname(), "localhost")
	if ip := net.ParseIP(u.Hostname()); ip != nil {
		loopback = ip.IsLoopback()
	}
	if u.Scheme != "https" && !(u.Scheme == "http" && loopback) {
		return nil, errors.New("MCP public origin requires HTTPS")
	}
	if config.ResourceURL != config.PublicOrigin+"/mcp" || config.Authorize == nil || config.AllowedOrigin == nil || config.Execute == nil || config.ServerName == "" || config.ServerVersion == "" || len(config.Tools) == 0 {
		return nil, errors.New("MCP requires complete resource, authority and tool composition")
	}
	names := make(map[string]struct{}, len(config.Tools))
	copied := make([]Tool, len(config.Tools))
	for i, tool := range config.Tools {
		if tool.Name == "" {
			return nil, errors.New("MCP tool name is empty")
		}
		if _, exists := names[tool.Name]; exists {
			return nil, errors.New("MCP tool name is duplicated")
		}
		var schema map[string]any
		if json.Unmarshal(tool.InputSchema, &schema) != nil || schema["type"] != "object" {
			return nil, errors.New("MCP tool schema must be an object schema")
		}
		names[tool.Name] = struct{}{}
		copied[i] = tool
		copied[i].InputSchema = append(json.RawMessage(nil), tool.InputSchema...)
	}
	config.Tools = copied
	return &Handler{config: config, names: names}, nil
}

// ServeHTTP handles only the exact MCP and discovery routes; it does not redirect.
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Cache-Control", "no-store")
	switch r.URL.Path {
	case "/.well-known/oauth-protected-resource":
		if r.Method != http.MethodGet {
			methodNotAllowed(w, http.MethodGet)
			return
		}
		writeJSON(w, 200, map[string]any{"resource": h.config.ResourceURL, "bearer_methods_supported": []string{"header"}})
		return
	case "/mcp":
	default:
		http.NotFound(w, r)
		return
	}
	origins := r.Header.Values("Origin")
	if len(origins) > 1 || (len(origins) == 1 && (strings.TrimSpace(origins[0]) == "" || !h.config.AllowedOrigin(origins[0]))) {
		writeJSON(w, 403, map[string]string{"error": "origin_not_allowed"})
		return
	}
	if r.Method != http.MethodPost && r.Method != http.MethodGet {
		methodNotAllowed(w, http.MethodPost)
		return
	}
	var bearer string
	if r.Method == http.MethodPost {
		auth := r.Header.Values("Authorization")
		if len(auth) == 1 {
			parts := strings.Fields(auth[0])
			if len(parts) == 2 && strings.EqualFold(parts[0], "Bearer") {
				bearer = parts[1]
			}
		}
		if bearer == "" {
			h.deny(w, Unauthenticated)
			return
		}

	}
	versions := r.Header.Values("MCP-Protocol-Version")
	if len(versions) > 1 || (len(versions) == 1 && strings.TrimSpace(versions[0]) != "" && !isSupportedMCPProtocolVersion(strings.TrimSpace(versions[0]))) {
		writeJSON(w, 400, map[string]string{"error": "unsupported_protocol_version"})
		return
	}
	accept := strings.Join(r.Header.Values("Accept"), ",")
	if !acceptsMediaType(accept, mcpSSEMediaType) || (r.Method == http.MethodPost && !acceptsMediaType(accept, mcpJSONMediaType)) {
		writeJSON(w, 406, map[string]string{"error": "not_acceptable"})
		return
	}
	if r.Method == http.MethodGet {
		methodNotAllowed(w, http.MethodPost)
		return
	}
	if len(r.Header.Values("Content-Type")) != 1 || !isJSONUTF8(r.Header.Get("Content-Type")) {
		writeJSON(w, 415, map[string]string{"error": "unsupported_media_type"})
		return
	}
	body, err := io.ReadAll(io.LimitReader(r.Body, maxRequestBytes+1))
	var bodyLimitError *http.MaxBytesError
	if len(body) > maxRequestBytes || errors.As(err, &bodyLimitError) {
		writeJSON(w, 413, errorResponse(nil, errInvalidRequest, "request too large"))
		return
	}
	if err != nil || !utf8.Valid(body) {
		writeJSON(w, 400, errorResponse(nil, errParse, "invalid request body"))
		return
	}
	var req rpcRequest
	if json.Unmarshal(body, &req) != nil {
		writeJSON(w, 400, errorResponse(nil, errParse, "invalid JSON"))
		return
	}
	// Only a registered tools/call name selects tool admission. Protocol methods
	// and invalid/unknown tool names receive the protocol admission policy.
	toolName := ""
	if req.Method == "tools/call" {
		var params struct {
			Name string `json:"name"`
		}
		if json.Unmarshal(req.Params, &params) == nil {
			if _, known := h.names[params.Name]; known {
				toolName = params.Name
			}
		}
	}
	ctx, authErr := h.config.Authorize(r.Context(), bearer, toolName)
	if authErr != nil || ctx == nil {
		kind := Unavailable
		var admission AdmissionError
		if errors.As(authErr, &admission) {
			kind = admission
		}
		h.deny(w, kind)
		return
	}
	r = r.WithContext(ctx)
	if req.JSONRPC != jsonRPCVersion || req.Method == "" || !validID(req.ID) {
		writeJSON(w, 400, errorResponse(nil, errInvalidRequest, "invalid request"))
		return
	}
	if len(req.ID) == 0 {
		if !strings.HasPrefix(req.Method, "notifications/") {
			writeJSON(w, 400, errorResponse(nil, errInvalidRequest, "request id required"))
			return
		}
		// Notifications never execute a tool, especially a write. This stateless
		// server sends no server-to-client requests and stores no MCP sessions.
		w.WriteHeader(http.StatusAccepted)
		return
	}
	if strings.HasPrefix(req.Method, "notifications/") {
		writeJSON(w, 400, errorResponse(req.ID, errInvalidRequest, "notification must omit id"))
		return
	}
	response := h.dispatch(r.Context(), req)
	writeJSON(w, 200, response)
}
func validID(id json.RawMessage) bool {
	if len(id) == 0 {
		return true
	}
	var value any
	if json.Unmarshal(id, &value) != nil {
		return false
	}
	switch value.(type) {
	case string, float64:
		return true
	default:
		return false
	}
}
func (h *Handler) dispatch(ctx context.Context, req rpcRequest) rpcResponse {
	switch req.Method {
	case "initialize":
		version, err := negotiateMCPProtocolVersion(req.Params)
		if err != nil {
			return errorResponse(req.ID, errInvalidParams, "initialize requires params.protocolVersion")
		}
		return resultResponse(req.ID, map[string]any{"protocolVersion": version, "serverInfo": map[string]string{"name": h.config.ServerName, "version": h.config.ServerVersion}, "capabilities": map[string]any{"tools": map[string]any{}}})
	case "ping", "notifications/initialized":
		return resultResponse(req.ID, map[string]any{})
	case "tools/list":
		return resultResponse(req.ID, map[string]any{"tools": h.config.Tools})
	case "tools/call":
		var args struct {
			Name      string          `json:"name"`
			Arguments json.RawMessage `json:"arguments"`
		}
		if json.Unmarshal(req.Params, &args) != nil {
			return errorResponse(req.ID, errInvalidParams, "invalid params")
		}
		if _, ok := h.names[args.Name]; !ok {
			return errorResponse(req.ID, errInvalidParams, "unknown tool")
		}
		result, err := h.config.Execute(ctx, args.Name, args.Arguments)
		if err != nil {
			return resultResponse(req.ID, map[string]any{"isError": true, "content": []map[string]string{{"type": "text", "text": "tool execution failed"}}})
		}
		return resultResponse(req.ID, result)
	default:
		return errorResponse(req.ID, errMethodNotFound, "method not found")
	}
}
func (h *Handler) deny(w http.ResponseWriter, kind AdmissionError) {
	code := http.StatusServiceUnavailable
	switch kind {
	case Unauthenticated:
		code = 401
		w.Header().Set("WWW-Authenticate", `Bearer realm="mcp"`)
	case Forbidden:
		code = 403
	case RateLimited:
		code = 429
	}
	writeJSON(w, code, map[string]string{"error": http.StatusText(code)})
}
func methodNotAllowed(w http.ResponseWriter, allow string) {
	w.Header().Set("Allow", allow)
	w.WriteHeader(405)
}
func writeJSON(w http.ResponseWriter, code int, value any) {
	encoded, err := json.Marshal(value)
	if err != nil {
		code = 500
		encoded = []byte(`{"error":"response unavailable"}`)
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(code)
	_, _ = w.Write(encoded)
}
