package conf

import (
	"encoding/json"
	"io"
	"strings"
	"v2ray.com/core"
	"v2ray.com/core/common/errors"
	v2net "v2ray.com/core/common/net"
	"v2ray.com/core/common/serial"
	json_reader "v2ray.com/core/tools/conf/json"
)

var (
	inboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
		"dokodemo-door": func() interface{} { return new(DokodemoConfig) },
		"http":          func() interface{} { return new(HttpServerConfig) },
		"shadowsocks":   func() interface{} { return new(ShadowsocksServerConfig) },
		"socks":         func() interface{} { return new(SocksServerConfig) },
		"vmess":         func() interface{} { return new(VMessInboundConfig) },
	}, "protocol", "settings")

	outboundConfigLoader = NewJSONConfigLoader(ConfigCreatorCache{
		"blackhole":   func() interface{} { return new(BlackholeConfig) },
		"freedom":     func() interface{} { return new(FreedomConfig) },
		"shadowsocks": func() interface{} { return new(ShadowsocksClientConfig) },
		"vmess":       func() interface{} { return new(VMessOutboundConfig) },
	}, "protocol", "settings")
)

type InboundConnectionConfig struct {
	Port          uint16          `json:"port"`
	Listen        *Address        `json:"listen"`
	Protocol      string          `json:"protocol"`
	StreamSetting *StreamConfig   `json:"streamSettings"`
	Settings      json.RawMessage `json:"settings"`
	AllowPassive  bool            `json:"allowPassive"`
	Tag           string          `json:"tag"`
}

func (v *InboundConnectionConfig) Build() (*core.InboundConnectionConfig, error) {
	config := new(core.InboundConnectionConfig)
	config.PortRange = &v2net.PortRange{
		From: uint32(v.Port),
		To:   uint32(v.Port),
	}
	if v.Listen != nil {
		if v.Listen.Family().IsDomain() {
			return nil, errors.New("Point: Unable to listen on domain address: " + v.Listen.Domain())
		}
		config.ListenOn = v.Listen.Build()
	}
	if v.StreamSetting != nil {
		ts, err := v.StreamSetting.Build()
		if err != nil {
			return nil, err
		}
		config.StreamSettings = ts
	}
	config.AllowPassiveConnection = v.AllowPassive

	jsonConfig, err := inboundConfigLoader.LoadWithID(v.Settings, v.Protocol)
	if err != nil {
		return nil, errors.Base(err).Message("Failed to load inbound config.")
	}
	ts, err := jsonConfig.(Buildable).Build()
	if err != nil {
		return nil, err
	}
	config.Settings = ts
	if len(v.Tag) > 0 {
		config.Tag = v.Tag
	}
	return config, nil
}

type OutboundConnectionConfig struct {
	Protocol      string          `json:"protocol"`
	SendThrough   *Address        `json:"sendThrough"`
	StreamSetting *StreamConfig   `json:"streamSettings"`
	ProxySettings *ProxyConfig    `json:"proxySettings"`
	Settings      json.RawMessage `json:"settings"`
	Tag           string          `json:"tag"`
}

func (v *OutboundConnectionConfig) Build() (*core.OutboundConnectionConfig, error) {
	config := new(core.OutboundConnectionConfig)
	rawConfig, err := outboundConfigLoader.LoadWithID(v.Settings, v.Protocol)
	if err != nil {
		return nil, errors.Base(err).Message("Failed to parse outbound config.")
	}
	ts, err := rawConfig.(Buildable).Build()
	if err != nil {
		return nil, err
	}
	config.Settings = ts

	if v.SendThrough != nil {
		address := v.SendThrough
		if address.Family().IsDomain() {
			return nil, errors.New("Invalid sendThrough address: " + address.String())
		}
		config.SendThrough = address.Build()
	}
	if v.StreamSetting != nil {
		ss, err := v.StreamSetting.Build()
		if err != nil {
			return nil, err
		}
		config.StreamSettings = ss
	}
	if v.ProxySettings != nil {
		ps, err := v.ProxySettings.Build()
		if err != nil {
			return nil, errors.Base(err).Message("Invalid outbound proxy settings.")
		}
		config.ProxySettings = ps
	}
	if len(v.Tag) > 0 {
		config.Tag = v.Tag
	}
	return config, nil
}

type InboundDetourAllocationConfig struct {
	Strategy    string  `json:"strategy"`
	Concurrency *uint32 `json:"concurrency"`
	RefreshMin  *uint32 `json:"refresh"`
}

func (v *InboundDetourAllocationConfig) Build() (*core.AllocationStrategy, error) {
	config := new(core.AllocationStrategy)
	switch strings.ToLower(v.Strategy) {
	case "always":
		config.Type = core.AllocationStrategy_Always
	case "random":
		config.Type = core.AllocationStrategy_Random
	case "external":
		config.Type = core.AllocationStrategy_External
	default:
		return nil, errors.New("Unknown allocation strategy: ", v.Strategy)
	}
	if v.Concurrency != nil {
		config.Concurrency = &core.AllocationStrategyConcurrency{
			Value: *v.Concurrency,
		}
	}

	if v.RefreshMin != nil {
		config.Refresh = &core.AllocationStrategyRefresh{
			Value: *v.RefreshMin,
		}
	}

	return config, nil
}

type InboundDetourConfig struct {
	Protocol      string                         `json:"protocol"`
	PortRange     *PortRange                     `json:"port"`
	ListenOn      *Address                       `json:"listen"`
	Settings      json.RawMessage                `json:"settings"`
	Tag           string                         `json:"tag"`
	Allocation    *InboundDetourAllocationConfig `json:"allocate"`
	StreamSetting *StreamConfig                  `json:"streamSettings"`
	AllowPassive  bool                           `json:"allowPassive"`
}

