package outbound

import (
	"context"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"net/netip"
	"runtime"
	"strconv"
	"strings"
	"sync"

	CN "github.com/Dreamacro/clash/common/net"
	"github.com/Dreamacro/clash/component/dialer"
	"github.com/Dreamacro/clash/component/proxydialer"
	"github.com/Dreamacro/clash/component/resolver"
	C "github.com/Dreamacro/clash/constant"
	"github.com/Dreamacro/clash/dns"
	"github.com/Dreamacro/clash/log"

	wireguard "github.com/metacubex/sing-wireguard"

	"github.com/sagernet/sing/common"
	"github.com/sagernet/sing/common/debug"
	E "github.com/sagernet/sing/common/exceptions"
	M "github.com/sagernet/sing/common/metadata"
	"github.com/sagernet/wireguard-go/device"
)

type WireGuard struct {
	*Base
	bind      *wireguard.ClientBind
	device    *device.Device
	tunDevice wireguard.Device
	dialer    proxydialer.SingDialer
	startOnce sync.Once
	startErr  error
	resolver  *dns.Resolver
	refP      *refProxyAdapter
}

type WireGuardOption struct {
	BasicOption
	WireGuardPeerOption
	Name                string `proxy:"name"`
	PrivateKey          string `proxy:"private-key"`
	Workers             int    `proxy:"workers,omitempty"`
	MTU                 int    `proxy:"mtu,omitempty"`
	UDP                 bool   `proxy:"udp,omitempty"`
	PersistentKeepalive int    `proxy:"persistent-keepalive,omitempty"`

	Peers []WireGuardPeerOption `proxy:"peers,omitempty"`

	RemoteDnsResolve bool     `proxy:"remote-dns-resolve,omitempty"`
	Dns              []string `proxy:"dns,omitempty"`
}

type WireGuardPeerOption struct {
	Server       string   `proxy:"server"`
	Port         int      `proxy:"port"`
	Ip           string   `proxy:"ip,omitempty"`
	Ipv6         string   `proxy:"ipv6,omitempty"`
	PublicKey    string   `proxy:"public-key,omitempty"`
	PreSharedKey string   `proxy:"pre-shared-key,omitempty"`
	Reserved     []uint8  `proxy:"reserved,omitempty"`
	AllowedIPs   []string `proxy:"allowed-ips,omitempty"`
}

type wgSingErrorHandler struct {
	name string
}

var _ E.Handler = (*wgSingErrorHandler)(nil)

func (w wgSingErrorHandler) NewError(ctx context.Context, err error) {
	if E.IsClosedOrCanceled(err) {
		log.SingLogger.Debug(fmt.Sprintf("[WG](%s) connection closed: %s", w.name, err))
		return
	}
	log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", w.name, err))
}

type wgNetDialer struct {
	tunDevice wireguard.Device
}

var _ dialer.NetDialer = (*wgNetDialer)(nil)

func (d wgNetDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
	return d.tunDevice.DialContext(ctx, network, M.ParseSocksaddr(address).Unwrap())
}

func (option WireGuardPeerOption) Addr() M.Socksaddr {
	return M.ParseSocksaddrHostPort(option.Server, uint16(option.Port))
}

func (option WireGuardPeerOption) Prefixes() ([]netip.Prefix, error) {
	localPrefixes := make([]netip.Prefix, 0, 2)
	if len(option.Ip) > 0 {
		if !strings.Contains(option.Ip, "/") {
			option.Ip = option.Ip + "/32"
		}
		if prefix, err := netip.ParsePrefix(option.Ip); err == nil {
			localPrefixes = append(localPrefixes, prefix)
		} else {
			return nil, E.Cause(err, "ip address parse error")
		}
	}
	if len(option.Ipv6) > 0 {
		if !strings.Contains(option.Ipv6, "/") {
			option.Ipv6 = option.Ipv6 + "/128"
		}
		if prefix, err := netip.ParsePrefix(option.Ipv6); err == nil {
			localPrefixes = append(localPrefixes, prefix)
		} else {
			return nil, E.Cause(err, "ipv6 address parse error")
		}
	}
	if len(localPrefixes) == 0 {
		return nil, E.New("missing local address")
	}
	return localPrefixes, nil
}

