// Copyright (c) 2020 RethinkDNS and its authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
//     Copyright 2019 The Outline Authors
//
//     Licensed under the Apache License, Version 2.0 (the "License");
//     you may not use this file except in compliance with the License.
//     You may obtain a copy of the License at
//
//          http://www.apache.org/licenses/LICENSE-2.0
//
//     Unless required by applicable law or agreed to in writing, software
//     distributed under the License is distributed on an "AS IS" BASIS,
//     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//     See the License for the specific language governing permissions and
//     limitations under the License.

package doh

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/http/httptrace"
	"net/netip"
	"net/url"
	"strconv"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	x "github.com/celzero/firestack/intra/backend"
	"github.com/celzero/firestack/intra/core"
	"github.com/celzero/firestack/intra/dialers"
	"github.com/celzero/firestack/intra/dnsx"
	"github.com/celzero/firestack/intra/ipn"
	"github.com/celzero/firestack/intra/log"
	"github.com/celzero/firestack/intra/protect"
	"github.com/celzero/firestack/intra/settings"
	"github.com/celzero/firestack/intra/xdns"
	"github.com/cloudflare/odoh-go"
	"github.com/miekg/dns"
)

const dohmimetype = "application/dns-message"

const DohPortU16 = uint16(443)

const maxEOFTries = uint8(2)

const purgethreshold = 1 * time.Minute

const echRetryPeriod = 8 * time.Hour

var errNoClient error = errors.New("no doh client")

type odohtransport struct {
	omu              sync.RWMutex // protects odohConfig
	odohproxyurl     string       // proxy url
	odohproxyname    string       // proxy hostname
	odohproxyport    uint16       // proxy port
	odohtargetname   string       // target hostname
	odohtargetpath   string       // target path
	odohConfig       *odoh.ObliviousDoHConfig
	odohConfigExpiry time.Time
	preferWK         bool // prefer .well-known over svcb/https probe
}

// TODO: Keep a context here so that queries can be canceled.
type transport struct {
	*odohtransport // stackoverflow.com/a/28505394
	ctx            context.Context
	done           context.CancelFunc
	id             string
	typ            string // dnsx.DOH / dnsx.ODOH
	url            string // endpoint URL
	hostname       string // endpoint hostname
	port           uint16
	skipTLSVerify  bool                        // skips tls verification
	tlsconfig      *tls.Config                 // preset tlsconfig for the endpoint
	echconfig      *core.Volatile[*tls.Config] // echconfig for the endpoint; may be nil
	echrejects     atomic.Uint32               // number of running ech rejections
	echlastattempt *core.Volatile[time.Time]   // last attempt fetching ech cfg
	pxcmu          sync.RWMutex                // protects pxclients
	pxclients      map[string]*proxytransport  // todo: use weak pointers for Proxy
	lastpurge      *core.Volatile[time.Time]   // last scrubbed time for stale pxclients
	preferGET      bool                        // saw 405 Method Not Allowed
	proxies        ipn.ProxyProvider           // proxy provider, may be nil
	relay          string                      // dial doh via relay, may be empty
	status         *core.Volatile[int]
	est            core.P2QuantileEstimator
}

var _ dnsx.Transport = (*transport)(nil)

// NewTransport returns a POST-only DoH transport.
// `id` identifies this transport.
// `rawurl` is the DoH template in string form.
// `addrs` is a list of IP addresses to bootstrap dialers.
// `px` is the proxy provider, may be nil (eg for id == dnsx.Default)
func NewTransport(ctx context.Context, id, rawurl string, addrs []string, px ipn.ProxyProvider) (*transport, error) {
	return newTransport(ctx, dnsx.DOH, id, rawurl, "", addrs, px)
}

