package dcs

import (
	
	
	
	
	
	
	
	
	
	

	

	
	
	
	
)

const (
	// defaultHTTPMaxWait is the default http_wait max_wait (ms): the server holds
	// the long-poll response open up to this long. Matches the MTProto default.
	defaultHTTPMaxWait = 25000
	// httpClientTimeoutMargin is added on top of max_wait for the HTTP client
	// timeout so a full-length long-poll response is not cut off.
	httpClientTimeoutMargin = 5 * time.Second
	// httpWaitRetryInterval throttles the poll loop when a wait frame cannot be
	// built (e.g. during key exchange) or a POST fails, avoiding a busy loop.
	httpWaitRetryInterval = 500 * time.Millisecond
	// httpInboxSize buffers response frames until Recv consumes them.
	httpInboxSize = 32
	// httpMaxConnsPerHost bounds concurrent connections of the default client to
	// a single DC, so a burst of sends cannot exhaust file descriptors.
	httpMaxConnsPerHost = 16
)

// recvResult is a decoded response delivered to Recv: either a frame or a
// transport-level protocol error (e.g. auth_key_not_found).
type recvResult struct {
	data []byte
	err  error
}

// httpConn implements transport.Conn over the MTProto HTTP transport.
//
// See https://core.telegram.org/mtproto/transports#http-transport.
//
// MTProto over HTTP is request/response: the server can only deliver messages
// as the body of a response to a client POST. Send posts one raw MTProto frame
// (framing is done by HTTP Content-Length, there is no codec tag) and the
// response body — the messages the server had queued for the session — is
// delivered to Recv through inbox. Send is non-blocking: the POST and its
// (possibly long-polling) response are handled on a separate goroutine so a 25s
// http_wait long-poll never stalls the mtproto write path.
type httpConn struct {
	client *http.Client
	// urls holds one /api endpoint per candidate DC address. Requests use the
	// current index; a failed POST rotates to the next candidate.
	urls   []string
	urlIdx atomic.Uint32

	// inbox buffers responses (frames or protocol errors) until Recv consumes
	// them.
	inbox chan recvResult

	// http_wait parameters in milliseconds, reported via HTTPWaitParams and used
	// by the mtproto layer to build http_wait messages.
	maxDelay  int
	waitAfter int
	maxWait   int

	ctx       context.Context
	cancel    context.CancelFunc
	startOnce sync.Once
	closeOnce sync.Once
}

func ( *http.Client,  []string, , ,  int) *httpConn {
	,  := context.WithCancel(context.Background())
	return &httpConn{
		client:    ,
		urls:      ,
		inbox:     make(chan recvResult, httpInboxSize),
		maxDelay:  ,
		waitAfter: ,
		maxWait:   ,
		ctx:       ,
		cancel:    ,
	}
}

var _ transport.Conn = (*httpConn)(nil)

// Send posts a single MTProto frame. It is non-blocking: the POST and response
// are handled on a separate goroutine, so a long-poll response never stalls the
// caller. Delivery reliability is provided by the mtproto rpc engine
// (acks/retransmits) and liveness by the ping loop; a failed POST is therefore
// dropped here and recovered upstream.
func ( *httpConn) ( context.Context,  *bin.Buffer) error {
	select {
	case <-.ctx.Done():
		return errors.Wrap(net.ErrClosed, "send")
	default:
	}

	// b.Buf is reused by the caller once Send returns; copy before handing it to
	// the POST goroutine.
	 := make([]byte, len(.Buf))
	copy(, .Buf)
	// One goroutine per send: bounded in practice by the mtproto rpc engine's
	// in-flight window and by the client's MaxConnsPerHost (which the goroutines
	// block on). All are cancelled on Close via c.ctx.
	go func() { _, _ = .roundtrip() }()
	return nil
}

// Recv blocks until an incoming frame or a transport-level protocol error is
// available.
func ( *httpConn) ( context.Context,  *bin.Buffer) error {
	select {
	case  := <-.inbox:
		if .err != nil {
			return .err
		}
		.Buf = append(.Buf[:0], .data...)
		return nil
	case <-.Done():
		return .Err()
	case <-.ctx.Done():
		return errors.Wrap(net.ErrClosed, "recv")
	}
}

// Close releases the connection and cancels in-flight requests.
func ( *httpConn) () error {
	.closeOnce.Do(.cancel)
	return nil
}

