// Container versions
//
// 1: for network, simple
// 2: for storage
// 3: for network, concealed (TBD)

package jess

import (
	"encoding/json"
	"fmt"

	"github.com/safing/structures/container"
	"github.com/safing/structures/dsd"
)

// Letter is the data format for encrypted data at rest or in transit.
type Letter struct { //nolint:maligned // TODO
	Version uint8  // signed, MAC'd (may not exist when wired)
	SuiteID string // signed, MAC'd (may not exist when wired)

	Nonce []byte  // signed, MAC'd
	Keys  []*Seal `json:",omitempty"` // signed, MAC'd

	Data       []byte  `json:",omitempty"` // signed, MAC'd
	Mac        []byte  `json:",omitempty"` // signed
	Signatures []*Seal `json:",omitempty"`

	// Flags for wire protocol
	ApplyKeys bool `json:",omitempty"` // MAC'd
}

// Seal holds a key, key exchange or signature within a letter.
type Seal struct {
	Scheme string `json:",omitempty"`

	// Key Establishment: Signet ID of recipient's signet
	// Signature: Signet ID of signer's signet
	ID string `json:",omitempty"`

	// Key Establishment: Public key or wrapped key
	// Signature: Signature value
	Value []byte `json:",omitempty"`
}

// Envelope returns an envelope built from the letter, configured for opening it.
func (letter *Letter) Envelope(requirements *Requirements) (*Envelope, error) {
	// basic checks
	if letter.Version == 0 {
		return nil, fmt.Errorf("letter does not specify version")
	}
	if len(letter.SuiteID) == 0 {
		return nil, fmt.Errorf("letter does not specify a suite")
	}

	// create envelope
	e := &Envelope{
		Version: letter.Version,
		SuiteID: letter.SuiteID,
	}

	// get and check suite
	err := e.LoadSuite()
	if err != nil {
		return nil, err
	}
	// default to full requirements
	if requirements == nil {
		requirements = NewRequirements()
	}
	// check suite against requirements
	err = e.suite.Provides.CheckComplianceTo(requirements)
	if err != nil {
		return nil, err
	}

	for _, seal := range letter.Keys {
		// handshake messages have ephermal encapsulation keys in first message
		if len(seal.ID) > 0 {
			if seal.Scheme == SignetSchemeKey || seal.Scheme == SignetSchemePassword {
				e.Secrets = append(e.Secrets, &Signet{
					Version: letter.Version,
					ID:      seal.ID,
					Scheme:  seal.Scheme,
				})
			} else {
				e.Recipients = append(e.Recipients, &Signet{
					Version: letter.Version,
					ID:      seal.ID,
					Scheme:  seal.Scheme,
				})
			}
		}
	}
	for _, seal := range letter.Signatures {
		e.Senders = append(e.Senders, &Signet{
			Version: letter.Version,
			ID:      seal.ID,
			Scheme:  seal.Scheme,
		})
	}

	e.opening = true
	return e, nil
}

// Open creates a session and opens the letter in one step.
func (letter *Letter) Open(requirements *Requirements, trustStore TrustStore) ([]byte, error) {
	e, err := letter.Envelope(requirements)
	if err != nil {
		return nil, err
	}

	s, err := e.Correspondence(trustStore)
	if err != nil {
		return nil, err
	}

	return s.Open(letter)
}

// Verify creates a session and verifies the letter in one step.
func (letter *Letter) Verify(requirements *Requirements, trustStore TrustStore) error {
	e, err := letter.Envelope(requirements)
	if err != nil {
		return err
	}

	s, err := e.initCorrespondence(trustStore, true)
	if err != nil {
		return err
	}

	return s.Verify(letter)
}

// WireCorrespondence creates a wire session (communication over a network connection) from a letter.
func (letter *Letter) WireCorrespondence(trustStore TrustStore) (*Session, error) {
	e, err := letter.Envelope(NewRequirements().Remove(SenderAuthentication))
	if err != nil {
		return nil, err
	}

	return e.WireCorrespondence(trustStore)
}