func NewWireGuard(option WireGuardOption) (*WireGuard, error) {
	outbound := &WireGuard{
		Base: &Base{
			name:   option.Name,
			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
			tp:     C.WireGuard,
			udp:    option.UDP,
			iface:  option.Interface,
			rmark:  option.RoutingMark,
			prefer: C.NewDNSPrefer(option.IPVersion),
		},
		dialer: proxydialer.NewByNameSingDialer(option.DialerProxy, dialer.NewDialer()),
	}
	runtime.SetFinalizer(outbound, closeWireGuard)

	var reserved [3]uint8
	if len(option.Reserved) > 0 {
		if len(option.Reserved) != 3 {
			return nil, E.New("invalid reserved value, required 3 bytes, got ", len(option.Reserved))
		}
		copy(reserved[:], option.Reserved)
	}
	var isConnect bool
	var connectAddr M.Socksaddr
	if len(option.Peers) < 2 {
		isConnect = true
		if len(option.Peers) == 1 {
			connectAddr = option.Peers[0].Addr()
		} else {
			connectAddr = option.Addr()
		}
	}
	outbound.bind = wireguard.NewClientBind(context.Background(), wgSingErrorHandler{outbound.Name()}, outbound.dialer, isConnect, connectAddr, reserved)

	var localPrefixes []netip.Prefix

	var privateKey string
	{
		bytes, err := base64.StdEncoding.DecodeString(option.PrivateKey)
		if err != nil {
			return nil, E.Cause(err, "decode private key")
		}
		privateKey = hex.EncodeToString(bytes)
	}
	ipcConf := "private_key=" + privateKey
	if peersLen := len(option.Peers); peersLen > 0 {
		localPrefixes = make([]netip.Prefix, 0, peersLen*2)
		for i, peer := range option.Peers {
			var peerPublicKey, preSharedKey string
			{
				bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
				if err != nil {
					return nil, E.Cause(err, "decode public key for peer ", i)
				}
				peerPublicKey = hex.EncodeToString(bytes)
			}
			if peer.PreSharedKey != "" {
				bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey)
				if err != nil {
					return nil, E.Cause(err, "decode pre shared key for peer ", i)
				}
				preSharedKey = hex.EncodeToString(bytes)
			}
			destination := peer.Addr()
			ipcConf += "\npublic_key=" + peerPublicKey
			ipcConf += "\nendpoint=" + destination.String()
			if preSharedKey != "" {
				ipcConf += "\npreshared_key=" + preSharedKey
			}
			if len(peer.AllowedIPs) == 0 {
				return nil, E.New("missing allowed_ips for peer ", i)
			}
			for _, allowedIP := range peer.AllowedIPs {
				ipcConf += "\nallowed_ip=" + allowedIP
			}
			if len(peer.Reserved) > 0 {
				if len(peer.Reserved) != 3 {
					return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved))
				}
				copy(reserved[:], option.Reserved)
				outbound.bind.SetReservedForEndpoint(destination, reserved)
			}
			prefixes, err := peer.Prefixes()
			if err != nil {
				return nil, err
			}
			localPrefixes = append(localPrefixes, prefixes...)
		}
	} else {
		var peerPublicKey, preSharedKey string
		{
			bytes, err := base64.StdEncoding.DecodeString(option.PublicKey)
			if err != nil {
				return nil, E.Cause(err, "decode peer public key")
			}
			peerPublicKey = hex.EncodeToString(bytes)
		}
		if option.PreSharedKey != "" {
			bytes, err := base64.StdEncoding.DecodeString(option.PreSharedKey)
			if err != nil {
				return nil, E.Cause(err, "decode pre shared key")
			}
			preSharedKey = hex.EncodeToString(bytes)
		}
		ipcConf += "\npublic_key=" + peerPublicKey
		ipcConf += "\nendpoint=" + connectAddr.String()
		if preSharedKey != "" {
			ipcConf += "\npreshared_key=" + preSharedKey
		}
		var err error
		localPrefixes, err = option.Prefixes()
		if err != nil {
			return nil, err
		}
		var has4, has6 bool
		for _, address := range localPrefixes {
			if address.Addr().Is4() {
				has4 = true
			} else {
				has6 = true
			}
		}
		if has4 {
			ipcConf += "\nallowed_ip=0.0.0.0/0"
		}
		if has6 {
			ipcConf += "\nallowed_ip=::/0"
		}
	}

	if option.PersistentKeepalive != 0 {
		ipcConf += fmt.Sprintf("\npersistent_keepalive_interval=%d", option.PersistentKeepalive)
	}
	mtu := option.MTU
	if mtu == 0 {
		mtu = 1408
	}
	if len(localPrefixes) == 0 {
		return nil, E.New("missing local address")
	}
	var err error
	outbound.tunDevice, err = wireguard.NewStackDevice(localPrefixes, uint32(mtu))
	if err != nil {
		return nil, E.Cause(err, "create WireGuard device")
	}
	outbound.device = device.NewDevice(context.Background(), outbound.tunDevice, outbound.bind, &device.Logger{
		Verbosef: func(format string, args ...interface{}) {
			log.SingLogger.Debug(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
		},
		Errorf: func(format string, args ...interface{}) {
			log.SingLogger.Error(fmt.Sprintf("[WG](%s) %s", option.Name, fmt.Sprintf(format, args...)))
		},
	}, option.Workers)
	if debug.Enabled {
		log.SingLogger.Trace(fmt.Sprintf("[WG](%s) created wireguard ipc conf: \n %s", option.Name, ipcConf))
	}
	err = outbound.device.IpcSet(ipcConf)
	if err != nil {
		return nil, E.Cause(err, "setup wireguard")
	}
	//err = outbound.tunDevice.Start()

	var has6 bool
	for _, address := range localPrefixes {
		if !address.Addr().Unmap().Is4() {
			has6 = true
			break
		}
	}

	refP := &refProxyAdapter{}
	outbound.refP = refP
	if option.RemoteDnsResolve && len(option.Dns) > 0 {
		nss, err := dns.ParseNameServer(option.Dns)
		if err != nil {
			return nil, err
		}
		for i := range nss {
			nss[i].ProxyAdapter = refP
		}
		outbound.resolver = dns.NewResolver(dns.Config{
			Main: nss,
			IPv6: has6,
		})
	}

	return outbound, nil
}

