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;

// 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.

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

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

        public LogManager(LoggerSettings settings, string category = null) : this(settings)
        {
            _category = string.IsNullOrWhiteSpace(category) ? "General" : category;
            SetupFileLogger(settings);

            // Subscribe to static events
            LogHelper.OnException += LogHelper_OnException;
            LogHelper.OnLogLevelDisabled += LogHelper_OnLogLevelDisabled;
        }

        public static LogManager Instance => _instance.Value;

        public LoggerSettings Settings { get; private set; }
        public ILoggerProvider LoggerProvider { get; private set; }
        public ILoggerFactory LoggerFactory { get; private set; }
        public ILogger Logger { get; private set; }
        public bool IsRunning { get; private set; }

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

        private DateTime CurrentDateTime => Settings.UseLocalTime ? DateTime.Now : DateTime.UtcNow;

        public event EventHandler<ErrorMessage> OnException;
        public event EventHandler<ErrorMessage> OnLogLevelDisabled;

        public async Task WriteAsync(object currentObject, ELogType logType = ELogType.INFO, bool? writeToConsole = null,
            string customSplunkSourceType = null, string grayLogFacility = null,
            string grayLogSource = null, string grayLogVersion = "1.1", bool disableSplunkSSL = false,
            DumpFormat dumpFormat = DumpFormat.Json, bool isDetailedDump = false, int? dumpDepth = null, int? maxCollectionItems = null)
        {
            if (_isDisposing || currentObject == null)
            {
                return;
            }

            await WriteAsync(currentObject.Dump(dumpFormat, isDetailedDump, dumpDepth, maxCollectionItems),
                logType, writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
                .ConfigureAwait(false);
        }

        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 (_isDisposing || exception == null)
            {
                return;
            }

            await WriteAsync(exception.FormatExceptionToMessage(module, method),
                criticalException ? ELogType.CRITICAL : ELogType.ERROR,
                writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
                .ConfigureAwait(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 (_isDisposing || string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE)
            {
                return;
            }

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

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

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

            IsRunning = true;
            CreateLogger();
            Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);
            _logDate = CurrentDateTime;
        }


        private async Task StopLoggingAsync()
        {
            if (_isDisposing)
            {
                return;
            }

            WriteStopMessage();
            IsRunning = false;
            await Task.CompletedTask.ConfigureAwait(false);
        }

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

        private void CreateLogger()
        {
            if (_isDisposing)
            {
                return;
            }

            LoggerProvider?.Dispose();
            LoggerFactory?.Dispose();

            IServiceCollection serviceCollection = new ServiceCollection();

            if (Settings.TypesToLog == null || !Settings.TypesToLog.Any())
            {
                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(config =>
                       {
                           var options = Settings.FileLoggerOptions;
                           config.MaxWriteTries = options.MaxWriteTries;
                           config.RetainedFileCountLimit = options.RetainedFileCountLimit;
                           config.FlushPeriod = options.FlushPeriod;
                           config.IsEnabled = options.IsEnabled;
                           config.BatchSize = options.BatchSize;
                           config.FileSizeLimit = options.FileSizeLimit;
                           config.LogDirectory = options.LogDirectory;
                           config.FileNamePrefix = options.FileNamePrefix;
                           config.MaxRolloverFiles = options.MaxRolloverFiles;
                           config.UseLocalTime = Settings.UseLocalTime;
                           config.UseMask = Settings.UseMask;
                           config.Mask = options.Mask;
                           config.UseDefaultMasking = Settings.UseDefaultMasking;
                           config.MaskedKeywords = options.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 (_isDisposing || string.IsNullOrWhiteSpace(message) || logType == ELogType.NONE || !IsLogLevelEnabled(logType))
            {
                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>
            {
                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)
            };

            try
            {
                await Task.WhenAll(tasks).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, new ErrorMessage { Message = ex.Message, Exception = ex });
            }

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

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

            return logType != ELogType.NONE &&
                   (Settings.TypesToLog == null || !Settings.TypesToLog.Any() || Settings.TypesToLog.Contains(logType));
        }

        private void LogHelper_OnException(object sender, ErrorMessage e) => OnException?.Invoke(sender, e);
        private void LogHelper_OnLogLevelDisabled(object sender, ErrorMessage e) => OnLogLevelDisabled?.Invoke(sender, e);

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

        protected virtual async Task DisposeAsync(bool disposing)
        {
            if (_isDisposing)
            {
                return;
            }

            if (disposing)
            {
                _isDisposing = true;

                await StopLoggingAsync().ConfigureAwait(false);

                LogHelper.OnException -= LogHelper_OnException;
                LogHelper.OnLogLevelDisabled -= LogHelper_OnLogLevelDisabled;

                _tokenSource?.Cancel();
                _tokenSource?.Dispose();
                _tokenSource = null;

                LoggerProvider?.Dispose();
                LoggerFactory?.Dispose();
            }
        }

        public static void Shutdown()
        {
            if (_instance.IsValueCreated)
            {
                _instance.Value.Dispose();
            }
        }

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

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

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

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

        private void SetupLogManager() => _logDate = CurrentDateTime;

        private static LoggerSettings CreateDefaultSettings() => new() { Id = DllInfo.ApplicationName };

        public bool AddSyslogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = null, bool convertToRfc5424 = false, bool convertToRfc3164 = false)
        {
            Settings.SysLogServers ??= new List<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;
        }

        public bool RemoveSyslogServer(string address, int port)
        {
            if (Settings.SysLogServers == null)
            {
                return false;
            }

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

            return true;
        }

        public bool RemoveSyslogServer(string nickName)
        {
            if (Settings.SysLogServers == null)
            {
                return false;
            }

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

            return true;
        }

        // Splunk
        public bool AddSplunkServer(string splunkHecUrl, string splunkHecToken, HttpClientHandler httpClientHandler = null, string nickName = null, List<ELogType> typesToLog = null)
        {
            Settings.SplunkServers ??= new List<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;
        }

        public bool RemoveSplunkServer(string splunkHecUrl, string splunkHecToken)
        {
            if (Settings.SplunkServers == null)
            {
                return false;
            }

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

            return true;
        }

        public bool RemoveSplunkServer(string nickName)
        {
            if (Settings.SplunkServers == null)
            {
                return false;
            }

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

            return true;
        }

        // Graylog
        public bool AddGrayLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = 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;
        }

        public bool RemoveGrayLogServer(string address, int port)
        {
            if (Settings.GrayLogServers == null)
            {
                return false;
            }

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

            return true;
        }

        public bool RemoveGrayLogServer(string nickName)
        {
            if (Settings.GrayLogServers == null)
            {
                return false;
            }

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

            return true;
        }

        // TCP
        public bool AddTcpLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = 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;
        }

        public bool RemoveTcpLogServer(string address, int port)
        {
            if (Settings.TcpServers == null)
            {
                return false;
            }

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

            return true;
        }

        public bool RemoveTcpLogServer(string nickName)
        {
            if (Settings.TcpServers == null)
            {
                return false;
            }

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

            return true;
        }

        // UDP
        public bool AddUdpLogServer(string address, int port, string nickName = null, List<ELogType> typesToLog = 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;
        }

        public bool RemoveUdpLogServer(string address, int port)
        {
            if (Settings.UdpServers == null)
            {
                return false;
            }

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

            return true;
        }

        public bool RemoveUdpLogServer(string nickName)
        {
            if (Settings.UdpServers == null)
            {
                return false;
            }

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

            return true;
        }
    }
}