// NewTransport returns a POST-only Oblivious DoH transport.
// `id` identifies this transport.
// `endpoint` is the ODoH proxy that liaisons with the target.
// `target` is the ODoH resolver.
// `addrs` is a list of IP addresses to bootstrap endpoint dialers.
// `px` is the proxy provider, never nil.
func NewOdohTransport(ctx context.Context, id, endpoint, target string, addrs []string, px ipn.ProxyProvider) (*transport, error) {
	return newTransport(ctx, dnsx.ODOH, id, endpoint, target, addrs, px)
}

func newTransport(ctx context.Context, typ, id, rawurl, otargeturl string, addrs []string, px ipn.ProxyProvider) (*transport, error) {
	isodoh := typ == dnsx.ODOH

	var renewed bool
	var relay string
	if id != dnsx.Bootstrap && px != nil {
		if p, _ := px.ProxyFor(id); p != nil {
			relay = p.ID().V()
		}
	}

	ctx, done := context.WithCancel(ctx)

	t := &transport{
		ctx:            ctx,
		done:           done,
		id:             id,
		typ:            typ,
		proxies:        px,    // may be nil
		relay:          relay, // may be empty
		status:         core.NewVolatile(dnsx.Start),
		pxclients:      make(map[string]*proxytransport),
		echconfig:      core.NewZeroVolatile[*tls.Config](),
		echlastattempt: core.NewZeroVolatile[time.Time](),
		lastpurge:      core.NewVolatile(time.Now()),
		est:            core.NewP50Estimator(ctx),
	}
	if !isodoh {
		parsedurl, err := url.Parse(rawurl)
		if err != nil {
			return nil, err
		}
		// use of "http" is an indication to turn-off TLS verification
		// for, odoh rawurl represents a proxy, which can operate on http
		if parsedurl.Scheme == "http" {
			log.I("doh: disabling tls verification for %s", rawurl)
			parsedurl.Scheme = "https"
			t.skipTLSVerify = true
		}
		if parsedurl.Scheme != "https" {
			return nil, fmt.Errorf("unsupported scheme %s", parsedurl.Scheme)
		}
		// for odoh, rawurl represents a proxy, which is optional
		if len(parsedurl.Hostname()) == 0 {
			return nil, fmt.Errorf("no hostname in %s", rawurl)
		}
		t.url = parsedurl.String()
		t.hostname = parsedurl.Hostname()
		t.port = DohPortU16
		if port, _ := strconv.ParseUint(parsedurl.Port(), 10, 16); port > 0 {
			t.port = uint16(port)
		}
		// addrs are pre-determined ip addresses for url / hostname
		renewed = dnsx.RegisterAddrs(t.id, t.hostname, addrs)
	} else {
		t.odohtransport = &odohtransport{}

		proxy := rawurl // may be empty
		configurl, err := url.Parse(odohconfigdns)
		if err != nil || configurl == nil || configurl.Hostname() == "" {
			return nil, core.JoinErr(errNoOdohConfigUrl, err)
		}
		targeturl, err := url.Parse(otargeturl)
		if err != nil || targeturl == nil || targeturl.Hostname() == "" {
			return nil, core.JoinErr(errNoOdohTarget, err)
		}
		proxyurl, _ := url.Parse(proxy) // ignore err as proxy may be empty

		// addrs are proxy addresses if proxy is not empty, otherwise target addresses
		if proxyurl != nil && proxyurl.Hostname() != "" {
			renewed = dnsx.RegisterAddrs(id, proxyurl.Hostname(), addrs)
			if len(proxyurl.Path) <= 1 { // should not be "" or "/"
				proxyurl.Path = odohproxypath // default: "/proxy"
			}
			t.odohproxyurl = proxyurl.String()
			t.odohproxyname = proxyurl.Hostname()
			t.odohproxyport = DohPortU16
			if port, _ := strconv.ParseUint(proxyurl.Port(), 10, 16); port > 0 {
				t.odohproxyport = uint16(port)
			}
		} else {
			renewed = dnsx.RegisterAddrs(id, targeturl.Hostname(), addrs)
		}

		t.url = configurl.String()        // odohconfigdns
		t.hostname = configurl.Hostname() // 1.1.1.1
		t.port = DohPortU16               // TODO: grab port from configUrl
		t.odohtargetname = targeturl.Hostname()
		if len(targeturl.Path) > 1 { // should not be "" or "/"
			t.odohtargetpath = targeturl.Path
		} else {
			t.odohtargetpath = odohtargetpath // default: "/dns-query"
		}
		log.I("doh: ODOH for %s -> %s", proxy, otargeturl)
	}

	echcfg := t.getOrCreateEchConfigIfNeeded()

	// TODO: ClientAuth
	// Supply a client certificate during TLS handshakes.
	// if auth != nil {
	// 	signer := newClientAuthWrapper(auth)
	// 	t.tlsconfig = &tls.Config{
	// 		GetClientCertificate: signer.GetClientCertificate,
	// 		ServerName:           t.hostname,
	// 	}
	// }

	t.tlsconfig = &tls.Config{
		InsecureSkipVerify: t.skipTLSVerify,
		MinVersion:         tls.VersionTLS12,
		// SNI (hostname) must always be inferred from http-request
		// ServerName:         t.hostname,
		SessionTicketsDisabled: false,
		ClientSessionCache:     core.TlsSessionCache(),
	}

	log.I("doh: new transport(%s): %s; relay? %t; addrs? %v; resolved? %t, ech? %t",
		t.typ, t.url, len(relay) > 0, addrs, renewed, echcfg != nil)
	return t, nil
}

