package exchange

import (
	"context"
	"crypto/rand"
	"math/big"

	"github.com/go-faster/errors"
	"go.uber.org/zap"

	"github.com/gotd/td/bin"
	"github.com/gotd/td/internal/crypto"
	"github.com/gotd/td/internal/mt"
	"github.com/gotd/td/internal/proto"
)

// Run runs client-side flow.
func (c ClientExchange) Run(ctx context.Context) (ClientExchangeResult, error) {
	// 1. DH exchange initiation.
	nonce, err := crypto.RandInt128(c.rand)
	if err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "client nonce generation")
	}
	b := new(bin.Buffer)

	c.log.Debug("Sending ReqPqMultiRequest")
	if err := c.writeUnencrypted(ctx, b, &mt.ReqPqMultiRequest{Nonce: nonce}); err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "write ReqPqMultiRequest")
	}

	// 2. Server sends response of the form
	// resPQ#05162463 nonce:int128 server_nonce:int128 pq:string server_public_key_fingerprints:Vector long = ResPQ;
	var res mt.ResPQ
	if err := c.readUnencrypted(ctx, b, &res); err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "read ResPQ response")
	}
	c.log.Debug("Received server ResPQ")
	if res.Nonce != nonce {
		return ClientExchangeResult{}, errors.New("ResPQ nonce mismatch")
	}
	serverNonce := res.ServerNonce

	// Selecting first public key that match fingerprint.
	var selectedPubKey PublicKey
