package outbound

import (
	"context"
	"fmt"
	"net"
	"net/netip"
	"strconv"
	"sync"

	N "github.com/metacubex/mihomo/common/net"
	"github.com/metacubex/mihomo/component/resolver"
	C "github.com/metacubex/mihomo/constant"

	mieruclient "github.com/enfein/mieru/v3/apis/client"
	mierucommon "github.com/enfein/mieru/v3/apis/common"
	mierumodel "github.com/enfein/mieru/v3/apis/model"
	mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
	"google.golang.org/protobuf/proto"
)

type Mieru struct {
	*Base
	option *MieruOption
	client mieruclient.Client
	mu     sync.Mutex
}

type MieruOption struct {
	BasicOption
	Name          string `proxy:"name"`
	Server        string `proxy:"server"`
	Port          int    `proxy:"port,omitempty"`
	PortRange     string `proxy:"port-range,omitempty"`
	Transport     string `proxy:"transport"`
	UDP           bool   `proxy:"udp,omitempty"`
	UserName      string `proxy:"username"`
	Password      string `proxy:"password"`
	Multiplexing  string `proxy:"multiplexing,omitempty"`
	HandshakeMode string `proxy:"handshake-mode,omitempty"`
}

type mieruPacketDialer struct {
	C.Dialer
}

var _ mierucommon.PacketDialer = (*mieruPacketDialer)(nil)

func (pd mieruPacketDialer) ListenPacket(ctx context.Context, network, laddr, raddr string) (net.PacketConn, error) {
	rAddrPort, err := netip.ParseAddrPort(raddr)
	if err != nil {
		return nil, fmt.Errorf("invalid address %s: %w", raddr, err)
	}
	return pd.Dialer.ListenPacket(ctx, network, laddr, rAddrPort)
}

type mieruDNSResolver struct {
	prefer C.DNSPrefer
}

var _ mierucommon.DNSResolver = (*mieruDNSResolver)(nil)

func (dr mieruDNSResolver) LookupIP(ctx context.Context, network, host string) (_ []net.IP, err error) {
	var ip netip.Addr
	switch dr.prefer {
	case C.IPv4Only:
		ip, err = resolver.ResolveIPv4WithResolver(ctx, host, resolver.ProxyServerHostResolver)
	case C.IPv6Only:
		ip, err = resolver.ResolveIPv6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
	case C.IPv6Prefer:
		ip, err = resolver.ResolveIPPrefer6WithResolver(ctx, host, resolver.ProxyServerHostResolver)
	default:
		ip, err = resolver.ResolveIPWithResolver(ctx, host, resolver.ProxyServerHostResolver)
	}
	if err != nil {
		return nil, fmt.Errorf("can't resolve ip: %w", err)
	}
	// TODO: handle IP4P (due to interface limitations, it's currently impossible to modify the port here)
	return []net.IP{ip.AsSlice()}, nil
}

// DialContext implements C.ProxyAdapter
func (m *Mieru) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
	if err := m.ensureClientIsRunning(); err != nil {
		return nil, err
	}
	addr := metadataToMieruNetAddrSpec(metadata)
	c, err := m.client.DialContext(ctx, addr)
	if err != nil {
		return nil, fmt.Errorf("dial to %s failed: %w", addr, err)
	}
	return NewConn(c, m), nil
}

// ListenPacketContext implements C.ProxyAdapter
func (m *Mieru) ListenPacketContext(ctx context.Context, metadata *C.Metadata) (_ C.PacketConn, err error) {
	if err = m.ResolveUDP(ctx, metadata); err != nil {
		return nil, err
	}
	if err := m.ensureClientIsRunning(); err != nil {
		return nil, err
	}
	c, err := m.client.DialContext(ctx, metadata.UDPAddr())
	if err != nil {
		return nil, fmt.Errorf("dial to %s failed: %w", metadata.UDPAddr(), err)
	}
	return newPacketConn(N.NewThreadSafePacketConn(mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(c))), m), nil
}

// SupportUOT implements C.ProxyAdapter
func (m *Mieru) SupportUOT() bool {
	return true
}

// ProxyInfo implements C.ProxyAdapter
func (m *Mieru) ProxyInfo() C.ProxyInfo {
	info := m.Base.ProxyInfo()
	info.DialerProxy = m.option.DialerProxy
	return info
}

func (m *Mieru) ensureClientIsRunning() error {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.client.IsRunning() {
		return nil
	}

	// Create a dialer and add it to the client config, before starting the client.
	config, err := m.client.Load()
	if err != nil {
		return err
	}
	config.Dialer = m.dialer
	config.PacketDialer = mieruPacketDialer{Dialer: m.dialer}
	config.Resolver = mieruDNSResolver{prefer: m.prefer}
	if err := m.client.Store(config); err != nil {
		return err
	}

	if err := m.client.Start(); err != nil {
		return fmt.Errorf("failed to start mieru client: %w", err)
	}
	return nil
}

func NewMieru(option MieruOption) (*Mieru, error) {
	config, err := buildMieruClientConfig(option)
	if err != nil {
		return nil, fmt.Errorf("failed to build mieru client config: %w", err)
	}
	c := mieruclient.NewClient()
	if err := c.Store(config); err != nil {
		return nil, fmt.Errorf("failed to store mieru client config: %w", err)
	}
	// Client is started lazily on the first use.

	var addr string
	if option.Port != 0 {
		addr = net.JoinHostPort(option.Server, strconv.Itoa(option.Port))
	} else {
		beginPort, _, _ := beginAndEndPortFromPortRange(option.PortRange)
		addr = net.JoinHostPort(option.Server, strconv.Itoa(beginPort))
	}
	outbound := &Mieru{
		Base: &Base{
			name:   option.Name,
			addr:   addr,
			tp:     C.Mieru,
			pdName: option.ProviderName,
			udp:    option.UDP,
			xudp:   false,
			iface:  option.Interface,
			rmark:  option.RoutingMark,
			prefer: option.IPVersion,
		},
		option: &option,
		client: c,
	}
	outbound.dialer = option.NewDialer(outbound.DialOptions())
	return outbound, nil
}

