//go:build !ios

package gobackend

import (
	"context"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	utls "github.com/refraction-networking/utls"
	"golang.org/x/net/http2"
)

// utlsSessionCache is shared by every uTLS handshake so TLS 1.3 tickets enable
// resumption (fewer round-trips) across requests and hosts.
var utlsSessionCache = utls.NewLRUClientSessionCache(0)

// utlsTransport dials with a Chrome TLS fingerprint and pools one healthy HTTP/2
// connection per host, re-dialing when it dies (e.g. after a network switch).
type utlsTransport struct {
	dialer *net.Dialer
	h2     *http2.Transport
	mu     sync.Mutex
	conns  map[string]pooledHTTP2ClientConn
}

type pooledHTTP2ClientConn interface {
	RoundTrip(*http.Request) (*http.Response, error)
	ReserveNewRequest() bool
	State() http2.ClientConnState
	Close() error
	Shutdown(context.Context) error
}

func newUTLSTransport() *utlsTransport {
	return &utlsTransport{
		dialer: &net.Dialer{
			Timeout:   10 * Second,
			KeepAlive: 30 * Second,
		},
		h2:    &http2.Transport{},
		conns: make(map[string]pooledHTTP2ClientConn),
	}
}

func (t *utlsTransport) RoundTrip(req *http.Request) (*http.Response, error) {
	if req.URL.Scheme != "https" {
		return sharedTransport.RoundTrip(req)
	}

	host := req.URL.Hostname()
	addr := net.JoinHostPort(host, t.getPort(req.URL))

	if cc := t.cachedConn(addr); cc != nil {
		resp, err := cc.RoundTrip(req)
		if err == nil {
			return resp, nil
		}
		if req.Context().Err() != nil {
			return nil, err
		}
		// A pooled conn can be silently dead after a network switch. Drop it
		// and, when the request is safely repeatable, fall through to a fresh
		// dial instead of failing where the old dial-per-request code would
		// have succeeded.
		t.invalidate(addr, cc)
		retryReq, ok := rewindRequestBody(req)
		if !ok {
			return nil, err
		}
		req = retryReq
	}

	tlsConn, proto, err := t.dial(req.Context(), host, addr)
	if err != nil {
		return nil, err
	}

	if proto != "h2" {
		// HTTP/1.1: single-use conn closed once the body is drained.
		transport := &http.Transport{
			DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
				return tlsConn, nil
			},
			DisableKeepAlives: true,
		}
		return transport.RoundTrip(req)
	}

	cc, err := t.h2.NewClientConn(tlsConn)
	if err != nil {
		tlsConn.Close()
		return nil, err
	}
	pooled := t.storeConn(addr, cc)
	return pooled.RoundTrip(req)
}

// rewindRequestBody returns a request whose body can be sent again after a
// failed attempt on a pooled connection: bodyless requests as-is, requests
// with GetBody with a rebuilt body, anything else not ok.
func rewindRequestBody(req *http.Request) (*http.Request, bool) {
	if req.Body == nil {
		return req, true
	}
	if req.GetBody == nil {
		return nil, false
	}
	body, err := req.GetBody()
	if err != nil {
		return nil, false
	}
	retryReq := req.Clone(req.Context())
	retryReq.Body = body
	return retryReq, true
}

// dial opens a TCP connection and completes the Chrome-fingerprint TLS handshake,
// returning the connection and negotiated ALPN protocol.
func (t *utlsTransport) dial(ctx context.Context, host, addr string) (*utls.UConn, string, error) {
	conn, err := dialWithDoHFallback(ctx, t.dialer, "tcp", addr)
	if err != nil {
		return nil, "", err
	}

	opts := GetNetworkCompatibilityOptions()
	tlsConn := utls.UClient(conn, &utls.Config{
		RootCAs:            supplementalRootCAs(),
		InsecureSkipVerify: opts.InsecureTLS,
		ServerName:         host,
		NextProtos:         []string{"h2", "http/1.1"},
		ClientSessionCache: utlsSessionCache,
	}, utls.HelloChrome_Auto)

	if err := tlsConn.Handshake(); err != nil {
		conn.Close()
		return nil, "", err
	}
	return tlsConn, tlsConn.ConnectionState().NegotiatedProtocol, nil
}

func (t *utlsTransport) cachedConn(addr string) pooledHTTP2ClientConn {
	t.mu.Lock()
	cc := t.conns[addr]
	if cc != nil && cc.ReserveNewRequest() {
		t.mu.Unlock()
		return cc
	}
	if cc != nil {
		delete(t.conns, addr)
	}
	t.mu.Unlock()
	if cc != nil {
		retirePooledHTTP2Conn(cc)
	}
	return nil
}

func (t *utlsTransport) invalidate(addr string, cc pooledHTTP2ClientConn) {
	t.mu.Lock()
	removed := false
	if t.conns[addr] == cc {
		delete(t.conns, addr)
		removed = true
	}
	t.mu.Unlock()
	if removed {
		retirePooledHTTP2Conn(cc)
	}
}

