// <copyright file="ZipkinActivityConversionExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using OpenTelemetry.Internal;
using OpenTelemetry.Trace;

namespace OpenTelemetry.Exporter.Zipkin.Implementation
{
    internal static class ZipkinActivityConversionExtensions
    {
        internal const string ZipkinErrorFlagTagName = "error";
        private const long TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
        private const long UnixEpochTicks = 621355968000000000L; // = DateTimeOffset.FromUnixTimeMilliseconds(0).Ticks
        private const long UnixEpochMicroseconds = UnixEpochTicks / TicksPerMicrosecond;

        private static readonly ConcurrentDictionary<(string, int), ZipkinEndpoint> RemoteEndpointCache = new();

        internal static ZipkinSpan ToZipkinSpan(this Activity activity, ZipkinEndpoint localEndpoint, bool useShortTraceIds = false)
        {
            var context = activity.Context;

            string parentId = activity.ParentSpanId == default ?
                null
                : EncodeSpanId(activity.ParentSpanId);

            var tagState = new TagEnumerationState
            {
                Tags = PooledList<KeyValuePair<string, object>>.Create(),
            };

            tagState.EnumerateTags(activity);

            // When status is set on Activity using the native Status field in activity,
            // which was first introduced in System.Diagnostic.DiagnosticSource 6.0.0.
            if (activity.Status != ActivityStatusCode.Unset)
            {
                if (activity.Status == ActivityStatusCode.Ok)
                {
                    PooledList<KeyValuePair<string, object>>.Add(
                    ref tagState.Tags,
                    new KeyValuePair<string, object>(
                        SpanAttributeConstants.StatusCodeKey,
                        "OK"));
                }

                // activity.Status is Error
                else
                {
                    PooledList<KeyValuePair<string, object>>.Add(
                        ref tagState.Tags,
                        new KeyValuePair<string, object>(
                            SpanAttributeConstants.StatusCodeKey,
                            "ERROR"));

                    // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status
                    PooledList<KeyValuePair<string, object>>.Add(
                        ref tagState.Tags,
                        new KeyValuePair<string, object>(
                            ZipkinErrorFlagTagName,
                            activity.StatusDescription ?? string.Empty));
                }
            }

            // In the case when both activity status and status tag were set,
            // activity status takes precedence over status tag.
            else if (tagState.StatusCode.HasValue && tagState.StatusCode != StatusCode.Unset)
            {
                PooledList<KeyValuePair<string, object>>.Add(
                    ref tagState.Tags,
                    new KeyValuePair<string, object>(
                        SpanAttributeConstants.StatusCodeKey,
                        StatusHelper.GetTagValueForStatusCode(tagState.StatusCode.Value)));

                // Error flag rule from https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status
                if (tagState.StatusCode == StatusCode.Error)
                {
                    PooledList<KeyValuePair<string, object>>.Add(
                        ref tagState.Tags,
                        new KeyValuePair<string, object>(
                            ZipkinErrorFlagTagName,
                            tagState.StatusDescription ?? string.Empty));
                }
            }

            var activitySource = activity.Source;
            if (!string.IsNullOrEmpty(activitySource.Name))
            {
                PooledList<KeyValuePair<string, object>>.Add(ref tagState.Tags, new KeyValuePair<string, object>("otel.library.name", activitySource.Name));
                if (!string.IsNullOrEmpty(activitySource.Version))
                {
                    PooledList<KeyValuePair<string, object>>.Add(ref tagState.Tags, new KeyValuePair<string, object>("otel.library.version", activitySource.Version));
                }
            }

            ZipkinEndpoint remoteEndpoint = null;
            if (activity.Kind == ActivityKind.Client || activity.Kind == ActivityKind.Producer)
            {
                PeerServiceResolver.Resolve(ref tagState, out string peerServiceName, out bool addAsTag);

                if (peerServiceName != null)
                {
                    remoteEndpoint = RemoteEndpointCache.GetOrAdd((peerServiceName, default), ZipkinEndpoint.Create);
                    if (addAsTag)
                    {
                        PooledList<KeyValuePair<string, object>>.Add(ref tagState.Tags, new KeyValuePair<string, object>(SemanticConventions.AttributePeerService, peerServiceName));
                    }
                }
            }

            EventEnumerationState eventState = default;
            eventState.EnumerateEvents(activity);

            return new ZipkinSpan(
                EncodeTraceId(context.TraceId, useShortTraceIds),
                parentId,
                EncodeSpanId(context.SpanId),
                ToActivityKind(activity),
                activity.DisplayName,
                activity.StartTimeUtc.ToEpochMicroseconds(),
                duration: activity.Duration.ToEpochMicroseconds(),
                localEndpoint,
                remoteEndpoint,
                eventState.Annotations,
                tagState.Tags,
                null,
                null);
        }