// roundtrip posts one frame and delivers the response to inbox. It reports
// whether anything was delivered (a frame or a protocol error) and returns an
// error only for transport-level failures (POST error, non-200), which callers
// use to back off.
func ( *httpConn) ( []byte) ( bool,  error) {
	 := .urls[.urlIndex()]
	,  := http.NewRequestWithContext(.ctx, http.MethodPost, , bytes.NewReader())
	if  != nil {
		return false, errors.Wrap(, "build request")
	}
	.Header.Set("Content-Type", "application/octet-stream")

	,  := .client.Do()
	if  != nil {
		.rotateURL() // fail over to the next candidate address
		return false, errors.Wrap(, "do request")
	}
	defer func() { _ = .Body.Close() }()

	if .StatusCode != http.StatusOK {
		_, _ = io.Copy(io.Discard, .Body)
		return false, errors.Errorf("unexpected status %d", .StatusCode)
	}
	,  := io.ReadAll(.Body)
	if  != nil {
		return false, errors.Wrap(, "read body")
	}
	switch {
	case len() == 0:
		// Empty body is a valid "dummy" long-poll response (max_wait elapsed with
		// no messages); nothing to deliver.
		return false, nil
	case len() == bin.Word:
		// A 4-byte body is a transport-level protocol error (e.g. -404 auth key
		// not found), not an encrypted message. The TCP codecs surface it via
		// checkProtocolError; do the same so the read loop can react (e.g.
		// regenerate the auth key) instead of feeding garbage to the decrypter.
		 := int32(binary.LittleEndian.Uint32())
		.deliver(recvResult{err: &codec.ProtocolErr{Code: -}})
		return true, nil
	default:
		.deliver(recvResult{data: })
		return true, nil
	}
}

// urlIndex returns the current candidate index. The modulo is taken on the
// unsigned counter so the index stays in range even if the counter wraps.
func ( *httpConn) () int {
	return int(.urlIdx.Load() % uint32(len(.urls)))
}

func ( *httpConn) ( recvResult) {
	select {
	case .inbox <- :
	case <-.ctx.Done():
	}
}

// rotateURL advances to the next candidate address after a failed POST. Failover
// is best-effort: a burst of concurrent failures may advance the index by more
// than one, but the poll loop's round-robin still reaches every candidate.
func ( *httpConn) () {
	if len(.urls) > 1 {
		.urlIdx.Add(1)
	}
}

// HTTPWaitParams reports the http_wait fields (milliseconds) the transport wants
// the mtproto layer to use. It is part of the mtproto HTTP long-poll capability.
func ( *httpConn) () (, ,  int) {
	return .maxDelay, .waitAfter, .maxWait
}

// StartHTTPWait starts the long-poll loop. frame yields a freshly-encrypted
// http_wait service message — only the mtproto layer can encrypt it. The loop
// keeps exactly one long-poll POST outstanding, re-issuing it as soon as the
// previous response arrives, to minimize update latency. It is safe to call at
// most once; further calls are no-ops.
//
// The loop runs in a goroutine whose lifetime is bound solely to Close: it is
// not part of the mtproto Run errgroup, so the connection must be closed to stop
// it (Conn.handleClose does this on teardown).
func ( *httpConn) ( func( context.Context) (*bin.Buffer, error)) {
	.startOnce.Do(func() {
		go .pollLoop()
	})
}

func ( *httpConn) ( func( context.Context) (*bin.Buffer, error)) {
	// A conforming server holds an empty response until max_wait; a response that
	// comes back much sooner than this signals a middlebox not honoring the
	// long-poll, and must be throttled to avoid a request storm.
	 := time.Duration(.maxWait) * time.Millisecond / 2

	for {
		if .ctx.Err() != nil {
			return
		}
		,  := (.ctx)
		if  != nil {
			// Wait frame cannot be built (e.g. key exchange in progress) or the
			// connection is closing. Back off to avoid a hot loop.
			if !.sleep(httpWaitRetryInterval) {
				return
			}
			continue
		}
		// Synchronous: blocks up to max_wait, then immediately loops to re-issue
		// the next http_wait (low update latency, one poll outstanding).
		 := time.Now()
		,  := .roundtrip(.Buf)
		switch {
		case  != nil:
			// POST failed; back off instead of hammering a dead connection. The
			// ping loop ultimately detects and tears down a dead connection.
			if !.sleep(httpWaitRetryInterval) {
				return
			}
		case ! && time.Since() < :
			// Instant empty 200 from a non-conforming middlebox: throttle.
			if !.sleep(httpWaitRetryInterval) {
				return
			}
		}
	}
}

// sleep waits for d, returning false if the connection was closed while waiting.
func ( *httpConn) ( time.Duration) bool {
	select {
	case <-.ctx.Done():
		return false
	case <-time.After():
		return true
	}
}

var _ Resolver = httpResolver{}

