package exportproxy

import (
	"bufio"
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net"
	"strconv"
)

func serveSOCKS(client net.Conn, config Config, dialer *net.Dialer, resolver *net.Resolver) error {
	reader := bufio.NewReader(client)
	version, err := reader.ReadByte()
	if err != nil || version != 5 {
		return errors.New("unsupported SOCKS version")
	}
	methodCount, err := reader.ReadByte()
	if err != nil {
		return err
	}
	methods := make([]byte, methodCount)
	if _, err := io.ReadFull(reader, methods); err != nil {
		return err
	}
	chosen := byte(0xff)
	if config.AuthEnabled && hasMethod(methods, 2) {
		chosen = 2
	} else if !config.AuthEnabled && hasMethod(methods, 0) {
		chosen = 0
	}
	if _, err := client.Write([]byte{5, chosen}); err != nil || chosen == 0xff {
		return errors.New("no acceptable SOCKS authentication method")
	}
	if chosen == 2 {
		if err := socksAuthenticate(reader, client, config); err != nil {
			return err
		}
	}
	header := make([]byte, 4)
	if _, err := io.ReadFull(reader, header); err != nil {
		return err
	}
	if header[0] != 5 || header[1] != 1 {
		_ = writeSocksReply(client, 7)
		return errors.New("only SOCKS5 CONNECT is supported")
	}
	host, port, err := readSocksAddress(reader, header[3])
	if err != nil {
		_ = writeSocksReply(client, 1)
		return err
	}
	ctx, cancel := context.WithTimeout(context.Background(), proxyTimeout)
	target, err := dialTarget(ctx, net.JoinHostPort(host, strconv.Itoa(port)), dialer, resolver)
	cancel()
	if err != nil {
		_ = writeSocksReply(client, 5)
		return err
	}
	defer target.Close()
	if err := writeSocksReply(client, 0); err != nil {
		return err
	}
	if buffered := reader.Buffered(); buffered > 0 {
		if value, err := reader.Peek(buffered); err == nil {
			_, _ = target.Write(value)
			_, _ = reader.Discard(buffered)
		}
	}
	pipe(client, target)
	return nil
}

func socksAuthenticate(reader *bufio.Reader, writer io.Writer, config Config) error {
	header := make([]byte, 2)
	if _, err := io.ReadFull(reader, header); err != nil || header[0] != 1 {
		return errors.New("invalid SOCKS authentication request")
	}
	username := make([]byte, int(header[1]))
	if _, err := io.ReadFull(reader, username); err != nil {
		return err
	}
	length, err := reader.ReadByte()
	if err != nil {
		return err
	}
	password := make([]byte, int(length))
	if _, err := io.ReadFull(reader, password); err != nil {
		return err
	}
	if string(username) != config.Username || string(password) != config.Password {
		_, _ = writer.Write([]byte{1, 1})
		return errors.New("SOCKS authentication failed")
	}
	_, err = writer.Write([]byte{1, 0})
	return err
}

func hasMethod(methods []byte, wanted byte) bool {
	for _, method := range methods {
		if method == wanted {
			return true
		}
	}
	return false
}

func readSocksAddress(reader *bufio.Reader, kind byte) (string, int, error) {
	var host string
	switch kind {
	case 1:
		value := make([]byte, 4)
		if _, err := io.ReadFull(reader, value); err != nil {
			return "", 0, err
		}
		host = net.IP(value).String()
	case 3:
		length, err := reader.ReadByte()
		if err != nil {
			return "", 0, err
		}
		value := make([]byte, int(length))
		if _, err := io.ReadFull(reader, value); err != nil {
			return "", 0, err
		}
		host = string(value)
	case 4:
		value := make([]byte, 16)
		if _, err := io.ReadFull(reader, value); err != nil {
			return "", 0, err
		}
		host = net.IP(value).String()
	default:
		return "", 0, fmt.Errorf("unsupported SOCKS address type %d", kind)
	}
	value := make([]byte, 2)
	if _, err := io.ReadFull(reader, value); err != nil {
		return "", 0, err
	}
	return host, int(binary.BigEndian.Uint16(value)), nil
}

func writeSocksReply(connection net.Conn, code byte) error {
	_, err := connection.Write([]byte{5, code, 0, 1, 0, 0, 0, 0, 0, 0})
	return err
}
