///////////////////////////////////////////////////////////////////////////////
// Copyright © 2020 xx network SEZC                                          //
//                                                                           //
// Use of this source code is governed by a license that can be found in the //
// LICENSE file                                                              //
///////////////////////////////////////////////////////////////////////////////

package auth

import (
	"fmt"
	"io"
	"strings"

	"github.com/cloudflare/circl/dh/sidh"
	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"
	"gitlab.com/elixxir/client/cmix"
	"gitlab.com/elixxir/client/cmix/message"
	"gitlab.com/elixxir/client/e2e"
	"gitlab.com/elixxir/client/e2e/ratchet"
	util "gitlab.com/elixxir/client/storage/utility"
	"gitlab.com/elixxir/crypto/contact"
	"gitlab.com/elixxir/crypto/cyclic"
	"gitlab.com/elixxir/crypto/diffieHellman"
	cAuth "gitlab.com/elixxir/crypto/e2e/auth"
	"gitlab.com/elixxir/primitives/fact"
	"gitlab.com/elixxir/primitives/format"
	"gitlab.com/xx_network/primitives/id"
)

const terminator = ";"

// Error constant strings. Any changes to these should go over usages of the
// affected messages in other applications (if applicable)
const (
	// ErrChannelExists is a message returned in state.Request when an
	// authenticated channel exists between the partner and me.
	ErrChannelExists = "Authenticated channel already established with partner"
)

// Request sends a contact request from the user identity in the imported e2e
// structure to the passed contact, as well as the passed facts (will error if
// they are too long).
// The other party must accept the request by calling Confirm in order to be
// able to send messages using e2e.Handler.SendE2e. When the other party does so,
// the "confirm" callback will get called.
// The round the request is initially sent on will be returned, but the request
// will be listed as a critical message, so the underlying cmix client will
// auto resend it in the event of failure.
// A request cannot be sent for a contact who has already received a request or
// who is already a partner.
func (s *state) Request(partner contact.Contact, myfacts fact.FactList) (id.Round, error) {
	// check that an authenticated channel does not already exist
	if _, err := s.e2e.GetPartner(partner.ID); err == nil ||
		!strings.Contains(err.Error(), ratchet.NoPartnerErrorStr) {
		return 0, errors.Errorf(ErrChannelExists)
	}

	return s.request(partner, myfacts, false)
}

// request internal helper
func (s *state) request(partner contact.Contact, myfacts fact.FactList,
	reset bool) (id.Round, error) {

	jww.INFO.Printf("request(...) called")

	//do key generation
	rng := s.rng.GetStream()
	defer rng.Close()

	me := s.e2e.GetReceptionID()

	dhGrp := s.e2e.GetGroup()

	dhPriv, dhPub := genDHKeys(dhGrp, rng)
	sidhPriv, sidhPub := util.GenerateSIDHKeyPair(
		sidh.KeyVariantSidhA, rng)

	historicalDHPriv := s.e2e.GetHistoricalDHPrivkey()
	historicalDHPub := diffieHellman.GeneratePublicKey(historicalDHPriv,
		dhGrp)

	if !dhGrp.Inside(partner.DhPubKey.GetLargeInt()) {
		return 0, errors.Errorf("partner's DH public key is not in the E2E "+
			"group; E2E group fingerprint is %d and DH key has %d",
			dhGrp.GetFingerprint(), partner.DhPubKey.GetGroupFingerprint())
	}
	ownership := cAuth.MakeOwnershipProof(historicalDHPriv,
		partner.DhPubKey, dhGrp)
	confirmFp := cAuth.MakeOwnershipProofFP(ownership)

	// Add the sent request and use the return to build the
	// send. This will replace the send with an old one if one was
	// in process, wasting the key generation above. This is
	// considered a reasonable loss due to the increase in code
	// simplicity of this approach
	sr, err := s.store.AddSent(partner.ID, partner.DhPubKey, dhPriv, dhPub,
		sidhPriv, sidhPub, confirmFp, reset)
	if err != nil {
		if sr == nil {
			return 0, err
		} else {
			jww.INFO.Printf("Resending request to %s from %s as "+
				"one was already sent", partner.ID, me)
			dhPriv = sr.GetMyPrivKey()
			dhPub = sr.GetMyPubKey()
			//sidhPriv = sr.GetMySIDHPrivKey()
			sidhPub = sr.GetMySIDHPubKey()
		}
	}

	// cMix fingerprint. Used in old versions by the recipient can recognize
	// this is a request message. Unchanged for backwards compatability
	// (the SIH is used now)
	requestfp := cAuth.MakeRequestFingerprint(partner.DhPubKey)

	// My fact data so we can display in the interface.
	msgPayload := []byte(myfacts.Stringify() + terminator)

	// Create the request packet.
	request, mac, err := createRequestAuth(me, msgPayload, ownership,
		dhPriv, dhPub, partner.DhPubKey, sidhPub,
		s.e2e.GetGroup(), s.net.GetMaxMessageLength())
	if err != nil {
		return 0, err
	}
	contents := request.Marshal()

	jww.TRACE.Printf("AuthRequest MYPUBKEY: %v", dhPub.TextVerbose(16, 0))
	jww.TRACE.Printf("AuthRequest PARTNERPUBKEY: %v",
		partner.DhPubKey.TextVerbose(16, 0))
	jww.TRACE.Printf("AuthRequest MYSIDHPUBKEY: %s",
		util.StringSIDHPubKey(sidhPub))

	jww.TRACE.Printf("AuthRequest HistoricalPUBKEY: %v",
		historicalDHPub.TextVerbose(16, 0))

	jww.TRACE.Printf("AuthRequest ECRPAYLOAD: %v", request.GetEcrPayload())
	jww.TRACE.Printf("AuthRequest MAC: %v", mac)

	jww.INFO.Printf("Requesting Auth with %s, msgDigest: %s, confirmFp: %s",
		partner.ID, format.DigestContents(contents), confirmFp)

	p := cmix.GetDefaultCMIXParams()
	p.DebugTag = "auth.Request"
	tag := s.params.RequestTag
	if reset {
		tag = s.params.ResetRequestTag
	}
	svc := message.Service{
		Identifier: partner.ID.Marshal(),
		Tag:        tag,
		Metadata:   nil,
	}
	round, _, err := s.net.Send(partner.ID, requestfp, svc, contents, mac, p)
	if err != nil {
		// if the send fails just set it to failed, it will
		// but automatically retried
		return 0, errors.WithMessagef(err, "Auth Request with %s "+
			"(msgDigest: %s) failed to transmit: %+v", partner.ID,
			format.DigestContents(contents), err)
	}

	em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+
		" on round %d", partner.ID, format.DigestContents(contents), round)
	jww.INFO.Print(em)
	s.event.Report(1, "Auth", "RequestSent", em)
	return round, nil

}

