namespace Hims.Api.Listeners
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Threading;
    using Microsoft.Extensions.Configuration;

    /// <summary>
    /// The daily trace listener.
    /// </summary>
    public sealed class DailyTraceListener : TraceListener
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="DailyTraceListener"/> class.
        /// </summary>
        /// <param name="configuration">
        /// The configuration.
        /// </param>
        public DailyTraceListener(IConfiguration configuration) => this.LogFolder = configuration.GetValue<string>("Directories:ErrorLogs");

        /// <summary>
        /// Gets or sets a value indicating whether use UTC time.
        /// </summary>
        private bool UseUtcTime { get; set; }

        /// <summary>
        /// Gets or sets the log folder.
        /// </summary>
        private string LogFolder { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether disposed.
        /// </summary>
        private bool Disposed { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether has header.
        /// </summary>
        private bool HasHeader { get; set; }

        /// <summary>
        /// Gets or sets the current log file path.
        /// </summary>
        private string CurrentLogFilePath { get; set; }

        /// <summary>
        /// Gets or sets the current log date.
        /// </summary>
        private DateTime? CurrentLogDate { get; set; }

        /// <summary>
        /// Gets or sets the log file stream.
        /// </summary>
        private FileStream LogFileStream { get; set; }

        /// <summary>
        /// Gets or sets the log file writer.
        /// </summary>
        private StreamWriter LogFileWriter { get; set; }

        /// <summary>
        /// Gets or sets the log locker.
        /// </summary>
        private SemaphoreSlim LogLocker { get; set; } = new SemaphoreSlim(1, 1);

        /// <summary>
        /// The use UTC.
        /// </summary>
        /// <returns>
        /// The <see cref="DailyTraceListener"/>.
        /// </returns>
        public DailyTraceListener UseUtc()
        {
            this.UseUtcTime = true;
            return this;
        }

        /// <summary>
        /// The use header.
        /// </summary>
        /// <returns>
        /// The <see cref="DailyTraceListener"/>.
        /// </returns>
        public DailyTraceListener UseHeader()
        {
            this.HasHeader = true;
            return this;
        }

        /// <summary>
        /// The invoke.
        /// </summary>
        /// <param name="from">
        /// The from.
        /// </param>
        /// <param name="route">
        /// The route.
        /// </param>
        /// <param name="message">
        /// The message.
        /// </param>
        /// <param name="description">
        /// The description.
        /// </param>
        public void Invoke(string from, string route, string message, string description)
        {
            var time = this.FormatTime(this.GetCurrentTime());
            this.WriteLine($"{time},{EscapeCsv(from)},{EscapeCsv(route)},{EscapeCsv(message)},{EscapeCsv(description)}");
        }

        /// <summary>
        /// The write.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        public override void Write(string message)
        {
            try
            {
                this.LogLocker.Wait();

                this.CheckDisposed();
                this.CheckFile();

                this.LogFileWriter.Write(message);
                this.LogFileWriter.Flush();
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
            }
            finally
            {
                this.LogLocker.Release();
            }
        }

        /// <summary>
        /// The write line.
        /// </summary>
        /// <param name="message">
        /// The message.
        /// </param>
        public override void WriteLine(string message)
        {
            this.Write(message + Environment.NewLine);
        }

        /// <summary>
        /// The dispose.
        /// </summary>
        /// <param name="disposing">
        /// The disposing.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            this.Disposed = true;

            try
            {
                this.LogFileWriter?.Dispose();
                this.LogFileStream?.Dispose();
                this.LogLocker.Dispose();
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
            }

            base.Dispose(disposing);
        }

        /// <summary>
        /// The escape CSV.
        /// </summary>
        /// <param name="input">
        /// The input.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        private static string EscapeCsv(string input)
        {
            for (var i = 0; i < input.Length; i++)
            {
                if (input[i] != ',' && input[i] != '\n')
                {
                    continue;
                }

                input = input.Replace("\"", "\"\"");
                return $"\"{input}\"";
            }

            return input;
        }

        /// <summary>
        /// The write header.
        /// </summary>
        private void WriteHeader()
        {
            this.LogFileWriter.WriteLine("Time,From,Router,Message,Description");
        }

        /// <summary>
        /// The format time.
        /// </summary>
        /// <param name="time">
        /// The time.
        /// </param>
        /// <returns>
        /// The <see cref="string"/>.
        /// </returns>
        private string FormatTime(DateTime time) => time.ToString("o");

        /// <summary>
        /// The get current time.
        /// </summary>
        /// <returns>
        /// The <see cref="DateTime"/>.
        /// </returns>
        private DateTime GetCurrentTime() => this.UseUtcTime ? DateTime.UtcNow : DateTime.Now;

        /// <summary>
        /// The initialize log file.
        /// </summary>
        private void InitializeLogFile()
        {
            this.CheckDisposed();

            try
            {
                this.LogFileWriter?.Dispose();

                if (this.LogFileStream != null)
                {
                    var logFileWriter = this.LogFileWriter;
                    logFileWriter?.Dispose();
                }
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.ToString());
            }

            var currentTime = this.GetCurrentTime();

            var fileName = $"{currentTime:yyyy-MM-dd}.log";
            this.CurrentLogFilePath = Path.Combine(this.LogFolder, fileName);

            // Ensure the folder is there
            Directory.CreateDirectory(this.LogFolder);

            // Create/Open log file
            this.LogFileStream = new FileStream(this.CurrentLogFilePath, FileMode.Append);
            this.LogFileWriter = new StreamWriter(this.LogFileStream);

            // Write Header if needed
            if (this.LogFileStream.Length == 0 && this.HasHeader)
            {
                this.WriteHeader();
            }
        }

        /// <summary>
        /// The check file.
        /// </summary>
        private void CheckFile()
        {
            this.CheckDisposed();

            var currentTime = this.GetCurrentTime();
            if (this.CurrentLogDate != null && currentTime.Date == this.CurrentLogDate)
            {
                return;
            }

            this.InitializeLogFile();
            this.CurrentLogDate = currentTime.Date;
        }

        /// <summary>
        /// The check disposed.
        /// </summary>
        private void CheckDisposed()
        {
            if (this.Disposed)
            {
                throw new InvalidOperationException("The Trace Listener is Disposed.");
            }
        }
    }
}
