package option

import (
	"reflect"

	"github.com/sagernet/sing-box/schema"
	"github.com/sagernet/sing/common/byteformats"
	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 HTTP2Options struct {
	IdleTimeout             badoption.Duration      `json:"idle_timeout,omitempty"`
	KeepAlivePeriod         badoption.Duration      `json:"keep_alive_period,omitempty"`
	StreamReceiveWindow     byteformats.MemoryBytes `json:"stream_receive_window,omitempty"`
	ConnectionReceiveWindow byteformats.MemoryBytes `json:"connection_receive_window,omitempty"`
	MaxConcurrentStreams    int                     `json:"max_concurrent_streams,omitempty"`
}

type QUICOptions struct {
	HTTP2Options
	InitialPacketSize       int  `json:"initial_packet_size,omitempty"`
	DisablePathMTUDiscovery bool `json:"disable_path_mtu_discovery,omitempty"`
}

type _HTTPClientOptions struct {
	Tag                     string               `json:"tag,omitempty"`
	Engine                  string               `json:"engine,omitempty" enum:"go,apple"`
	Version                 int                  `json:"version,omitempty" enum:"0,1,2,3"`
	DisableVersionFallback  bool                 `json:"disable_version_fallback,omitempty"`
	Headers                 badoption.HTTPHeader `json:"headers,omitempty"`
	HTTP2Options            HTTP2Options         `json:"-"`
	HTTP3Options            QUICOptions          `json:"-"`
	DefaultOutbound         bool                 `json:"-"`
	DisableEmptyDirectCheck bool                 `json:"-"`
	ResolveOnDetour         bool                 `json:"-"`
	DirectResolver          bool                 `json:"-"`
	OutboundTLSOptionsContainer
	DialerOptions
}

type (
	HTTPClient        _HTTPClientOptions
	HTTPClientOptions _HTTPClientOptions
)

func (h HTTPClient) Options() HTTPClientOptions {
	options := HTTPClientOptions(h)
	options.Tag = ""
	return options
}

func (o HTTPClientOptions) IsEmpty() bool {
	if o.Tag != "" {
		return false
	}
	o.DefaultOutbound = false
	o.ResolveOnDetour = false
	o.DirectResolver = false
	return reflect.ValueOf(_HTTPClientOptions(o)).IsZero()
}

func (o HTTPClientOptions) MarshalJSON() ([]byte, error) {
	if o.Tag != "" {
		return json.Marshal(o.Tag)
	}
	return badjson.MarshallObjects(_HTTPClientOptions(o), httpClientVariant(_HTTPClientOptions(o)))
}

func (o *HTTPClientOptions) UnmarshalJSON(content []byte) error {
	if len(content) > 0 && content[0] == '"' {
		*o = HTTPClientOptions{}
		return json.Unmarshal(content, &o.Tag)
	}
	var options _HTTPClientOptions
	err := json.Unmarshal(content, &options)
	if err != nil {
		return err
	}
	err = unmarshalHTTPClientVersionOptions(content, &options, &options)
	if err != nil {
		return err
	}
	options.Tag = ""
	*o = HTTPClientOptions(options)
	return nil
}

func (h HTTPClient) MarshalJSON() ([]byte, error) {
	return badjson.MarshallObjects(_HTTPClientOptions(h), httpClientVariant(_HTTPClientOptions(h)))
}

func (h *HTTPClient) UnmarshalJSON(content []byte) error {
	err := json.Unmarshal(content, (*_HTTPClientOptions)(h))
	if err != nil {
		return err
	}
	return unmarshalHTTPClientVersionOptions(content, (*_HTTPClientOptions)(h), (*_HTTPClientOptions)(h))
}

func unmarshalHTTPClientVersionOptions(content []byte, baseStruct any, options *_HTTPClientOptions) error {
	switch options.Version {
	case 1:
		return json.UnmarshalDisallowUnknownFields(content, baseStruct)
	case 0, 2:
		options.Version = 2
		return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP2Options)
	case 3:
		return badjson.UnmarshallExcluded(content, baseStruct, &options.HTTP3Options)
	default:
		return E.New("unknown HTTP version: ", options.Version)
	}
}

func httpClientVariant(options _HTTPClientOptions) any {
	switch options.Version {
	case 1:
		return nil
	case 0, 2:
		return options.HTTP2Options
	case 3:
		return options.HTTP3Options
	default:
		return nil
	}
}

func describeHTTPClientObject(builder schema.Builder) (*schema.Node, error) {
	node := schema.StrictObject()
	err := builder.FlattenStruct(node, reflect.TypeFor[HTTPClient]())
	if err != nil {
		return nil, err
	}
	err = builder.FlattenStruct(node, reflect.TypeFor[QUICOptions]())
	if err != nil {
		return nil, err
	}
	return node, nil
}

func (h HTTPClient) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
	return builder.Define("HTTPClient", func() (*schema.Node, error) {
		return describeHTTPClientObject(builder)
	})
}

func (o HTTPClientOptions) DescribeSchema(builder schema.Builder) (*schema.Node, error) {
	return builder.Define("HTTPClientReference", func() (*schema.Node, error) {
		clientObject, err := describeHTTPClientObject(builder)
		if err != nil {
			return nil, err
		}
		clientObject.Properties.Remove("tag")
		return schema.AnyOf(schema.TagReferenceNode("http_client"), clientObject), nil
	})
}
