using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace EonaCat.Logger.EonaCatCoreLogger;

[ProviderAlias("EonaCatFileLogger")]
public sealed class FileLoggerProvider : BatchingLoggerProvider
{
    private readonly string _path;
    private readonly string _fileNamePrefix;
    private readonly int _maxFileSize;
    private readonly int _maxRetainedFiles;
    private readonly int _maxRolloverFiles;

    private string _logFile;
    private long _currentFileSize;

    private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
    private readonly ConcurrentDictionary<string, long> _fileSizes = new();
    private readonly SemaphoreSlim _writeLock = new(1, 1);
    private readonly SemaphoreSlim _rolloverLock = new(1, 1);

    private readonly LoggerScopedContext _context = new();

    public string LogFile => _logFile ?? string.Empty;

    public event EventHandler<ErrorMessage> OnError;

    public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
    {
        var o = options.Value;
        _path = o.LogDirectory;
        _fileNamePrefix = o.FileNamePrefix;
        _maxFileSize = o.FileSizeLimit;
        _maxRetainedFiles = o.RetainedFileCountLimit;
        _maxRolloverFiles = o.MaxRolloverFiles;
        IncludeCorrelationId = o.IncludeCorrelationId;

        Directory.CreateDirectory(_path);
        InitializeCurrentFile();
    }

    public bool IncludeCorrelationId { get; }

    internal override async Task WriteMessagesAsync(
        IReadOnlyList<LogMessage> messages,
        CancellationToken token)
    {
        if (messages.Count == 0)
        {
            return;
        }

        Directory.CreateDirectory(_path);

        // Group messages by date
        foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day)))
        {
            var file = GetFullName(group.Key);
            InitializeFile(file);

            var stringBuilder = _buffers.GetOrAdd(file, _ => new StringBuilder(4096));

            lock (stringBuilder)
            {
                foreach (var message in group)
                {
                    AppendMessage(stringBuilder, message);
                }
            }

            await FlushAsync(file, stringBuilder, token).ConfigureAwait(false);
            DeleteOldLogFiles();
        }
    }

    private void AppendMessage(StringBuilder sb, LogMessage msg)
    {
        // Ensure correlation id exists (once per async context)
        if (IncludeCorrelationId)
        {
            var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
            _context.Set("CorrelationId", cid);
        }

        sb.Append(msg.Message);

        var ctx = _context.GetAll();
        if (ctx.Count > 0)
        {
            sb.Append(" [");
            bool first = true;
            foreach (var kv in ctx)
            {
                if (!first)
                {
                    sb.Append(' ');
                }

                sb.Append(kv.Key).Append('=').Append(kv.Value);
                first = false;
            }
            sb.Append(']');
        }

        sb.AppendLine();
    }

    public void InitializeCurrentFile()
    {
        if (!string.IsNullOrEmpty(_logFile))
        {
            return;
        }

        _logFile = GetFullName((DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day));
        _currentFileSize = File.Exists(_logFile) ? new FileInfo(_logFile).Length : 0;
    }

    private async Task FlushAsync(string file, StringBuilder stringBuilder, CancellationToken token)
    {
        if (stringBuilder.Length == 0)
        {
            return;
        }

        await _writeLock.WaitAsync(token).ConfigureAwait(false);
        try
        {
            var text = stringBuilder.ToString();
            byte[] bytesToWrite = Encoding.UTF8.GetBytes(text);

            using (var fileStream = new FileStream(
                file,
                FileMode.Append,
                FileAccess.Write,
                FileShare.Read,
                64 * 1024,
                useAsync: true))
            {
                await fileStream.WriteAsync(bytesToWrite, 0, bytesToWrite.Length, token);
            }

            // Update current file size correctly
            _fileSizes.AddOrUpdate(file, bytesToWrite.Length, (_, old) => old + bytesToWrite.Length);
            _currentFileSize = _fileSizes[file];

            stringBuilder.Clear();

            if (_fileSizes[file] >= _maxFileSize)
            {
                await RollOverAsync(file).ConfigureAwait(false);
            }
        }
        finally
        {
            _writeLock.Release();
        }
    }

    private async Task RollOverAsync(string file)
    {
        await _rolloverLock.WaitAsync().ConfigureAwait(false);
        try
        {
            var dir = Path.GetDirectoryName(file);
            var name = Path.GetFileNameWithoutExtension(file);
            var ext = Path.GetExtension(file);

            for (int i = _maxRolloverFiles - 1; i >= 1; i--)
            {
                var src = Path.Combine(dir, $"{name}.{i}{ext}");
                var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}");

                if (File.Exists(dst))
                {
                    File.Delete(dst);
                }

                if (File.Exists(src))
                {
                    File.Move(src, dst);
                }
            }

            var firstRoll = Path.Combine(dir, $"{name}.1{ext}");
            if (File.Exists(file))
            {
                File.Move(file, firstRoll);
            }

            _fileSizes[file] = 0;
        }
        finally
        {
            _rolloverLock.Release();
        }
    }

    private void InitializeFile(string file)
    {
        _fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0);
        _buffers.TryAdd(file, new StringBuilder(4096));
    }

    private (int Year, int Month, int Day) GetGrouping(LogMessage m) =>
        (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day);

    private string GetFullName((int Year, int Month, int Day) g) =>
        string.IsNullOrWhiteSpace(_fileNamePrefix)
            ? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log")
            : Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");

    private void DeleteOldLogFiles()
    {
        if (_maxRetainedFiles <= 0)
        {
            return;
        }

        var files = new DirectoryInfo(_path)
            .GetFiles($"{_fileNamePrefix}*")
            .OrderByDescending(f =>
            {
                // Parse date from filename instead of CreationTimeUtc
                var name = Path.GetFileNameWithoutExtension(f.Name);
                var parts = name.Split('_');
                var datePart = parts.Length > 1 ? parts[1] : parts[0];
                if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt))
                {
                    return dt;
                }

                return DateTime.MinValue;
            })
            .Skip(_maxRetainedFiles);

        foreach (var f in files)
        {
            try { f.Delete(); } catch { }
        }
    }
}
