// Copyright The OpenTelemetry Authors
//
// 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 resourceprocessor

import (
	"context"

	resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"

	"go.opentelemetry.io/collector/component"
	"go.opentelemetry.io/collector/consumer"
	"go.opentelemetry.io/collector/consumer/consumerdata"
)

type resourceTraceProcessor struct {
	resource     *resourcepb.Resource
	capabilities component.ProcessorCapabilities
	next         consumer.TraceConsumerOld
}

func newResourceTraceProcessor(next consumer.TraceConsumerOld, cfg *Config) *resourceTraceProcessor {
	resource := createResource(cfg)
	return &resourceTraceProcessor{
		next:         next,
		capabilities: component.ProcessorCapabilities{MutatesConsumedData: !isEmptyResource(resource)},
		resource:     resource,
	}
}

// ConsumeTraceData implements the TraceProcessor interface
func (rtp *resourceTraceProcessor) ConsumeTraceData(ctx context.Context, td consumerdata.TraceData) error {
	return rtp.next.ConsumeTraceData(ctx, consumerdata.TraceData{
		Node:         td.Node,
		Resource:     mergeResource(td.Resource, rtp.resource),
		Spans:        td.Spans,
		SourceFormat: td.SourceFormat,
	})
}

// GetCapabilities returns the ProcessorCapabilities assocciated with the resource processor.
func (rtp *resourceTraceProcessor) GetCapabilities() component.ProcessorCapabilities {
	return rtp.capabilities
}

// Start is invoked during service startup.
func (*resourceTraceProcessor) Start(ctx context.Context, host component.Host) error {
	return nil
}

// Shutdown is invoked during service shutdown.
func (*resourceTraceProcessor) Shutdown(context.Context) error {
	return nil
}

type resourceMetricProcessor struct {
	resource     *resourcepb.Resource
	capabilities component.ProcessorCapabilities
	next         consumer.MetricsConsumerOld
}

func newResourceMetricProcessor(next consumer.MetricsConsumerOld, cfg *Config) *resourceMetricProcessor {
	resource := createResource(cfg)
	return &resourceMetricProcessor{
		resource:     resource,
		capabilities: component.ProcessorCapabilities{MutatesConsumedData: !isEmptyResource(resource)},
		next:         next,
	}
}

// GetCapabilities returns the ProcessorCapabilities assocciated with the resource processor.
func (rmp *resourceMetricProcessor) GetCapabilities() component.ProcessorCapabilities {
	return rmp.capabilities
}

// Start is invoked during service startup.
func (*resourceMetricProcessor) Start(ctx context.Context, host component.Host) error {
	return nil
}

// Shutdown is invoked during service shutdown.
func (*resourceMetricProcessor) Shutdown(context.Context) error {
	return nil
}

// ConsumeMetricsData implements the MetricsProcessor interface
func (rmp *resourceMetricProcessor) ConsumeMetricsData(ctx context.Context, md consumerdata.MetricsData) error {
	return rmp.next.ConsumeMetricsData(ctx, consumerdata.MetricsData{
		Node:     md.Node,
		Resource: mergeResource(md.Resource, rmp.resource),
		Metrics:  md.Metrics,
	})
}

func createResource(cfg *Config) *resourcepb.Resource {
	rpb := &resourcepb.Resource{
		Type:   cfg.ResourceType,
		Labels: map[string]string{},
	}
	for k, v := range cfg.Labels {
		rpb.Labels[k] = v
	}
	return rpb
}

func mergeResource(to, from *resourcepb.Resource) *resourcepb.Resource {
	if isEmptyResource(from) {
		return to
	}
	if to == nil {
		if from.Type == "" {
			// Since resource without type would be invalid, we keep resource as nil
			return nil
		}
		to = &resourcepb.Resource{Labels: map[string]string{}}
	}
	if from.Type != "" {
		// Only change resource type if it was configured
		to.Type = from.Type
	}
	if from.Labels != nil {
		if to.Labels == nil {
			to.Labels = make(map[string]string, len(from.Labels))
		}

		for k, v := range from.Labels {
			to.Labels[k] = v
		}
	}
	return to
}

func isEmptyResource(resource *resourcepb.Resource) bool {
	return resource.Type == "" && len(resource.Labels) == 0
}