type proxytransport struct {
	p  ipn.Proxy
	c  *http.Client
	c3 *http.Client
}

func (t *transport) ech() []byte {
	// host http.Client connects to may change on redirects
	name := t.hostname
	if t.typ == dnsx.ODOH && len(t.odohproxyname) > 0 {
		name = t.odohproxyname
	}
	if len(name) <= 0 {
		return nil
	} else if v, err := dialers.ECH(name); err != nil {
		log.W("doh: ech(%s): %v", name, err)
		return nil
	} else {
		log.V("doh: ech(%s): sz %d", name, len(v))
		return v
	}
}

func (t *transport) echVerifyFn() func(tls.ConnectionState) error {
	if t.skipTLSVerify {
		return func(info tls.ConnectionState) error {
			log.V("doh: skip ech verify for %s via %s", t.hostname, info.ServerName)
			return nil // never reject
		}
	}
	return nil // delegate to stdlib
}

func h2(d protect.DialFn, c *tls.Config) *http.Transport {
	return &http.Transport{
		Dial:                d,
		ForceAttemptHTTP2:   true,
		IdleConnTimeout:     3 * time.Minute,
		TLSHandshakeTimeout: 7 * time.Second,
		// Android's DNS-over-TLS sets it to 30s
		ResponseHeaderTimeout: 20 * time.Second,
		// SNI (hostname) must always be inferred from http-request
		TLSClientConfig: c,
	}
}

// always called from a go-routine
func (t *transport) purgeProxyClients() {
	lastpurge := t.lastpurge.Load()
	if time.Since(lastpurge) <= purgethreshold {
		return
	}
	if ok := t.lastpurge.Cas(lastpurge, time.Now()); !ok {
		log.I("doh: purge proxy clients: race...")
		return
	}
	t.pxcmu.Lock()
	defer t.pxcmu.Unlock()
	for id, pxtr := range t.pxclients {
		if pxtr == nil {
			continue
		} else if pxtr.p == nil {
			delete(t.pxclients, id)
			continue
		} else if orig, err := t.proxies.ProxyFor(id); err != nil {
			delete(t.pxclients, id)
			log.W("doh: purge proxy clients: %s %v", id, err)
			continue
		} else {
			diff := pxtr.p != orig
			note := log.V
			if diff {
				note = log.I
				delete(t.pxclients, id)
				continue
			}
			note("doh: purge proxy clients: remove? %t %s", diff, id)
		}
	}
}

