using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Extensions;
using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.Extensions;
using EonaCat.Logger.Servers.GrayLog;
using EonaCat.Logger.Servers.Splunk;
using EonaCat.Logger.Servers.Syslog;
using EonaCat.Logger.Servers.Tcp;
using EonaCat.Logger.Servers.Udp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace EonaCat.Logger.Managers
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    public class LogManager : ILogManager, IDisposable
    {
        private static readonly Lazy<LogManager> _instance = new(() => new LogManager(CreateDefaultSettings()));
        private readonly CancellationTokenSource _tokenSource = new();
        private DateTime _logDate;
        private bool _isDisposing;
        private string _category;

        public LogManager(LoggerSettings settings)
        {
            Settings = settings;
            SetupLogManager();
        }

        public LogManager(LoggerSettings settings, string category = null)
        {
            _category = category;
            if (string.IsNullOrWhiteSpace(category))
            {
                _category = "General";
            }
            Settings = settings;
            SetupFileLogger(settings);
            SetupLogManager();
            LogHelper.OnException += LogHelper_OnException;
            LogHelper.OnLogLevelDisabled += LogHelper_OnLogLevelDisabled;
        }

        private void LogHelper_OnLogLevelDisabled(object sender, ErrorMessage e)
        {
            OnLogLevelDisabled?.Invoke(sender, e);
        }

        private DateTime CurrentDateTime => Settings.UseLocalTime ? DateTime.Now : DateTime.UtcNow;
        public ILoggerProvider LoggerProvider { get; private set; }
        public ILoggerFactory LoggerFactory { get; private set; }
        public ILogger Logger { get; private set; }

        public string CurrentLogFile => LoggerProvider is FileLoggerProvider fileLoggerProvider
            ? fileLoggerProvider.LogFile
            : string.Empty;

        public bool IsRunning { get; private set; }

        public static LogManager Instance => _instance.Value;

        public LoggerSettings Settings { get; set; } = CreateDefaultSettings();

        public void Dispose()
        {
            DisposeAsync(true).GetAwaiter().GetResult();
            GC.SuppressFinalize(this);
        }

        public async Task WriteAsync(Exception exception, string module = null, string method = null,
            bool criticalException = false,
            bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null,
            string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false)
        {
            if (exception == null)
            {
                return;
            }

            await WriteAsync(exception.FormatExceptionToMessage(module, method),
                criticalException ? ELogType.CRITICAL : ELogType.ERROR, writeToConsole, 
                customSplunkSourceType, grayLogFacility, grayLogSource,
                grayLogVersion, disableSplunkSSL = false);
        }

        public async Task WriteAsync(string message, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
            string customSplunkSourceType = null,
            string grayLogFacility = null, string grayLogSource = null,
            string grayLogVersion = "1.1", bool disableSplunkSSL = false)
        {
            if (logType == ELogType.NONE)
            {
                return;
            }

            await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole, 
                customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL);
        }

        /// <summary>
        /// Gets fired when an exception occurs during logging
        /// </summary>
        public event EventHandler<ErrorMessage> OnException;

        /// <summary>
        /// Gets fired when the log level is disabled and the user tries to log a message
        /// </summary>
        public event EventHandler<ErrorMessage> OnLogLevelDisabled;

        private static LoggerSettings CreateDefaultSettings()
        {
            var settings = new LoggerSettings
            {
                Id = DllInfo.ApplicationName,
            };
            return settings;
        }

        protected virtual async Task DisposeAsync(bool disposing)
        {
            if (disposing)
            {
                _isDisposing = true;
                await StopLoggingAsync();
                await Task.Delay(100);
            }
        }

        public async Task StartNewLogAsync()
        {
            if (_tokenSource.IsCancellationRequested)
            {
                return;
            }

            if (IsRunning && CurrentDateTime.Date > _logDate.Date)
            {
                await StopLoggingAsync();
            }

            IsRunning = true;

            CreateLogger();

            Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);

            _logDate = CurrentDateTime;
        }

        private void CreateLogger()
        {
            // Dispose of previous ServiceProvider if it exists
            LoggerProvider?.Dispose();
            LoggerFactory?.Dispose();

            IServiceCollection serviceCollection = new ServiceCollection();

            if (Settings.TypesToLog == null)
            {
                Settings.TypesToLog = new List<ELogType> { ELogType.INFO, ELogType.WARNING, ELogType.ERROR, ELogType.TRAFFIC, ELogType.DEBUG, ELogType.CRITICAL, ELogType.TRACE };
            }

            serviceCollection.AddLogging(builder => builder.SetMinimumLevel(Settings.TypesToLog.Max().ToLogLevel())
                .AddEonaCatFileLogger(configuration =>
                {
                    var fileLoggerOptions = Settings.FileLoggerOptions;
                    configuration.MaxWriteTries = fileLoggerOptions.MaxWriteTries;
                    configuration.RetainedFileCountLimit = fileLoggerOptions.RetainedFileCountLimit;
                    configuration.FlushPeriod = fileLoggerOptions.FlushPeriod;
                    configuration.IsEnabled = fileLoggerOptions.IsEnabled;
                    configuration.BatchSize = fileLoggerOptions.BatchSize;
                    configuration.FileSizeLimit = fileLoggerOptions.FileSizeLimit;
                    configuration.LogDirectory = fileLoggerOptions.LogDirectory;
                    configuration.FileNamePrefix = fileLoggerOptions.FileNamePrefix;
                    configuration.MaxRolloverFiles = fileLoggerOptions.MaxRolloverFiles;
                    configuration.UseLocalTime = Settings.UseLocalTime;
                    configuration.UseMask = Settings.UseMask;
                    configuration.Mask = fileLoggerOptions.Mask;
                    configuration.UseDefaultMasking = Settings.UseDefaultMasking;
                    configuration.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
                }));

            var serviceProvider = serviceCollection.BuildServiceProvider();
            LoggerProvider = serviceProvider.GetService<ILoggerProvider>();
            LoggerFactory = serviceProvider.GetService<ILoggerFactory>();
            Logger = LoggerFactory.CreateLogger(Settings.Id);
            LogHelper.SendToFile(Logger, Settings, ELogType.INFO, LogHelper.GetStartupMessage());
        }

        private async Task InternalWriteAsync(DateTime dateTime, string message, ELogType logType = ELogType.INFO,
    bool? writeToConsole = null, string customSplunkSourceType = null, string grayLogFacility = null,
    string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false)
        {
            if (string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE || !IsLogLevelEnabled(logType) || _isDisposing)
            {
                return;
            }

            if (!IsRunning)
            {
                await StartNewLogAsync().ConfigureAwait(false);
            }

            var messageWithHeader = LogHelper.FormatMessageWithHeader(Settings, logType, message, dateTime, _category);
            var writeToConsoleValue = writeToConsole.GetValueOrDefault(Settings.EnableConsole);

            LogHelper.SendToFile(Logger, Settings, logType, message);

            if (writeToConsoleValue)
            {
                LogHelper.SendToConsole(Settings, logType, messageWithHeader);
            }

            var tasks = new List<Task>(5)
            {
                LogHelper.SendToSysLogServersAsync(Settings, message, logType, dateTime, _category),
                LogHelper.SendToSplunkServersAsync(Settings, customSplunkSourceType ?? logType.ToString(), messageWithHeader, disableSplunkSSL),
                LogHelper.SendToGrayLogServersAsync(Settings, messageWithHeader, logType, grayLogFacility, grayLogSource, grayLogVersion),
                LogHelper.SendToTcpLogServersAsync(Settings, messageWithHeader, logType),
                LogHelper.SendToUdpLogServersAsync(Settings, messageWithHeader, logType),
            };

            if (tasks.Count > 0)
            {
                await Task.WhenAll(tasks).ConfigureAwait(false);
            }

            var logMessage = new EonaCatLogMessage
            {
                DateTime = dateTime,
                Message = message,
                LogType = logType,
                Origin = string.IsNullOrEmpty(Settings.LogOrigin) ? "LogManager" : Settings.LogOrigin
            };
            Settings.OnLogEvent(logMessage);
        }

        private bool IsLogLevelEnabled(ELogType logType)
        {
            if (Settings == null)
            {
                OnLogLevelDisabled?.Invoke(this, new ErrorMessage { Message = "Settings is null." });
                return false;
            }

            if (logType == ELogType.NONE)
            {
                return false;
            }

            var isEnabled = (Settings.TypesToLog == null || Settings.TypesToLog.Count == 0 || Settings.TypesToLog.Contains(logType));
            if (!isEnabled)
            {
                return false;
            }
            return true;
        }

        public void Reset()
        {
            Settings.ResetLogEvent();
        }

        private void LogHelper_OnException(object sender, ErrorMessage e)
        {
            OnException?.Invoke(sender, e);
        }

        private void SetupFileLogger(LoggerSettings settings = null, string logFolder = null, bool defaultPrefix = true)
        {
            if (settings == null)
            {
                settings = Settings;
            }

            if (!settings.EnableFileLogging)
            {
                return;
            }

            if (logFolder != null)
            {
                settings.FileLoggerOptions.LogDirectory = logFolder;
            }

            if (string.IsNullOrWhiteSpace(settings.FileLoggerOptions.FileNamePrefix))
            {
                settings.FileLoggerOptions.FileNamePrefix = defaultPrefix ? "EonaCat" : string.Empty;
            }
        }

        private void SetupLogManager()
        {
            _logDate = CurrentDateTime;
        }

        private Task StopLoggingAsync()
        {
            WriteStopMessage();
            IsRunning = false;
            return Task.CompletedTask;
        }

        private void WriteStopMessage()
        {
            var stopMessage = $"{DllInfo.ApplicationName} stopped.{Environment.NewLine}";
            LogHelper.SendToFile(Logger, Settings, ELogType.INFO, stopMessage);
        }

        public void DeleteCurrentLogFile()
        {
            if (!string.IsNullOrEmpty(CurrentLogFile))
            {
                File.Delete(CurrentLogFile);
            }
        }

        /// <summary>
        /// Add Syslog server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <param name="nickName"></param>
        /// <param name="typesToLog"></param>
        /// <param name="convertToRfc5424"></param>
        /// <param name="convertToRfc3164"></param>
        /// <returns></returns>
        public bool AddSyslogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
        {
            if (Settings.SysLogServers == null)
            {
                Settings.SysLogServers = new List<Servers.Syslog.Syslog>();
            }

            if (Settings.SysLogServers.Any(x => (nickName != null && x.Nickname == nickName) || nickName == null && x.Hostname == address && x.Port == port))
            {
                return false;
            }

            Settings.SysLogServers.Add(new Syslog(address, port, nickName, typesToLog, convertToRfc5424, convertToRfc3164));
            return true;
        }

        /// <summary>
        /// Remove the sysLog server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool RemoveSyslogServer(string address, int port)
        {
            if (Settings.SysLogServers == null)
            {
                return false;
            }

            var delete = Settings.SysLogServers.FirstOrDefault(x => x.Hostname == address && x.Port == port);
            if (delete != null)
            {
                Settings.SysLogServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Remove the sysLog server
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public bool RemoveSyslogServer(string nickName)
        {
            if (Settings.SysLogServers == null)
            {
                return false;
            }

            var delete = Settings.SysLogServers.FirstOrDefault(x => x.Nickname == nickName);
            if (delete != null)
            {
                Settings.SysLogServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Add Splunk server
        /// </summary>
        /// <param name="splunkHecUrl"></param>
        /// <param name="splunkHecToken"></param>
        /// <param name="httpClientHandler"></param>
        /// <param name="nickName"></param>
        /// <param name="typesToLog"></param>
        /// <returns></returns>
        public bool AddSplunkServer(string splunkHecUrl, string splunkHecToken, HttpClientHandler httpClientHandler = null, string nickName = null, List<ELogType> typesToLog = null)
        {
            if (Settings.SplunkServers == null)
            {
                Settings.SplunkServers = new List<Logger.Servers.Splunk.Splunk>();
            }

            if (Settings.SplunkServers.Any(x => (nickName != null && x.Nickname == nickName) || nickName == null && x.SplunkHecUrl == splunkHecUrl && x.SplunkHecToken == splunkHecToken))
            {
                return false;
            }

            Settings.SplunkServers.Add(new Splunk(splunkHecUrl, splunkHecToken, httpClientHandler, nickName, typesToLog));
            return true;
        }

        /// <summary>
        /// Remove the Splunk server
        /// </summary>
        /// <param name="splunkHecUrl"></param>
        /// <param name="splunkHecToken"></param>
        /// <returns></returns>
        public bool RemoveSplunkServer(string splunkHecUrl, string splunkHecToken)
        {
            if (Settings.SplunkServers == null)
            {
                return false;
            }

            var delete = Settings.SplunkServers.FirstOrDefault(x => x.SplunkHecUrl == splunkHecUrl && x.SplunkHecToken == splunkHecToken);
            if (delete != null)
            {
                Settings.SplunkServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Remove the Splunk server
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public bool RemoveSplunkServer(string nickName)
        {
            if (Settings.SplunkServers == null)
            {
                return false;
            }

            var delete = Settings.SplunkServers.FirstOrDefault(x => x.Nickname == nickName);
            if (delete != null)
            {
                Settings.SplunkServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Add Graylog server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <param name="nickName"></param>
        /// <param name="typesToLog"></param>
        /// <returns></returns>
        public bool AddGrayLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null)
        {
            if (Settings.GrayLogServers == null)
            {
                Settings.GrayLogServers = new List<Graylog>();
            }

            if (Settings.GrayLogServers.Any(x => (nickName != null && x.Nickname == nickName) || nickName == null && x.Hostname == address && x.Port == port))
            {
                return false;
            }

            Settings.GrayLogServers.Add(new Graylog(address, port, nickName, typesToLog));
            return true;
        }

        /// <summary>
        /// Remove the GrayLog server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool RemoveGrayLogServer(string address, int port)
        {
            if (Settings.GrayLogServers == null)
            {
                return false;
            }

            var delete = Settings.GrayLogServers.FirstOrDefault(x => x.Hostname == address && x.Port == port);
            if (delete != null)
            {
                Settings.GrayLogServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Remove the GrayLog server
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public bool RemoveGrayLogServer(string nickName)
        {
            if (Settings.GrayLogServers == null)
            {
                return false;
            }

            var delete = Settings.GrayLogServers.FirstOrDefault(x => x.Nickname == nickName);
            if (delete != null)
            {
                Settings.GrayLogServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Add Tcp Server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <param name="nickName"></param>
        /// <param name="typesToLog"></param>
        /// <returns></returns>
        public bool AddTcpLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null)
        {
            if (Settings.TcpServers == null)
            {
                Settings.TcpServers = new List<Servers.Tcp.Tcp>();
            }

            if (Settings.TcpServers.Any(x => (nickName != null && x.Nickname == nickName) || (nickName == null && x.Hostname == address && x.Port == port)))
            {
                return false;
            }

            Settings.TcpServers.Add(new Tcp(address, port, nickName, typesToLog));
            return true;
        }

        /// <summary>
        /// Remove the Tcp server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool RemoveTcpLogServer(string address, int port)
        {
            if (Settings.TcpServers == null)
            {
                return false;
            }

            var delete = Settings.TcpServers.FirstOrDefault(x => x.Hostname == address && x.Port == port);
            if (delete != null)
            {
                Settings.TcpServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Remove the Tcp server
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public bool RemoveTcpLogServer(string nickName)
        {
            if (Settings.TcpServers == null)
            {
                return false;
            }

            var delete = Settings.TcpServers.FirstOrDefault(x => x.Nickname == nickName);
            if (delete != null)
            {
                Settings.TcpServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Add Udp Server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <param name="nickName"></param>
        /// <param name="typesToLog"></param>
        /// <returns></returns>
        public bool AddUdpLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null)
        {
            if (Settings.UdpServers == null)
            {
                Settings.UdpServers = new List<Servers.Udp.Udp>();
            }

            if (Settings.UdpServers.Any(x => (nickName != null && x.Nickname == nickName) || nickName == null && x.Hostname == address && x.Port == port))
            {
                return false;
            }

            Settings.UdpServers.Add(new Udp(address, port, nickName, typesToLog));
            return true;
        }

        /// <summary>
        /// Remove the Udp server
        /// </summary>
        /// <param name="address"></param>
        /// <param name="port"></param>
        /// <returns></returns>
        public bool RemoveUdpLogServer(string address, int port)
        {
            if (Settings.UdpServers == null)
            {
                return false;
            }

            var delete = Settings.UdpServers.FirstOrDefault(x => x.Hostname == address && x.Port == port);
            if (delete != null)
            {
                Settings.UdpServers.Remove(delete);
            }
            return true;
        }

        /// <summary>
        /// Remove the Udp server
        /// </summary>
        /// <param name="nickName"></param>
        /// <returns></returns>
        public bool RemoveUdpLogServer(string nickName)
        {
            if (Settings.UdpServers == null)
            {
                return false;
            }

            var delete = Settings.UdpServers.FirstOrDefault(x => x.Nickname == nickName);
            if (delete != null)
            {
                Settings.UdpServers.Remove(delete);
            }
            return true;
        }
    }
}