package tapdance

import (
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"strconv"
	"time"

	"github.com/refraction-networking/gotapdance/ed25519/extra25519"
	"golang.org/x/crypto/curve25519"
)

const AES_GCM_TAG_SIZE = 16

const timeoutMax = 30000
const timeoutMin = 20000

const sendLimitMax = 15614
const sendLimitMin = 14400

// timeout for sending TD request and getting a response
const deadlineConnectTDStationMin = 11175
const deadlineConnectTDStationMax = 14231

// deadline to establish TCP connection to decoy
const deadlineTCPtoDecoyMin = deadlineConnectTDStationMin
const deadlineTCPtoDecoyMax = deadlineConnectTDStationMax

// during reconnects we send FIN to server and wait until we get FIN back
const waitForFINDieMin = 2 * deadlineConnectTDStationMin
const waitForFINDieMax = 2 * deadlineConnectTDStationMax

const maxInt16 = int16(^uint16(0) >> 1) // max msg size -> might have to chunk
//const minInt16 = int16(-maxInt16 - 1)

type flowType int8

/*______________________TapdanceFlowConn Mode Chart _________________________________\
|FlowType     |Default Tag|Diff from old-school bidirectional       | Engines spawned|
|-------------|-----------|-----------------------------------------|----------------|
|Bidirectional| HTTP GET  |                                         | Writer, Reader |
|Upload       | HTTP POST | acquires upload                         | Writer, Reader |
|ReadOnly     | HTTP GET  | yields upload, writer sync ignored      | Reader         |
|Rendezvous   | HTTP GET  | passes data in handshake and shuts down |                |
\_____________|___________|_________________________________________|_______________*/

const (
	flowUpload        flowType = 0x1
	flowReadOnly      flowType = 0x2
	flowBidirectional flowType = 0x4
	flowRendezvous    flowType = 0x0 // rendezvous flows shutdown after handshake
)

func (m *flowType) Str() string {
	switch *m {
	case flowUpload:
		return "FlowUpload"
	case flowReadOnly:
		return "FlowReadOnly"
	case flowBidirectional:
		return "FlowBidirectional"
	default:
		return strconv.Itoa(int(*m))
	}
}

type msgType int8

const (
	msgRawData  msgType = 1
	msgProtobuf msgType = 2
)

func (m *msgType) Str() string {
	switch *m {
	case msgRawData:
		return "msg raw_data"
	case msgProtobuf:
		return "msg protobuf"
	default:
		return strconv.Itoa(int(*m))
	}
}

var errMsgClose = errors.New("MSG CLOSE")
var errNotImplemented = errors.New("Not implemented")

type tdTagType int8

const (
	tagHttpGetIncomplete  tdTagType = 0
	tagHttpGetComplete    tdTagType = 1
	tagHttpPostIncomplete tdTagType = 2
)

func (m *tdTagType) Str() string {
	switch *m {
	case tagHttpGetIncomplete:
		return "HTTP GET Incomplete"
	case tagHttpGetComplete:
		return "HTTP GET Complete"
	case tagHttpPostIncomplete:
		return "HTTP POST Incomplete"
	default:
		return strconv.Itoa(int(*m))
	}
}

// Fixed-Size-Payload has a 1 byte flags field.
// bit 0 (1 << 7) determines if flow is bidirectional(0) or upload-only(1)
// bit 1 (1 << 6) enables dark-decoys
// bits 2-5 are unassigned
// bit 6 determines whether PROXY-protocol-formatted string will be sent
// bit 7 (1 << 0) signals to use TypeLen outer proto
var (
	tdFlagUploadOnly = uint8(1 << 7)
	// tdFlagDarkDecoy   = uint8(1 << 6)
	tdFlagProxyHeader = uint8(1 << 1)
	tdFlagUseTIL      = uint8(1 << 0)
)

var default_flags = tdFlagUseTIL

// Global EnableProxyProtocol() is deprecated,
// use tapdance.Dialer with UseProxyHeader flag instead
//
// Requests station to send client's IP to covert in following form:
// PROXY TCP4 x.x.x.x 127.0.0.1 1111 1234\r\n
//       ^__^ ^_____^ ^_________________^
//      proto clientIP      garbage
func EnableProxyProtocol() {
	Logger().Println("tapdance.EnableProxyProtocol() is deprecated, " +
		"use tapdance.Dialer with UseProxyHeader flag instead.")
	default_flags |= tdFlagProxyHeader
	return
}

var tlsSecretLog string

func SetTlsLogFilename(filename string) error {
	tlsSecretLog = filename
	// Truncate file
	f, err := os.Create(filename)
	if err != nil {
		return err
	}
	return f.Close()
}

func WriteTlsLog(clientRandom, masterSecret []byte) error {
	if tlsSecretLog != "" {
		f, err := os.OpenFile(tlsSecretLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
		if err != nil {
			return err
		}

		_, err = fmt.Fprintf(f, "CLIENT_RANDOM %s %s\n",
			hex.EncodeToString(clientRandom),
			hex.EncodeToString(masterSecret))
		if err != nil {
			return err
		}

		return f.Close()
	}
	return nil
}

// How much time to sleep on trying to connect to decoys to prevent overwhelming them
func sleepBeforeConnect(attempt int) (waitTime <-chan time.Time) {
	if attempt >= 6 { // return nil for first 6 attempts
		waitTime = time.After(time.Second * 1)
	}
	return
}

// takes Station's Public Key
// returns Shared Secret, and Eligator Representative
func generateEligatorTransformedKey(stationPubkey []byte) ([]byte, []byte, error) {
	if len(stationPubkey) != 32 {
		return nil, nil, errors.New("Unexpected station pubkey length. Expected: 32." +
			" Received: " + strconv.Itoa(len(stationPubkey)) + ".")
	}
	var sharedSecret, clientPrivate, clientPublic, representative [32]byte
	for ok := false; ok != true; {
		var sliceKeyPrivate []byte = clientPrivate[:]
		_, err := rand.Read(sliceKeyPrivate)
		if err != nil {
			return nil, nil, err
		}

		ok = extra25519.ScalarBaseMult(&clientPublic, &representative, &clientPrivate)
	}
	var stationPubkeyByte32 [32]byte
	copy(stationPubkeyByte32[:], stationPubkey)
	curve25519.ScalarMult(&sharedSecret, &clientPrivate, &stationPubkeyByte32)

	// extra25519.ScalarBaseMult does not randomize most significant bit(sign of y_coord?)
	// Other implementations of elligator may have up to 2 non-random bits.
	// Here we randomize the bit, expecting it to be flipped back to 0 on station
	randByte := make([]byte, 1)
	_, err := rand.Read(randByte)
	if err != nil {
		return nil, nil, err
	}
	representative[31] |= (0xC0 & randByte[0])
	return sharedSecret[:], representative[:], nil
}