func (t *transport) getOrCreateEchConfigIfNeeded() *tls.Config {
	echcfg := t.echconfig.Load()
	if echcfg != nil {
		return echcfg
	}

	prev := t.echlastattempt.Load()
	if time.Since(prev) < echRetryPeriod {
		return nil
	}
	refetch := t.echlastattempt.Cas(prev, time.Now())
	if !refetch {
		return nil
	}

	if ech := t.ech(); len(ech) > 0 {
		echcfg = &tls.Config{
			InsecureSkipVerify:                  t.skipTLSVerify,
			MinVersion:                          tls.VersionTLS13, // must be 1.3
			EncryptedClientHelloConfigList:      ech,
			SessionTicketsDisabled:              false,
			ClientSessionCache:                  core.TlsSessionCache(),
			EncryptedClientHelloRejectionVerify: t.echVerifyFn(),
		}
		t.echconfig.Store(echcfg)
	}

	ok := echcfg == nil
	logeif(!ok)("doh: %s fetch ech... ok? %t", t.ID(), ok)
	return echcfg
}

func (t *transport) httpClientsFor(p ipn.Proxy) (c3, c *http.Client) {
	pid := p.ID().V()
	t.pxcmu.RLock()
	pxtr, ok := t.pxclients[pid]
	same := pxtr != nil && pxtr.p.Handle() == p.Handle()
	if ok && same {
		c = pxtr.c
		c3 = pxtr.c3
	}
	t.pxcmu.RUnlock()

	pdial := p.Dialer().Dial
	if c != nil {
		if c3 == nil {
			if echcfg := t.getOrCreateEchConfigIfNeeded(); echcfg != nil {
				c3 = new(http.Client)
				c3.Transport = h2(pdial, echcfg)
				t.updateHttpClientsFor(p, c, c3)
			}
		}
		return c3, c // c3 may be nil
	}

	var client http.Client
	var client3 *http.Client
	client.Transport = h2(pdial, t.tlsconfig)
	if echcfg := t.echconfig.Load(); echcfg != nil {
		client3 = new(http.Client)
		client3.Transport = h2(pdial, echcfg)
	}

	// last writer wins
	t.updateHttpClientsFor(p, &client, client3)

	// check if other proxies need to be purged
	go t.purgeProxyClients()

	return client3, &client
}

// updateHttpClientsFor only updates non-nil http clients dialing via Proxy p.
func (t *transport) updateHttpClientsFor(p ipn.Proxy, c, c3 *http.Client) {
	if c == nil && c3 == nil {
		log.E("doh: %s cannot set/update, all clients nil", t.ID())
		return
	}

	pid := p.ID().V()

	t.pxcmu.Lock()
	defer t.pxcmu.Unlock()

	if pt := t.pxclients[pid]; pt != nil {
		if c != nil {
			pt.c = c
		}
		if c3 != nil {
			pt.c3 = c3
		}
	} else {
		if c == nil {
			log.E("doh: %s cannot set http client to nil", t.ID())
			return
		}
		t.pxclients[pid] = &proxytransport{
			p:  p,
			c:  c,
			c3: c3, // may be nil
		}
	}
}

// Given a raw DNS query (including the query ID), this function sends the
// query.  If the query is successful, it returns the response and a nil qerr.  Otherwise,
// it returns a SERVFAIL response and a qerr with a status value indicating the cause.
// Independent of the query's success or failure, this function also returns the
// address of the server on a best-effort basis, or nil if the address could not
// be determined.
func (t *transport) doDoh(pid string, q *dns.Msg) (response *dns.Msg, rpid, blocklists, region string, ech bool, elapsed time.Duration, qerr *dnsx.QueryError) {
	start := time.Now()
	if qerr = dnsx.WillErr(t); qerr != nil {
		return
	}

	padQuery(q)
	// zero out the query id
	id := q.Id
	q.Id = 0

	req, err := t.asDohRequest(q)
	if err != nil {
		log.D("doh: failed to create request: %v", err)
		elapsed = time.Since(start)
		qerr = dnsx.NewInternalQueryError(err)
		return
	}

	response, rpid, blocklists, region, ech, elapsed, qerr = t.send(pid, req)

	// restore dns query id
	q.Id = id
	if response != nil {
		response.Id = id
	} else { // override response with servfail
		response = xdns.Servfail(q)
	}
	return
}

