// 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 event

import (
	"encoding/json"
	"fmt"

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

// EncryptionEventContent represents the content of a m.room.encryption state event.
// https://spec.matrix.org/v1.2/client-server-api/#mroomencryption
type EncryptionEventContent struct {
	// The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
	Algorithm id.Algorithm `json:"algorithm"`
	// How long the session should be used before changing it. 604800000 (a week) is the recommended default.
	RotationPeriodMillis int64 `json:"rotation_period_ms,omitempty"`
	// How many messages should be sent before changing the session. 100 is the recommended default.
	RotationPeriodMessages int `json:"rotation_period_msgs,omitempty"`
}

// EncryptedEventContent represents the content of a m.room.encrypted message event.
// https://spec.matrix.org/v1.2/client-server-api/#mroomencrypted
//
// Note that sender_key and device_id are deprecated in Megolm events as of https://github.com/matrix-org/matrix-spec-proposals/pull/3700
type EncryptedEventContent struct {
	Algorithm id.Algorithm `json:"algorithm"`
	SenderKey id.SenderKey `json:"sender_key,omitempty"`
	// Deprecated: Matrix v1.3
	DeviceID id.DeviceID `json:"device_id,omitempty"`
	// Only present for Megolm events
	SessionID id.SessionID `json:"session_id,omitempty"`

	Ciphertext json.RawMessage `json:"ciphertext"`

	MegolmCiphertext []byte         `json:"-"`
	OlmCiphertext    OlmCiphertexts `json:"-"`

	RelatesTo *RelatesTo `json:"m.relates_to,omitempty"`
	Mentions  *Mentions  `json:"m.mentions,omitempty"`
}

type OlmCiphertexts map[id.Curve25519]struct {
	Body string        `json:"body"`
	Type id.OlmMsgType `json:"type"`
}

type serializableEncryptedEventContent EncryptedEventContent

func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error {
	err := json.Unmarshal(data, (*serializableEncryptedEventContent)(content))
	if err != nil {
		return err
	}
	switch content.Algorithm {
	case id.AlgorithmOlmV1:
		content.OlmCiphertext = make(OlmCiphertexts)
		return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext)
	case id.AlgorithmMegolmV1:
		if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' {
			return id.InputNotJSONString
		}
		content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1]
	}
	return nil
}

func (content *EncryptedEventContent) MarshalJSON() ([]byte, error) {
	var err error
	switch content.Algorithm {
	case id.AlgorithmOlmV1:
		content.Ciphertext, err = json.Marshal(content.OlmCiphertext)
	case id.AlgorithmMegolmV1:
		content.Ciphertext = make([]byte, len(content.MegolmCiphertext)+2)
		content.Ciphertext[0] = '"'
		content.Ciphertext[len(content.Ciphertext)-1] = '"'
		copy(content.Ciphertext[1:len(content.Ciphertext)-1], content.MegolmCiphertext)
	}
	if err != nil {
		return nil, err
	}
	return json.Marshal((*serializableEncryptedEventContent)(content))
}

// RoomKeyEventContent represents the content of a m.room_key to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mroom_key
type RoomKeyEventContent struct {
	Algorithm  id.Algorithm `json:"algorithm"`
	RoomID     id.RoomID    `json:"room_id"`
	SessionID  id.SessionID `json:"session_id"`
	SessionKey string       `json:"session_key"`

	MaxAge      int64 `json:"com.beeper.max_age_ms"`
	MaxMessages int   `json:"com.beeper.max_messages"`
	IsScheduled bool  `json:"com.beeper.is_scheduled"`
}

// ForwardedRoomKeyEventContent represents the content of a m.forwarded_room_key to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mforwarded_room_key
type ForwardedRoomKeyEventContent struct {
	RoomKeyEventContent
	SenderKey          id.SenderKey `json:"sender_key"`
	SenderClaimedKey   id.Ed25519   `json:"sender_claimed_ed25519_key"`
	ForwardingKeyChain []string     `json:"forwarding_curve25519_key_chain"`

	MaxAge      int64 `json:"com.beeper.max_age_ms"`
	MaxMessages int   `json:"com.beeper.max_messages"`
	IsScheduled bool  `json:"com.beeper.is_scheduled"`
}

type KeyRequestAction string

const (
	KeyRequestActionRequest = "request"
	KeyRequestActionCancel  = "request_cancellation"
)

// RoomKeyRequestEventContent represents the content of a m.room_key_request to_device event.
// https://spec.matrix.org/v1.2/client-server-api/#mroom_key_request
type RoomKeyRequestEventContent struct {
	Body               RequestedKeyInfo `json:"body"`
	Action             KeyRequestAction `json:"action"`
	RequestingDeviceID id.DeviceID      `json:"requesting_device_id"`
	RequestID          string           `json:"request_id"`
}

type RequestedKeyInfo struct {
	Algorithm id.Algorithm `json:"algorithm"`
	RoomID    id.RoomID    `json:"room_id"`
	SenderKey id.SenderKey `json:"sender_key"`
	SessionID id.SessionID `json:"session_id"`
}

type RoomKeyWithheldCode string

const (
	RoomKeyWithheldBlacklisted  RoomKeyWithheldCode = "m.blacklisted"
	RoomKeyWithheldUnverified   RoomKeyWithheldCode = "m.unverified"
	RoomKeyWithheldUnauthorized RoomKeyWithheldCode = "m.unauthorised"
	RoomKeyWithheldUnavailable  RoomKeyWithheldCode = "m.unavailable"
	RoomKeyWithheldNoOlmSession RoomKeyWithheldCode = "m.no_olm"

	RoomKeyWithheldBeeperRedacted RoomKeyWithheldCode = "com.beeper.redacted"
)

type RoomKeyWithheldEventContent struct {
	RoomID    id.RoomID           `json:"room_id,omitempty"`
	Algorithm id.Algorithm        `json:"algorithm"`
	SessionID id.SessionID        `json:"session_id,omitempty"`
	SenderKey id.SenderKey        `json:"sender_key"`
	Code      RoomKeyWithheldCode `json:"code"`
	Reason    string              `json:"reason,omitempty"`
}

const groupSessionWithheldMsg = "group session has been withheld: %s"

func (withheld *RoomKeyWithheldEventContent) Error() string {
	switch withheld.Code {
	case RoomKeyWithheldBlacklisted, RoomKeyWithheldUnverified, RoomKeyWithheldUnauthorized, RoomKeyWithheldUnavailable, RoomKeyWithheldNoOlmSession:
		return fmt.Sprintf(groupSessionWithheldMsg, withheld.Code)
	default:
		return fmt.Sprintf(groupSessionWithheldMsg+" (%s)", withheld.Code, withheld.Reason)
	}
}

func (withheld *RoomKeyWithheldEventContent) Is(other error) bool {
	otherWithheld, ok := other.(*RoomKeyWithheldEventContent)
	if !ok {
		return false
	}
	return withheld.Code == "" || otherWithheld.Code == "" || withheld.Code == otherWithheld.Code
}

type DummyEventContent struct{}