// Close implements C.ProxyAdapter
func (m *Mieru) Close() error {
	m.mu.Lock()
	defer m.mu.Unlock()
	if m.client != nil && m.client.IsRunning() {
		return m.client.Stop()
	}
	return nil
}

func metadataToMieruNetAddrSpec(metadata *C.Metadata) mierumodel.NetAddrSpec {
	spec := mierumodel.NetAddrSpec{
		Net: metadata.NetWork.String(),
	}
	if metadata.Host != "" {
		spec.AddrSpec = mierumodel.AddrSpec{
			FQDN: metadata.Host,
			Port: int(metadata.DstPort),
		}
	} else {
		spec.AddrSpec = mierumodel.AddrSpec{
			IP:   metadata.DstIP.AsSlice(),
			Port: int(metadata.DstPort),
		}
	}
	return spec
}

func buildMieruClientConfig(option MieruOption) (*mieruclient.ClientConfig, error) {
	if err := validateMieruOption(option); err != nil {
		return nil, fmt.Errorf("failed to validate mieru option: %w", err)
	}

	var transportProtocol = mierupb.TransportProtocol_UNKNOWN_TRANSPORT_PROTOCOL.Enum()
	switch option.Transport {
	case "TCP":
		transportProtocol = mierupb.TransportProtocol_TCP.Enum()
	case "UDP":
		transportProtocol = mierupb.TransportProtocol_UDP.Enum()
	}
	var server *mierupb.ServerEndpoint
	if net.ParseIP(option.Server) != nil {
		// server is an IP address
		if option.PortRange != "" {
			server = &mierupb.ServerEndpoint{
				IpAddress: proto.String(option.Server),
				PortBindings: []*mierupb.PortBinding{
					{
						PortRange: proto.String(option.PortRange),
						Protocol:  transportProtocol,
					},
				},
			}
		} else {
			server = &mierupb.ServerEndpoint{
				IpAddress: proto.String(option.Server),
				PortBindings: []*mierupb.PortBinding{
					{
						Port:     proto.Int32(int32(option.Port)),
						Protocol: transportProtocol,
					},
				},
			}
		}
	} else {
		// server is a domain name
		if option.PortRange != "" {
			server = &mierupb.ServerEndpoint{
				DomainName: proto.String(option.Server),
				PortBindings: []*mierupb.PortBinding{
					{
						PortRange: proto.String(option.PortRange),
						Protocol:  transportProtocol,
					},
				},
			}
		} else {
			server = &mierupb.ServerEndpoint{
				DomainName: proto.String(option.Server),
				PortBindings: []*mierupb.PortBinding{
					{
						Port:     proto.Int32(int32(option.Port)),
						Protocol: transportProtocol,
					},
				},
			}
		}
	}
	config := &mieruclient.ClientConfig{
		Profile: &mierupb.ClientProfile{
			ProfileName: proto.String(option.Name),
			User: &mierupb.User{
				Name:     proto.String(option.UserName),
				Password: proto.String(option.Password),
			},
			Servers: []*mierupb.ServerEndpoint{server},
		},
		DNSConfig: &mierucommon.ClientDNSConfig{
			BypassDialerDNS: true,
		},
	}
	if multiplexing, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; ok {
		config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
			Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
		}
	}
	if handshakeMode, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; ok {
		config.Profile.HandshakeMode = (*mierupb.HandshakeMode)(&handshakeMode)
	}
	return config, nil
}

func validateMieruOption(option MieruOption) error {
	if option.Name == "" {
		return fmt.Errorf("name is empty")
	}
	if option.Server == "" {
		return fmt.Errorf("server is empty")
	}
	if option.Port == 0 && option.PortRange == "" {
		return fmt.Errorf("either port or port-range must be set")
	}
	if option.Port != 0 && option.PortRange != "" {
		return fmt.Errorf("port and port-range cannot be set at the same time")
	}
	if option.Port != 0 && (option.Port < 1 || option.Port > 65535) {
		return fmt.Errorf("port must be between 1 and 65535")
	}
	if option.PortRange != "" {
		begin, end, err := beginAndEndPortFromPortRange(option.PortRange)
		if err != nil {
			return fmt.Errorf("invalid port-range format")
		}
		if begin < 1 || begin > 65535 {
			return fmt.Errorf("begin port must be between 1 and 65535")
		}
		if end < 1 || end > 65535 {
			return fmt.Errorf("end port must be between 1 and 65535")
		}
		if begin > end {
			return fmt.Errorf("begin port must be less than or equal to end port")
		}
	}

	if option.Transport != "TCP" && option.Transport != "UDP" {
		return fmt.Errorf("transport must be TCP or UDP")
	}
	if option.UserName == "" {
		return fmt.Errorf("username is empty")
	}
	if option.Password == "" {
		return fmt.Errorf("password is empty")
	}
	if option.Multiplexing != "" {
		if _, ok := mierupb.MultiplexingLevel_value[option.Multiplexing]; !ok {
			return fmt.Errorf("invalid multiplexing level: %s", option.Multiplexing)
		}
	}
	if option.HandshakeMode != "" {
		if _, ok := mierupb.HandshakeMode_value[option.HandshakeMode]; !ok {
			return fmt.Errorf("invalid handshake mode: %s", option.HandshakeMode)
		}
	}
	return nil
}

func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
	var begin, end int
	_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
	return begin, end, err
}
