// Copyright 2016 VMware, Inc. All Rights Reserved.
//
// Licensed 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 trace

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"sync/atomic"
	"time"

	"github.com/Sirupsen/logrus"

	"github.com/vmware/govmomi/vim25/types"
)

type OperationKey string

const OpTraceKey OperationKey = "traceKey"

var opIDPrefix = os.Getpid()

// opCount is a monotonic counter which increments on Start()
var opCount uint64

type Operation struct {
	context.Context
	operation

	// Logger is used to configure an Operation-specific destination for log messages, in addition
	// to the global logger. This logger is passed to any children which are created.
	Logger *logrus.Logger
}

type operation struct {
	t  []Message
	id string
}

func newOperation(ctx context.Context, id string, skip int, msg string) Operation {
	op := operation{

		// Can be used to trace based on this number which is unique per chain
		// of operations
		id: id,

		// Start the trace.
		t: []Message{*newTrace(msg, skip, id)},
	}

	// We need to be able to identify this operation across API (and process)
	// boundaries.  So add the trace as a value to the embedded context.  We
	// stash the values individually in the context because we can't assign
	// the operation itself as a value to the embedded context (it's circular)
	ctx = context.WithValue(ctx, OpTraceKey, op)

	// By adding the op.id any operations passed to govmomi will result
	// in the op.id being logged in vSphere (vpxa / hostd) as the prefix to opID
	// For example if the op.id was 299.16 hostd would show
	// verbose hostd[12281B70] [Originator@6876 sub=PropertyProvider opID=299.16-5b05 user=root]
	ctx = context.WithValue(ctx, types.ID{}, op.id)

	o := Operation{
		Context:   ctx,
		operation: op,
	}

	return o
}

// Creates a header string to be printed.
func (o *Operation) header() string {
	return fmt.Sprintf("op=%s", o.id)
}

// Err returns a non-nil error value after Done is closed.  Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed.  No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
func (o Operation) Err() error {

	// Walk up the contexts from which this context was created and get their errors
	if err := o.Context.Err(); err != nil {
		buf := &bytes.Buffer{}

		// Add a frame for this Err call, then walk the stack
		currFrame := newTrace("Err", 2, o.id)
		fmt.Fprintf(buf, "%s: %s error: %s\n", currFrame.funcName, o.t[0].msg, err)

		// handle the carriage return
		numFrames := len(o.t)

		for i, t := range o.t {
			fmt.Fprintf(buf, "%-15s:%d %s", t.funcName, t.lineNo, t.msg)

			// don't add a cr on the last frame
			if i != numFrames-1 {
				buf.WriteByte('\n')
			}
		}

		// Print the error
		o.Errorf(buf.String())

		return err
	}

	return nil
}

func (o Operation) String() string {
	return o.header()
}

func (o *Operation) ID() string {
	return o.id
}

func (o *Operation) Auditf(format string, args ...interface{}) {
	o.Infof(format, args...)
}

func (o *Operation) Infof(format string, args ...interface{}) {
	o.Info(fmt.Sprintf(format, args...))
}

func (o *Operation) Info(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Infof("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Info(msg)
	}
}

func (o *Operation) Debugf(format string, args ...interface{}) {
	o.Debug(fmt.Sprintf(format, args...))
}

func (o *Operation) Debug(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Debugf("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Debug(msg)
	}
}

func (o *Operation) Warnf(format string, args ...interface{}) {
	o.Warn(fmt.Sprintf(format, args...))
}

func (o *Operation) Warn(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Warnf("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Warn(msg)
	}
}

func (o *Operation) Errorf(format string, args ...interface{}) {
	o.Error(fmt.Sprintf(format, args...))
}

func (o *Operation) Error(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Errorf("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Error(msg)
	}
}

func (o *Operation) Panicf(format string, args ...interface{}) {
	o.Panic(fmt.Sprintf(format, args...))
}

func (o *Operation) Panic(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Panicf("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Panic(msg)
	}
}

func (o *Operation) Fatalf(format string, args ...interface{}) {
	o.Fatal(fmt.Sprintf(format, args...))
}

func (o *Operation) Fatal(args ...interface{}) {
	msg := fmt.Sprint(args...)

	Logger.Fatalf("%s: %s", o.header(), msg)

	if o.Logger != nil {
		o.Logger.Fatal(msg)
	}
}

func (o Operation) newChildCommon(ctx context.Context, opID string, msg string) Operation {
	child := newOperation(ctx, opID, 5, msg)
	child.t = append(child.t, o.t...)
	child.Logger = o.Logger
	return child
}

func (o Operation) newChild(ctx context.Context, msg string) Operation {
	return o.newChildCommon(ctx, o.id, msg)
}

