// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package bsr

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"

	"github.com/hashicorp/boundary/internal/bsr/internal/checksum"
	"github.com/hashicorp/boundary/internal/bsr/internal/is"
	"github.com/hashicorp/boundary/internal/bsr/kms"
	"github.com/hashicorp/boundary/internal/storage"
	"github.com/hashicorp/go-kms-wrapping/v2/extras/crypto"
	"google.golang.org/protobuf/proto"
)

const (
	bsrFileNameTemplate        = "%s.bsr"
	connectionFileNameTemplate = "%s.connection"
	channelFileNameTemplate    = "%s.channel"
	messagesFileNameTemplate   = "messages-%s.data"
	requestsFileNameTemplate   = "requests-%s.data"
	sessionMetaFileName        = "session-meta.json"

	bsrPubKeyFileName           = "bsrKey.pub"
	wrappedBsrKeyFileName       = "wrappedBsrKey"
	wrappedPrivKeyFileName      = "wrappedPrivKey"
	pubKeyBsrSignatureFileName  = "pubKeyBsrSignature.sign"
	pubKeySelfSignatureFileName = "pubKeySelfSignature.sign"

	// bsrBufferSize is the buffer size for files in a BSR.
	// 65 * storage.LogicalBlockSize is equivalent to 260KiB
	bsrBufferSize = 65 * storage.LogicalBlockSize
)

// Session is the top level container in a bsr that contains the files for
// a recorded session.
type Session struct {
	*container
	multiplexed bool

	Meta        *SessionRecordingMeta
	SessionMeta *SessionMeta
	Summary     SessionSummary
}

// GetBsrFileName formats a session recording id into the BSR filename format
func GetBsrFileName(sessionRecordingId string) string {
	return fmt.Sprintf(bsrFileNameTemplate, sessionRecordingId)
}

// NewSession creates a Session container for a given session id.
func NewSession(ctx context.Context, meta *SessionRecordingMeta, sessionMeta *SessionMeta, f storage.FS, keys *kms.Keys, options ...Option) (*Session, error) {
	const op = "bsr.NewSession"

	switch {
	case is.Nil(meta):
		return nil, fmt.Errorf("%s: missing meta: %w", op, ErrInvalidParameter)
	case is.Nil(sessionMeta):
		return nil, fmt.Errorf("%s: missing session meta: %w", op, ErrInvalidParameter)
	case meta.Id == "":
		return nil, fmt.Errorf("%s: missing session id: %w", op, ErrInvalidParameter)
	case !is.Nil(sessionMeta.StaticHost) && !is.Nil(sessionMeta.DynamicHost):
		return nil, fmt.Errorf("%s: sessionMeta cannot contain both static and dynamic host information: %w", op, ErrInvalidParameter)
	case len(sessionMeta.StaticJSONCredentials) == 0 &&
		len(sessionMeta.StaticUsernamePasswordCredentials) == 0 &&
		len(sessionMeta.StaticSshPrivateKeyCredentials) == 0 &&
		len(sessionMeta.VaultGenericLibraries) == 0 &&
		len(sessionMeta.VaultSshCertificateLibraries) == 0:
		return nil, fmt.Errorf("%s: missing credential information: %w", op, ErrInvalidParameter)
	case is.Nil(sessionMeta.User):
		return nil, fmt.Errorf("%s: missing session user: %w", op, ErrInvalidParameter)
	case is.Nil(sessionMeta.Target):
		return nil, fmt.Errorf("%s: missing session target: %w", op, ErrInvalidParameter)
	case is.Nil(sessionMeta.Worker):
		return nil, fmt.Errorf("%s: missing session worker: %w", op, ErrInvalidParameter)
	case is.Nil(f):
		return nil, fmt.Errorf("%s: missing storage fs: %w", op, ErrInvalidParameter)
	case is.Nil(keys):
		return nil, fmt.Errorf("%s: missing kms keys: %w", op, ErrInvalidParameter)
	}

	opts := getOpts(options...)

	c, err := f.New(ctx, GetBsrFileName(meta.Id))
	if err != nil {
		return nil, err
	}

	nc, err := newContainer(ctx, SessionContainer, c, keys)
	if err != nil {
		return nil, err
	}

	// Sync keys and signatures
	err = persistBsrSessionKeys(ctx, keys, nc)
	if err != nil {
		return nil, errors.Join(err, fmt.Errorf("%s: %w", op, ErrBsrKeyPersistenceFailure))
	}

	sm, err := nc.create(ctx, sessionMetaFileName)
	if err != nil {
		return nil, err
	}
	smw, err := checksum.NewFile(ctx, sm, nc.checksums)
	if err != nil {
		return nil, err
	}
	enc := json.NewEncoder(smw)
	if err := enc.Encode(sessionMeta); err != nil {
		smw.Close()
		return nil, err
	}
	if err := smw.Close(); err != nil {
		return nil, err
	}

	err = meta.writeMeta(ctx, nc)
	if err != nil {
		return nil, err
	}

	return &Session{
		container:   nc,
		multiplexed: opts.withSupportsMultiplex,
		Meta:        meta,
		SessionMeta: sessionMeta,
	}, nil
}

