using EonaCat.Json;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.Logger.EonaCatCoreLogger
{
    // 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 ElasticSearchLogger : ILogger, IDisposable
    {
        private readonly string _categoryName;
        private readonly ElasticSearchLoggerOptions _options;
        private readonly HttpClient _httpClient;
        private readonly ConcurrentQueue<string> _queue = new();
        private readonly CancellationTokenSource _cts = new();
        private readonly Task _flushTask;

        private readonly LoggerScopedContext _context = new();
        public bool IncludeCorrelationId { get; set; }
        public event EventHandler<Exception> OnException;
        public event EventHandler<string> OnInvalidStatusCode;

        public ElasticSearchLogger(string categoryName, ElasticSearchLoggerOptions options)
        {
            _categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName));
            _options = options ?? throw new ArgumentNullException(nameof(options));
            IncludeCorrelationId = options.IncludeCorrelationId;

            _httpClient = new HttpClient();
            _flushTask = Task.Run(() => FlushLoopAsync(_cts.Token));
        }

        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 (!_options.IsEnabled || formatter == null)
            {
                return;
            }

            string correlationId = null;
            if (IncludeCorrelationId)
            {
                correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
                _context.Set("CorrelationId", correlationId);
            }

            var logDoc = new
            {
                timestamp = DateTime.UtcNow,
                level = logLevel.ToString(),
                category = _categoryName,
                message = formatter(state, exception),
                exception = exception?.ToString(),
                eventId = eventId.Id,
                customContext = _context.GetAll()
            };

            var json = JsonHelper.ToJson(logDoc);

            _queue.Enqueue(json);

            while (_queue.Count > _options.MaxBufferSize && _queue.TryDequeue(out _)) 
            {
                // Discard oldest log entries to maintain buffer size
            }
        }

        private async Task FlushLoopAsync(CancellationToken token)
        {
            try
            {
                while (!token.IsCancellationRequested)
                {
                    await Task.Delay(TimeSpan.FromSeconds(_options.FlushIntervalSeconds), token);
                    await FlushBufferAsync(token);
                }
            }
            catch (OperationCanceledException) 
            {
                // Do nothing
            }

            await FlushBufferAsync(token);
        }

        private async Task FlushBufferAsync(CancellationToken token)
        {
            if (_queue.IsEmpty)
            {
                return;
            }

            var sb = new StringBuilder();
            int batchSize = 0;

            while (_queue.TryDequeue(out var log))
            {
                if (_options.UseBulkInsert)
                {
                    sb.AppendLine("{\"index\":{}}");
                }
                sb.AppendLine(log);
                batchSize++;
            }

            if (batchSize == 0)
            {
                return;
            }

            string indexName = $"{_options.IndexName}-{DateTime.UtcNow:yyyy.MM.dd}";
            string url = $"{_options.Uri.TrimEnd('/')}/{(_options.UseBulkInsert ? "_bulk" : indexName + "/_doc")}";

            using var request = new HttpRequestMessage(HttpMethod.Post, url);
            request.Headers.Accept.ParseAdd("application/json");

            if (!string.IsNullOrWhiteSpace(_options.Username))
            {
                var authToken = Convert.ToBase64String(
                    Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}")
                );
                request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authToken);
            }

            foreach (var header in _options.CustomHeaders)
            {
                request.Headers.TryAddWithoutValidation(header.Key, header.Value);
            }

            // Dynamic headers
            var dynamicHeaders = new Dictionary<string, string>
            {
                { "index", _options.IndexName },
                { "date", DateTime.UtcNow.ToString("yyyy-MM-dd") },
                { "timestamp", DateTime.UtcNow.ToString("o") }
            };

            foreach (var header in _options.TemplateHeaders)
            {
                var value = ReplaceTemplate(header.Value, dynamicHeaders);
                request.Headers.TryAddWithoutValidation(header.Key, value);
            }

            foreach (var kv in _context.GetAll())
            {
                request.Headers.TryAddWithoutValidation($"X-Context-{kv.Key}", kv.Value);
            }

            request.Content = new StringContent(sb.ToString(), Encoding.UTF8, "application/json");

            try
            {
                var response = await _httpClient.SendAsync(request, token);
                if (!response.IsSuccessStatusCode)
                {
                    var errorContent = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    OnInvalidStatusCode?.Invoke(this, $"ElasticSearch request failed: {response.StatusCode}, {errorContent}");
                }
            }
            catch (Exception ex)
            {
                OnException?.Invoke(this, ex);
            }
        }

        private string ReplaceTemplate(string template, Dictionary<string, string> values)
        {
            foreach (var kv in values)
            {
                template = template.Replace($"{{{kv.Key}}}", kv.Value);
            }

            return template;
        }

        public void Dispose()
        {
            _cts.Cancel();
            try 
            {
                _flushTask.Wait(); 
            }
            catch 
            {
                // Do nothing
            }

            _cts.Dispose();
            _httpClient.Dispose();
        }
    }
}
