package v2raywebsocket

import (
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/option"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"

	"github.com/gorilla/websocket"
)

var _ adapter.V2RayClientTransport = (*Client)(nil)

type Client struct {
	dialer              *websocket.Dialer
	uri                 string
	headers             http.Header
	maxEarlyData        uint32
	earlyDataHeaderName string
}

func NewClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayWebsocketOptions, tlsConfig *tls.Config) adapter.V2RayClientTransport {
	wsDialer := &websocket.Dialer{
		NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
			return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
		},
		TLSClientConfig:  tlsConfig,
		ReadBufferSize:   4 * 1024,
		WriteBufferSize:  4 * 1024,
		HandshakeTimeout: time.Second * 8,
	}
	var uri url.URL
	if tlsConfig == nil {
		uri.Scheme = "ws"
	} else {
		uri.Scheme = "wss"
	}
	uri.Host = serverAddr.String()
	uri.Path = options.Path
	if !strings.HasPrefix(uri.Path, "/") {
		uri.Path = "/" + uri.Path
	}
	headers := make(http.Header)
	for key, value := range options.Headers {
		headers.Set(key, value)
	}
	return &Client{
		wsDialer,
		uri.String(),
		headers,
		options.MaxEarlyData,
		options.EarlyDataHeaderName,
	}
}

func (c *Client) DialContext(ctx context.Context) (net.Conn, error) {
	if c.maxEarlyData <= 0 {
		conn, response, err := c.dialer.DialContext(ctx, c.uri, c.headers)
		if err == nil {
			return &WebsocketConn{Conn: conn}, nil
		}
		return nil, wrapDialError(response, err)
	} else {
		return &EarlyWebsocketConn{Client: c, create: make(chan struct{})}, nil
	}
}

func wrapDialError(response *http.Response, err error) error {
	if response == nil {
		return err
	}
	return E.Extend(err, "HTTP ", response.StatusCode, " ", response.Status)
}