package vmess

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/md5"
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"fmt"
	"hash/fnv"
	"io"
	"math/rand"
	"net"
	"runtime"
	"strings"
	"time"

	"golang.org/x/crypto/chacha20poly1305"

	"github.com/nadoo/glider/pool"
)

// Request Options
const (
	OptBasicFormat byte = 0
	OptChunkStream byte = 1
	// OptReuseTCPConnection byte = 2
	OptMetadataObfuscate byte = 4
)

// Security types
const (
	SecurityAES128GCM        byte = 3
	SecurityChacha20Poly1305 byte = 4
	SecurityNone             byte = 5
)

// CmdType is the type of vmess cmd
type CmdType byte

// Cmd Types
const (
	CmdTCP CmdType = 1
	CmdUDP CmdType = 2
)

// Client is a vmess client.
type Client struct {
	users    []*User
	count    int
	opt      byte
	aead     bool
	security byte
}

// Conn is a connection to vmess server.
type Conn struct {
	user     *User
	opt      byte
	aead     bool
	security byte

	atyp Atyp
	addr Addr
	port Port

	reqBodyIV   [16]byte
	reqBodyKey  [16]byte
	reqRespV    byte
	respBodyIV  [16]byte
	respBodyKey [16]byte

	writeChunkSizeParser ChunkSizeEncoder
	readChunkSizeParser  ChunkSizeDecoder

	net.Conn
	dataReader io.Reader
	dataWriter io.Writer
}

// NewClient returns a new vmess client.
func NewClient(uuidStr, security string, alterID int, aead bool) (*Client, error) {
	uuid, err := StrToUUID(uuidStr)
	if err != nil {
		return nil, err
	}

	c := &Client{}
	user := NewUser(uuid)
	c.users = append(c.users, user)
	c.users = append(c.users, user.GenAlterIDUsers(alterID)...)
	c.count = len(c.users)

	c.opt = OptChunkStream | OptMetadataObfuscate
	c.aead = aead

	security = strings.ToLower(security)
	switch security {
	case "aes-128-gcm":
		c.security = SecurityAES128GCM
	case "chacha20-poly1305":
		c.security = SecurityChacha20Poly1305
	case "none":
		c.security = SecurityNone
	case "":
		c.security = SecurityChacha20Poly1305
		if runtime.GOARCH == "amd64" || runtime.GOARCH == "s390x" || runtime.GOARCH == "arm64" {
			c.security = SecurityAES128GCM
		}
	default:
		return nil, errors.New("unknown security type: " + security)
	}

	// NOTE: give rand a new seed to avoid the same sequence of values
	rand.Seed(time.Now().UnixNano())

	return c, nil
}

// NewConn returns a new vmess conn.
func (c *Client) NewConn(rc net.Conn, target string, cmd CmdType) (*Conn, error) {
	r := rand.Intn(c.count)
	conn := &Conn{user: c.users[r], opt: c.opt, aead: c.aead, security: c.security, Conn: rc}

	var err error
	conn.atyp, conn.addr, conn.port, err = ParseAddr(target)
	if err != nil {
		return nil, err
	}

	randBytes := pool.GetBuffer(32)
	rand.Read(randBytes)
	copy(conn.reqBodyIV[:], randBytes[:16])
	copy(conn.reqBodyKey[:], randBytes[16:32])
	pool.PutBuffer(randBytes)

	conn.reqRespV = byte(rand.Intn(1 << 8))

	if conn.aead {
		bodyIV := sha256.Sum256(conn.reqBodyIV[:])
		bodyKey := sha256.Sum256(conn.reqBodyKey[:])
		copy(conn.respBodyIV[:], bodyIV[:16])
		copy(conn.respBodyKey[:], bodyKey[:16])
	} else {
		conn.respBodyIV = md5.Sum(conn.reqBodyIV[:])
		conn.respBodyKey = md5.Sum(conn.reqBodyKey[:])

		// MD5 Auth
		err = conn.Auth()
		if err != nil {
			return nil, err
		}
	}
	conn.writeChunkSizeParser = NewShakeSizeParser(conn.reqBodyIV[:])
	conn.readChunkSizeParser = NewShakeSizeParser(conn.respBodyIV[:])

	// Request
	err = conn.Request(cmd)
	if err != nil {
		return nil, err
	}

	return conn, nil
}

// Auth send auth info: HMAC("md5", UUID, UTC).
func (c *Conn) Auth() error {
	ts := pool.GetBuffer(8)
	defer pool.PutBuffer(ts)

	binary.BigEndian.PutUint64(ts, uint64(time.Now().UTC().Unix()))

	h := hmac.New(md5.New, c.user.UUID[:])
	h.Write(ts)

	_, err := c.Conn.Write(h.Sum(nil))
	return err
}