// persistBsrSessionKeys will marshal, write, and close BSR keys locally before syncing to
// storage. Any error while syncing the key files should result in the termination of
// the session this recording is for.
// The key files synced are:
// * the BSR public key, bsrKey.pub
// * the wrapped BSR key, wrappedBsrKey
// * the wrapped private key, wrappedPrivKey
// * the public key BSR signature, pubKeyBsrSignature.sign
// * the public key self signature, pubKeySelfSignature.sign
func persistBsrSessionKeys(ctx context.Context, keys *kms.Keys, c *container) error {
	const op = "bsr.persistBsrSessionKeys"
	switch {
	case keys.PubKey == nil:
		return fmt.Errorf("%s: missing kms pub key: %w", op, ErrInvalidParameter)
	case keys.WrappedBsrKey == nil:
		return fmt.Errorf("%s: missing kms wrapped BSR key: %w", op, ErrInvalidParameter)
	case keys.WrappedPrivKey == nil:
		return fmt.Errorf("%s: missing kms wrapped priv key: %w", op, ErrInvalidParameter)
	case keys.PubKeyBsrSignature == nil:
		return fmt.Errorf("%s: missing kms pub key BSR signature: %w", op, ErrInvalidParameter)
	case keys.PubKeySelfSignature == nil:
		return fmt.Errorf("%s: missing kms pub key self signature: %w", op, ErrInvalidParameter)
	}

	keyFiles := map[string]proto.Message{
		bsrPubKeyFileName:           keys.PubKey,
		wrappedBsrKeyFileName:       keys.WrappedBsrKey,
		wrappedPrivKeyFileName:      keys.WrappedPrivKey,
		pubKeyBsrSignatureFileName:  keys.PubKeyBsrSignature,
		pubKeySelfSignatureFileName: keys.PubKeySelfSignature,
	}
	for f, k := range keyFiles {
		b, err := proto.Marshal(k)
		if err != nil {
			return fmt.Errorf("%s: failed to marshal data for %s: %w", op, f, err)
		}
		err = c.syncBsrKey(ctx, f, b)
		if err != nil {
			return fmt.Errorf("%s: failed syncing bsr key %s: %w", op, f, err)
		}
	}

	return nil
}