type httpResolver struct {
	client     *http.Client
	scheme     string
	port       int
	preferIPv6 bool
	maxDelay   int
	waitAfter  int
	maxWait    int
	// startIdx rotates the preferred candidate across successive Primary calls
	// (i.e. across reconnects), so a dead first address does not trap the client.
	startIdx *atomic.Uint32
}

func ( httpResolver) ( []tg.DCOption) []string {
	// Rotate the starting candidate per call for cross-reconnect failover. Add(1)-1
	// makes the first call start at index 0 (the highest-priority DC); the modulo
	// is taken on the unsigned counter to stay in range on wrap.
	 := int((.startIdx.Add(1) - 1) % uint32(len()))
	 := make([]string, len())
	for  := range  {
		 := [(+)%len()]
		 := net.JoinHostPort(.IPAddress, strconv.Itoa(.port))
		[] = .scheme + "://" +  + "/api"
	}
	return 
}

func ( httpResolver) ( context.Context,  int,  List) (transport.Conn, error) {
	 := FindPrimaryDCs(.Options, , .preferIPv6)
	// The HTTP transport cannot speak to TCP-obfuscated-only DCs.
	 := 0
	for ,  := range  {
		if !.TCPObfuscatedOnly {
			[] = 
			++
		}
	}
	 = [:]
	if len() == 0 {
		return nil, errors.Errorf("no addresses for DC %d", )
	}
	return newHTTPConn(.client, .urls(), .maxDelay, .waitAfter, .maxWait), nil
}

func ( httpResolver) ( context.Context,  int,  List) (transport.Conn, error) {
	return nil, errors.Errorf("can't resolve %d: MediaOnly is unsupported", )
}

func ( httpResolver) ( context.Context,  int,  List) (transport.Conn, error) {
	return nil, errors.Errorf("can't resolve %d: CDN is unsupported", )
}

// HTTPOptions is HTTP resolver creation options.
type HTTPOptions struct {
	// Client is the HTTP client used for POST requests. If nil, a client with a
	// bounded transport and a timeout derived from MaxWait is used. A custom
	// client's timeout must exceed MaxWait, otherwise long-poll responses are cut
	// off. For Scheme "https" a custom client with an appropriate tls.Config
	// (SNI/certificates for the DC) is required.
	Client *http.Client
	// Scheme is "http" (default) or "https".
	//
	// Note: https support is experimental. Connecting over https to a bare DC IP
	// needs a custom Client whose tls.Config matches the DC certificate; the
	// default client will fail TLS verification.
	Scheme string
	// Port overrides the transport port. Defaults to 80 for http and 443 for
	// https.
	//
	// The HTTP transport uses this fixed port for every DC and intentionally
	// ignores tg.DCOption.Port (which is the TCP MTProto port), per the MTProto
	// HTTP transport specification.
	Port int
	// PreferIPv6 gives IPv6 DCs higher precedence.
	// Default is to prefer IPv4 DCs over IPv6.
	PreferIPv6 bool
	// MaxDelay, WaitAfter and MaxWait are the http_wait fields in milliseconds.
	// Defaults: 0, 0, 25000.
	//
	// See https://core.telegram.org/mtproto/service_messages.
	MaxDelay  int
	WaitAfter int
	MaxWait   int
}

func ( *HTTPOptions) () {
	if .Scheme == "" {
		.Scheme = "http"
	}
	if .Port == 0 {
		if .Scheme == "https" {
			.Port = 443
		} else {
			.Port = 80
		}
	}
	if .MaxWait == 0 {
		.MaxWait = defaultHTTPMaxWait
	}
	if .Client == nil {
		.Client = &http.Client{
			Timeout: time.Duration(.MaxWait)*time.Millisecond + httpClientTimeoutMargin,
			Transport: &http.Transport{
				Proxy:               http.ProxyFromEnvironment,
				MaxConnsPerHost:     httpMaxConnsPerHost,
				MaxIdleConnsPerHost: httpMaxConnsPerHost,
			},
		}
	}
}

// HTTP creates an MTProto-over-HTTP DC resolver with http_wait long-polling.
//
// It suits environments where a raw persistent MTProto TCP socket cannot be
// held but HTTP POST to the DC is possible. Updates are delivered via http_wait
// long-polling, so their latency is bounded by the round-trip rather than being
// instantaneous. MediaOnly and CDN DCs are not supported.
//
// See https://core.telegram.org/mtproto/transports#http-transport.
func ( HTTPOptions) Resolver {
	.setDefaults()
	return httpResolver{
		client:     .Client,
		scheme:     .Scheme,
		port:       .Port,
		preferIPv6: .PreferIPv6,
		maxDelay:   .MaxDelay,
		waitAfter:  .WaitAfter,
		maxWait:    .MaxWait,
		startIdx:   new(atomic.Uint32),
	}
}