func closeWireGuard(w *WireGuard) {
	if w.device != nil {
		w.device.Close()
	}
	_ = common.Close(w.tunDevice)
}

func (w *WireGuard) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
	options := w.Base.DialOptions(opts...)
	w.dialer.SetDialer(dialer.NewDialer(options...))
	var conn net.Conn
	w.startOnce.Do(func() {
		w.startErr = w.tunDevice.Start()
	})
	if w.startErr != nil {
		return nil, w.startErr
	}
	if !metadata.Resolved() || w.resolver != nil {
		r := resolver.DefaultResolver
		if w.resolver != nil {
			w.refP.SetProxyAdapter(w)
			defer w.refP.ClearProxyAdapter()
			r = w.resolver
		}
		options = append(options, dialer.WithResolver(r))
		options = append(options, dialer.WithNetDialer(wgNetDialer{tunDevice: w.tunDevice}))
		conn, err = dialer.NewDialer(options...).DialContext(ctx, "tcp", metadata.RemoteAddress())
	} else {
		conn, err = w.tunDevice.DialContext(ctx, "tcp", M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
	}
	if err != nil {
		return nil, err
	}
	if conn == nil {
		return nil, E.New("conn is nil")
	}
	return NewConn(CN.NewRefConn(conn, w), w), nil
}