// OpenSession retrieves a BSR from storage using the sessionRecordingId and initializes it for reading.
// Encryption keys necessary for checking signed files will be unwrapped using the keyUnwrapFn
// Signature and checksum files will then be verified.
// Fields on the underlying container will be populated so that the returned Session can be used for BSR
// playback and conversion to formats such as asciinema
func OpenSession(ctx context.Context, sessionRecordingId string, f storage.FS, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (s *Session, err error) {
	const op = "bsr.OpenSession"
	switch {
	case sessionRecordingId == "":
		return nil, fmt.Errorf("%s: missing session recording id: %w", op, ErrInvalidParameter)
	case f == nil:
		return nil, fmt.Errorf("%s: missing storage: %w", op, ErrInvalidParameter)
	case keyUnwrapFn == nil:
		return nil, fmt.Errorf("%s: missing key unwrap function: %w", op, ErrInvalidParameter)
	}

	c, err := f.Open(ctx, GetBsrFileName(sessionRecordingId))
	if err != nil {
		return nil, err
	}

	keyPopFn := func(c *container) (*kms.Keys, error) {
		return c.loadKeys(ctx, keyUnwrapFn)
	}
	cc, err := openContainer(ctx, SessionContainer, c, keyPopFn)
	if err != nil {
		return nil, err
	}

	// Load and verify recording metadata
	sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
	if err != nil {
		cc.metaFile.Close()
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	defer func() {
		if closeErr := sha256Reader.Close(); closeErr != nil {
			err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
		}
	}()

	meta, err := decodeSessionRecordingMeta(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}
	err = cc.verifyMetadata(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}

	sessionMeta := &SessionMeta{}
	if err := cc.decodeJsonFile(ctx, sessionMetaFileName, sessionMeta); err != nil {
		return nil, err
	}

	af, ok := summaryAllocFuncs.get(meta.Protocol, SessionContainer)
	if !ok {
		return nil, fmt.Errorf("%s: failed to get summary type", op)
	}

	summary := af(ctx)
	if err := cc.decodeJsonFile(ctx, fmt.Sprintf(summaryFileNameTemplate, SessionContainer), summary); err != nil {
		return nil, err
	}
	sessionSummary := summary.(SessionSummary)

	session := &Session{
		container:   cc,
		Meta:        meta,
		SessionMeta: sessionMeta,
		Summary:     sessionSummary,
	}

	return session, nil
}

// Validation provides the results from validating a bsr.
type Validation struct {
	SessionRecordingId         string
	Valid                      bool
	SessionRecordingValidation *ContainerValidation
}

// ContainerValidation contains the results from validating a container in a bsr.
type ContainerValidation struct {
	Name                    string
	ContainerType           ContainerType
	Error                   error
	FileChecksumValidations ContainerChecksumValidation
	SubContainers           []*ContainerValidation
}

// Validate retrieves a BSR from storage using the sessionRecordingId and validates the BSR.
// All files and sub container files will be verified by comparing it against checksums for
// each file in SHA256SUM file.
//
// Validation will continue even if there's an error encountered during validation.
// The validation error will be added to the ContainerValidation struct "Error" field for that container.
func Validate(ctx context.Context, sessionRecordingId string, f storage.FS, keyUnwrapFn kms.KeyUnwrapCallbackFunc) (v *Validation, err error) {
	const op = "bsr.Validate"

	switch {
	case sessionRecordingId == "":
		return nil, fmt.Errorf("%s: missing session recording id: %w", op, ErrInvalidParameter)
	case f == nil:
		return nil, fmt.Errorf("%s: missing storage: %w", op, ErrInvalidParameter)
	case keyUnwrapFn == nil:
		return nil, fmt.Errorf("%s: missing key unwrap function: %w", op, ErrInvalidParameter)
	}

	session, err := OpenSession(ctx, sessionRecordingId, f, keyUnwrapFn)
	if err != nil {
		return nil, fmt.Errorf("%s: failed to retrieve session for %s: %w", op, sessionRecordingId, err)
	}
	defer func() {
		if closeErr := session.Close(ctx); closeErr != nil {
			err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
		}
	}()

	validation := &Validation{
		SessionRecordingId: sessionRecordingId,
		Valid:              true,
	}

	sessionContainerValidation := validateContainer(ctx, validation, SessionContainer, session.container, session.Meta.Id)
	validation.SessionRecordingValidation = sessionContainerValidation

	// Validate all connections under session
	for connId := range session.Meta.connections {
		// Get connection id
		connKey := connId
		lastDotIndex := strings.LastIndex(connId, ".connection")
		if lastDotIndex == -1 {
			validation.Valid = false
			sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, &ContainerValidation{
				Name:          connId,
				ContainerType: ConnectionContainer,
				Error:         fmt.Errorf("%s: malformed BSR for: %s", op, connId),
			})
			continue
		}

		connKey = connId[:lastDotIndex]

		// Open current connection
		connection, err := session.OpenConnection(ctx, connKey)
		if err != nil {
			validation.Valid = false
			sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, &ContainerValidation{
				Name:          connId,
				ContainerType: ConnectionContainer,
				Error:         fmt.Errorf("%s: failed to retrieve connection for %s: %w", op, connId, err),
			})
			continue
		}

		connectionContainerValidation := validateContainer(ctx, validation, ConnectionContainer, connection.container, connection.Meta.Id)
		sessionContainerValidation.SubContainers = append(sessionContainerValidation.SubContainers, connectionContainerValidation)

		// Validate all channels under current connection
		for chId := range connection.Meta.channels {
			// Get channel id
			chKey := chId
			lastDotIndex := strings.LastIndex(chId, ".channel")
			if lastDotIndex == -1 {
				validation.Valid = false
				sessionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, &ContainerValidation{
					Name:          chId,
					ContainerType: ChannelContainer,
					Error:         fmt.Errorf("%s: malformed BSR for: %s", op, chId),
				})
				continue
			}

			chKey = chId[:lastDotIndex]

			// Open current connection
			channel, err := connection.OpenChannel(ctx, chKey)
			if err != nil {
				validation.Valid = false
				sessionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, &ContainerValidation{
					Name:          chId,
					ContainerType: ChannelContainer,
					Error:         fmt.Errorf("%s: failed to retrieve channel for %s: %w", op, chId, err),
				})
				continue
			}

			channelContainerValidation := validateContainer(ctx, validation, ChannelContainer, channel.container, channel.Meta.Id)
			err = channel.Close(ctx)
			if err != nil {
				channelContainerValidation.Error = errors.Join(channelContainerValidation.Error, fmt.Errorf("%s: failed to close channel for %s: %w", op, chId, err))
			}
			connectionContainerValidation.SubContainers = append(connectionContainerValidation.SubContainers, channelContainerValidation)
		}

		err = connection.Close(ctx)
		if err != nil {
			connectionContainerValidation.Error = errors.Join(connectionContainerValidation.Error, fmt.Errorf("%s: failed to close connection for %s: %w", op, connId, err))
		}
	}

	return validation, nil
}

