// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package crypto

import (
	"bytes"
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"crypto/sha512"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"math"

	"golang.org/x/crypto/pbkdf2"

	"maunium.net/go/mautrix/crypto/olm"
	"maunium.net/go/mautrix/id"
)

type SenderClaimedKeys struct {
	Ed25519 id.Ed25519 `json:"ed25519"`
}

type ExportedSession struct {
	Algorithm         id.Algorithm      `json:"algorithm"`
	ForwardingChains  []string          `json:"forwarding_curve25519_key_chain"`
	RoomID            id.RoomID         `json:"room_id"`
	SenderKey         id.SenderKey      `json:"sender_key"`
	SenderClaimedKeys SenderClaimedKeys `json:"sender_claimed_keys"`
	SessionID         id.SessionID      `json:"session_id"`
	SessionKey        string            `json:"session_key"`
}

// The default number of pbkdf2 rounds to use when exporting keys
const defaultPassphraseRounds = 100000

const exportPrefix = "-----BEGIN MEGOLM SESSION DATA-----\n"
const exportSuffix = "-----END MEGOLM SESSION DATA-----\n"
// Only version 0x01 is currently specified in the spec
const exportVersion1 = 0x01
// The standard for wrapping base64 is 76 bytes
const exportLineLengthLimit = 76
// Byte count for version + salt + iv + number of rounds
const exportHeaderLength = 1 + 16 + 16 + 4
// SHA-256 hash length
const exportHashLength = 32

func computeKey(passphrase string, salt []byte, rounds int) (encryptionKey, hashKey []byte) {
	key := pbkdf2.Key([]byte(passphrase), salt, rounds, 64, sha512.New)
	encryptionKey = key[:32]
	hashKey = key[32:]
	return
}

func makeExportIV() []byte {
	iv := make([]byte, 16)
	_, err := rand.Read(iv)
	if err != nil {
		panic(olm.NotEnoughGoRandom)
	}
	// Set bit 63 to zero
	iv[7] &= 0b11111110
	return iv
}

func makeExportKeys(passphrase string) (encryptionKey, hashKey, salt, iv []byte) {
	salt = make([]byte, 16)
	_, err := rand.Read(salt)
	if err != nil {
		panic(olm.NotEnoughGoRandom)
	}

	encryptionKey, hashKey = computeKey(passphrase, salt, defaultPassphraseRounds)

	iv = makeExportIV()
	return
}

func exportSessions(sessions []*InboundGroupSession) ([]ExportedSession, error) {
	export := make([]ExportedSession, len(sessions))
	for i, session := range sessions {
		key, err := session.Internal.Export(session.Internal.FirstKnownIndex())
		if err != nil {
			return nil, fmt.Errorf("failed to export session: %w", err)
		}
		export[i] = ExportedSession{
			Algorithm:         id.AlgorithmMegolmV1,
			ForwardingChains:  session.ForwardingChains,
			RoomID:            session.RoomID,
			SenderKey:         session.SenderKey,
			SenderClaimedKeys: SenderClaimedKeys{},
			SessionID:         session.ID(),
			SessionKey:        key,
		}
	}
	return export, nil
}

func exportSessionsJSON(sessions []*InboundGroupSession) ([]byte, error) {
	exportedSessions, err := exportSessions(sessions)
	if err != nil {
		return nil, err
	}
	return json.Marshal(exportedSessions)
}

func min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

func formatKeyExportData(data []byte) []byte {
	base64Data := make([]byte, base64.StdEncoding.EncodedLen(len(data)))
	base64.StdEncoding.Encode(base64Data, data)

	// Prefix + data and newline for each 76 characters of data + suffix
	outputLength := len(exportPrefix) +
		len(base64Data) + int(math.Ceil(float64(len(base64Data))/exportLineLengthLimit)) +
		len(exportSuffix)

	var buf bytes.Buffer
	buf.Grow(outputLength)
	buf.WriteString(exportPrefix)
	for ptr := 0; ptr < len(base64Data); ptr += exportLineLengthLimit {
		buf.Write(base64Data[ptr:min(ptr+exportLineLengthLimit, len(base64Data))])
		buf.WriteRune('\n')
	}
	buf.WriteString(exportSuffix)
	if buf.Len() != buf.Cap() || buf.Len() != outputLength {
		panic(fmt.Errorf("unexpected length %d / %d / %d", buf.Len(), buf.Cap(), outputLength))
	}
	return buf.Bytes()
}

// ExportKeys exports the given Megolm sessions with the format specified in the Matrix spec.
// See https://matrix.org/docs/spec/client_server/r0.6.1#key-exports
func ExportKeys(passphrase string, sessions []*InboundGroupSession) ([]byte, error) {
	// Make all the keys necessary for exporting
	encryptionKey, hashKey, salt, iv := makeExportKeys(passphrase)
	// Export all the given sessions and put them in JSON
	unencryptedData, err := exportSessionsJSON(sessions)
	if err != nil {
		return nil, err
	}

	// The export data consists of:
	// 1 byte of export format version
	// 16 bytes of salt
	// 16 bytes of IV (initialization vector)
	// 4 bytes of the number of rounds
	// the encrypted export data
	// 32 bytes of the hash of all the data above

	exportData := make([]byte, exportHeaderLength+len(unencryptedData)+exportHashLength)
	dataWithoutHashLength := len(exportData) - exportHashLength

	// Create the header for the export data
	exportData[0] = exportVersion1
	copy(exportData[1:17], salt)
	copy(exportData[17:33], iv)
	binary.BigEndian.PutUint32(exportData[33:37], defaultPassphraseRounds)

	// Encrypt data with AES-256-CTR
	block, _ := aes.NewCipher(encryptionKey)
	cipher.NewCTR(block, iv).XORKeyStream(exportData[exportHeaderLength:dataWithoutHashLength], unencryptedData)

	// Hash all the data with HMAC-SHA256 and put it at the end
	mac := hmac.New(sha256.New, hashKey)
	mac.Write(exportData[:dataWithoutHashLength])
	mac.Sum(exportData[:dataWithoutHashLength])

	// Format the export (prefix, base64'd exportData, suffix) and return
	return formatKeyExportData(exportData), nil
}
