package tracing

// Copyright 2018 Microsoft Corporation
//
//  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.

import (
	"context"
	"fmt"
	"net/http"
	"os"

	"contrib.go.opencensus.io/exporter/ocagent"
	"go.opencensus.io/plugin/ochttp"
	"go.opencensus.io/plugin/ochttp/propagation/tracecontext"
	"go.opencensus.io/stats/view"
	"go.opencensus.io/trace"
)

var (
	// Transport is the default tracing RoundTripper. The custom options setter will control
	// if traces are being emitted or not.
	Transport = NewTransport()

	// enabled is the flag for marking if tracing is enabled.
	enabled = false

	// Sampler is the tracing sampler. If tracing is disabled it will never sample. Otherwise
	// it will be using the parent sampler or the default.
	sampler = trace.NeverSample()

	// Views for metric instrumentation.
	views = map[string]*view.View{}

	// the trace exporter
	traceExporter trace.Exporter
)

func init() {
	enableFromEnv()
}

func enableFromEnv() {
	_, ok := os.LookupEnv("AZURE_SDK_TRACING_ENABLED")
	_, legacyOk := os.LookupEnv("AZURE_SDK_TRACING_ENABELD")
	if ok || legacyOk {
		agentEndpoint, ok := os.LookupEnv("OCAGENT_TRACE_EXPORTER_ENDPOINT")

		if ok {
			EnableWithAIForwarding(agentEndpoint)
		} else {
			Enable()
		}
	}
}

// NewTransport returns a new instance of a tracing-aware RoundTripper.
func NewTransport() *ochttp.Transport {
	return &ochttp.Transport{
		Propagation:     &tracecontext.HTTPFormat{},
		GetStartOptions: getStartOptions,
	}
}

// IsEnabled returns true if monitoring is enabled for the sdk.
func IsEnabled() bool {
	return enabled
}

// Enable will start instrumentation for metrics and traces.
func Enable() error {
	enabled = true
	sampler = nil

	err := initStats()
	return err
}

// Disable will disable instrumentation for metrics and traces.
func Disable() {
	disableStats()
	sampler = trace.NeverSample()
	if traceExporter != nil {
		trace.UnregisterExporter(traceExporter)
	}
	enabled = false
}

// EnableWithAIForwarding will start instrumentation and will connect to app insights forwarder
// exporter making the metrics and traces available in app insights.
func EnableWithAIForwarding(agentEndpoint string) (err error) {
	err = Enable()
	if err != nil {
		return err
	}

	traceExporter, err := ocagent.NewExporter(ocagent.WithInsecure(), ocagent.WithAddress(agentEndpoint))
	if err != nil {
		return err
	}
	trace.RegisterExporter(traceExporter)
	return
}

// getStartOptions is the custom options setter for the ochttp package.
func getStartOptions(*http.Request) trace.StartOptions {
	return trace.StartOptions{
		Sampler: sampler,
	}
}

// initStats registers the views for the http metrics
func initStats() (err error) {
	clientViews := []*view.View{
		ochttp.ClientCompletedCount,
		ochttp.ClientRoundtripLatencyDistribution,
		ochttp.ClientReceivedBytesDistribution,
		ochttp.ClientSentBytesDistribution,
	}
	for _, cv := range clientViews {
		vn := fmt.Sprintf("Azure/go-autorest/tracing-%s", cv.Name)
		views[vn] = cv.WithName(vn)
		err = view.Register(views[vn])
		if err != nil {
			return err
		}
	}
	return
}

// disableStats will unregister the previously registered metrics
func disableStats() {
	for _, v := range views {
		view.Unregister(v)
	}
}

// StartSpan starts a trace span
func StartSpan(ctx context.Context, name string) context.Context {
	ctx, _ = trace.StartSpan(ctx, name, trace.WithSampler(sampler))
	return ctx
}

// EndSpan ends a previously started span stored in the context
func EndSpan(ctx context.Context, httpStatusCode int, err error) {
	span := trace.FromContext(ctx)

	if span == nil {
		return
	}

	if err != nil {
		span.SetStatus(trace.Status{Message: err.Error(), Code: toTraceStatusCode(httpStatusCode)})
	}
	span.End()
}

// toTraceStatusCode converts HTTP Codes to OpenCensus codes as defined
// at https://github.com/census-instrumentation/opencensus-specs/blob/master/trace/HTTP.md#status
func toTraceStatusCode(httpStatusCode int) int32 {
	switch {
	case http.StatusOK <= httpStatusCode && httpStatusCode < http.StatusBadRequest:
		return trace.StatusCodeOK
	case httpStatusCode == http.StatusBadRequest:
		return trace.StatusCodeInvalidArgument
	case httpStatusCode == http.StatusUnauthorized: // 401 is actually unauthenticated.
		return trace.StatusCodeUnauthenticated
	case httpStatusCode == http.StatusForbidden:
		return trace.StatusCodePermissionDenied
	case httpStatusCode == http.StatusNotFound:
		return trace.StatusCodeNotFound
	case httpStatusCode == http.StatusTooManyRequests:
		return trace.StatusCodeResourceExhausted
	case httpStatusCode == 499:
		return trace.StatusCodeCancelled
	case httpStatusCode == http.StatusNotImplemented:
		return trace.StatusCodeUnimplemented
	case httpStatusCode == http.StatusServiceUnavailable:
		return trace.StatusCodeUnavailable
	case httpStatusCode == http.StatusGatewayTimeout:
		return trace.StatusCodeDeadlineExceeded
	default:
		return trace.StatusCodeUnknown
	}
}