// storeConn caches cc, but if a concurrent dial already cached a healthy conn for
// addr it discards the freshly built cc (no in-flight requests) and returns the
// existing one, avoiding a leaked connection.
func (t *utlsTransport) storeConn(addr string, cc pooledHTTP2ClientConn) pooledHTTP2ClientConn {
	t.mu.Lock()
	if existing := t.conns[addr]; existing != nil && existing.ReserveNewRequest() {
		t.mu.Unlock()
		_ = cc.Close()
		return existing
	}
	stale := t.conns[addr]
	t.conns[addr] = cc
	_ = cc.ReserveNewRequest()
	t.mu.Unlock()
	if stale != nil {
		retirePooledHTTP2Conn(stale)
	}
	return cc
}

// retirePooledHTTP2Conn prevents new streams while allowing existing streams
// to finish. The independent watchdog also bounds Shutdown implementations
// that block before observing their context.
func pooledHTTP2RetirementTimeout(state http2.ClientConnState) time.Duration {
	if state.StreamsActive > 0 || state.StreamsPending > 0 || state.StreamsReserved > 0 {
		return 0
	}
	return 5 * Second
}

func retirePooledHTTP2Conn(conn pooledHTTP2ClientConn) {
	go func() {
		retirePooledHTTP2ConnWithTimeout(conn, pooledHTTP2RetirementTimeout(conn.State()))
	}()
}

func retirePooledHTTP2ConnWithTimeout(conn pooledHTTP2ClientConn, timeout time.Duration) {
	go func() {
		var closeOnce sync.Once
		forceClose := func() {
			closeOnce.Do(func() { _ = conn.Close() })
		}
		if timeout <= 0 {
			if err := conn.Shutdown(context.Background()); err != nil {
				forceClose()
			}
			return
		}

		watchdogDone := make(chan struct{})
		watchdog := time.AfterFunc(timeout, func() {
			forceClose()
			close(watchdogDone)
		})
		if err := conn.Shutdown(context.Background()); err != nil {
			forceClose()
		}
		if !watchdog.Stop() {
			<-watchdogDone
		}
	}()
}

// closeIdleConnections drops every pooled conn so the next request re-dials —
// needed after a network switch, where pooled conns are silently dead and the
// first request would otherwise hang on one until its timeout. Conns are shut
// down gracefully so in-flight streams finish (or fail) before the close.
func (t *utlsTransport) closeIdleConnections() {
	t.mu.Lock()
	conns := t.conns
	t.conns = make(map[string]pooledHTTP2ClientConn)
	t.mu.Unlock()
	for _, cc := range conns {
		retirePooledHTTP2Conn(cc)
	}
}

// closeUTLSIdleConnections lets platform-neutral code (CloseIdleConnections)
// reach the uTLS pool; the ios build provides a no-op stub.
func closeUTLSIdleConnections() {
	cloudflareBypassTransport.closeIdleConnections()
}

func (t *utlsTransport) getPort(u *url.URL) string {
	if u.Port() != "" {
		return u.Port()
	}
	if u.Scheme == "https" {
		return "443"
	}
	return "80"
}

var cloudflareBypassTransport = newUTLSTransport()

var cloudflareBypassClient = &http.Client{
	Transport: cloudflareBypassTransport,
	Timeout:   DefaultTimeout,
}

func GetCloudflareBypassClient() *http.Client {
	return cloudflareBypassClient
}

func DoRequestWithCloudflareBypass(req *http.Request) (*http.Response, error) {
	req.Header.Set("User-Agent", userAgentForURL(req.URL))

	resp, err := sharedClient.Do(req)
	if err == nil {
		if resp.StatusCode == 403 || resp.StatusCode == 503 {
			body, readErr := io.ReadAll(resp.Body)
			resp.Body.Close()

			if readErr == nil {
				bodyStr := strings.ToLower(string(body))
				cloudflareMarkers := []string{
					"cloudflare", "cf-ray", "checking your browser",
					"please wait", "ddos protection", "ray id",
					"enable javascript", "challenge-platform",
				}

				isCloudflare := false
				for _, marker := range cloudflareMarkers {
					if strings.Contains(bodyStr, marker) {
						isCloudflare = true
						break
					}
				}

				if isCloudflare {
					LogDebug("HTTP", "Cloudflare detected, retrying with Chrome TLS fingerprint...")

					reqCopy := req.Clone(req.Context())
					reqCopy.Header.Set("User-Agent", userAgentForURL(reqCopy.URL))

					return cloudflareBypassClient.Do(reqCopy)
				}
			}

			return &http.Response{
				Status:     resp.Status,
				StatusCode: resp.StatusCode,
				Header:     resp.Header,
				Body:       io.NopCloser(strings.NewReader(string(body))),
			}, nil
		}
		return resp, nil
	}

	if isTLSHandshakeOrResetError(err) {
		LogDebug("HTTP", "TLS error detected, retrying with Chrome TLS fingerprint: %v", err)

		reqCopy := req.Clone(req.Context())
		reqCopy.Header.Set("User-Agent", userAgentForURL(reqCopy.URL))

		return cloudflareBypassClient.Do(reqCopy)
	}

	CheckAndLogISPBlocking(err, req.URL.String(), "HTTP")
	return nil, err
}