Loop:
	for _, key := range c.keys {
		f := key.Fingerprint()

		for _, fingerprint := range res.ServerPublicKeyFingerprints {
			if fingerprint == f {
				selectedPubKey = key
				break Loop
			}
		}
	}
	if selectedPubKey.Zero() {
		return ClientExchangeResult{}, ErrKeyFingerprintNotFound
	}

	// The pq is a representation of a natural number (in binary big endian format).
	// SetBytes is also big endian.
	pq := big.NewInt(0).SetBytes(res.Pq)
	// Normally pq is less than or equal to 2^63-1.
	pqMax := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(63), nil)
	if pq.Cmp(pqMax) > 0 {
		return ClientExchangeResult{}, errors.New("server provided bad pq")
	}

	start := c.clock.Now()
	// 3. Client decomposes pq into prime factors such that p < q.
	// Performing proof of work.
	p, q, err := crypto.DecomposePQ(pq, c.rand)
	if err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "decompose pq")
	}
	c.log.Debug("PQ decomposing complete", zap.Duration("took", c.clock.Now().Sub(start)))
	// Make a copy of p and q values to reduce allocations.
	pBytes := p.Bytes()
	qBytes := q.Bytes()

	// 4. Client sends query to server.
	// req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:string q:string
	//   public_key_fingerprint:long encrypted_data:string = Server_DH_Params
	newNonce, err := crypto.RandInt256(c.rand)
	if err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "generate new nonce")
	}

	var encryptedData []byte
	pqInnerData := &mt.PQInnerDataDC{
		Pq:          res.Pq,
		Nonce:       nonce,
		NewNonce:    newNonce,
		ServerNonce: serverNonce,
		P:           pBytes,
		Q:           qBytes,
		DC:          c.dc,
	}
	b.Reset()
	if err := pqInnerData.Encode(b); err != nil {
		return ClientExchangeResult{}, err
	}

	// `encrypted_data := RSA_PAD(data, server_public_key);`
	data, err := crypto.RSAPad(b.Buf, selectedPubKey.RSA, c.rand)
	if err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "encrypted_data generation")
	}

	encryptedData = data

	reqDHParams := &mt.ReqDHParamsRequest{
		Nonce:                nonce,
		ServerNonce:          serverNonce,
		P:                    pBytes,
		Q:                    qBytes,
		PublicKeyFingerprint: selectedPubKey.Fingerprint(),
		EncryptedData:        encryptedData,
	}
	c.log.Debug("Sending ReqDHParamsRequest")
	if err := c.writeUnencrypted(ctx, b, reqDHParams); err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "write ReqDHParamsRequest")
	}

	// 5. Server responds with Server_DH_Params.
	if err := c.conn.Recv(ctx, b); err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "read ServerDHParams message")
	}
	c.log.Debug("Received server ServerDHParams")

	var plaintextMsg proto.UnencryptedMessage
	if err := plaintextMsg.Decode(b); err != nil {
		return ClientExchangeResult{}, errors.Wrap(err, "decode ServerDHParams message")
	}

	b.ResetTo(plaintextMsg.MessageData)
	dhParams, err := mt.DecodeServerDHParams(b)
	if err != nil {
		return ClientExchangeResult{}, err
	}
	switch p := dhParams.(type) {
	case *mt.ServerDHParamsOk:
		// Success.
		if p.Nonce != nonce {
			return ClientExchangeResult{}, errors.New("ServerDHParamsOk nonce mismatch")
		}
		if p.ServerNonce != serverNonce {
			return ClientExchangeResult{}, errors.New("ServerDHParamsOk server nonce mismatch")
		}

		key, iv := crypto.TempAESKeys(newNonce.BigInt(), serverNonce.BigInt())
		// Decrypting inner data.
		data, err := crypto.DecryptExchangeAnswer(p.EncryptedAnswer, key, iv)
		if err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "exchange answer decrypt")
		}
		b.ResetTo(data)

		innerData := mt.ServerDHInnerData{}
		if err := innerData.Decode(b); err != nil {
			return ClientExchangeResult{}, err
		}
		if innerData.Nonce != nonce {
			return ClientExchangeResult{}, errors.New("ServerDHInnerData nonce mismatch")
		}
		if innerData.ServerNonce != serverNonce {
			return ClientExchangeResult{}, errors.New("ServerDHInnerData server nonce mismatch")
		}

		dhPrime := big.NewInt(0).SetBytes(innerData.DhPrime)
		g := big.NewInt(int64(innerData.G))
		if err := crypto.CheckDH(innerData.G, dhPrime); err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "check DH params")
		}
		gA := big.NewInt(0).SetBytes(innerData.GA)

		// 6. Random number b is computed:
		randMax := big.NewInt(0).SetBit(big.NewInt(0), crypto.RSAKeyBits, 1)
		bParam, err := rand.Int(c.rand, randMax)
		if err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "number b generation")
		}
		// g_b = g^b mod dh_prime
		gB := big.NewInt(0).Exp(g, bParam, dhPrime)

		// Checking key exchange parameters.
		if err := crypto.CheckDHParams(dhPrime, g, gA, gB); err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "key exchange failed: invalid params")
		}

		clientInnerData := mt.ClientDHInnerData{
			ServerNonce: innerData.ServerNonce,
			Nonce:       innerData.Nonce,
			GB:          gB.Bytes(),
			// first attempt
			RetryID: 0,
		}
		b.Reset()
		if err := clientInnerData.Encode(b); err != nil {
			return ClientExchangeResult{}, err
		}
		clientEncrypted, err := crypto.EncryptExchangeAnswer(c.rand, b.Buf, key, iv)
		if err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "exchange answer encrypt")
		}

		setParamsReq := &mt.SetClientDHParamsRequest{
			Nonce:         nonce,
			ServerNonce:   reqDHParams.ServerNonce,
			EncryptedData: clientEncrypted,
		}
		c.log.Debug("Sending SetClientDHParamsRequest")
		if err := c.writeUnencrypted(ctx, b, setParamsReq); err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "write SetClientDHParamsRequest")
		}

		// 7. Computing auth_key using formula (g_a)^b mod dh_prime
		authKey := big.NewInt(0).Exp(gA, bParam, dhPrime)

		b.Reset()
		if err := c.conn.Recv(ctx, b); err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "read DhGen message")
		}
		c.log.Debug("Received server DhGen")

		if err := plaintextMsg.Decode(b); err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "decode DhGen message")
		}
		b.ResetTo(plaintextMsg.MessageData)
		dhSetRes, err := mt.DecodeSetClientDHParamsAnswer(b)
		if err != nil {
			return ClientExchangeResult{}, errors.Wrap(err, "decode DhGen answer")
		}
		switch v := dhSetRes.(type) {
		case *mt.DhGenOk: // dh_gen_ok#3bcbf734
			if v.Nonce != nonce {
				return ClientExchangeResult{}, errors.New("DhGenOk nonce mismatch")
			}
			if v.ServerNonce != serverNonce {
				return ClientExchangeResult{}, errors.New("DhGenOk server nonce mismatch")
			}

			var key crypto.Key
			authKey.FillBytes(key[:])
			authKeyID := key.ID()

			// Checking received hash.
			nonceHash1 := crypto.NonceHash1(newNonce, key)
			serverSalt := crypto.ServerSalt(newNonce, v.ServerNonce)

			if nonceHash1 != v.NewNonceHash1 {
				return ClientExchangeResult{}, errors.New("key exchange verification failed: hash mismatch")
			}

			// Generating new session id and salt.
			sessionID, err := crypto.NewSessionID(c.rand)
			if err != nil {
				return ClientExchangeResult{}, err
			}

			return ClientExchangeResult{
				AuthKey:    crypto.AuthKey{Value: key, ID: authKeyID},
				SessionID:  sessionID,
				ServerSalt: serverSalt,
			}, nil
		case *mt.DhGenRetry: // dh_gen_retry#46dc1fb9
			return ClientExchangeResult{}, errors.Errorf("retry required: %x", v.NewNonceHash2)
		case *mt.DhGenFail: // dh_gen_fail#a69dae02
			return ClientExchangeResult{}, errors.Errorf("dh_hen_fail: %x", v.NewNonceHash3)
		default:
			return ClientExchangeResult{}, errors.Errorf("unexpected SetClientDHParamsRequest result %T", v)
		}
	case *mt.ServerDHParamsFail:
		return ClientExchangeResult{}, errors.New("server respond with server_DH_params_fail")
	default:
		return ClientExchangeResult{}, errors.Errorf("unexpected ReqDHParamsRequest result %T", p)
	}
}
