package tailnet

import (
	"bufio"
	"context"
	"log"
	"net/http"
	"strings"
	"sync"

	"nhooyr.io/websocket"
	"tailscale.com/derp"
	"tailscale.com/net/wsconn"
)

// WithWebsocketSupport returns an http.Handler that upgrades
// connections to the "derp" subprotocol to WebSockets and
// passes them to the DERP server.
// Taken from: https://github.com/tailscale/tailscale/blob/e3211ff88ba85435f70984cf67d9b353f3d650d8/cmd/derper/websocket.go#L21
func WithWebsocketSupport(s *derp.Server, base http.Handler) (http.Handler, func()) {
	var mu sync.Mutex
	var waitGroup sync.WaitGroup
	ctx, cancelFunc := context.WithCancel(context.Background())

	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			up := strings.ToLower(r.Header.Get("Upgrade"))

			// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
			// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
			// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
			if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
				base.ServeHTTP(w, r)
				return
			}

			mu.Lock()
			if ctx.Err() != nil {
				mu.Unlock()
				return
			}
			waitGroup.Add(1)
			mu.Unlock()
			defer waitGroup.Done()
			c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
				Subprotocols:   []string{"derp"},
				OriginPatterns: []string{"*"},
				// Disable compression because we transmit WireGuard messages that
				// are not compressible.
				// Additionally, Safari has a broken implementation of compression
				// (see https://github.com/nhooyr/websocket/issues/218) that makes
				// enabling it actively harmful.
				CompressionMode: websocket.CompressionDisabled,
			})
			if err != nil {
				log.Printf("websocket.Accept: %v", err)
				return
			}
			defer c.Close(websocket.StatusInternalError, "closing")
			if c.Subprotocol() != "derp" {
				c.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol")
				return
			}
			wc := wsconn.NetConn(ctx, c, websocket.MessageBinary)
			brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
			s.Accept(ctx, wc, brw, r.RemoteAddr)
		}), func() {
			cancelFunc()
			mu.Lock()
			waitGroup.Wait()
			mu.Unlock()
		}
}