func (t *transport) fetch(pid string, req *http.Request) (*http.Response, string, bool, error) {
	ustr := req.URL.String()

	uerr := func(e error) *url.Error {
		if e == nil {
			return nil
		}
		if e, ok := e.(*url.Error); ok {
			return e
		}
		return &url.Error{
			Op:  req.Method,
			URL: ustr,
			Err: e,
		}
	}

	r, rpid, echdialer, err := t.multifetch(req, pid)
	if err != nil {
		log.W("doh: fetch: %s, mayech? %t / echdialer? %t, err: %v",
			ustr, t.echconfig.Load() != nil, echdialer, err)
		return r, rpid, echdialer, uerr(err)
	}
	return r, rpid, echdialer, nil
}

func (t *transport) multifetch(req *http.Request, pid string) (res *http.Response, rpid string, echdialer bool, err error) {
	px, err := t.prepare(pid)
	if err != nil || px == nil {
		return nil, "", false, core.OneErr(err, dnsx.ErrNoProxyProvider)
	}

	rpid = ipn.ViaID(px)
	c3, c0 := t.httpClientsFor(px) // c3 may be nil

	if settings.Debug {
		log.VV("doh: using proxy %s+%s:%s ech? %t / other? %t",
			px.ID(), rpid, px.GetAddr(), c3 != nil, c0 != nil)
	}

	clients := []*http.Client{c3, c0}

	var cont, sent bool
	for _, c := range clients {
		if c == nil { // c may be nil (ex: if no ech)
			continue
		}
		cont = true
		sent = false
		for i := uint8(0); cont && i < maxEOFTries; i++ {
			cont = false
			sent = true
			if res, err = c.Do(req); err == nil {
				return res, rpid, c == c3, nil // res is never nil here
			}
			if eerr := new(tls.ECHRejectionError); errors.As(err, &eerr) {
				cont = true
				ech := eerr.RetryConfigList

				echcfg := t.echconfig.Load()
				useech := echcfg != nil
				if len(ech) <= 0 && useech {
					ech = t.ech() // todo: use t.echconfig.ServerName?
					log.I("doh: fetch #%d: err %v; grab new ech? %t",
						i, eerr, len(ech) > 0)
				}
				if len(ech) > 0 && useech {
					echcfg.EncryptedClientHelloConfigList = ech
					c.Transport = h2(px.Dialer().Dial, echcfg)
					t.echconfig.Store(echcfg) // update ech config
					t.echlastattempt.Store(time.Now())
					t.updateHttpClientsFor(px, nil, c) // update c3
				}
				n := t.echrejects.Add(1)
				log.I("doh: fetch #%d: ech rejected; retry? %t, ech? %t; total rejects: %d",
					i, len(ech) > 0, useech, n)
			} else if uerr, ok := err.(*url.Error); ok {
				eof := uerr.Err == io.EOF
				if eof && res != nil {
					log.D("doh: fetch #%d: EOF; but res exists! %t", i)
					return res, rpid, c == c3, nil
				} // continue if EOF
				cont = eof || uerr.Err == io.ErrUnexpectedEOF
			} // terminate if not EOF
			log.W("doh: fetch #%d (cont? %t) px: %s[%s]; err: %v", i, cont, pid, rpid, err)
		}
	}
	if !sent && err == nil { // should never happen
		log.E("doh: fetch: no client sent request %d", len(clients))
	}
	return nil, rpid, false, core.OneErr(err, errNoClient)
}