// ToJSON serializes the letter to json.
func (letter *Letter) ToJSON() ([]byte, error) {
	return json.Marshal(letter)
}

// LetterFromJSON loads a json-serialized letter.
func LetterFromJSON(data []byte) (*Letter, error) {
	letter := &Letter{}

	err := json.Unmarshal(data, letter)
	if err != nil {
		return nil, err
	}

	return letter, nil
}

// ToDSD serializes the letter to the given dsd format.
func (letter *Letter) ToDSD(dsdFormat uint8) ([]byte, error) {
	data, err := dsd.Dump(letter, dsdFormat)
	if err != nil {
		return nil, err
	}

	return data, nil
}

// LetterFromDSD loads a dsd-serialized letter.
func LetterFromDSD(data []byte) (*Letter, error) {
	letter := &Letter{}

	_, err := dsd.Load(data, letter)
	if err != nil {
		return nil, err
	}

	return letter, nil
}

const (
	// Field IDs for signing
	// These IDs MUST NOT CHANGE.

	fieldIDLetterVersion uint64 = 1 // signed, MAC'd (may not exist when wired)
	fieldIDLetterSuiteID uint64 = 2 // signed, MAC'd (may not exist when wired)
	fieldIDLetterNonce   uint64 = 3 // signed, MAC'd
	fieldIDLetterKeys    uint64 = 4 // signed, MAC'd
	fieldIDLetterMac     uint64 = 5 // signed

	fieldIDSealScheme uint64 = 16 // signed, MAC'd
	fieldIDSealID     uint64 = 17 // signed, MAC'd
	fieldIDSealValue  uint64 = 18 // signed, MAC'd
)

func (letter *Letter) compileAssociatedData() []byte {
	// every field is transformed and prepended with a static ID
	// this makes it easy to stay backward compatible without hassling around with versioning when fields are added

	c := container.New()

	if letter.Version > 0 {
		c.AppendNumber(fieldIDLetterVersion) // append field ID
		c.AppendNumber(uint64(letter.Version))
	}
	if len(letter.SuiteID) > 0 {
		c.AppendNumber(fieldIDLetterSuiteID)    // append field ID
		c.AppendAsBlock([]byte(letter.SuiteID)) // append field content with length
	}
	if len(letter.Nonce) > 0 {
		c.AppendNumber(fieldIDLetterNonce) // append field ID
		c.AppendAsBlock(letter.Nonce)      // append field content with length
	}
	if len(letter.Keys) > 0 {
		c.AppendNumber(fieldIDLetterKeys) // append field ID
		c.AppendInt(len(letter.Keys))     // append number of keys
		for i, seal := range letter.Keys {
			c.AppendInt(i)                // append index
			seal.compileAssociatedData(c) // append field content with length
		}
	}

	return c.CompileData()
}

func (letter *Letter) compileAssociatedSigningData(associatedData []byte) []byte {
	// compile basic associated data if not yet done
	if len(associatedData) == 0 {
		associatedData = letter.compileAssociatedData()
	}

	// return if there is no Mac
	if len(letter.Mac) == 0 {
		return associatedData
	}

	// add Mac to associated data and return
	c := container.New(associatedData)
	c.AppendNumber(fieldIDLetterMac) // append field ID
	c.AppendAsBlock(letter.Mac)      // append field content with length

	return c.CompileData()
}

func (seal *Seal) compileAssociatedData(c *container.Container) {
	if seal.Scheme != "" {
		c.AppendNumber(fieldIDSealScheme)    // append field ID
		c.AppendAsBlock([]byte(seal.Scheme)) // append field content with length
	}
	if seal.ID != "" {
		c.AppendNumber(fieldIDSealID)    // append field ID
		c.AppendAsBlock([]byte(seal.ID)) // append field content with length
	}
	if len(seal.Value) > 0 {
		c.AppendNumber(fieldIDSealValue) // append field ID
		c.AppendAsBlock(seal.Value)      // append field content with length
	}
}