// ValidateContainer validates the checksums of all files in a container
func validateContainer(ctx context.Context, v *Validation, ct ContainerType, c *container, name string) *ContainerValidation {
	const op = "bsr.(Validate).ValidateContainer"

	containerValidation := &ContainerValidation{
		Name:          name,
		ContainerType: ct,
	}

	containerChecksumValidation, err := c.ValidateChecksums(ctx)
	if err != nil {
		v.Valid = false
		containerValidation.Error = fmt.Errorf("%s: failed to validate %s: %w", op, name, err)
		return containerValidation
	}

	containerValidation.FileChecksumValidations = containerChecksumValidation

	failedChecksums := containerValidation.FileChecksumValidations.GetFailedItems()

	// Update validation field only if there are failed checksums
	if len(failedChecksums) > 0 {
		v.Valid = false
	}

	return containerValidation
}

// Close closes the Session container.
func (s *Session) Close(ctx context.Context) error {
	if !is.Nil(s.container) {
		return s.container.close(ctx)
	}
	return nil
}

// Connection is a container in a bsr for a specific connection in a session
// container. It contains the files for the recorded connection.
type Connection struct {
	*container
	multiplexed bool

	Meta    *ConnectionRecordingMeta
	session *Session
	Summary ConnectionSummary
}

