package internal

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/newrelic/go-agent/internal/cat"
)

// Bitfield values for the TxnCrossProcess.Type field.
const (
	txnCrossProcessSynthetics = (1 << 0)
	txnCrossProcessInbound    = (1 << 1)
	txnCrossProcessOutbound   = (1 << 2)
)

var (
	// ErrAccountNotTrusted indicates that, while the inbound headers were valid,
	// the account ID within them is not trusted by the user's application.
	ErrAccountNotTrusted = errors.New("account not trusted")
)

// TxnCrossProcess contains the metadata required for CAT and Synthetics
// headers, transaction events, and traces.
type TxnCrossProcess struct {
	// The user side switch controlling whether CAT is enabled or not.
	Enabled bool

	// The user side switch controlling whether Distributed Tracing is enabled or not
	// This is required by synthetics support.  If Distributed Tracing is enabled,
	// any synthetics functionality that is triggered should not set nr.guid.
	DistributedTracingEnabled bool

	// Rather than copying in the entire ConnectReply, here are the fields that
	// we need to support CAT.
	CrossProcessID  []byte
	EncodingKey     []byte
	TrustedAccounts trustedAccountSet

	// CAT state for a given transaction.
	Type                uint8
	ClientID            string
	GUID                string
	TripID              string
	PathHash            string
	AlternatePathHashes map[string]bool
	ReferringPathHash   string
	ReferringTxnGUID    string
	Synthetics          *cat.SyntheticsHeader

	// The encoded synthetics header received as part of the request headers, if
	// any. By storing this here, we avoid needing to marshal the invariant
	// Synthetics struct above each time an external segment is created.
	SyntheticsHeader string
}

// CrossProcessMetadata represents the metadata that must be transmitted with
// an external request for CAT to work.
type CrossProcessMetadata struct {
	ID         string
	TxnData    string
	Synthetics string
}

// Init initialises a TxnCrossProcess based on the given application connect
// reply.
func (txp *TxnCrossProcess) Init(enabled bool, dt bool, reply *ConnectReply) {
	txp.CrossProcessID = []byte(reply.CrossProcessID)
	txp.EncodingKey = []byte(reply.EncodingKey)
	txp.DistributedTracingEnabled = dt
	txp.Enabled = enabled
	txp.TrustedAccounts = reply.TrustedAccounts
}

// CreateCrossProcessMetadata generates request metadata that enable CAT and
// Synthetics support for an external segment.
func (txp *TxnCrossProcess) CreateCrossProcessMetadata(txnName, appName string) (CrossProcessMetadata, error) {
	metadata := CrossProcessMetadata{}

	// Regardless of the user's CAT settings, if there was a synthetics header in
	// the inbound request, a synthetics header should always be included in the
	// outbound request headers.
	if txp.IsSynthetics() {
		metadata.Synthetics = txp.SyntheticsHeader
	}

	if txp.Enabled {
		txp.SetOutbound(true)
		txp.requireTripID()

		id, err := txp.outboundID()
		if err != nil {
			return metadata, err
		}

		txnData, err := txp.outboundTxnData(txnName, appName)
		if err != nil {
			return metadata, err
		}

		metadata.ID = id
		metadata.TxnData = txnData
	}

	return metadata, nil
}

// Finalise handles any end-of-transaction tasks. In practice, this simply
// means ensuring the path hash is set if it hasn't already been.
func (txp *TxnCrossProcess) Finalise(txnName, appName string) error {
	if txp.Enabled && txp.Used() {
		_, err := txp.setPathHash(txnName, appName)
		return err
	}

	// If there was no CAT activity, then do nothing, successfully.
	return nil
}

// IsInbound returns true if the transaction had inbound CAT headers.
func (txp *TxnCrossProcess) IsInbound() bool {
	return 0 != (txp.Type & txnCrossProcessInbound)
}

// IsOutbound returns true if the transaction has generated outbound CAT
// headers.
func (txp *TxnCrossProcess) IsOutbound() bool {
	// We don't actually use this anywhere today, but it feels weird not having
	// it.
	return 0 != (txp.Type & txnCrossProcessOutbound)
}