func (t *transport) prepare(pid string) (px ipn.Proxy, err error) {
	userelay := len(t.relay) > 0
	hasproxy := t.proxies != nil
	useproxy := len(pid) != 0 // if pid == dnsx.NetNoProxy, then px is ipn.Block
	useech := t.echconfig.Load() != nil

	if userelay || useproxy {
		if userelay { // relay takes precedence
			pid = t.relay
		}
		if hasproxy {
			px, err = t.proxies.ProxyFor(pid)
		} else {
			err = dnsx.ErrNoProxyProvider
		}
	} else {
		err = dnsx.ErrNoProxyProvider
		log.W("doh: no proxy %s ech? %t; err: %v", pid, useech, err)
	}
	return
}

func (t *transport) do(pid string, req *http.Request) (ans []byte, rpid, blocklists, region string, withech bool, elapsed time.Duration, qerr *dnsx.QueryError) {
	var server net.Addr
	var conn net.Conn
	start := time.Now()
	// either t.hostname or t.odohtargetname or t.odohproxy
	hostname := req.URL.Hostname()

	// Error cleanup function.  If the query fails, this function will close the
	// underlying socket and disconfirm the server IP.  Empirically, sockets often
	// become unresponsive after a network change, causing timeouts on all requests.
	defer func() {
		elapsed = time.Since(start)

		// server addr would be of relay / proxy (ex: 127.0.0.1:9050) if used
		usedrelay := len(t.relay) > 0
		usedproxy := !dnsx.IsLocalProxy(pid) // pid == dnsx.NetNoProxy => ipn.Block
		hasserveraddr := server != nil && !usedrelay && !usedproxy

		if hostname != t.hostname {
			log.I("doh: redirected %s => %s", t.hostname, hostname)
			t.hostname = hostname
		}
		if hasserveraddr {
			if qerr == nil {
				// record a working IP address for this server
				dialers.Confirm3(hostname, server)
				return
			} else {
				ok := dialers.Disconfirm3(hostname, server)
				log.D("doh: disconfirming %s, %s done? %t", hostname, server, ok)
			}
		}
		if qerr != nil {
			log.I("doh: close failing doh conn %s; why? %v", hostname, qerr)
			core.CloseConn(conn)
		}
	}()

	// Add a trace to the request in order to expose the server's IP address.
	// Only GotConn performs any action; the other methods just provide debug logs.
	// GotConn runs before client.Do() returns, so there is no data race when
	// reading the variables it has set.
	trace := httptrace.ClientTrace{
		GotConn: func(info httptrace.GotConnInfo) {
			log.V("doh: got-conn(%v)", info)
			if info.Conn == nil {
				return
			}
			conn = info.Conn
			// info.Conn is a DuplexConn, so RemoteAddr is actually a TCPAddr.
			// if the conn is proxied, then RemoteAddr is that of the proxy
			server = conn.RemoteAddr()
		},
		ConnectStart: func(network, addr string) {
			start = time.Now() // re...start
			log.VV("doh: connect-start(%s, %s)", network, addr)
		},
		TLSHandshakeDone: func(state tls.ConnectionState, err error) {
			log.VV("doh: %s tls%d (resumed? %t, done? %t, ech? %t); err? %v",
				state.ServerName, state.Version, state.DidResume, state.HandshakeComplete, state.ECHAccepted, err)
			withech = state.ECHAccepted
		},
		WroteRequest: func(info httptrace.WroteRequestInfo) {
			log.VV("doh: wrote-req(%v)", info)
		},
	}
	req = req.WithContext(httptrace.WithClientTrace(req.Context(), &trace))

	log.VV("doh: sending query to: %s", t.hostname)

	res, rpid, echdialer, err := t.fetch(pid, req)

	withech = withech || echdialer

	if err != nil || res == nil {
		qerr = dnsx.NewSendFailedQueryError(err)
		return
	}

	blocklists, region = t.rdnsHeaders(&res.Header)
	// todo: check if content-type is [doh|odoh] mime type

	ans, err = io.ReadAll(res.Body)
	if err != nil {
		qerr = dnsx.NewSendFailedQueryError(err)
		return
	}
	core.Close(res.Body)
	if settings.Debug {
		log.V("doh: closed response of sz %d; used ech? %t", len(ans), withech)
	}

	// update the hostname, which could have changed due to a redirect
	// for ex, 1.1.1.1 or cloudflare-dns.com => one.one.one.one
	hostname = res.Request.URL.Hostname()

	sc := res.StatusCode
	if sc != http.StatusOK { // 4xx
		if sc >= http.StatusBadRequest && sc < http.StatusInternalServerError {
			qerr = dnsx.NewClientQueryError(fmt.Errorf("http-status: %d", sc))
		} else {
			qerr = dnsx.NewTransportQueryError(fmt.Errorf("http-status: %d", sc))
		}
		if !t.preferGET { // flip on 404 or 405; then remain on GET
			t.preferGET = sc == http.StatusMethodNotAllowed || sc == http.StatusNotFound
		}
		return
	}

	return
}

