package option

import (
	"net/http"
	"strings"

	Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
	"github.com/sagernet/sing-box/common/xray/utils"
	C "github.com/sagernet/sing-box/constant"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/json"
	"github.com/sagernet/sing/common/json/badjson"
	"github.com/sagernet/sing/common/json/badoption"
)

type _V2RayTransportOptions struct {
	Type               string                  `json:"type"`
	HTTPOptions        V2RayHTTPOptions        `json:"-"`
	WebsocketOptions   V2RayWebsocketOptions   `json:"-"`
	QUICOptions        V2RayQUICOptions        `json:"-"`
	GRPCOptions        V2RayGRPCOptions        `json:"-"`
	HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
	XHTTPOptions       V2RayXHTTPOptions       `json:"-"`
	KCPOptions         V2RayKCPOptions         `json:"-"`
}

type V2RayTransportOptions _V2RayTransportOptions

func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
	var v any
	switch o.Type {
	case C.V2RayTransportTypeHTTP:
		v = o.HTTPOptions
	case C.V2RayTransportTypeWebsocket:
		v = o.WebsocketOptions
	case C.V2RayTransportTypeQUIC:
		v = o.QUICOptions
	case C.V2RayTransportTypeGRPC:
		v = o.GRPCOptions
	case C.V2RayTransportTypeHTTPUpgrade:
		v = o.HTTPUpgradeOptions
	case C.V2RayTransportTypeXHTTP:
		v = o.XHTTPOptions
	case C.V2RayTransportTypeKCP:
		v = o.KCPOptions
	case "":
		return nil, E.New("missing transport type")
	default:
		return nil, E.New("unknown transport type: " + o.Type)
	}
	return badjson.MarshallObjects(_V2RayTransportOptions(o), v)
}

func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
	err := json.Unmarshal(bytes, (*_V2RayTransportOptions)(o))
	if err != nil {
		return err
	}
	var v any
	switch o.Type {
	case C.V2RayTransportTypeHTTP:
		v = &o.HTTPOptions
	case C.V2RayTransportTypeWebsocket:
		v = &o.WebsocketOptions
	case C.V2RayTransportTypeQUIC:
		v = &o.QUICOptions
	case C.V2RayTransportTypeGRPC:
		v = &o.GRPCOptions
	case C.V2RayTransportTypeHTTPUpgrade:
		v = &o.HTTPUpgradeOptions
	case C.V2RayTransportTypeXHTTP:
		v = &o.XHTTPOptions
	case C.V2RayTransportTypeKCP:
		v = &o.KCPOptions
	default:
		return E.New("unknown transport type: " + o.Type)
	}
	err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v)
	if err != nil {
		return err
	}
	return nil
}

type V2RayHTTPOptions struct {
	Host        badoption.Listable[string] `json:"host,omitempty"`
	Path        string                     `json:"path,omitempty"`
	Method      string                     `json:"method,omitempty"`
	Headers     badoption.HTTPHeader       `json:"headers,omitempty"`
	IdleTimeout badoption.Duration         `json:"idle_timeout,omitempty"`
	PingTimeout badoption.Duration         `json:"ping_timeout,omitempty"`
}

type V2RayWebsocketOptions struct {
	Path                string               `json:"path,omitempty"`
	Headers             badoption.HTTPHeader `json:"headers,omitempty"`
	MaxEarlyData        uint32               `json:"max_early_data,omitempty"`
	EarlyDataHeaderName string               `json:"early_data_header_name,omitempty"`
}

type V2RayQUICOptions struct{}

type V2RayGRPCOptions struct {
	ServiceName         string             `json:"service_name,omitempty"`
	IdleTimeout         badoption.Duration `json:"idle_timeout,omitempty"`
	PingTimeout         badoption.Duration `json:"ping_timeout,omitempty"`
	PermitWithoutStream bool               `json:"permit_without_stream,omitempty"`
	ForceLite           bool               `json:"-"` // for test
}

type V2RayHTTPUpgradeOptions struct {
	Host    string               `json:"host,omitempty"`
	Path    string               `json:"path,omitempty"`
	Headers badoption.HTTPHeader `json:"headers,omitempty"`
}