// NewConnection creates a Connection container for a given connection id.
func (s *Session) NewConnection(ctx context.Context, meta *ConnectionRecordingMeta) (*Connection, error) {
	const op = "bsr.(Session).NewConnection"

	switch {
	case is.Nil(meta):
		return nil, fmt.Errorf("%s: missing connection meta: %w", op, ErrInvalidParameter)
	case meta.Id == "":
		return nil, fmt.Errorf("%s: missing connection id: %w", op, ErrInvalidParameter)
	}

	name := fmt.Sprintf(connectionFileNameTemplate, meta.Id)
	sc, err := s.container.container.SubContainer(ctx, name, storage.WithCreateFile(), storage.WithFileAccessMode(storage.WriteOnly))
	if err != nil {
		return nil, err
	}
	if _, err := s.WriteMeta(ctx, "connection", name); err != nil {
		return nil, err
	}

	nc, err := newContainer(ctx, ConnectionContainer, sc, s.keys)
	if err != nil {
		return nil, err
	}
	if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
		return nil, err
	}
	return &Connection{
		container:   nc,
		multiplexed: s.multiplexed,
		Meta:        meta,
		session:     s,
	}, nil
}

// OpenConnection will open and validate a BSR connection
func (s *Session) OpenConnection(ctx context.Context, connId string) (conn *Connection, err error) {
	const op = "bsr.(Session).OpenConnection"
	switch {
	case connId == "":
		return nil, fmt.Errorf("%s: missing connection id: %w", op, ErrInvalidParameter)
	}

	name := fmt.Sprintf(connectionFileNameTemplate, connId)

	switch {
	case !s.Meta.connections[name]:
		return nil, fmt.Errorf("%s: connection id does not exist within this session: %w", op, ErrInvalidParameter)
	}

	c, err := s.container.container.SubContainer(ctx, name)
	if err != nil {
		return nil, err
	}

	keyPopFn := func(c *container) (*kms.Keys, error) {
		return s.keys, nil
	}
	cc, err := openContainer(ctx, ConnectionContainer, c, keyPopFn)
	if err != nil {
		return nil, err
	}

	// Load and verify connection metadata
	sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
	if err != nil {
		cc.metaFile.Close()
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	defer func() {
		if closeErr := sha256Reader.Close(); closeErr != nil {
			err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
		}
	}()

	sm, err := decodeConnectionRecordingMeta(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}
	err = cc.verifyMetadata(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}

	af, ok := summaryAllocFuncs.get(s.Meta.Protocol, ConnectionContainer)
	if !ok {
		return nil, fmt.Errorf("%s: failed to get summary type", op)
	}

	summary := af(ctx)
	if err := cc.decodeJsonFile(ctx, fmt.Sprintf(summaryFileNameTemplate, ConnectionContainer), summary); err != nil {
		return nil, err
	}
	connectionSummary := summary.(ConnectionSummary)

	connection := &Connection{
		container: cc,
		Meta:      sm,
		session:   s,
		Summary:   connectionSummary,
	}

	return connection, nil
}

// NewChannel creates a Channel container for a given channel id.
func (c *Connection) NewChannel(ctx context.Context, meta *ChannelRecordingMeta) (*Channel, error) {
	const op = "bsr.(Connection).NewChannel"

	if !c.multiplexed {
		return nil, fmt.Errorf("%s: connection cannot make channels: %w", op, ErrNotSupported)
	}

	switch {
	case is.Nil(meta):
		return nil, fmt.Errorf("%s: missing channel meta: %w", op, ErrInvalidParameter)
	case meta.Id == "":
		return nil, fmt.Errorf("%s: missing channel id: %w", op, ErrInvalidParameter)
	}

	name := fmt.Sprintf(channelFileNameTemplate, meta.Id)
	sc, err := c.container.container.SubContainer(ctx, name, storage.WithCreateFile(), storage.WithFileAccessMode(storage.WriteOnly))
	if err != nil {
		return nil, err
	}
	if _, err := c.WriteMeta(ctx, "channel", name); err != nil {
		return nil, err
	}
	nc, err := newContainer(ctx, ChannelContainer, sc, c.keys)
	if err != nil {
		return nil, err
	}
	if _, err := nc.WriteMeta(ctx, "id", meta.Id); err != nil {
		return nil, err
	}
	if _, err := nc.WriteMeta(ctx, "channelType", meta.Type); err != nil {
		return nil, err
	}
	return &Channel{
		container: nc,
		Meta:      meta,
	}, nil
}

// OpenChannel will open and validate a BSR channel
func (c *Connection) OpenChannel(ctx context.Context, chanId string) (ch *Channel, err error) {
	const op = "bsr.OpenChannel"
	switch {
	case chanId == "":
		return nil, fmt.Errorf("%s: missing channel id: %w", op, ErrInvalidParameter)
	}

	name := fmt.Sprintf(channelFileNameTemplate, chanId)
	switch {
	case !c.Meta.channels[name]:
		return nil, fmt.Errorf("%s: channel id does not exist within this connection: %w", op, ErrInvalidParameter)
	}

	con, err := c.container.container.SubContainer(ctx, name)
	if err != nil {
		return nil, err
	}

	keyPopFn := func(cn *container) (*kms.Keys, error) {
		return c.keys, nil
	}
	cc, err := openContainer(ctx, ChannelContainer, con, keyPopFn)
	if err != nil {
		return nil, err
	}

	// Load and verify channel metadata
	sha256Reader, err := crypto.NewSha256SumReader(ctx, cc.metaFile)
	if err != nil {
		cc.metaFile.Close()
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	defer func() {
		if closeErr := sha256Reader.Close(); closeErr != nil {
			err = errors.Join(err, fmt.Errorf("%s: %w", op, closeErr))
		}
	}()

	sm, err := decodeChannelRecordingMeta(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}
	err = cc.verifyMetadata(ctx, sha256Reader)
	if err != nil {
		return nil, err
	}

	af, ok := summaryAllocFuncs.get(c.session.Meta.Protocol, ChannelContainer)
	if !ok {
		return nil, fmt.Errorf("%s: failed to get summary type", op)
	}

	summary := af(ctx)
	if err := cc.decodeJsonFile(ctx, fmt.Sprintf(summaryFileNameTemplate, ChannelContainer), summary); err != nil {
		return nil, err
	}
	channelSummary := summary.(ChannelSummary)

	channel := &Channel{
		container: cc,
		Meta:      sm,
		Summary:   channelSummary,
	}

	return channel, nil
}

// NewMessagesWriter creates a writer for recording channel messages.
func (c *Connection) NewMessagesWriter(ctx context.Context, dir Direction) (io.Writer, error) {
	const op = "bsr.(Connection).NewMessagesWriter"

	switch {
	case !ValidDirection(dir):
		return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
	}

	messagesName := fmt.Sprintf(messagesFileNameTemplate, dir.String())
	_, err := c.container.WriteMeta(ctx, "messages", dir.String())
	if err != nil {
		return nil, err
	}

	m, err := c.container.create(ctx, messagesName,
		storage.WithCreateFile(),
		storage.WithFileAccessMode(storage.ReadWrite),
		storage.WithBuffer(bsrBufferSize),
	)
	if err != nil {
		return nil, err
	}

	return checksum.NewFile(ctx, m, c.checksums)
}

// NewRequestsWriter creates a writer for recording connection requests.
func (c *Connection) NewRequestsWriter(ctx context.Context, dir Direction) (storage.Writer, error) {
	const op = "bsr.(Connection).NewRequestsWriter"

	switch {
	case !ValidDirection(dir):
		return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
	}

	requestName := fmt.Sprintf(requestsFileNameTemplate, dir.String())
	_, err := c.container.WriteMeta(ctx, "requests", dir.String())
	if err != nil {
		return nil, err
	}

	m, err := c.container.create(ctx, requestName,
		storage.WithCreateFile(),
		storage.WithFileAccessMode(storage.ReadWrite),
		storage.WithBuffer(bsrBufferSize),
	)
	if err != nil {
		return nil, err
	}

	return checksum.NewFile(ctx, m, c.checksums)
}

// Close closes the Connection container.
func (c *Connection) Close(ctx context.Context) error {
	if !is.Nil(c.container) {
		return c.container.close(ctx)
	}
	return nil
}

// Channel is a container in a bsr for a specific channel in a session
// container. It contains the files for the recorded channel.
type Channel struct {
	*container

	Meta    *ChannelRecordingMeta
	Summary ChannelSummary
}

// Close closes the Channel container.
func (c *Channel) Close(ctx context.Context) error {
	if !is.Nil(c.container) {
		return c.container.close(ctx)
	}
	return nil
}

// NewMessagesWriter creates a writer for recording channel messages.
func (c *Channel) NewMessagesWriter(ctx context.Context, dir Direction) (storage.Writer, error) {
	const op = "bsr.(Channel).NewMessagesWriter"

	switch {
	case !ValidDirection(dir):
		return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
	}

	messagesName := fmt.Sprintf(messagesFileNameTemplate, dir.String())
	_, err := c.container.WriteMeta(ctx, "messages", dir.String())
	if err != nil {
		return nil, err
	}
	m, err := c.container.create(ctx, messagesName,
		storage.WithCreateFile(),
		storage.WithFileAccessMode(storage.ReadWrite),
		storage.WithBuffer(bsrBufferSize),
	)
	if err != nil {
		return nil, err
	}

	return checksum.NewFile(ctx, m, c.checksums)
}

// NewRequestsWriter creates a writer for recording channel requests.
func (c *Channel) NewRequestsWriter(ctx context.Context, dir Direction) (storage.Writer, error) {
	const op = "bsr.(Channel).NewRequestsWriter"

	switch {
	case !ValidDirection(dir):
		return nil, fmt.Errorf("%s: invalid direction: %w", op, ErrInvalidParameter)
	}

	requestName := fmt.Sprintf(requestsFileNameTemplate, dir.String())
	_, err := c.container.WriteMeta(ctx, "requests", dir.String())
	if err != nil {
		return nil, err
	}
	m, err := c.container.create(ctx, requestName,
		storage.WithCreateFile(),
		storage.WithFileAccessMode(storage.ReadWrite),
		storage.WithBuffer(bsrBufferSize),
	)
	if err != nil {
		return nil, err
	}

	return checksum.NewFile(ctx, m, c.checksums)
}

// OpenMessageScanner opens a ChunkScanner for a channel's recorded messages.
func (c *Channel) OpenMessageScanner(ctx context.Context, dir Direction) (*ChunkScanner, error) {
	const op = "bsr.(Channel).OpenMessageScanner"

	messagesName := fmt.Sprintf(messagesFileNameTemplate, dir.String())
	m, err := c.container.container.OpenFile(ctx, messagesName, storage.WithFileAccessMode(storage.ReadOnly))
	if err != nil {
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	expectedSum, err := c.shaSums.Sum(messagesName)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	return NewChunkScanner(ctx, m, WithSha256Sum(expectedSum))
}

// OpenRequestScanner opens a ChunkScanner for a channel's recorded requests.
func (c *Channel) OpenRequestScanner(ctx context.Context, dir Direction) (*ChunkScanner, error) {
	const op = "bsr.(Channel).OpenRequestScanner"

	requestName := fmt.Sprintf(requestsFileNameTemplate, dir.String())
	m, err := c.container.container.OpenFile(ctx, requestName, storage.WithFileAccessMode(storage.ReadOnly))
	if err != nil {
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	expectedSum, err := c.shaSums.Sum(requestName)
	if err != nil {
		return nil, fmt.Errorf("%s: %w", op, err)
	}
	return NewChunkScanner(ctx, m, WithSha256Sum(expectedSum))
}
