using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace EonaCat.Logger.EonaCatCoreLogger
{
    public sealed class XmlFileLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly XmlFileLoggerOptions _options;
        private readonly string _filePath;
        private readonly LoggerScopedContext _context = new();
        private readonly SemaphoreSlim _fileLock = new(1, 1);

        public bool IncludeCorrelationId { get; set; }
        public event EventHandler<Exception> OnException;

        public XmlFileLogger(string categoryName, XmlFileLoggerOptions options)
        {
            _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            _filePath = Path.Combine(_options.LogDirectory, _options.FileName);
            IncludeCorrelationId = options.IncludeCorrelationId;

            try
            {
                Directory.CreateDirectory(_options.LogDirectory);
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        public void SetContext(string key, string value) => _context.Set(key, value);
        public void ClearContext() => _context.Clear();
        public string GetContext(string key) => _context.Get(key);
        public IDisposable BeginScope<TState>(TState state) => _context.BeginScope(state);
        public bool IsEnabled(LogLevel logLevel) => _options.IsEnabled;

        public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
            Exception exception, Func<TState, Exception, string> formatter)
        {
            if (!IsEnabled(logLevel) || formatter == null)
            {
                return;
            }

            _ = LogAsync(logLevel, state, exception, formatter);
        }

        private async Task LogAsync<TState>(LogLevel logLevel, TState state, Exception exception, Func<TState, Exception, string> formatter)
        {
            try
            {
                if (IncludeCorrelationId)
                {
                    var correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
                    _context.Set("CorrelationId", correlationId);
                }

                var logElement = new XElement("log",
                    new XElement("timestamp", DateTime.UtcNow.ToString("o")),
                    new XElement("level", logLevel.ToString()),
                    new XElement("category", _categoryName),
                    new XElement("message", formatter(state, exception))
                );

                var contextData = _context.GetAll();
                if (contextData.Count > 0)
                {
                    var contextElement = new XElement("context");
                    foreach (var kv in contextData)
                    {
                        contextElement.Add(new XElement(kv.Key, kv.Value));
                    }

                    logElement.Add(contextElement);
                }

                if (exception != null)
                {
                    logElement.Add(new XElement("exception", exception.ToString()));
                }

                string logString = logElement + Environment.NewLine;

                await _fileLock.WaitAsync().ConfigureAwait(false);
                
                try
                {
                    using (var fs = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, true))
                    using (var sw = new StreamWriter(fs, Encoding.UTF8))
                    {
                        await sw.WriteAsync(logString).ConfigureAwait(false);
                    }
                }
                finally
                {
                    _fileLock.Release();
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        public void Dispose() => _fileLock.Dispose();
    }
}