// genDHKeys is a short helper to generate a Diffie-Helman Keypair
func genDHKeys(dhGrp *cyclic.Group, csprng io.Reader) (priv, pub *cyclic.Int) {
	numBytes := len(dhGrp.GetPBytes())
	newPrivKey := diffieHellman.GeneratePrivateKey(numBytes, dhGrp, csprng)
	newPubKey := diffieHellman.GeneratePublicKey(newPrivKey, dhGrp)
	return newPrivKey, newPubKey
}

// createRequestAuth Creates the request packet, including encrypting the
// required parts of it.
func createRequestAuth(sender *id.ID, payload, ownership []byte, myDHPriv,
	myDHPub, theirDHPub *cyclic.Int, mySIDHPub *sidh.PublicKey,
	dhGrp *cyclic.Group, cMixSize int) (*baseFormat, []byte, error) {
	/*generate embedded message structures and check payload*/
	dhPrimeSize := dhGrp.GetP().ByteLen()

	// FIXME: This base -> ecr -> request structure is a little wonky.
	// We should refactor so that is is more direct.
	// I recommend we move to a request object that takes:
	//   sender, dhPub, sidhPub, ownershipProof, payload
	// with a Marshal/Unmarshal that takes the Dh/grp needed to gen
	// the session key and encrypt or decrypt.

	// baseFmt wraps ecrFmt. ecrFmt is encrypted
	baseFmt := newBaseFormat(cMixSize, dhPrimeSize)
	// ecrFmt wraps requestFmt
	ecrFmt := newEcrFormat(baseFmt.GetEcrPayloadLen())
	requestFmt, err := newRequestFormat(ecrFmt)
	if err != nil {
		return nil, nil, errors.Errorf("failed to make request format: %+v", err)
	}

	if len(payload) > requestFmt.MsgPayloadLen() {
		return nil, nil, errors.Errorf(
			"Combined message longer than space "+
				"available in payload; available: %v, length: %v",
			requestFmt.MsgPayloadLen(), len(payload))
	}

	/*encrypt payload*/
	requestFmt.SetID(sender)
	requestFmt.SetMsgPayload(payload)
	ecrFmt.SetOwnership(ownership)
	ecrFmt.SetSidHPubKey(mySIDHPub)
	ecrPayload, mac := cAuth.Encrypt(myDHPriv, theirDHPub, ecrFmt.data,
		dhGrp)
	/*construct message*/
	baseFmt.SetEcrPayload(ecrPayload)
	baseFmt.SetPubKey(myDHPub)

	return &baseFmt, mac, nil
}

func (s *state) GetReceivedRequest(partner *id.ID) (contact.Contact, error) {
	return s.store.GetReceivedRequest(partner)
}

func (s *state) VerifyOwnership(received, verified contact.Contact,
	e2e e2e.Handler) bool {
	return VerifyOwnership(received, verified, e2e)
}

func (s *state) DeleteRequest(partnerID *id.ID) error {
	return s.store.DeleteRequest(partnerID)
}

func (s *state) DeleteAllRequests() error {
	return s.store.DeleteAllRequests()
}

func (s *state) DeleteSentRequests() error {
	return s.store.DeleteSentRequests()
}

func (s *state) DeleteReceiveRequests() error {
	return s.store.DeleteReceiveRequests()
}