        internal static string EncodeSpanId(ActivitySpanId spanId)
        {
            return spanId.ToHexString();
        }

        internal static long ToEpochMicroseconds(this DateTimeOffset dateTimeOffset)
        {
            // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid
            // the last digit being off by one for dates that result in negative Unix times
            long microseconds = dateTimeOffset.Ticks / TicksPerMicrosecond;
            return microseconds - UnixEpochMicroseconds;
        }

        internal static long ToEpochMicroseconds(this TimeSpan timeSpan)
        {
            return timeSpan.Ticks / TicksPerMicrosecond;
        }

        internal static long ToEpochMicroseconds(this DateTime utcDateTime)
        {
            // Truncate sub-microsecond precision before offsetting by the Unix Epoch to avoid
            // the last digit being off by one for dates that result in negative Unix times
            long microseconds = utcDateTime.Ticks / TicksPerMicrosecond;
            return microseconds - UnixEpochMicroseconds;
        }

        private static string EncodeTraceId(ActivityTraceId traceId, bool useShortTraceIds)
        {
            var id = traceId.ToHexString();

            if (id.Length > 16 && useShortTraceIds)
            {
                id = id.Substring(id.Length - 16, 16);
            }

            return id;
        }

        private static string ToActivityKind(Activity activity)
        {
            return activity.Kind switch
            {
                ActivityKind.Server => "SERVER",
                ActivityKind.Producer => "PRODUCER",
                ActivityKind.Consumer => "CONSUMER",
                ActivityKind.Client => "CLIENT",
                _ => null,
            };
        }

        internal struct TagEnumerationState : PeerServiceResolver.IPeerServiceState
        {
            public PooledList<KeyValuePair<string, object>> Tags;

            public string PeerService { get; set; }

            public int? PeerServicePriority { get; set; }

            public string HostName { get; set; }

            public string IpAddress { get; set; }

            public long Port { get; set; }

            public StatusCode? StatusCode { get; set; }

            public string StatusDescription { get; set; }

            public void EnumerateTags(Activity activity)
            {
                foreach (ref readonly var tag in activity.EnumerateTagObjects())
                {
                    if (tag.Value == null)
                    {
                        continue;
                    }

                    string key = tag.Key;

                    if (tag.Value is string strVal)
                    {
                        PeerServiceResolver.InspectTag(ref this, key, strVal);

                        if (key == SpanAttributeConstants.StatusCodeKey)
                        {
                            this.StatusCode = StatusHelper.GetStatusCodeForTagValue(strVal);
                            continue;
                        }
                        else if (key == SpanAttributeConstants.StatusDescriptionKey)
                        {
                            // Description is sent as `error` but only if StatusCode is Error. See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk_exporters/zipkin.md#status
                            this.StatusDescription = strVal;
                            continue;
                        }
                        else if (key == ZipkinErrorFlagTagName)
                        {
                            // Ignore `error` tag if it exists, it will be added based on StatusCode + StatusDescription.
                            continue;
                        }
                    }
                    else if (tag.Value is int intVal && tag.Key == SemanticConventions.AttributeNetPeerPort)
                    {
                        PeerServiceResolver.InspectTag(ref this, key, intVal);
                    }

                    PooledList<KeyValuePair<string, object>>.Add(ref this.Tags, tag);
                }
            }
        }

        private struct EventEnumerationState
        {
            public bool Created;

            public PooledList<ZipkinAnnotation> Annotations;

            public void EnumerateEvents(Activity activity)
            {
                var enumerator = activity.EnumerateEvents();

                if (enumerator.MoveNext())
                {
                    this.Annotations = PooledList<ZipkinAnnotation>.Create();
                    this.Created = true;

                    do
                    {
                        ref readonly var @event = ref enumerator.Current;

                        PooledList<ZipkinAnnotation>.Add(ref this.Annotations, new ZipkinAnnotation(@event.Timestamp.ToEpochMicroseconds(), @event.Name));
                    }
                    while (enumerator.MoveNext());
                }
            }
        }
    }
}