// Request sends request to server.
func (c *Conn) Request(cmd CmdType) error {
	buf := pool.GetBytesBuffer()
	defer pool.PutBytesBuffer(buf)

	// Request
	buf.WriteByte(1)           // Ver
	buf.Write(c.reqBodyIV[:])  // IV
	buf.Write(c.reqBodyKey[:]) // Key
	buf.WriteByte(c.reqRespV)  // V
	buf.WriteByte(c.opt)       // Opt

	// pLen and Sec
	paddingLen := rand.Intn(16)
	pSec := byte(paddingLen<<4) | c.security // P(4bit) and Sec(4bit)
	buf.WriteByte(pSec)

	buf.WriteByte(0)         // reserved
	buf.WriteByte(byte(cmd)) // cmd

	// target
	binary.Write(buf, binary.BigEndian, uint16(c.port)) // port
	buf.WriteByte(byte(c.atyp))                         // atyp
	buf.Write(c.addr)                                   // addr

	// padding
	if paddingLen > 0 {
		padding := pool.GetBuffer(paddingLen)
		rand.Read(padding)
		buf.Write(padding)
		pool.PutBuffer(padding)
	}

	// F
	fnv1a := fnv.New32a()
	fnv1a.Write(buf.Bytes())
	buf.Write(fnv1a.Sum(nil))

	if c.aead {
		encHeader := sealVMessAEADHeader(c.user.CmdKey, buf.Bytes())
		_, err := c.Conn.Write(encHeader)
		return err
	}

	block, err := aes.NewCipher(c.user.CmdKey[:])
	if err != nil {
		return err
	}

	stream := cipher.NewCFBEncrypter(block, TimestampHash(time.Now().UTC()))
	stream.XORKeyStream(buf.Bytes(), buf.Bytes())

	_, err = c.Conn.Write(buf.Bytes())

	return err
}

// DecodeRespHeader decodes response header.
func (c *Conn) DecodeRespHeader() error {
	if c.aead {
		buf, err := openVMessAEADHeader(c.respBodyKey, c.respBodyIV, c.Conn)
		if err != nil {
			return err
		}

		if len(buf) < 4 {
			return errors.New("unexpected buffer length")
		}

		if buf[0] != c.reqRespV {
			return errors.New("unexpected response header")
		}

		// TODO: Dynamic port support
		if buf[2] != 0 {
			// dataLen := int32(buf[3])
			return errors.New("dynamic port is not supported now")
		}
		return nil
	}

	block, err := aes.NewCipher(c.respBodyKey[:])
	if err != nil {
		return err
	}
	stream := cipher.NewCFBDecrypter(block, c.respBodyIV[:])

	buf := pool.GetBuffer(4)
	defer pool.PutBuffer(buf)

	_, err = io.ReadFull(c.Conn, buf)
	if err != nil {
		return err
	}
	stream.XORKeyStream(buf, buf)

	if buf[0] != c.reqRespV {
		return errors.New("unexpected response header")
	}

	// TODO: Dynamic port support
	if buf[2] != 0 {
		// dataLen := int32(buf[3])
		return errors.New("dynamic port is not supported now")
	}

	return nil
}

func (c *Conn) Write(b []byte) (n int, err error) {
	if c.dataWriter != nil {
		return c.dataWriter.Write(b)
	}

	c.dataWriter = c.Conn
	if c.opt&OptChunkStream == OptChunkStream {
		switch c.security {
		case SecurityNone:
			c.dataWriter = ChunkedWriter(c.Conn, c.writeChunkSizeParser)

		case SecurityAES128GCM:
			block, _ := aes.NewCipher(c.reqBodyKey[:])
			aead, _ := cipher.NewGCM(block)
			c.dataWriter = AEADWriter(c.Conn, aead, c.reqBodyIV[:], c.writeChunkSizeParser)

		case SecurityChacha20Poly1305:
			key := pool.GetBuffer(32)
			t := md5.Sum(c.reqBodyKey[:])
			copy(key, t[:])
			t = md5.Sum(key[:16])
			copy(key[16:], t[:])
			aead, _ := chacha20poly1305.New(key)
			c.dataWriter = AEADWriter(c.Conn, aead, c.reqBodyIV[:], c.writeChunkSizeParser)
			pool.PutBuffer(key)
		}
	}

	return c.dataWriter.Write(b)
}

func (c *Conn) Read(b []byte) (n int, err error) {
	if c.dataReader != nil {
		return c.dataReader.Read(b)
	}

	err = c.DecodeRespHeader()
	if err != nil {
		return 0, fmt.Errorf("[vmess] error in DecodeRespHeader: %w", err)
	}

	c.dataReader = c.Conn
	if c.opt&OptChunkStream == OptChunkStream {
		switch c.security {
		case SecurityNone:
			c.dataReader = ChunkedReader(c.Conn, c.readChunkSizeParser)

		case SecurityAES128GCM:
			block, _ := aes.NewCipher(c.respBodyKey[:])
			aead, _ := cipher.NewGCM(block)
			c.dataReader = AEADReader(c.Conn, aead, c.respBodyIV[:], c.readChunkSizeParser)

		case SecurityChacha20Poly1305:
			key := pool.GetBuffer(32)
			t := md5.Sum(c.respBodyKey[:])
			copy(key, t[:])
			t = md5.Sum(key[:16])
			copy(key[16:], t[:])
			aead, _ := chacha20poly1305.New(key)
			c.dataReader = AEADReader(c.Conn, aead, c.respBodyIV[:], c.readChunkSizeParser)
			pool.PutBuffer(key)
		}
	}

	return c.dataReader.Read(b)
}
