package wire

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/gorilla/websocket"
)

// gatewayWebSocketHandshakeAbsoluteCap bounds the effective WS handshake
// timeout even when the underlying transport advertises larger values. 30s
// matches the HTTP-side GatewayResponseHeaderTimeout; tests that derive the
// WS handshake policy from transport fields must respect this cap.
const gatewayWebSocketHandshakeAbsoluteCap = 30 * time.Second

// GatewayWebSocketDial is the immutable, per-call WebSocket dialer exposed
// by Protocol as part of its ServerGatewayConnection capability. The caller
// supplies only sanitized URL/header/subprotocol inputs; Protocol owns every
// transport/security field on the gorilla Dialer it constructs.
//
// The underlying http.Transport must be gateway-suitable: it must carry a
// non-nil DialContext (no legacy Dial/DialTLS), must not pin HTTP/2-only
// protocols, and must not configure any of the proxy-connect hooks or
// per-host limits that gorilla cannot honor. Unsupported transports fail
// fast with ErrUnsupportedGatewayTransport BEFORE any proxy lookup or
// network dial.
//
// Production wire.New constructs the control-plane http.Client with
// Transport=nil; this method resolves nil to http.DefaultTransport (rejecting
// the dial if the global has been replaced with a non-*http.Transport value)
// and clones the result so the gorilla Dialer never shares the global
// transport directly with concurrent callers.
func (c *Protocol) GatewayWebSocketDial(
	ctx context.Context,
	targetURL string,
	header http.Header,
	subprotocols []string,
) (*websocket.Conn, *http.Response, error) {
	source, err := extractGatewayWebSocketTransport(c.httpClient)
	if err != nil {
		return nil, nil, err
	}
	// Private immutable clone so the gorilla Dialer never shares the
	// resolved transport (which may be http.DefaultTransport in production)
	// with concurrent callers. Clone preserves DialContext, Proxy,
	// TLSClientConfig, and the timeout fields the handshake policy reads.
	transport := source.Clone()
	handshakeTimeout, err := c.gatewayWebSocketHandshakeTimeout()
	if err != nil {
		return nil, nil, err
	}
	dialer := websocket.Dialer{
		NetDialContext:    transport.DialContext,
		Proxy:             transport.Proxy,
		HandshakeTimeout:  handshakeTimeout,
		Subprotocols:      append([]string(nil), subprotocols...),
		EnableCompression: false,
	}
	if transport.TLSClientConfig != nil {
		cloned := transport.TLSClientConfig.Clone()
		cloned.NextProtos = []string{"http/1.1"}
		dialer.TLSClientConfig = cloned
	}
	parsed, err := url.Parse(targetURL)
	if err != nil {
		return nil, nil, err
	}
	return dialer.DialContext(ctx, parsed.String(), header)
}

// gatewayWebSocketHandshakeTimeout derives the effective WS handshake
// timeout from the underlying transport's policy fields. The smallest
// non-zero value among TLSHandshakeTimeout and ResponseHeaderTimeout wins,
// capped at gatewayWebSocketHandshakeAbsoluteCap. If both fields are zero,
// the absolute cap is used directly.
func (c *Protocol) gatewayWebSocketHandshakeTimeout() (time.Duration, error) {
	transport, err := extractGatewayWebSocketTransport(c.httpClient)
	if err != nil {
		return 0, err
	}
	candidates := make([]time.Duration, 0, 2)
	if transport.TLSHandshakeTimeout > 0 {
		candidates = append(candidates, transport.TLSHandshakeTimeout)
	}
	if transport.ResponseHeaderTimeout > 0 {
		candidates = append(candidates, transport.ResponseHeaderTimeout)
	}
	if len(candidates) == 0 {
		return gatewayWebSocketHandshakeAbsoluteCap, nil
	}
	effective := candidates[0]
	for _, remaining := range candidates[1:] {
		if remaining < effective {
			effective = remaining
		}
	}
	if effective > gatewayWebSocketHandshakeAbsoluteCap {
		effective = gatewayWebSocketHandshakeAbsoluteCap
	}
	return effective, nil
}

