// <copyright file="InstrumentationWithActivitySource.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.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Examples.Console
{
    internal class InstrumentationWithActivitySource : IDisposable
    {
        private const string RequestPath = "/api/request";
        private readonly SampleServer server = new();
        private readonly SampleClient client = new();

        public void Start(ushort port = 19999)
        {
            var url = $"http://localhost:{port.ToString(CultureInfo.InvariantCulture)}{RequestPath}/";
            this.server.Start(url);
            this.client.Start(url);
        }

        public void Dispose()
        {
            this.client.Dispose();
            this.server.Dispose();
        }

        private class SampleServer : IDisposable
        {
            private readonly HttpListener listener = new();

            public void Start(string url)
            {
                this.listener.Prefixes.Add(url);
                this.listener.Start();

                Task.Run(() =>
                {
                    using var source = new ActivitySource("Samples.SampleServer");

                    while (this.listener.IsListening)
                    {
                        try
                        {
                            var context = this.listener.GetContext();

                            using var activity = source.StartActivity(
                                $"{context.Request.HttpMethod}:{context.Request.Url.AbsolutePath}",
                                ActivityKind.Server);

                            var headerKeys = context.Request.Headers.AllKeys;
                            foreach (var headerKey in headerKeys)
                            {
                                string headerValue = context.Request.Headers[headerKey];
                                activity?.SetTag($"http.header.{headerKey}", headerValue);
                            }

                            string requestContent;
                            using (var childSpan = source.StartActivity("ReadStream", ActivityKind.Consumer))
                            using (var reader = new StreamReader(context.Request.InputStream, context.Request.ContentEncoding))
                            {
                                requestContent = reader.ReadToEnd();
                                childSpan.AddEvent(new ActivityEvent("StreamReader.ReadToEnd"));
                            }

                            activity?.SetTag("request.content", requestContent);
                            activity?.SetTag("request.length", requestContent.Length.ToString());

                            var echo = Encoding.UTF8.GetBytes("echo: " + requestContent);
                            context.Response.ContentEncoding = Encoding.UTF8;
                            context.Response.ContentLength64 = echo.Length;
                            context.Response.OutputStream.Write(echo, 0, echo.Length);
                            context.Response.Close();
                        }
                        catch (Exception)
                        {
                            // expected when closing the listener.
                        }
                    }
                });
            }

            public void Dispose()
            {
                ((IDisposable)this.listener).Dispose();
            }
        }

        private class SampleClient : IDisposable
        {
            private CancellationTokenSource cts;
            private Task requestTask;

            public void Start(string url)
            {
                this.cts = new CancellationTokenSource();
                var cancellationToken = this.cts.Token;

                this.requestTask = Task.Run(
                    async () =>
                    {
                        using var source = new ActivitySource("Samples.SampleClient");
                        using var client = new HttpClient();

                        var count = 1;
                        while (!cancellationToken.IsCancellationRequested)
                        {
                            var content = new StringContent($"client message: {DateTime.Now}", Encoding.UTF8);

                            using (var activity = source.StartActivity("POST:" + RequestPath, ActivityKind.Client))
                            {
                                count++;

                                activity?.AddEvent(new ActivityEvent("PostAsync:Started"));
                                using var response = await client.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
                                activity?.AddEvent(new ActivityEvent("PostAsync:Ended"));

                                activity?.SetTag("http.status_code", (int)response.StatusCode);

                                var responseContent = await response.Content.ReadAsStringAsync();
                                activity?.SetTag("response.content", responseContent);
                                activity?.SetTag("response.length", responseContent.Length.ToString(CultureInfo.InvariantCulture));

                                foreach (var header in response.Headers)
                                {
                                    if (header.Value is IEnumerable<object> enumerable)
                                    {
                                        activity?.SetTag($"http.header.{header.Key}", string.Join(",", enumerable));
                                    }
                                    else
                                    {
                                        activity?.SetTag($"http.header.{header.Key}", header.Value.ToString());
                                    }
                                }
                            }

                            try
                            {
                                await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
                            }
                            catch (TaskCanceledException)
                            {
                                return;
                            }
                        }
                    },
                    cancellationToken);
            }

            public void Dispose()
            {
                if (this.cts != null)
                {
                    this.cts.Cancel();
                    this.requestTask.Wait();
                    this.requestTask.Dispose();
                    this.cts.Dispose();
                }
            }
        }
    }
}