func (w *WireGuard) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) {
	options := w.Base.DialOptions(opts...)
	w.dialer.SetDialer(dialer.NewDialer(options...))
	var pc net.PacketConn
	w.startOnce.Do(func() {
		w.startErr = w.tunDevice.Start()
	})
	if w.startErr != nil {
		return nil, w.startErr
	}
	if err != nil {
		return nil, err
	}
	if (!metadata.Resolved() || w.resolver != nil) && metadata.Host != "" {
		r := resolver.DefaultResolver
		if w.resolver != nil {
			w.refP.SetProxyAdapter(w)
			defer w.refP.ClearProxyAdapter()
			r = w.resolver
		}
		ip, err := resolver.ResolveIPWithResolver(ctx, metadata.Host, r)
		if err != nil {
			return nil, errors.New("can't resolve ip")
		}
		metadata.DstIP = ip
	}
	pc, err = w.tunDevice.ListenPacket(ctx, M.SocksaddrFrom(metadata.DstIP, metadata.DstPort).Unwrap())
	if err != nil {
		return nil, err
	}
	if pc == nil {
		return nil, E.New("packetConn is nil")
	}
	return newPacketConn(CN.NewRefPacketConn(pc, w), w), nil
}

// IsL3Protocol implements C.ProxyAdapter
func (w *WireGuard) IsL3Protocol(metadata *C.Metadata) bool {
	return true
}

type refProxyAdapter struct {
	proxyAdapter C.ProxyAdapter
	count        int
	mutex        sync.Mutex
}

func (r *refProxyAdapter) SetProxyAdapter(proxyAdapter C.ProxyAdapter) {
	r.mutex.Lock()
	defer r.mutex.Unlock()
	r.proxyAdapter = proxyAdapter
	r.count++
}

func (r *refProxyAdapter) ClearProxyAdapter() {
	r.mutex.Lock()
	defer r.mutex.Unlock()
	r.count--
	if r.count == 0 {
		r.proxyAdapter = nil
	}
}

func (r *refProxyAdapter) Name() string {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.Name()
	}
	return ""
}

func (r *refProxyAdapter) Type() C.AdapterType {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.Type()
	}
	return C.AdapterType(0)
}

func (r *refProxyAdapter) Addr() string {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.Addr()
	}
	return ""
}

func (r *refProxyAdapter) SupportUDP() bool {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.SupportUDP()
	}
	return false
}

func (r *refProxyAdapter) SupportXUDP() bool {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.SupportXUDP()
	}
	return false
}

func (r *refProxyAdapter) SupportTFO() bool {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.SupportTFO()
	}
	return false
}

func (r *refProxyAdapter) MarshalJSON() ([]byte, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.MarshalJSON()
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.StreamConnContext(ctx, c, metadata)
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.Conn, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.DialContext(ctx, metadata, opts...)
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (C.PacketConn, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.ListenPacketContext(ctx, metadata, opts...)
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) SupportUOT() bool {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.SupportUOT()
	}
	return false
}

func (r *refProxyAdapter) SupportWithDialer() C.NetWork {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.SupportWithDialer()
	}
	return C.InvalidNet
}

func (r *refProxyAdapter) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.Conn, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.DialContextWithDialer(ctx, dialer, metadata)
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) ListenPacketWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (C.PacketConn, error) {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.ListenPacketWithDialer(ctx, dialer, metadata)
	}
	return nil, C.ErrNotSupport
}

func (r *refProxyAdapter) IsL3Protocol(metadata *C.Metadata) bool {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.IsL3Protocol(metadata)
	}
	return false
}

func (r *refProxyAdapter) Unwrap(metadata *C.Metadata, touch bool) C.Proxy {
	if r.proxyAdapter != nil {
		return r.proxyAdapter.Unwrap(metadata, touch)
	}
	return nil
}

var _ C.ProxyAdapter = (*refProxyAdapter)(nil)