// IsSynthetics returns true if the transaction had inbound Synthetics headers.
func (txp *TxnCrossProcess) IsSynthetics() bool {
	// Technically, this is redundant: the presence of a non-nil Synthetics
	// pointer should be sufficient to determine if this is a synthetics
	// transaction. Nevertheless, it's convenient to have the Type field be
	// non-zero if any CAT behaviour has occurred.
	return 0 != (txp.Type&txnCrossProcessSynthetics) && nil != txp.Synthetics
}

// ParseAppData decodes the given appData value.
func (txp *TxnCrossProcess) ParseAppData(encodedAppData string) (*cat.AppDataHeader, error) {
	if !txp.Enabled {
		return nil, nil
	}
	if encodedAppData != "" {
		rawAppData, err := deobfuscate(encodedAppData, txp.EncodingKey)
		if err != nil {
			return nil, err
		}

		appData := &cat.AppDataHeader{}
		if err := json.Unmarshal(rawAppData, appData); err != nil {
			return nil, err
		}

		return appData, nil
	}

	return nil, nil
}

// CreateAppData creates the appData value that should be sent with a response
// to ensure CAT operates as expected.
func (txp *TxnCrossProcess) CreateAppData(name string, queueTime, responseTime time.Duration, contentLength int64) (string, error) {
	// If CAT is disabled, do nothing, successfully.
	if !txp.Enabled {
		return "", nil
	}

	data, err := json.Marshal(&cat.AppDataHeader{
		CrossProcessID:        string(txp.CrossProcessID),
		TransactionName:       name,
		QueueTimeInSeconds:    queueTime.Seconds(),
		ResponseTimeInSeconds: responseTime.Seconds(),
		ContentLength:         contentLength,
		TransactionGUID:       txp.GUID,
	})
	if err != nil {
		return "", err
	}

	obfuscated, err := obfuscate(data, txp.EncodingKey)
	if err != nil {
		return "", err
	}

	return obfuscated, nil
}

// Used returns true if any CAT or Synthetics related functionality has been
// triggered on the transaction.
func (txp *TxnCrossProcess) Used() bool {
	return 0 != txp.Type
}

// SetInbound sets the inbound CAT flag. This function is provided only for
// internal and unit testing purposes, and should not be used outside of this
// package normally.
func (txp *TxnCrossProcess) SetInbound(inbound bool) {
	if inbound {
		txp.Type |= txnCrossProcessInbound
	} else {
		txp.Type &^= txnCrossProcessInbound
	}
}

// SetOutbound sets the outbound CAT flag. This function is provided only for
// internal and unit testing purposes, and should not be used outside of this
// package normally.
func (txp *TxnCrossProcess) SetOutbound(outbound bool) {
	if outbound {
		txp.Type |= txnCrossProcessOutbound
	} else {
		txp.Type &^= txnCrossProcessOutbound
	}
}

// SetSynthetics sets the Synthetics CAT flag. This function is provided only
// for internal and unit testing purposes, and should not be used outside of
// this package normally.
func (txp *TxnCrossProcess) SetSynthetics(synthetics bool) {
	if synthetics {
		txp.Type |= txnCrossProcessSynthetics
	} else {
		txp.Type &^= txnCrossProcessSynthetics
	}
}

// handleInboundRequestHeaders parses the CAT headers from the given metadata
// and updates the relevant fields on the provided TxnData.
func (txp *TxnCrossProcess) handleInboundRequestHeaders(metadata CrossProcessMetadata) error {
	if txp.Enabled && metadata.ID != "" && metadata.TxnData != "" {
		if err := txp.handleInboundRequestEncodedCAT(metadata.ID, metadata.TxnData); err != nil {
			return err
		}
	}

	if metadata.Synthetics != "" {
		if err := txp.handleInboundRequestEncodedSynthetics(metadata.Synthetics); err != nil {
			return err
		}
	}

	return nil
}

func (txp *TxnCrossProcess) handleInboundRequestEncodedCAT(encodedID, encodedTxnData string) error {
	rawID, err := deobfuscate(encodedID, txp.EncodingKey)
	if err != nil {
		return err
	}

	rawTxnData, err := deobfuscate(encodedTxnData, txp.EncodingKey)
	if err != nil {
		return err
	}

	if err := txp.handleInboundRequestID(rawID); err != nil {
		return err
	}

	return txp.handleInboundRequestTxnData(rawTxnData)
}

