// Licensed to Apache Software Foundation (ASF) under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Apache Software Foundation (ASF) 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 core

import (
	"fmt"
	"reflect"
	"runtime/debug"

	"github.com/pkg/errors"

	"github.com/apache/skywalking-go/plugins/core/reporter"
	"github.com/apache/skywalking-go/plugins/core/tracing"
)

var snapshotType = reflect.TypeOf(&SnapshotSpan{})

func (t *Tracer) Tracing() interface{} {
	return t
}

func (t *Tracer) Logger() interface{} {
	return t.Log
}

func (t *Tracer) DebugStack() []byte {
	return debug.Stack()
}

func (t *Tracer) CreateEntrySpan(operationName string, extractor interface{}, opts ...interface{}) (s interface{}, err error) {
	ctx, tracingSpan, noop := t.createNoop(operationName)
	if noop {
		return tracingSpan, nil
	}
	defer func() {
		saveSpanToActiveIfNotError(ctx, s, err)
	}()
	// if parent span is entry span, then use parent span as result
	if tracingSpan != nil && tracingSpan.IsEntry() && reflect.ValueOf(tracingSpan).Type() != snapshotType {
		tracingSpan.SetOperationName(operationName)
		return tracingSpan, nil
	}
	var ref = &SpanContext{}
	if err1 := ref.Decode(extractor.(tracing.ExtractorWrapper).Fun()); err1 != nil {
		return nil, err1
	}
	if !ref.Valid {
		ref = nil
	}

	span, _, err := t.createSpan0(ctx, tracingSpan, opts, withRef(ref), withSpanType(SpanTypeEntry), withOperationName(operationName))
	return span, err
}

func (t *Tracer) CreateLocalSpan(operationName string, opts ...interface{}) (s interface{}, err error) {
	ctx, tracingSpan, noop := t.createNoop(operationName)
	if noop {
		return tracingSpan, nil
	}
	defer func() {
		saveSpanToActiveIfNotError(ctx, s, err)
	}()

	span, _, err := t.createSpan0(ctx, tracingSpan, opts, withSpanType(SpanTypeLocal), withOperationName(operationName))
	return span, err
}

func (t *Tracer) CreateExitSpan(operationName, peer string, injector interface{}, opts ...interface{}) (s interface{}, err error) {
	ctx, tracingSpan, noop := t.createNoop(operationName)
	if noop {
		return tracingSpan, nil
	}
	defer func() {
		saveSpanToActiveIfNotError(ctx, s, err)
	}()

	// if parent span is exit span, then use parent span as result
	if tracingSpan != nil && tracingSpan.IsExit() && reflect.ValueOf(tracingSpan).Type() != snapshotType {
		return tracingSpan, nil
	}
	span, noop, err := t.createSpan0(ctx, tracingSpan, opts, withSpanType(SpanTypeExit), withOperationName(operationName), withPeer(peer))
	if err != nil {
		return nil, err
	}
	if noop {
		return span, nil
	}
	spanContext := &SpanContext{}
	reportedSpan, ok := span.(SegmentSpan)
	if !ok {
		return nil, errors.New(fmt.Sprintf("span type is wrong: %T", span))
	}

	firstSpan := reportedSpan.GetSegmentContext().FirstSpan
	spanContext.Sample = 1
	spanContext.TraceID = reportedSpan.GetSegmentContext().TraceID
	spanContext.ParentSegmentID = reportedSpan.GetSegmentContext().SegmentID
	spanContext.ParentSpanID = reportedSpan.GetSegmentContext().SpanID
	spanContext.ParentService = t.ServiceEntity.ServiceName
	spanContext.ParentServiceInstance = t.ServiceEntity.ServiceInstanceName
	spanContext.ParentEndpoint = firstSpan.GetOperationName()
	spanContext.AddressUsedAtClient = peer
	spanContext.CorrelationContext = reportedSpan.GetSegmentContext().CorrelationContext

	err = spanContext.Encode(injector.(tracing.InjectorWrapper).Fun())
	if err != nil {
		return nil, err
	}
	return span, nil
}

func (t *Tracer) ActiveSpan() interface{} {
	ctx := getTracingContext()
	if ctx == nil || ctx.ActiveSpan() == nil {
		return nil
	}
	span := ctx.ActiveSpan()
	return span
}

func (t *Tracer) GetRuntimeContextValue(key string) interface{} {
	context := getTracingContext()
	if context == nil {
		return nil
	}
	return context.Runtime.Get(key)
}

func (t *Tracer) SetRuntimeContextValue(key string, value interface{}) {
	context := getTracingContext()
	if context == nil {
		context = NewTracingContext()
		SetGLS(context)
	}
	context.Runtime.Set(key, value)
}

func (t *Tracer) CaptureContext() interface{} {
	ctx := getTracingContext()
	if ctx == nil {
		return nil
	}
	snapshot := &ContextSnapshot{
		activeSpan: newSnapshotSpan(ctx.ActiveSpan()),
		runtime:    ctx.Runtime.clone(),
	}
	return snapshot
}

func (t *Tracer) ContinueContext(snapshot interface{}) {
	if snapshot == nil {
		return
	}
	if snap, ok := snapshot.(*ContextSnapshot); ok {
		ctx := getTracingContext()
		if ctx == nil {
			ctx = NewTracingContext()
			SetGLS(ctx)
		}
		ctx.activeSpanLock.Lock()
		defer ctx.activeSpanLock.Unlock()
		ctx.activeSpan = snap.activeSpan
		ctx.Runtime = snap.runtime
	}
}