type V2RayXHTTPBaseOptions struct {
	Host                 string                     `json:"host,omitempty"`
	Path                 string                     `json:"path,omitempty"`
	Headers              map[string]string          `json:"headers,omitempty"`
	DomainStrategy       DomainStrategy             `json:"domain_strategy,omitempty"`
	XPaddingBytes        Xbadoption.Range           `json:"x_padding_bytes"`
	NoGRPCHeader         bool                       `json:"no_grpc_header,omitempty"`
	NoSSEHeader          bool                       `json:"no_sse_header,omitempty"`
	ScMaxEachPostBytes   *Xbadoption.Range          `json:"sc_max_each_post_bytes"`
	ScMinPostsIntervalMs *Xbadoption.Range          `json:"sc_min_posts_interval_ms"`
	ScMaxBufferedPosts   int64                      `json:"sc_max_buffered_posts,omitempty"`
	ScStreamUpServerSecs *Xbadoption.Range          `json:"sc_stream_up_server_secs"`
	ServerMaxHeaderBytes int                        `json:"server_max_header_bytes"`
	TrustedXForwardedFor badoption.Listable[string] `json:"trusted_x_forwarded_for,omitempty"`
	Xmux                 *V2RayXHTTPXmuxOptions     `json:"xmux"`
	XPaddingObfsMode     bool                       `json:"x_padding_obfs_mode,omitempty"`
	XPaddingKey          string                     `json:"x_padding_key,omitempty"`
	XPaddingHeader       string                     `json:"x_padding_header,omitempty"`
	XPaddingPlacement    string                     `json:"x_padding_placement,omitempty"`
	XPaddingMethod       string                     `json:"x_padding_method,omitempty"`
	UplinkHTTPMethod     string                     `json:"uplink_http_method,omitempty"`
	SessionPlacement     string                     `json:"session_placement,omitempty"`
	SessionKey           string                     `json:"session_key,omitempty"`
	SeqPlacement         string                     `json:"seq_placement,omitempty"`
	SeqKey               string                     `json:"seq_key,omitempty"`
	UplinkDataPlacement  string                     `json:"uplink_data_placement,omitempty"`
	UplinkDataKey        string                     `json:"uplink_data_key,omitempty"`
	UplinkChunkSize      *Xbadoption.Range          `json:"uplink_chunk_size,omitempty"`
}

type _V2RayXHTTPOptions struct {
	Mode string `json:"mode"`
	V2RayXHTTPBaseOptions
	Download *V2RayXHTTPDownloadOptions `json:"download"`
}

type V2RayXHTTPOptions _V2RayXHTTPOptions

type V2RayXHTTPDownloadOptions struct {
	V2RayXHTTPBaseOptions
	ServerOptions
	OutboundTLSOptionsContainer
	Detour string `json:"detour,omitempty"`
}

const (
	PlacementQueryInHeader = "queryInHeader"
	PlacementCookie        = "cookie"
	PlacementHeader        = "header"
	PlacementQuery         = "query"
	PlacementPath          = "path"
	PlacementBody          = "body"
	PlacementAuto          = "auto"
)

func (c V2RayXHTTPOptions) MarshalJSON() ([]byte, error) {
	return json.Marshal((*_V2RayXHTTPOptions)(&c))
}

func (c *V2RayXHTTPOptions) UnmarshalJSON(bytes []byte) error {
	err := json.Unmarshal(bytes, (*_V2RayXHTTPOptions)(c))
	if err != nil {
		return err
	}
	switch c.Mode {
	case "":
		c.Mode = "auto"
	case "auto", "packet-up", "stream-up", "stream-one":
	default:
		return E.New("unsupported mode: " + c.Mode)
	}
	err = checkV2RayXHTTPBaseOptions(c.Mode, &c.V2RayXHTTPBaseOptions)
	if err != nil {
		return err
	}
	if c.Download != nil {
		err = checkV2RayXHTTPBaseOptions(c.Mode, &c.Download.V2RayXHTTPBaseOptions)
		if err != nil {
			return err
		}
	}
	return nil
}

