// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

// Package applayer provides common definitions with common fields
// for use with application layer protocols among beats.
package applayer

import (
	"errors"
	"time"

	"github.com/elastic/beats/libbeat/beat"
	"github.com/elastic/beats/libbeat/common"
	"github.com/elastic/beats/libbeat/common/streambuf"
)

// A Message its direction indicator
type NetDirection uint8

const (
	// Message due to a response by server
	NetReverseDirection NetDirection = 0

	// Message was send by client
	NetOriginalDirection NetDirection = 1
)

// Transport type indicator. One of TransportUdp or TransportTcp
type Transport uint8

const (
	TransportUDP Transport = iota
	TransportTCP
)

// String returns the transport type its textual representation.
func (t Transport) String() string {
	switch t {
	case TransportUDP:
		return "udp"
	case TransportTCP:
		return "tcp"
	default:
		return "invalid"
	}
}

// A Stream provides buffering data if stream based protocol is used.
// Use Init to initialize a stream with en empty buffer and buffering limit.
// A Stream its zero value is a valid unlimited stream buffer.
type Stream struct {
	// Buf provides the buffering with parsing support
	Buf streambuf.Buffer

	// MaxDataInStream sets the maximum number of bytes held in buffer.
	// If limit is reached append function will return an error.
	MaxDataInStream int
}

// A Transaction defines common fields for all application layer protocols.
type Transaction struct {
	// Type is the name of the application layer protocol transaction be represented.
	Type string

	// Transaction source and destination IPs and Ports.
	Tuple common.IPPortTuple

	// Transport layer type
	Transport Transport

	// Src describes the transaction source/initiator endpoint
	Src common.Endpoint

	// Dst describes the transaction destination endpoint
	Dst common.Endpoint

	// Ts sets the transaction its initial timestamp
	Ts TransactionTimestamp

	// ResponseTime is the transaction duration in milliseconds. Should be set
	// to -1 if duration is unknown
	ResponseTime int32

	// Status of final transaction
	Status string // see libbeat/common/statuses.go

	// Notes holds a list of interesting events and errors encountered when
	// processing the transaction
	Notes []string

	// BytesIn is the number of bytes returned by destination endpoint
	BytesIn uint64

	// BytesOut is the number of bytes send by source endpoint to destination endpoint
	BytesOut uint64
}

// TransactionTimestamp defines a transaction its initial timestamps as unix
// timestamp in milliseconds and time.Time struct.
type TransactionTimestamp struct {
	Millis int64
	Ts     time.Time
}

// Message defines common application layer message fields. Some of these fields
// are required to initialize a Transaction (see (*Transaction).InitWithMsg).
type Message struct {
	Ts           time.Time
	Tuple        common.IPPortTuple
	Transport    Transport
	CmdlineTuple *common.CmdlineTuple
	Direction    NetDirection
	IsRequest    bool
	Size         uint64
	Notes        []string
}

// Error code if stream exceeds max allowed size on Append.
var ErrStreamTooLarge = errors.New("Stream data too large")

// Init initializes a stream with an empty buffer and max size. Calling Init
// twice will fully re-initialize the buffer, such that calling Init before putting
// the stream in some object pool, no memory will be leaked.
func (stream *Stream) Init(maxDataInStream int) {
	stream.MaxDataInStream = maxDataInStream
	stream.Buf = streambuf.Buffer{}
}

// Reset will remove all bytes already read from the buffer.
func (stream *Stream) Reset() {
	stream.Buf.Reset()
}

// Append adds data to the Stream its buffer. If internal buffer is nil, data
// will be retained as is. Use Write if you don't intend to retain the buffer in
// the stream.
func (stream *Stream) Append(data []byte) error {
	err := stream.Buf.Append(data)
	if err != nil {
		return err
	}

	if stream.MaxDataInStream > 0 && stream.Buf.Total() > stream.MaxDataInStream {
		return ErrStreamTooLarge
	}
	return nil
}

// Write copies data to the Stream its buffer. The data slice will not be
// retained by the buffer.
func (stream *Stream) Write(data []byte) (int, error) {
	n, err := stream.Buf.Write(data)
	if err != nil {
		return n, err
	}

	if stream.MaxDataInStream > 0 && stream.Buf.Total() > stream.MaxDataInStream {
		return n, ErrStreamTooLarge
	}
	return n, nil
}

// Init initializes some common fields. ResponseTime, Status, BytesIn and
// BytesOut are initialized to zero and must be filled by application code.
func (t *Transaction) Init(
	typ string,
	tuple common.IPPortTuple,
	transport Transport,
	direction NetDirection,
	time time.Time,
	cmdline *common.CmdlineTuple,
	notes []string,
) {
	t.Type = typ
	t.Transport = transport
	t.Tuple = tuple

	// transactions have microseconds resolution
	t.Ts.Ts = time
	t.Ts.Millis = int64(time.UnixNano() / 1000)
	t.Src, t.Dst = common.MakeEndpointPair(tuple.BaseTuple, cmdline)
	t.Notes = notes

	if direction == NetReverseDirection {
		t.Src, t.Dst = t.Dst, t.Src
	}
}

// InitWithMsg initializes some common fields from a Message. ResponseTime,
// Status, BytesIn and BytesOut are initialized to zero and must be filled by
// application code.
func (t *Transaction) InitWithMsg(
	typ string,
	msg *Message,
) {
	t.Init(
		typ,
		msg.Tuple,
		msg.Transport,
		msg.Direction,
		msg.Ts,
		msg.CmdlineTuple,
		nil,
	)
}

// Event fills common event fields.
func (t *Transaction) Event(event *beat.Event) error {
	event.Timestamp = t.Ts.Ts

	fields := event.Fields
	fields["type"] = t.Type
	fields["responsetime"] = t.ResponseTime
	fields["src"] = &t.Src
	fields["dst"] = &t.Dst
	fields["transport"] = t.Transport.String()
	fields["bytes_out"] = t.BytesOut
	fields["bytes_in"] = t.BytesIn
	fields["status"] = t.Status
	if len(t.Notes) > 0 {
		fields["notes"] = t.Notes
	}
	return nil
}

// AddNotes appends some notes to a message.
func (m *Message) AddNotes(n ...string) {
	m.Notes = append(m.Notes, n...)
}