func (t *Tracer) CleanContext() {
	SetGLS(nil)
}

func (t *Tracer) GetCorrelationContextValue(key string) string {
	span := t.ActiveSpan()
	if span == nil {
		return ""
	}
	switch reportedSpan := span.(type) {
	case *SegmentSpanImpl:
		return reportedSpan.Context().GetCorrelationContextValue(key)
	case *RootSegmentSpan:
		return reportedSpan.Context().GetCorrelationContextValue(key)
	default:
		return ""
	}
}

func (t *Tracer) SetCorrelationContextValue(key, value string) {
	span := t.ActiveSpan()
	if span == nil {
		return
	}
	switch reportedSpan := span.(type) {
	case *SegmentSpanImpl:
		if len(value) > t.correlation.MaxValueSize {
			return
		}
		if len(reportedSpan.GetSegmentContext().CorrelationContext) >= t.correlation.MaxKeyCount {
			return
		}
		reportedSpan.Context().SetCorrelationContextValue(key, value)
	case *RootSegmentSpan:
		if len(value) > t.correlation.MaxValueSize {
			return
		}
		if len(reportedSpan.GetSegmentContext().CorrelationContext) >= t.correlation.MaxKeyCount {
			return
		}
		reportedSpan.Context().SetCorrelationContextValue(key, value)
	default:
	}
}

type ContextSnapshot struct {
	activeSpan TracingSpan
	runtime    *RuntimeContext
}

func (s *ContextSnapshot) IsValid() bool {
	return s.activeSpan != nil && s.runtime != nil
}

func (t *Tracer) createNoop(operationName string) (*TracingContext, TracingSpan, bool) {
	if !t.InitSuccess() || t.Reporter.ConnectionStatus() == reporter.ConnectionStatusDisconnect {
		GetSo11y(t).MeasureTracingContextCreation(false, true)
		return nil, newNoopSpan(t), true
	}
	if tracerIgnore(operationName, t.ignoreSuffix, t.traceIgnorePath) {
		GetSo11y(t).MeasureTracingContextCreation(false, true)
		return nil, newNoopSpan(t), true
	}
	ctx := getTracingContext()
	if ctx != nil {
		span := ctx.ActiveSpan()
		noop, ok := span.(*NoopSpan)
		if ok {
			// increase the stack count for ensure the noop span can be clear in the context
			noop.enterNoSpan()
		}
		return ctx, span, ok
	}
	ctx = NewTracingContext()
	return ctx, nil, false
}

func (t *Tracer) createSpan0(ctx *TracingContext, parent TracingSpan, pluginOpts []interface{},
	coreOpts ...interface{}) (s TracingSpan, noop bool, err error) {
	ds := NewDefaultSpan(t, parent)
	var parentSpan SegmentSpan
	if parent != nil {
		tmpSpan, ok := parent.(SegmentSpan)
		if ok {
			parentSpan = tmpSpan
		}
	}
	isForceSample := len(ds.Refs) > 0
	// Try to sample when it is not force sample
	if parentSpan == nil && !isForceSample {
		isSampled := t.Sampler.IsSampled(ds.OperationName)
		if !isSampled {
			GetSo11y(t).MeasureTracingContextCreation(false, true)
			GetSo11y(t).MeasureLeakedTracingContext(true)
			// Filter by sample just return noop span
			return newNoopSpan(t), true, nil
		}
	}
	// process the opts from agent core for prepare building segment span
	for _, opt := range coreOpts {
		opt.(tracing.SpanOption).Apply(ds)
	}
	s, err = NewSegmentSpan(ctx, ds, parentSpan)
	if err != nil {
		return nil, false, err
	}
	// process the opts from plugin, split opts because the DefaultSpan not contains the tracing context information(AdaptSpan)
	for _, opt := range pluginOpts {
		opt.(tracing.SpanOption).Apply(s)
	}
	GetSo11y(t).MeasureTracingContextCreation(isForceSample, false)
	return s, false, nil
}

func withSpanType(spanType SpanType) tracing.SpanOption {
	return buildSpanOption(func(span *DefaultSpan) {
		span.SpanType = spanType
	})
}

func withOperationName(opName string) tracing.SpanOption {
	return buildSpanOption(func(span *DefaultSpan) {
		span.OperationName = opName
	})
}

func withRef(sc reporter.SpanContext) tracing.SpanOption {
	return buildSpanOption(func(span *DefaultSpan) {
		if sc == nil {
			return
		}
		v := reflect.ValueOf(sc)
		if v.Interface() == reflect.Zero(v.Type()).Interface() {
			return
		}
		span.Refs = append(span.Refs, sc)
	})
}

func withPeer(peer string) tracing.SpanOption {
	return buildSpanOption(func(span *DefaultSpan) {
		span.Peer = peer
	})
}

type spanOpImpl struct {
	exe func(s *DefaultSpan)
}

func (s *spanOpImpl) Apply(span interface{}) {
	if segmentSpan, ok := span.(*DefaultSpan); ok {
		s.exe(segmentSpan)
	}
}

func buildSpanOption(e func(s *DefaultSpan)) tracing.SpanOption {
	return &spanOpImpl{exe: e}
}

func getTracingContext() *TracingContext {
	gls := GetGLS()
	if gls == nil {
		return nil
	}
	return gls.(*TracingContext)
}

func saveSpanToActiveIfNotError(ctx *TracingContext, span interface{}, err error) {
	if err != nil || span == nil {
		return
	}
	ctx.SaveActiveSpan(span.(TracingSpan))
	SetGLS(ctx)
}