func (t *transport) send(pid string, req *http.Request) (msg *dns.Msg, rpid, blocklists, region string, ech bool, elapsed time.Duration, qerr *dnsx.QueryError) {
	var ans []byte
	var err error
	ans, rpid, blocklists, region, ech, elapsed, qerr = t.do(pid, req)
	if qerr != nil {
		return
	}
	msg, err = xdns.AsMsg2(ans)
	if msg == nil {
		qerr = dnsx.NewBadResponseQueryError(fmt.Errorf("parse err: %v", err))
		return
	}
	return
}

func (t *transport) rdnsHeaders(h *http.Header) (blocklistStamp, region string) {
	if h == nil { // should not be nil
		return
	}
	blocklistStamp = h.Get(xdns.GetBlocklistStampHeaderKey())
	// X-Nile-Region:[sin]
	region = h.Get(xdns.GetRethinkDNSRegionHeaderKey1())
	if len(region) <= 0 {
		// Cf-Ray:[d1e2a3d4b5e6e7f8-SIN]
		if ck := h.Get(xdns.GetRethinkDNSRegionHeaderKey2()); len(ck) > 0 {
			_, region, _ = strings.Cut(ck, "-")
		}
	}
	// too long:
	// log.VV("doh: header %s; region %s; stamp %v", h, region, blocklistStamp)
	return
}

func (t *transport) asDohRequest(msg *dns.Msg) (req *http.Request, err error) {
	var q []byte
	q, err = msg.Pack()
	if err != nil {
		return
	}
	if t.preferGET {
		url := t.url + "?dns=" + base64.RawURLEncoding.EncodeToString(q)
		req, err = http.NewRequest(http.MethodGet, url, nil)
	} else {
		req, err = http.NewRequest(http.MethodPost, t.url, bytes.NewBuffer(q))
	}
	if err != nil {
		return
	}
	req.Header.Set("content-type", dohmimetype)
	req.Header.Set("accept", dohmimetype)
	if settings.SetUserAgent.Load() {
		req.Header.Set("user-agent", settings.IntraUa)
	}
	return
}

func (t *transport) ID() *x.Gostr {
	return x.StrOf(t.id)
}

func (t *transport) Type() *x.Gostr {
	return x.StrOf(t.typ)
}

func (t *transport) chooseProxy(pids ...string) string {
	host, port := t.hostport()
	return dnsx.ChooseHealthyProxyHostPort("doh: "+t.id, host, port, pids, t.proxies)
}

func (t *transport) hostport() (addr string, port uint16) {
	addr = t.hostname
	port = t.port
	if t.typ == dnsx.ODOH && len(t.odohproxyname) > 0 {
		addr = t.odohproxyname
		port = t.odohproxyport
	}
	return
}