func (txp *TxnCrossProcess) handleInboundRequestID(raw []byte) error {
	id, err := cat.NewIDHeader(raw)
	if err != nil {
		return err
	}

	if !txp.TrustedAccounts.IsTrusted(id.AccountID) {
		return ErrAccountNotTrusted
	}

	txp.SetInbound(true)
	txp.ClientID = string(raw)
	txp.setRequireGUID()

	return nil
}

func (txp *TxnCrossProcess) handleInboundRequestTxnData(raw []byte) error {
	txnData := &cat.TxnDataHeader{}
	if err := json.Unmarshal(raw, txnData); err != nil {
		return err
	}

	txp.SetInbound(true)
	if txnData.TripID != "" {
		txp.TripID = txnData.TripID
	} else {
		txp.setRequireGUID()
		txp.TripID = txp.GUID
	}
	txp.ReferringTxnGUID = txnData.GUID
	txp.ReferringPathHash = txnData.PathHash

	return nil
}

func (txp *TxnCrossProcess) handleInboundRequestEncodedSynthetics(encoded string) error {
	raw, err := deobfuscate(encoded, txp.EncodingKey)
	if err != nil {
		return err
	}

	if err := txp.handleInboundRequestSynthetics(raw); err != nil {
		return err
	}

	txp.SyntheticsHeader = encoded
	return nil
}

func (txp *TxnCrossProcess) handleInboundRequestSynthetics(raw []byte) error {
	synthetics := &cat.SyntheticsHeader{}
	if err := json.Unmarshal(raw, synthetics); err != nil {
		return err
	}

	// The specced behaviour here if the account isn't trusted is to disable the
	// synthetics handling, but not CAT in general, so we won't return an error
	// here.
	if txp.TrustedAccounts.IsTrusted(synthetics.AccountID) {
		txp.SetSynthetics(true)
		txp.setRequireGUID()
		txp.Synthetics = synthetics
	}

	return nil
}

func (txp *TxnCrossProcess) outboundID() (string, error) {
	return obfuscate(txp.CrossProcessID, txp.EncodingKey)
}

func (txp *TxnCrossProcess) outboundTxnData(txnName, appName string) (string, error) {
	pathHash, err := txp.setPathHash(txnName, appName)
	if err != nil {
		return "", err
	}

	data, err := json.Marshal(&cat.TxnDataHeader{
		GUID:     txp.GUID,
		TripID:   txp.TripID,
		PathHash: pathHash,
	})
	if err != nil {
		return "", err
	}

	return obfuscate(data, txp.EncodingKey)
}

// setRequireGUID ensures that the transaction has a valid GUID, and sets the
// nr.guid and trip ID if they are not already set.  If the customer has enabled
// DistributedTracing, then the new style of guid will be set elsewhere.
func (txp *TxnCrossProcess) setRequireGUID() {
	if txp.DistributedTracingEnabled {
		return
	}

	if txp.GUID != "" {
		return
	}

	txp.GUID = fmt.Sprintf("%x", RandUint64())

	if txp.TripID == "" {
		txp.requireTripID()
	}
}

// requireTripID ensures that the transaction has a valid trip ID.
func (txp *TxnCrossProcess) requireTripID() {
	if !txp.Enabled {
		return
	}
	if txp.TripID != "" {
		return
	}

	txp.setRequireGUID()
	txp.TripID = txp.GUID
}

// setPathHash generates a path hash, sets the transaction's path hash to
// match, and returns it. This function will also ensure that the alternate
// path hashes are correctly updated.
func (txp *TxnCrossProcess) setPathHash(txnName, appName string) (string, error) {
	pathHash, err := cat.GeneratePathHash(txp.ReferringPathHash, txnName, appName)
	if err != nil {
		return "", err
	}

	if pathHash != txp.PathHash {
		if txp.PathHash != "" {
			// Lazily initialise the alternate path hashes if they haven't been
			// already.
			if txp.AlternatePathHashes == nil {
				txp.AlternatePathHashes = make(map[string]bool)
			}

			// The spec limits us to a maximum of 10 alternate path hashes.
			if len(txp.AlternatePathHashes) < 10 {
				txp.AlternatePathHashes[txp.PathHash] = true
			}
		}
		txp.PathHash = pathHash
	}

	return pathHash, nil
}
