package operation

import (
	"context"
	"strconv"
	"sync"
	"time"
)

type Kind int

const (
	// KindSpan is a generic operation. It can be used to split up processing
	// within a single program.
	KindSpan Kind = 0

	// KindServer represents a request inbound to the current program.
	KindServer Kind = 1

	// KindClient describes a request created by the current program which
	// will be processed elsewhere.
	KindClient Kind = 2
)

func (op Kind) String() string {
	switch op {
	case KindSpan:
		return "span"
	case KindServer:
		return "server"
	case KindClient:
		return "client"
	default:
		d := strconv.Itoa(int(op))
		return `%!Kind(` + d + `)`
	}
}

type Name struct {
	// Kind indicates if the Op represents an inbound call (KindServer), an
	// outbound call (KindClient), or an internal division between portions of
	// a single process (KindSpan).
	Kind Kind

	// Group is a logical grouping of operations, such as a package or service
	// name. Together, Group and Method form a complete name for the
	// operation.
	Group string
	// Method is the name of an operation. Each program should have a finite
	// number of Method names, usually corresponding to a function to run or a
	// type of task to execute.
	Method string
}

// Status describes the result of an Op.
type Status struct {
	// Code is a status code. Zero indicates success.
	//
	// Code describes Twirp statuses, as mapped to numbers by gRPC. See
	// Google's documentation for a list of numeric status codes.
	//
	// https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
	Code int32

	// Message is a supplementary string to describe the Status.
	//
	// It may hold a Twirp-specific error that doesn't have a numeric mapping
	// ("bad_route"), a error type from an AWS API, or a short free-form
	// string.
	Message string
}

var twirpStatus = map[int32]string{
	0:  "",
	1:  "canceled",
	2:  "unknown",
	3:  "invalid_argument",
	4:  "deadline_exceeded",
	5:  "not_found",
	6:  "already_exists",
	7:  "permission_denied",
	8:  "resource_exhausted",
	9:  "failed_precondition",
	10: "aborted",
	11: "out_of_range",
	12: "unimplemented",
	13: "internal",
	14: "unavailable",
	15: "data_loss",
	16: "unauthenticated",
}

// TwirpErrorCode returns the Twirp ErrorCode string corresponding to this
// Status. It returns the empty string if and only if the Status indicates no
// error.
func (s Status) TwirpErrorCode() string {
	if name, ok := twirpStatus[s.Code]; ok {
		return name
	}
	return "unknown"
}

// An Op describes an operation that is currently in progress. This view of
// the operation provides the ability to set the status of the operation and
// to mark it as complete.
//
// Most code should will report information about the operation through the
// View type.
type Op struct {
	name Name

	startTime time.Time

	monitors []*MonitorPoints

	// viewMu protects fields that can be written concurrently via a View
	//
	// TODO: what should the observers be allowed to change?
	viewMu      sync.Mutex
	opDone      bool // the operation itself is no longer active
	reportsDone bool // all observers have processed the Op

	status Status
}

// End marks the end time of the Op. It calls the End callback for each
// OpMonitor that provided one.
func (op *Op) End() {
	if op == nil {
		return
	}

	op.viewMu.Lock()
	opDone := op.opDone
	op.opDone = true
	op.viewMu.Unlock()

	if opDone {
		return
	}

	defer func() {
		op.viewMu.Lock()
		op.reportsDone = true
		op.viewMu.Unlock()
	}()

	report := &Report{
		StartTime: op.startTime,
		EndTime:   time.Now(),
		Status:    op.status,
	}

	for i := len(op.monitors) - 1; i >= 0; i-- {
		if fn := op.monitors[i].End; fn != nil {
			fn(report)
		}
	}
}

// SetStatus records the Op's Status. The End method uses the most recent
// Status value.
func (op *Op) SetStatus(status Status) {
	if op == nil {
		return
	}

	op.viewMu.Lock()
	defer op.viewMu.Unlock()
	if !op.opDone {
		op.status = status
	}
}

// View returns a value that provides limited access to the Op.
func (op *Op) View() *View {
	return &View{op: op}
}

// ApplyToContext returns a Context with the Op's request-related data
// attached. If an OpMonitor has provided an "ApplyToContext" callback, it
// will be called to attach the data.
func (op *Op) ApplyToContext(ctx context.Context) context.Context {
	if op == nil {
		return ctx
	}

	for _, mon := range op.monitors {
		if fn := mon.ApplyToContext; fn != nil {
			ctx = fn(ctx)
		}
	}

	ctx = NewViewContext(ctx, op.View())

	return ctx
}

// MonitorPoints is a set of optional callbacks that let an OpMonitor keep
// track of an Op's lifecycle.
//
// Unused callbacks may be left as nil.
type MonitorPoints struct {
	// End is called when the Op is complete.
	End func(report *Report)

	// ApplyToContext is called when the Op (or a View to the Op) is applied
	// to a new context. This allows users to describe patterns like
	// background processing initiated by a short-lived request.
	//
	// There is no call to ApplyToContext when an operation is started.
	ApplyToContext func(ctx context.Context) context.Context
}

// A Report describes an operation that has finished.
type Report struct {
	StartTime time.Time
	EndTime   time.Time
	Status    Status
}

// An OpMonitor passively observes an Op. It has an opportunity to modify the
// Context to be used for the Op, such as to add request-tracing data. The
// functions on the returned MonitorPoints are called during the request
// lifecycle; implementors should provide functions for any points they'd like
// to monitor or -- within limits -- to affect.
type OpMonitor interface {
	MonitorOp(parent context.Context, name Name) (context.Context, *MonitorPoints)
}