func (o Operation) newChildWithChainedID(ctx context.Context, msg string) Operation {
	childOpID := fmt.Sprintf("%s.%d", o.id, atomic.AddUint64(&opCount, 1))
	return o.newChildCommon(ctx, childOpID, msg)
}

func opID(opNum uint64) string {
	return fmt.Sprintf("%d.%d", opIDPrefix, opNum)
}

// NewOperation will return a new operation with operationID added as a value to the context
func NewOperation(ctx context.Context, format string, args ...interface{}) Operation {
	o := newOperation(ctx, opID(atomic.AddUint64(&opCount, 1)), 3, fmt.Sprintf(format, args...))

	frame := o.t[0]
	o.Debugf("[NewOperation] %s [%s:%d]", o.header(), frame.funcName, frame.lineNo)
	return o
}

// NewOperationFromID returns a an Operation with the incoming ID if valid
// It creates a parent operation with the incoming ID and a child with
// the parent operation ID as a prefix and a monotonically incremented
// integer as the suffix
func NewOperationFromID(ctx context.Context, ID *string, format string, args ...interface{}) Operation {
	var o Operation
	if ID == nil || *ID == "" {
		o = newOperation(ctx, opID(atomic.AddUint64(&opCount, 1)), 3, fmt.Sprintf(format, args...))
	} else {
		msg := fmt.Sprintf(format, args...)
		o = newOperation(ctx, *ID, 3, msg).newChildWithChainedID(ctx, msg)
	}

	frame := o.t[0]
	o.Debugf("[NewOperationFromID] %s [%s:%d]", o.header(), frame.funcName, frame.lineNo)
	return o
}

// NewOperationWithLoggerFrom will return a new operation with operationID added as a value to the
// context and logging settings copied from the supplied operation.
//
// Deprecated: This method was added to aid in converting old code to use operation-based logging.
//             Its use almost always indicates a broken context/operation model (e.g., a context
//             being improperly stored in a struct instead of being passed between functions).
func NewOperationWithLoggerFrom(ctx context.Context, oldOp Operation, format string, args ...interface{}) Operation {
	op := NewOperation(ctx, format, args...)
	op.Logger = oldOp.Logger
	return op
}

// WithTimeout creates a new operation from parent with context.WithTimeout
func WithTimeout(parent *Operation, timeout time.Duration, format string, args ...interface{}) (Operation, context.CancelFunc) {
	ctx, cancelFunc := context.WithTimeout(parent.Context, timeout)
	op := parent.newChild(ctx, fmt.Sprintf(format, args...))

	return op, cancelFunc
}

// WithDeadline creates a new operation from parent with context.WithDeadline
func WithDeadline(parent *Operation, expiration time.Time, format string, args ...interface{}) (Operation, context.CancelFunc) {
	ctx, cancelFunc := context.WithDeadline(parent.Context, expiration)
	op := parent.newChild(ctx, fmt.Sprintf(format, args...))

	return op, cancelFunc
}

// WithCancel creates a new operation from parent with context.WithCancel
func WithCancel(parent *Operation, format string, args ...interface{}) (Operation, context.CancelFunc) {
	ctx, cancelFunc := context.WithCancel(parent.Context)
	op := parent.newChild(ctx, fmt.Sprintf(format, args...))

	return op, cancelFunc
}

// WithValue creates a new operation from parent with context.WithValue
func WithValue(parent *Operation, key, val interface{}, format string, args ...interface{}) Operation {
	ctx := context.WithValue(parent.Context, key, val)
	op := parent.newChild(ctx, fmt.Sprintf(format, args...))

	return op
}

// FromOperation creates a child operation from the one supplied
// uses the same context as the parent
func FromOperation(parent Operation, format string, args ...interface{}) Operation {
	return parent.newChild(parent.Context, fmt.Sprintf(format, args...))
}

// FromContext will return an Operation
//
// The Operation returned will be one of the following:
//   The operation in the context value
//   The operation passed as the context param
//   A new operation
func FromContext(ctx context.Context, message string, args ...interface{}) Operation {

	// do we have an operation
	if op, ok := ctx.(Operation); ok {
		return op
	}

	// do we have a context w/the op added as a value
	if op, ok := ctx.Value(OpTraceKey).(operation); ok {
		// ensure we have an initialized operation
		if op.id == "" {
			return NewOperation(ctx, message, args...)
		}
		// return an operation based off the context value
		return Operation{
			Context:   ctx,
			operation: op,
		}
	}

	op := newOperation(ctx, opID(atomic.AddUint64(&opCount, 1)), 3, fmt.Sprintf(message, args...))
	frame := op.t[0]
	Logger.Debugf("%s: [OperationFromContext] [%s:%d]", op.id, frame.funcName, frame.lineNo)

	// return the new operation
	return op
}
