package applications

import (
	"crypto/sha256"
	"git.openprivacy.ca/cwtch.im/tapir"
	"git.openprivacy.ca/cwtch.im/tapir/primitives/core"
	"git.openprivacy.ca/openprivacy/log"
	ristretto "github.com/gtank/ristretto255"
)

// ProofOfWorkApplication forces the incoming connection to do proof of work before granting a capability
type ProofOfWorkApplication struct {
	TranscriptApp
}

// transcript constants
const (
	PoWApp       = "pow-app"
	PoWSeed      = "pow-seed"
	PoWChallenge = "pow-challenge"
	PoWPRNG      = "pow-prng"
	PoWSolution  = "pow-solution"
)

// SuccessfulProofOfWorkCapability is given when a successfully PoW Challenge has been Completed
const SuccessfulProofOfWorkCapability = tapir.Capability("SuccessfulProofOfWorkCapability")

// NewInstance should always return a new instantiation of the application.
func (powapp *ProofOfWorkApplication) NewInstance() tapir.Application {
	return new(ProofOfWorkApplication)
}

// Init is run when the connection is first started.
func (powapp *ProofOfWorkApplication) Init(connection tapir.Connection) {
	powapp.Transcript().NewProtocol(PoWApp)
	if connection.IsOutbound() {
		powapp.Transcript().AddToTranscript(PoWSeed, connection.Expect())
		solution := powapp.solveChallenge(powapp.Transcript().CommitToTranscript(PoWChallenge), powapp.transcript.CommitToPRNG(PoWPRNG))
		powapp.transcript.AddToTranscript(PoWSolution, solution)
		connection.Send(solution)
		connection.SetCapability(SuccessfulProofOfWorkCapability) // We can self grant.because the server will close the connection on failure
		return
	}

	// We may be the first application, in which case we need to randomize the transcript challenge
	// We use the random hostname of the inbound server (if we've authenticated them then the challenge will
	// already be sufficiently randomized, so this doesn't hurt)
	// It does sadly mean an additional round trip.
	powapp.Transcript().AddToTranscript(PoWSeed, []byte(connection.Hostname()))
	connection.Send([]byte(connection.Hostname()))
	solution := connection.Expect()
	challenge := powapp.Transcript().CommitToTranscript(PoWChallenge)
	// soft-commitment to the prng, doesn't force the client to use it (but we could technically check that it did, not necessary for the security of this App)
	powapp.transcript.CommitToPRNG(PoWPRNG)
	powapp.transcript.AddToTranscript(PoWSolution, solution)
	if powapp.validateChallenge(challenge, solution) {
		connection.SetCapability(SuccessfulProofOfWorkCapability)
		return
	}
}

// SolveChallenge takes in a challenge and a message and returns a solution
// The solution is a 24 byte nonce which when hashed with the challenge and the message
// produces a sha256 hash with Difficulty leading 0s
func (powapp *ProofOfWorkApplication) solveChallenge(challenge []byte, prng core.PRNG) []byte {
	solved := false
	var sum [32]byte
	solution := []byte{}
	solve := make([]byte, len(challenge)+32)
	// reuse our allocation
	buf := make([]byte, 64)
	next := new(ristretto.Scalar)
	encodedSolution := make([]byte, 0, 32)
	for !solved {
		err := prng.Next(buf, next)
		if err != nil {
			// this will cause the challenge to fail...
			log.Errorf("error completing challenge: %v", err)
			return nil
		}

		//lint:ignore SA1019 API this is "deprecated", but without it it will cause an allocation on every single check
		solution = next.Encode(encodedSolution)

		copy(solve[0:], solution[:])
		copy(solve[len(solution):], challenge[:])
		sum = sha256.Sum256(solve)

		solved = true
		for i := 0; i < 2; i++ {
			if sum[i] != 0x00 {
				solved = false
			}
		}
		// reuse this allocated memory next time...
		encodedSolution = encodedSolution[:0]
	}
	log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
	return solution[:]
}

// ValidateChallenge returns true if the message and spamguard pass the challenge
func (powapp *ProofOfWorkApplication) validateChallenge(challenge []byte, solution []byte) bool {
	if len(solution) != 32 {
		return false
	}
	solve := make([]byte, len(challenge)+32)
	copy(solve[0:], solution[0:32])
	copy(solve[32:], challenge[:])
	sum := sha256.Sum256(solve)

	for i := 0; i < 2; i++ {
		if sum[i] != 0x00 {
			return false
		}
	}
	log.Debugf("Validated Challenge %v: %v %v\n", challenge, solution, sum)
	return true
}