// extractGatewayWebSocketTransport returns the *http.Transport backing the
// protocol's HTTP client, after validating that the transport is suitable
// for gorilla/websocket's Dialer. Custom RoundTrippers, legacy Dial/DialTLS,
// proxy-connect hooks, per-host limits, response-header byte caps, and
// HTTP/2-only protocol restrictions are rejected before any network or
// proxy-lookup call.
//
// A nil client.Transport (the production wire.New shape) is resolved to
// http.DefaultTransport. If the global default has been replaced with a
// non-*http.Transport value, the dial fails fast: http.Client.Do would
// otherwise fall back to that same global and silently bypass the watchdog,
// and the gorilla Dialer cannot honor a custom RoundTripper's DialContext.
// The returned transport is the SHARED source transport; callers that need
// an immutable snapshot (e.g., gorilla Dialer construction) must Clone it
// themselves so concurrent callers never observe mutations to the global.
func extractGatewayWebSocketTransport(client *http.Client) (*http.Transport, error) {
	if client == nil {
		return nil, fmt.Errorf("%w: client is nil", ErrUnsupportedGatewayTransport)
	}
	roundTripper := client.Transport
	if roundTripper == nil {
		dt, ok := http.DefaultTransport.(*http.Transport)
		if !ok || dt == nil {
			return nil, fmt.Errorf("%w: http.DefaultTransport is not *http.Transport", ErrUnsupportedGatewayTransport)
		}
		roundTripper = dt
	}
	transport, ok := roundTripper.(*http.Transport)
	if !ok || transport == nil {
		return nil, ErrUnsupportedGatewayTransport
	}
	if transport.DialContext == nil {
		// gorilla's Dialer requires a DialContext; a nil value would
		// silently fall back to the transport's legacy Dial, which the
		// WS path must not exercise.
		return nil, fmt.Errorf("%w: transport DialContext is nil", ErrUnsupportedGatewayTransport)
	}
	if transport.DialTLSContext != nil {
		return nil, fmt.Errorf("%w: transport DialTLSContext is set", ErrUnsupportedGatewayTransport)
	}
	if transport.Dial != nil {
		return nil, fmt.Errorf("%w: legacy Dial is set", ErrUnsupportedGatewayTransport)
	}
	if transport.DialTLS != nil {
		return nil, fmt.Errorf("%w: legacy DialTLS is set", ErrUnsupportedGatewayTransport)
	}
	if transport.ProxyConnectHeader != nil {
		return nil, fmt.Errorf("%w: ProxyConnectHeader is set", ErrUnsupportedGatewayTransport)
	}
	if transport.GetProxyConnectHeader != nil {
		return nil, fmt.Errorf("%w: GetProxyConnectHeader is set", ErrUnsupportedGatewayTransport)
	}
	if transport.OnProxyConnectResponse != nil {
		return nil, fmt.Errorf("%w: OnProxyConnectResponse is set", ErrUnsupportedGatewayTransport)
	}
	if transport.MaxConnsPerHost != 0 {
		return nil, fmt.Errorf("%w: MaxConnsPerHost is set", ErrUnsupportedGatewayTransport)
	}
	if transport.MaxResponseHeaderBytes != 0 {
		return nil, fmt.Errorf("%w: MaxResponseHeaderBytes is set", ErrUnsupportedGatewayTransport)
	}
	if transport.Protocols != nil && !transport.Protocols.HTTP1() {
		return nil, fmt.Errorf("%w: transport Protocols excludes HTTP/1.1", ErrUnsupportedGatewayTransport)
	}
	return transport, nil
}

// errInvalidWebSocketURL is returned when the caller-supplied target URL is
// not parseable. Kept unexported so callers receive the parse error directly.
var errInvalidWebSocketURL = errors.New("invalid websocket target URL")
