package applications

import (
	"crypto/subtle"
	"encoding/json"
	"git.openprivacy.ca/cwtch.im/tapir"
	"git.openprivacy.ca/cwtch.im/tapir/primitives"
	torProvider "git.openprivacy.ca/openprivacy/connectivity/tor"
	"git.openprivacy.ca/openprivacy/log"
	"golang.org/x/crypto/ed25519"
)

// AuthMessage is exchanged between peers to obtain the Auth Capability
type AuthMessage struct {
	LongTermPublicKey  ed25519.PublicKey
	EphemeralPublicKey ed25519.PublicKey
}

// AuthCapability defines the Authentication Capability granted by AuthApp
const AuthCapability = tapir.Capability("AuthenticationCapability")

// AuthApp is the concrete Application type that handles Authentication
type AuthApp struct {
	TranscriptApp
}

// NewInstance creates a new instance of the AuthApp
func (ea AuthApp) NewInstance() tapir.Application {
	return new(AuthApp)
}

// Init runs the entire AuthApp protocol, at the end of the protocol either the connection is granted AUTH capability
// or the connection is closed.
func (ea *AuthApp) Init(connection tapir.Connection) {
	ea.TranscriptApp.Init(connection)
	longTermPubKey := ed25519.PublicKey(connection.ID().PublicKeyBytes())
	ephemeralIdentity, _ := primitives.InitializeEphemeralIdentity()
	authMessage := AuthMessage{LongTermPublicKey: longTermPubKey, EphemeralPublicKey: ephemeralIdentity.PublicKey()}
	serialized, _ := json.Marshal(authMessage)
	connection.Send(serialized)
	message := connection.Expect()

	var remoteAuthMessage AuthMessage
	err := json.Unmarshal(message, &remoteAuthMessage)
	if err != nil {
		connection.Close()
		return
	}

	// If we are an outbound connection we can perform an additional check to ensure that the server sent us back the correct long term
	// public key
	if connection.IsOutbound() && torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey) != connection.Hostname() {
		log.Errorf("The remote server (%v) has attempted to authenticate with a different public key %v", connection.Hostname(), torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey))
		connection.Close()
		return
	}

	// Perform the triple-diffie-hellman exchange.
	key, err := primitives.Perform3DH(connection.ID(), &ephemeralIdentity, remoteAuthMessage.LongTermPublicKey, remoteAuthMessage.EphemeralPublicKey, connection.IsOutbound())
	if err != nil {
		log.Errorf("Failed Auth Challenge %v", err)
		connection.Close()
		return
	}
	connection.SetEncryptionKey(key)

	// We just successfully unmarshaled both of these, so we can safely ignore the err return from these functions.
	challengeRemote, _ := json.Marshal(remoteAuthMessage)
	challengeLocal, _ := json.Marshal(authMessage)

	// Define canonical labels so both sides of the connection can generate the same key
	var outboundAuthMessage []byte
	var outboundHostname string
	var inboundAuthMessage []byte
	var inboundHostname string

	if connection.IsOutbound() {
		outboundHostname = connection.ID().Hostname()
		inboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
		outboundAuthMessage = challengeLocal
		inboundAuthMessage = challengeRemote
	} else {
		outboundHostname = torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
		inboundHostname = connection.ID().Hostname()
		outboundAuthMessage = challengeRemote
		inboundAuthMessage = challengeLocal
	}

	// Derive a challenge from the transcript of the public parameters of this authentication protocol
	transcript := ea.Transcript()
	transcript.NewProtocol("auth-app")
	transcript.AddToTranscript("outbound-hostname", []byte(outboundHostname))
	transcript.AddToTranscript("inbound-hostname", []byte(inboundHostname))
	transcript.AddToTranscript("outbound-challenge", outboundAuthMessage)
	transcript.AddToTranscript("inbound-challenge", inboundAuthMessage)
	challengeBytes := transcript.CommitToTranscript("3dh-auth-challenge")

	// If debug is turned on we will dump the transcript to log.
	// There is nothing sensitive in this transcript
	log.Debugf("Transcript: %s", transcript.OutputTranscriptToAudit())

	// Since we have set the encryption key on the connection the connection will encrypt any messages we send with that key
	// To test that the remote peer has done the same we calculate a challenge hash based on the transcript so far and send it to them
	// along with our hostname
	// We expect the remote to do the same, and compare the two.
	// If successful we extend our auth capability to the connection and reassert the hostname.
	// We note that the only successful scenario here requires that the remote peer have successfully derived the same
	// encryption key and the same transcript challenge.
	connection.Send(append(challengeBytes, []byte(connection.ID().Hostname())...))
	remoteChallenge := connection.Expect()
	assertedHostname := torProvider.GetTorV3Hostname(remoteAuthMessage.LongTermPublicKey)
	if subtle.ConstantTimeCompare(append(challengeBytes, []byte(assertedHostname)...), remoteChallenge) == 1 {
		connection.SetHostname(assertedHostname)
		connection.SetCapability(AuthCapability)
	} else {
		log.Debugf("Failed Decrypt Challenge: [%x] [%x]\n", remoteChallenge, challengeBytes)
		connection.Close()
	}
}