func checkV2RayXHTTPBaseOptions(mode string, options *V2RayXHTTPBaseOptions) error {
	// Priority (client): host > serverName > address
	for k := range options.Headers {
		if strings.ToLower(k) == "host" {
			return E.New(`"headers" can't contain "host"`)
		}
	}

	if options.XPaddingBytes.From <= 0 || options.XPaddingBytes.To <= 0 {
		return E.New("x_padding_bytes cannot be disabled")
	}

	if options.XPaddingKey == "" {
		options.XPaddingKey = "x_padding"
	}

	if options.XPaddingHeader == "" {
		options.XPaddingHeader = "X-Padding"
	}

	switch options.XPaddingPlacement {
	case "":
		options.XPaddingPlacement = "queryInHeader"
	case "cookie", "header", "query", "queryInHeader":
	default:
		return E.New("unsupported padding placement: " + options.XPaddingPlacement)
	}

	switch options.XPaddingMethod {
	case "":
		options.XPaddingMethod = "repeat-x"
	case "repeat-x", "tokenish":
	default:
		return E.New("unsupported padding method: " + options.XPaddingMethod)
	}

	switch options.UplinkDataPlacement {
	case "":
		options.UplinkDataPlacement = PlacementAuto
	case PlacementAuto, PlacementBody:
	case PlacementCookie, PlacementHeader:
		if mode != "packet-up" {
			return E.New("UplinkDataPlacement can be " + options.UplinkDataPlacement + " only in packet-up mode")
		}
	default:
		return E.New("unsupported uplink data placement: " + options.UplinkDataPlacement)
	}

	if options.UplinkHTTPMethod == "" {
		options.UplinkHTTPMethod = "POST"
	}
	options.UplinkHTTPMethod = strings.ToUpper(options.UplinkHTTPMethod)

	if options.UplinkHTTPMethod == "GET" && mode != "packet-up" {
		return E.New("uplink_http_method can be GET only in packet-up mode")
	}

	switch options.SessionPlacement {
	case "":
		options.SessionPlacement = "path"
	case "path", "cookie", "header", "query":
	default:
		return E.New("unsupported session placement: " + options.SessionPlacement)
	}

	switch options.SeqPlacement {
	case "":
		options.SeqPlacement = "path"
	case "path", "cookie", "header", "query":
	default:
		return E.New("unsupported seq placement: " + options.SeqPlacement)
	}

	if options.SessionPlacement != "path" && options.SessionKey == "" {
		switch options.SessionPlacement {
		case "cookie", "query":
			options.SessionKey = "x_session"
		case "header":
			options.SessionKey = "X-Session"
		}
	}

	if options.SeqPlacement != "path" && options.SeqKey == "" {
		switch options.SeqPlacement {
		case "cookie", "query":
			options.SeqKey = "x_seq"
		case "header":
			options.SeqKey = "X-Seq"
		}
	}

	if options.UplinkDataPlacement != PlacementBody && options.UplinkDataKey == "" {
		switch options.UplinkDataPlacement {
		case PlacementCookie:
			options.UplinkDataKey = "x_data"
		case PlacementAuto, PlacementHeader:
			options.UplinkDataKey = "X-Data"
		}
	}

	if options.ServerMaxHeaderBytes < 0 {
		return E.New("invalid negative value of maxHeaderBytes")
	}

	if options.Xmux == nil {
		options.Xmux = &V2RayXHTTPXmuxOptions{}
		options.Xmux.MaxConcurrency.From = 1
		options.Xmux.MaxConcurrency.To = 1
		options.Xmux.HMaxRequestTimes.From = 600
		options.Xmux.HMaxRequestTimes.To = 900
		options.Xmux.HMaxReusableSecs.From = 1800
		options.Xmux.HMaxReusableSecs.To = 3000
	} else if options.Xmux.MaxConnections.To > 0 && options.Xmux.MaxConcurrency.To > 0 {
		return E.New("max_connections cannot be specified together with max_concurrency")
	}
	return nil
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
	pathAndQuery := strings.SplitN(c.Path, "?", 2)
	path := pathAndQuery[0]
	if path == "" || path[0] != '/' {
		path = "/" + path
	}
	if path[len(path)-1] != '/' {
		path = path + "/"
	}
	return path
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string {
	pathAndQuery := strings.SplitN(c.Path, "?", 2)
	query := ""
	if len(pathAndQuery) > 1 {
		query = pathAndQuery[1]
	}
	return query
}

func (c *V2RayXHTTPBaseOptions) GetRequestHeader() http.Header {
	header := http.Header{}
	for k, v := range c.Headers {
		header.Add(k, v)
	}
	utils.TryDefaultHeadersWith(header, "fetch")
	return header
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range {
	if c.XPaddingBytes.To == 0 {
		return Xbadoption.Range{
			From: 100,
			To:   1000,
		}
	}
	return c.XPaddingBytes
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkHTTPMethod() string {
	if c.UplinkHTTPMethod == "" {
		return "POST"
	}
	return c.UplinkHTTPMethod
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
	if c.ScMaxEachPostBytes == nil {
		return Xbadoption.Range{
			From: 1000000,
			To:   1000000,
		}
	}
	return *c.ScMaxEachPostBytes
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedScMinPostsIntervalMs() Xbadoption.Range {
	if c.ScMinPostsIntervalMs == nil {
		return Xbadoption.Range{
			From: 30,
			To:   30,
		}
	}
	return *c.ScMinPostsIntervalMs
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxBufferedPosts() int {
	if c.ScMaxBufferedPosts == 0 {
		return 30
	}

	return int(c.ScMaxBufferedPosts)
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.Range {
	if c.ScStreamUpServerSecs == nil {
		return Xbadoption.Range{
			From: 20,
			To:   80,
		}
	}
	return *c.ScStreamUpServerSecs
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkChunkSize() Xbadoption.Range {
	if c.UplinkChunkSize == nil || c.UplinkChunkSize.To == 0 {
		switch c.UplinkDataPlacement {
		case PlacementCookie:
			return Xbadoption.Range{
				From: 2 * 1024, // 2 KiB
				To:   3 * 1024, // 3 KiB
			}
		case PlacementHeader:
			return Xbadoption.Range{
				From: 3 * 1000, // 3 KB
				To:   4 * 1000, // 4 KB
			}
		default:
			return c.GetNormalizedScMaxEachPostBytes()
		}
	} else if c.UplinkChunkSize.From < 64 {
		return Xbadoption.Range{
			From: 64,
			To:   max(64, c.UplinkChunkSize.To),
		}
	}

	return *c.UplinkChunkSize
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedServerMaxHeaderBytes() int {
	if c.ServerMaxHeaderBytes <= 0 {
		return 8192
	} else {
		return c.ServerMaxHeaderBytes
	}
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionPlacement() string {
	if c.SessionPlacement == "" {
		return PlacementPath
	}
	return c.SessionPlacement
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqPlacement() string {
	if c.SeqPlacement == "" {
		return PlacementPath
	}
	return c.SeqPlacement
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedUplinkDataPlacement() string {
	if c.UplinkDataPlacement == "" {
		return PlacementBody
	}
	return c.UplinkDataPlacement
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedSessionKey() string {
	if c.SessionKey != "" {
		return c.SessionKey
	}
	switch c.GetNormalizedSessionPlacement() {
	case PlacementHeader:
		return "X-Session"
	case PlacementCookie, PlacementQuery:
		return "x_session"
	default:
		return ""
	}
}

func (c *V2RayXHTTPBaseOptions) GetNormalizedSeqKey() string {
	if c.SeqKey != "" {
		return c.SeqKey
	}
	switch c.GetNormalizedSeqPlacement() {
	case PlacementHeader:
		return "X-Seq"
	case PlacementCookie, PlacementQuery:
		return "x_seq"
	default:
		return ""
	}
}

type V2RayXHTTPXmuxOptions struct {
	MaxConcurrency   Xbadoption.Range `json:"max_concurrency"`
	MaxConnections   Xbadoption.Range `json:"max_connections"`
	CMaxReuseTimes   Xbadoption.Range `json:"c_max_reuse_times"`
	HMaxRequestTimes Xbadoption.Range `json:"h_max_request_times"`
	HMaxReusableSecs Xbadoption.Range `json:"h_max_reusable_secs"`
	HKeepAlivePeriod int64            `json:"h_keep_alive_period"`
}

func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConcurrency() Xbadoption.Range {
	return m.MaxConcurrency
}

func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConnections() Xbadoption.Range {
	return m.MaxConnections
}

func (m *V2RayXHTTPXmuxOptions) GetNormalizedCMaxReuseTimes() Xbadoption.Range {
	return m.CMaxReuseTimes
}

func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxRequestTimes() Xbadoption.Range {
	return m.HMaxRequestTimes
}

func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxReusableSecs() Xbadoption.Range {
	return m.HMaxReusableSecs
}

type V2RayKCPOptions struct {
	MTU              uint32 `json:"mtu,omitempty"`
	TTI              uint32 `json:"tti,omitempty"`
	UplinkCapacity   uint32 `json:"uplink_capacity,omitempty"`
	DownlinkCapacity uint32 `json:"downlink_capacity,omitempty"`
	Congestion       bool   `json:"congestion,omitempty"`
	ReadBufferSize   uint32 `json:"read_buffer_size,omitempty"`
	WriteBufferSize  uint32 `json:"write_buffer_size,omitempty"`
	HeaderType       string `json:"header_type,omitempty"`
	Seed             string `json:"seed,omitempty"`
}

func (k *V2RayKCPOptions) GetMTU() uint32 {
	if k.MTU == 0 {
		return 1350
	}
	return k.MTU
}

func (k *V2RayKCPOptions) GetTTI() uint32 {
	if k.TTI == 0 {
		return 50
	}
	return k.TTI
}

func (k *V2RayKCPOptions) GetUplinkCapacity() uint32 {
	if k.UplinkCapacity == 0 {
		return 12
	}
	return k.UplinkCapacity
}

func (k *V2RayKCPOptions) GetDownlinkCapacity() uint32 {
	if k.DownlinkCapacity == 0 {
		return 100
	}
	return k.DownlinkCapacity
}

func (k *V2RayKCPOptions) GetReadBufferSize() uint32 {
	if k.ReadBufferSize == 0 {
		return 1
	}
	return k.ReadBufferSize
}

func (k *V2RayKCPOptions) GetWriteBufferSize() uint32 {
	if k.WriteBufferSize == 0 {
		return 1
	}
	return k.WriteBufferSize
}

func (k *V2RayKCPOptions) GetHeaderType() string {
	if k.HeaderType == "" {
		return "none"
	}
	return k.HeaderType
}