func (t *transport) Query(network string, q *dns.Msg, smm *x.DNSSummary) (r *dns.Msg, err error) {
	var rpid, pid, blocklists, region string
	var ech bool
	var elapsed time.Duration
	var qerr *dnsx.QueryError

	if t.id == dnsx.Bootstrap { // bootstrap/default never be proxied
		pid = dnsx.NetBaseProxy
	} else if r := t.relay; len(r) > 0 {
		pid = t.chooseProxy(r)
	} else {
		_, pids := xdns.Net2ProxyID(network)
		pid = t.chooseProxy(pids...)
	}

	if t.typ == dnsx.DOH {
		r, rpid, blocklists, region, ech, elapsed, qerr = t.doDoh(pid, q)
	} else {
		r, ech, elapsed, qerr = t.doOdoh(pid, q)
	}

	smm.Server = t.getAddr()
	if ech {
		smm.Server = dnsx.EchPrefix + smm.Server
	}

	status := dnsx.Complete

	if qerr != nil {
		status = qerr.Status()
		err = qerr.Unwrap()
	}
	t.status.Store(status)

	t.est.Add(elapsed.Seconds())
	smm.Latency = elapsed.Seconds()
	smm.RData = xdns.GetInterestingRData(r)
	smm.RCode = xdns.Rcode(r)
	smm.RTtl = xdns.RTtl(r)
	smm.Status = status
	smm.Region = region
	smm.Blocklists = blocklists
	if t.typ == dnsx.ODOH && len(t.odohproxyname) > 0 {
		smm.PID = t.odohproxyname // odoh proxy
		smm.RPID = pid            // other proxy, if any
	} else {
		smm.PID = pid   // proxy, if any
		smm.RPID = rpid // hopping proxy, if any
	}
	if err != nil {
		smm.Msg = err.Error()
	}
	if settings.Debug {
		log.V("doh: (p/px/via %s/%s/%s); a:%d/sz:%d/pad:%d, q: %s:%d, data: %s, code: %d, via: %s, err? %v",
			network, pid, rpid, xdns.Len(r), xdns.Size(r), xdns.EDNS0PadLen(r), smm.QName, smm.QType, smm.RData, smm.RCode, smm.PID, err)
	}
	return r, err
}

func (t *transport) P50() int64 {
	return t.est.Get()
}

func (t *transport) GetAddr() *x.Gostr {
	return x.StrOf(t.getAddr())
}

func (t *transport) getAddr() string {
	addr := t.hostname
	if t.typ == dnsx.ODOH {
		addr = t.odohtargetname
	}

	if t.skipTLSVerify {
		addr = dnsx.NoPkiPrefix + addr
	}
	// doh transports could be "dnsx.Bootstrap"
	prefix := dnsx.PrefixFor(t.id)
	if len(prefix) > 0 {
		addr = prefix + addr
	}
	return addr
}

func (t *transport) GetRelay() x.Proxy {
	if r := t.relay; len(r) > 0 {
		px, _ := t.proxies.ProxyFor(r)
		return px
	}
	return nil
}

func (t *transport) IPPorts() (ipps []netip.AddrPort) {
	addr := t.hostname
	port := t.port
	if t.typ == dnsx.ODOH && len(t.odohproxyname) > 0 {
		addr = t.odohproxyname
		port = t.odohproxyport
	}
	for _, ip := range dialers.For(addr) {
		ipps = append(ipps, netip.AddrPortFrom(ip, port))
	}
	return // may be nil
}

func (t *transport) Status() int {
	if px := t.GetRelay(); px != nil {
		if px.Status() == ipn.TPU { // relay paused => transport paused
			return dnsx.Paused
		}
	}
	return t.status.Load()
}

func (t *transport) Stop() error {
	t.status.Store(dnsx.DEnd)
	t.done()
	return nil
}

func logeif(cond bool) log.LogFn {
	if cond {
		return log.E
	}
	return log.D
}