func (v *InboundDetourConfig) Build() (*core.InboundConnectionConfig, error) {
	config := new(core.InboundConnectionConfig)
	if v.PortRange == nil {
		return nil, errors.New("Port range not specified in InboundDetour.")
	}
	config.PortRange = v.PortRange.Build()

	if v.ListenOn != nil {
		if v.ListenOn.Family().IsDomain() {
			return nil, errors.New("Unable to listen on domain address: ", v.ListenOn.Domain())
		}
		config.ListenOn = v.ListenOn.Build()
	}
	config.Tag = v.Tag
	if v.Allocation != nil {
		as, err := v.Allocation.Build()
		if err != nil {
			return nil, err
		}
		config.AllocationStrategy = as
	}
	if v.StreamSetting != nil {
		ss, err := v.StreamSetting.Build()
		if err != nil {
			return nil, err
		}
		config.StreamSettings = ss
	}
	config.AllowPassiveConnection = v.AllowPassive

	rawConfig, err := inboundConfigLoader.LoadWithID(v.Settings, v.Protocol)
	if err != nil {
		return nil, errors.Base(err).Message("Failed to load inbound detour config.")
	}
	ts, err := rawConfig.(Buildable).Build()
	if err != nil {
		return nil, err
	}
	config.Settings = ts
	return config, nil
}

type OutboundDetourConfig struct {
	Protocol      string          `json:"protocol"`
	SendThrough   *Address        `json:"sendThrough"`
	Tag           string          `json:"tag"`
	Settings      json.RawMessage `json:"settings"`
	StreamSetting *StreamConfig   `json:"streamSettings"`
	ProxySettings *ProxyConfig    `json:"proxySettings"`
}

func (v *OutboundDetourConfig) Build() (*core.OutboundConnectionConfig, error) {
	config := new(core.OutboundConnectionConfig)
	config.Tag = v.Tag

	if v.SendThrough != nil {
		address := v.SendThrough
		if address.Family().IsDomain() {
			return nil, errors.New("Point: Unable to send through: " + address.String())
		}
		config.SendThrough = address.Build()
	}

	if v.StreamSetting != nil {
		ss, err := v.StreamSetting.Build()
		if err != nil {
			return nil, err
		}
		config.StreamSettings = ss
	}

	rawConfig, err := outboundConfigLoader.LoadWithID(v.Settings, v.Protocol)
	if err != nil {
		return nil, errors.Base(err).Message("Failed to parse to outbound detour config.")
	}
	ts, err := rawConfig.(Buildable).Build()
	if err != nil {
		return nil, err
	}

	if v.ProxySettings != nil {
		ps, err := v.ProxySettings.Build()
		if err != nil {
			return nil, errors.Base(err).Message("Invalid outbound detour proxy settings.")
		}
		config.ProxySettings = ps
	}
	config.Settings = ts
	return config, nil
}

type Config struct {
	Port            uint16                    `json:"port"` // Port of this Point server.
	LogConfig       *LogConfig                `json:"log"`
	RouterConfig    *RouterConfig             `json:"routing"`
	DNSConfig       *DnsConfig                `json:"dns"`
	InboundConfig   *InboundConnectionConfig  `json:"inbound"`
	OutboundConfig  *OutboundConnectionConfig `json:"outbound"`
	InboundDetours  []InboundDetourConfig     `json:"inboundDetour"`
	OutboundDetours []OutboundDetourConfig    `json:"outboundDetour"`
	Transport       *TransportConfig          `json:"transport"`
}

func (v *Config) Build() (*core.Config, error) {
	config := new(core.Config)

	if v.LogConfig != nil {
		config.Log = v.LogConfig.Build()
	}

	if v.Transport != nil {
		ts, err := v.Transport.Build()
		if err != nil {
			return nil, err
		}
		config.Transport = ts
	}

	if v.RouterConfig != nil {
		routerConfig, err := v.RouterConfig.Build()
		if err != nil {
			return nil, err
		}
		config.App = append(config.App, serial.ToTypedMessage(routerConfig))
	}

	if v.DNSConfig != nil {
		config.App = append(config.App, serial.ToTypedMessage(v.DNSConfig.Build()))
	}

	if v.InboundConfig == nil {
		return nil, errors.New("No inbound config specified.")
	}

	if v.InboundConfig.Port == 0 && v.Port > 0 {
		v.InboundConfig.Port = v.Port
	}

	ic, err := v.InboundConfig.Build()
	if err != nil {
		return nil, err
	}
	config.Inbound = append(config.Inbound, ic)

	for _, rawInboundConfig := range v.InboundDetours {
		ic, err := rawInboundConfig.Build()
		if err != nil {
			return nil, err
		}
		config.Inbound = append(config.Inbound, ic)
	}

	oc, err := v.OutboundConfig.Build()
	if err != nil {
		return nil, err
	}
	config.Outbound = append(config.Outbound, oc)

	for _, rawOutboundConfig := range v.OutboundDetours {
		oc, err := rawOutboundConfig.Build()
		if err != nil {
			return nil, err
		}
		config.Outbound = append(config.Outbound, oc)
	}

	return config, nil
}

func init() {
	core.RegisterConfigLoader(core.ConfigFormat_JSON, func(input io.Reader) (*core.Config, error) {
		jsonConfig := &Config{}
		decoder := json.NewDecoder(&json_reader.Reader{
			Reader: input,
		})
		err := decoder.Decode(jsonConfig)
		if err != nil {
			return nil, errors.Base(err).Message("Invalid V2Ray config.")
		}

		return jsonConfig.Build()
	})
}
