using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows
{
    // 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.

    /// <summary>
    /// High-performance file sink with batching, rotation, compression, sampling,
    /// enrichment, retention policies, and pluggable secondary writers.
    /// </summary>
    public sealed class FileFlow : FlowBase
    {
        public event EventHandler<string> OnDirectoryException;
        private const int FileBufferSize = 131072; // 128 KB
        private const int WriterBufferSize = 131072; // 128 KB
        private readonly int _batchSize;
        private const int QueueCapacity = 8192;
        public bool IgnoreConsoleErrors { get; set; }

        private static readonly Dictionary<LogLevel, string> LevelStrings =
            new Dictionary<LogLevel, string>
            {
                { LogLevel.Trace,   "TRACE" },
                { LogLevel.Debug,   "DEBUG" },
                { LogLevel.Information,    "INFO"  },
                { LogLevel.Warning, "WARN"  },
                { LogLevel.Error,   "ERROR" },
                { LogLevel.Critical,   "CRITICAL" },
            };

        private static readonly char[] CsvSpecialChars = { ',', '"', '\n', '\r' };
        private static readonly string CachedMachineName = Environment.MachineName;
        private static readonly int CachedPid = Process.GetCurrentProcess().Id;

        private const string CsvHeader = "timestamp,level,category,message,exception,properties\r\n";

        private readonly BlockingCollection<LogEvent> _queue;
        private readonly ConcurrentQueue<string> _compressionQueue = new ConcurrentQueue<string>();
        private readonly CancellationTokenSource _cts = new CancellationTokenSource();
        private readonly Thread _writerThread;
        private readonly Thread _compressionThread;
        private readonly Task _flushTask;
        private readonly Task _retentionTask;
        private readonly Stopwatch _uptime = Stopwatch.StartNew();
        private readonly SemaphoreSlim _compressionSignal = new SemaphoreSlim(0, int.MaxValue);

        private readonly string _directory;
        private readonly string _filePrefix;
        private readonly long _maxFileSize;
        private readonly long _maxDirectorySize;
        private readonly FileRetentionPolicy _retention;
        private readonly TimestampMode _timestampMode;
        private readonly TimeSpan _flushInterval;
        private readonly FileOutputFormat _outputFormat;
        private readonly CompressionFormat _compressionFormat;
        private readonly string _template;
        private readonly bool _useCategoryRouting;
        private readonly HashSet<LogLevel> _logLevelsForSeparateFiles;
        private readonly long _maxMemoryBytes;

        private volatile SamplingPolicy _samplingPolicy;
        private volatile Action<LogEvent> _onDrop;
        private volatile Action<string> _onRotate;
        private List<Action<LogEvent, string>> _secondaryWriters;
        private readonly object _secondaryWritersLock = new object();
        private int _correlationSeed;

        private readonly List<Func<LogEvent, bool>> _filters = new List<Func<LogEvent, bool>>();
        private readonly object _filtersLock = new object();

        private readonly ConcurrentDictionary<string, long> _deduplicationCache
            = new ConcurrentDictionary<string, long>(StringComparer.Ordinal);
        private TimeSpan _deduplicationWindow = TimeSpan.Zero;
        private volatile bool _deduplicationEnabled;

        private string _dateFormat = "yyyyMMdd";
        private volatile Exception _lastError;
        private long _lastErrorTimestamp;
        private long _totalErrors;

        private readonly List<KeyValuePair<string, Func<LogEvent, object>>> _enrichers
            = new List<KeyValuePair<string, Func<LogEvent, object>>>();

        private long _currentMemoryBytes;

        // Rate limiting
        private int _maxEventsPerSecond;
        private bool _rateLimitEnabled => _maxEventsPerSecond > 0;
        private long _rateLimitWindowStart;
        private int _rateLimitCounter;
        private readonly object _rateLimitLock = new object();

        // Auto-flush on error
        private volatile bool _autoFlushOnError;

        // Scoped properties (AsyncLocal for ambient context)
        private static readonly AsyncLocal<Dictionary<string, object>> _scopeProperties
            = new AsyncLocal<Dictionary<string, object>>();

        private long _totalBytesWritten;
        private long _totalRotations;

        private readonly Dictionary<string, OpenFile> _openFiles
            = new Dictionary<string, OpenFile>(StringComparer.OrdinalIgnoreCase);
        private readonly object _fileLock = new object();

        private string _fileExtension = ".log";

        private List<Action<LogEvent, StringBuilder>> _compiledTemplate;
        private readonly Dictionary<string, Action<LogEvent, StringBuilder>> _customTokens
            = new Dictionary<string, Action<LogEvent, StringBuilder>>(StringComparer.OrdinalIgnoreCase);

        private sealed class OpenFile : IDisposable
        {
            public readonly FileStream Stream;
            public readonly StreamWriter Writer;
            public readonly DateTime Date;
            public long Size;
            public bool HasCsvHeader;
            public bool HasXmlHeader;

            public OpenFile(FileStream fs, StreamWriter sw, DateTime date)
            {
                Stream = fs;
                Writer = sw;
                Date = date;
                Size = fs.Length;
            }

            public void Dispose()
            {
                try { Writer.Flush(); } catch { }
                try { Writer.Dispose(); } catch { }
                try { Stream.Dispose(); } catch { }
            }
        }

        public FileFlow(
            string directory,
            string filePrefix = "log",
            long maxFileSize = 200 * 1024 * 1024,
            long maxDirectorySize = 2L * 1024 * 1024 * 1024,
            FileRetentionPolicy retention = null,
            int flushIntervalMs = 2000,
            int batchSize = 1,
            LogLevel minimumLevel = LogLevel.Trace,
            bool useCategoryRouting = false,
            LogLevel[] logLevelsForSeparateFiles = null,
            TimestampMode timestampMode = TimestampMode.Utc,
            BackpressureStrategy backpressure = BackpressureStrategy.DropOldest,
            FileOutputFormat outputFormat = FileOutputFormat.Text,
            CompressionFormat compression = CompressionFormat.GZip,
            string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}",
            long maxMemoryBytes = 20 * 1024 * 1024)
            : base("File:" + Path.Combine(directory, filePrefix), minimumLevel)
        {
            if (directory == null)
            {
                throw new ArgumentNullException("directory");
            }

            if (filePrefix == null)
            {
                throw new ArgumentNullException("filePrefix");
            }

            if (template == null)
            {
                throw new ArgumentNullException("template");
            }

            CheckForProcessTermination();

            _batchSize = batchSize <= 0 ? 1 : batchSize;
            _directory = directory;
            _filePrefix = filePrefix;
            _template = template;
            _maxFileSize = maxFileSize;
            _maxDirectorySize = maxDirectorySize > 0 ? maxDirectorySize : 10L * maxFileSize;
            _retention = retention ?? new FileRetentionPolicy();
            _timestampMode = timestampMode;
            _useCategoryRouting = useCategoryRouting;
            _maxMemoryBytes = maxMemoryBytes;
            _flushInterval = TimeSpan.FromMilliseconds(flushIntervalMs);
            _outputFormat = outputFormat;
            _compressionFormat = compression;

            _logLevelsForSeparateFiles = logLevelsForSeparateFiles != null
                ? new HashSet<LogLevel>(logLevelsForSeparateFiles)
                : new HashSet<LogLevel>();

            SetFileExtension(outputFormat);
            CompileTemplate(template);

            // Resolve relative path
            if (_directory.StartsWith("./", StringComparison.Ordinal))
            {
                _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2));
            }

            // Never allow logging initialization to crash the host because of a bad
            // directory (ACLs, antivirus locks, read-only locations, etc.).
            // Fall back to a per-user temp directory.
            try
            {
                Directory.CreateDirectory(_directory);
            }
            catch
            {
                try
                {
                    var processId = Process.GetCurrentProcess().Id;
                    var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString());
                    Directory.CreateDirectory(newDirectory);
                    OnDirectoryException?.Invoke(this, $"FileFlow: Could not create directory: '{_directory}', using directory '{newDirectory}' instead");
                    _directory = newDirectory;
                }
                catch
                {
                    var newDirectory = Path.GetTempPath();
                    OnDirectoryException?.Invoke(this, $"FileFlow: Could not create directory: '{_directory}', using directory '{newDirectory}' instead");
                    _directory = newDirectory;

                    // Last resort: disable file output by pointing to a safe-ish temp path.
                    // The writer thread still runs and swallows failures.
                }
            }

            // BlockingCollection with bounded capacity
            _queue = new BlockingCollection<LogEvent>(new ConcurrentQueue<LogEvent>(), QueueCapacity);

            // Dedicated writer thread
            _writerThread = new Thread(WriterThreadBody)
            {
                IsBackground = true,
                Name = "FileFlow.Writer[" + filePrefix + "]",
                Priority = ThreadPriority.AboveNormal,
            };
            _writerThread.Start();

            // Dedicated compression thread
            _compressionThread = new Thread(CompressionThreadBody)
            {
                IsBackground = true,
                Name = "FileFlow.Compress[" + filePrefix + "]",
                Priority = ThreadPriority.BelowNormal,
            };
            _compressionThread.Start();

            _flushTask = flushIntervalMs > 0
                ? Task.Factory.StartNew(PeriodicFlushLoop, TaskCreationOptions.LongRunning)
                : Task.FromResult(0);

            _retentionTask = Task.Factory.StartNew(RetentionLoop, TaskCreationOptions.LongRunning);
        }

        private void CheckForProcessTermination()
        {
            AppDomain.CurrentDomain.ProcessExit += (_, __) =>
            {
                try
                {
                    DisposeAsync().AsTask().GetAwaiter().GetResult();
                }
                catch 
                {
                    // Do nothing
                }
            };

            Console.CancelKeyPress += (_, e) =>
            {
                try
                {
                    DisposeAsync().AsTask().GetAwaiter().GetResult();
                }
                catch
                {
                    // Do nothing
                }
            };
        }

        /// <summary>Add an ambient property enricher applied to every event.</summary>
        public FileFlow EnrichWith(string key, Func<LogEvent, object> valueFactory)
        {
            if (key == null)
            {
                throw new ArgumentNullException("key");
            }

            if (valueFactory == null)
            {
                throw new ArgumentNullException("valueFactory");
            }

            _enrichers.Add(new KeyValuePair<string, Func<LogEvent, object>>(key, valueFactory));
            return this;
        }

        /// <summary>Add a static ambient property.</summary>
        public FileFlow EnrichWith(string key, object value)
        {
            return EnrichWith(key, _ => value);
        }

        /// <summary>Configure sampling: only log 1 in <paramref name="rate"/> events
        /// that match <paramref name="predicate"/>.</summary>
        public FileFlow WithSampling(int rate, Func<LogEvent, bool> predicate = null)
        {
            _samplingPolicy = new SamplingPolicy { Rate = rate, Predicate = predicate };
            return this;
        }

        /// <summary>Callback invoked when an event is dropped due to backpressure.</summary>
        public FileFlow OnEventDropped(Action<LogEvent> callback)
        {
            _onDrop = callback;
            return this;
        }

        /// <summary>Callback invoked with the archived path after each file rotation.</summary>
        public FileFlow OnFileRotated(Action<string> callback)
        {
            _onRotate = callback;
            return this;
        }

        /// <summary>Fan-out: also invoke <paramref name="writer"/> for every formatted log line.</summary>
        public FileFlow AddSecondaryWriter(Action<LogEvent, string> writer)
        {
            if (writer == null)
            {
                throw new ArgumentNullException("writer");
            }

            lock (_secondaryWritersLock)
            {
                if (_secondaryWriters == null)
                {
                    _secondaryWriters = new List<Action<LogEvent, string>>();
                }

                _secondaryWriters.Add(writer);
            }
            return this;
        }

        /// <summary>Register a custom template token (e.g. {mytoken}).</summary>
        public FileFlow RegisterToken(string name, Action<LogEvent, StringBuilder> formatter)
        {
            if (formatter == null)
            {
                throw new ArgumentNullException("formatter");
            }

            _customTokens[name] = formatter;
            return this;
        }

        /// <summary>Change the minimum log level at runtime (thread-safe).</summary>
        public void SetMinimumLevel(LogLevel level)
        {
            MinimumLevel = level;
        }

        /// <summary>Add a custom filter predicate. Events are logged only if ALL filters return true.</summary>
        public FileFlow WithFilter(Func<LogEvent, bool> predicate)
        {
            if (predicate == null)
            {
                throw new ArgumentNullException("predicate");
            }

            lock (_filtersLock)
            {
                _filters.Add(predicate);
            }
            return this;
        }

        /// <summary>Enable deduplication: suppress identical messages within the given time window.</summary>
        public FileFlow WithDeduplication(TimeSpan window)
        {
            if (window <= TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException("window", "Deduplication window must be positive.");
            }

            _deduplicationWindow = window;
            _deduplicationEnabled = true;
            return this;
        }

        /// <summary>Configure a custom date format for log file names (default: yyyyMMdd).</summary>
        public FileFlow WithDateFormat(string dateFormat)
        {
            if (string.IsNullOrWhiteSpace(dateFormat))
            {
                throw new ArgumentNullException("dateFormat");
            }

            _dateFormat = dateFormat;
            return this;
        }

        /// <summary>Limit the flow to a maximum number of events per second. Events exceeding the limit are dropped.</summary>
        public FileFlow WithRateLimit(int maxEventsPerSecond)
        {
            _maxEventsPerSecond = maxEventsPerSecond;
            return this;
        }

        /// <summary>When enabled, the file stream is flushed immediately after writing Error or Critical level events.</summary>
        public FileFlow WithAutoFlushOnError(bool enabled = true)
        {
            _autoFlushOnError = enabled;
            return this;
        }

        /// <summary>Push scoped properties that will be included in all log events written on the current async context.</summary>
        public IDisposable BeginScope(params KeyValuePair<string, object>[] properties)
        {
            var previous = _scopeProperties.Value;
            var merged = previous != null
                ? new Dictionary<string, object>(previous)
                : new Dictionary<string, object>();

            foreach (var kv in properties)
            {
                merged[kv.Key] = kv.Value;
            }

            _scopeProperties.Value = merged;
            return new ScopeDisposable(previous);
        }

        /// <summary>Push a single scoped property.</summary>
        public IDisposable BeginScope(string key, object value)
        {
            return BeginScope(new KeyValuePair<string, object>(key, value));
        }

        /// <summary>Returns the current queue depth (number of pending events).</summary>
        public int GetQueueDepth()
        {
            return _queue.Count;
        }

        /// <summary>Returns the current estimated memory usage of the queue in bytes.</summary>
        public long GetMemoryPressureBytes()
        {
            return Interlocked.Read(ref _currentMemoryBytes);
        }

        /// <summary>Generates a fingerprint hash for an exception to assist with grouping.</summary>
        public static string GetExceptionFingerprint(Exception ex)
        {
            if (ex == null)
            {
                return null;
            }

            string source = string.Concat(
                ex.GetType().FullName, "|",
                ex.TargetSite?.Name ?? string.Empty, "|",
                ex.StackTrace != null && ex.StackTrace.Length > 0
                    ? ex.StackTrace.Substring(0, Math.Min(200, ex.StackTrace.Length))
                    : string.Empty);

            // Simple FNV-1a hash
            unchecked
            {
                uint hash = 2166136261;
                foreach (char c in source)
                {
                    hash ^= c;
                    hash *= 16777619;
                }
                return hash.ToString("x8");
            }
        }

        private sealed class ScopeDisposable : IDisposable
        {
            private readonly Dictionary<string, object> _previous;

            public ScopeDisposable(Dictionary<string, object> previous)
            {
                _previous = previous;
            }

            public void Dispose()
            {
                _scopeProperties.Value = _previous;
            }
        }

        /// <summary>Returns true if the flow is healthy (no recent errors and writer thread alive).</summary>
        public bool IsHealthy()
        {
            if (!IsEnabled)
            {
                return false;
            }

            if (!_writerThread.IsAlive)
            {
                return false;
            }

            long lastErr = Interlocked.Read(ref _lastErrorTimestamp);
            if (lastErr > 0)
            {
                TimeSpan since = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - lastErr);
                if (since < TimeSpan.FromMinutes(1))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>Returns the last error encountered by the writer, or null if none.</summary>
        public Exception GetLastError()
        {
            return _lastError;
        }

        /// <summary>Returns the total number of write errors encountered.</summary>
        public long GetTotalErrors()
        {
            return Interlocked.Read(ref _totalErrors);
        }

        /// <summary>Returns live throughput and health metrics.</summary>
        public LogStats GetStats()
        {
            long written = Interlocked.Read(ref BlastedCount);
            long dropped = Interlocked.Read(ref DroppedCount);
            long bytes = Interlocked.Read(ref _totalBytesWritten);
            long rots = Interlocked.Read(ref _totalRotations);
            double elapsed = _uptime.Elapsed.TotalSeconds;
            double wps = elapsed > 0 ? written / elapsed : 0;
            return new LogStats(written, dropped, rots, bytes, wps);
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public override Task<WriteResult> BlastAsync(
            LogEvent logEvent,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled || !IsLogLevelEnabled(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            SamplingPolicy sp = _samplingPolicy;
            if (sp != null && !sp.ShouldLog(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            if (!PassesFilters(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            if (_deduplicationEnabled && IsDuplicate(logEvent))
            {
                return Task.FromResult(WriteResult.LevelFiltered);
            }

            if (_rateLimitEnabled && !TryPassRateLimit())
            {
                Interlocked.Increment(ref DroppedCount);
                Action<LogEvent> drop = _onDrop;
                if (drop != null)
                {
                    drop(logEvent);
                }

                return Task.FromResult(WriteResult.Dropped);
            }

            return Task.FromResult(TryEnqueue(logEvent));
        }

        public override Task<WriteResult> BlastBatchAsync(
            ReadOnlyMemory<LogEvent> logEvents,
            CancellationToken cancellationToken = default(CancellationToken))
        {
            if (!IsEnabled)
            {
                return Task.FromResult(WriteResult.FlowDisabled);
            }

            WriteResult result = WriteResult.Success;
            SamplingPolicy sp = _samplingPolicy;
            ReadOnlySpan<LogEvent> span = logEvents.Span;

            for (int i = 0; i < span.Length; i++)
            {
                LogEvent e = span[i];
                if (e.Level < MinimumLevel)
                {
                    continue;
                }

                if (sp != null && !sp.ShouldLog(e))
                {
                    continue;
                }

                if (!PassesFilters(e))
                {
                    continue;
                }

                if (_deduplicationEnabled && IsDuplicate(e))
                {
                    continue;
                }

                if (_rateLimitEnabled && !TryPassRateLimit())
                {
                    Interlocked.Increment(ref DroppedCount);
                    Action<LogEvent> drop = _onDrop;
                    if (drop != null)
                    {
                        drop(e);
                    }

                    result = WriteResult.Dropped;
                    continue;
                }

                if (TryEnqueue(e) == WriteResult.Dropped)
                {
                    result = WriteResult.Dropped;
                }
            }

            return Task.FromResult(result);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private WriteResult TryEnqueue(LogEvent log)
        {
            long size = EstimateSize(log);
            long current = Interlocked.Read(ref _currentMemoryBytes);

            if (current + size > _maxMemoryBytes)
            {
                Interlocked.Increment(ref DroppedCount);
                Action<LogEvent> drop = _onDrop;
                if (drop != null)
                {
                    drop(log);
                }

                return WriteResult.Dropped;
            }

            if (!_queue.TryAdd(log))
            {
                Interlocked.Increment(ref DroppedCount);
                Action<LogEvent> drop = _onDrop;
                if (drop != null)
                {
                    drop(log);
                }

                return WriteResult.Dropped;
            }

            Interlocked.Add(ref _currentMemoryBytes, size);
            Interlocked.Increment(ref BlastedCount);
            return WriteResult.Success;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool TryPassRateLimit()
        {
            long now = DateTime.UtcNow.Ticks;
            lock (_rateLimitLock)
            {
                long elapsed = now - _rateLimitWindowStart;
                if (elapsed >= TimeSpan.TicksPerSecond)
                {
                    _rateLimitWindowStart = now;
                    _rateLimitCounter = 1;
                    return true;
                }

                if (_rateLimitEnabled && _rateLimitCounter >= _maxEventsPerSecond)
                {
                    return false;
                }

                _rateLimitCounter++;
                return true;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool PassesFilters(LogEvent log)
        {
            lock (_filtersLock)
            {
                for (int i = 0; i < _filters.Count; i++)
                {
                    if (!_filters[i](log))
                    {
                        return false;
                    }
                }
            }
            return true;
        }

        private bool IsDuplicate(LogEvent log)
        {
            string key = string.Concat(
                log.Level.ToString(), "|",
                log.Category ?? string.Empty, "|",
                log.Message.Length > 0 ? log.Message.ToString() : string.Empty);

            long nowTicks = DateTime.UtcNow.Ticks;
            long windowTicks = _deduplicationWindow.Ticks;

            long existing;
            if (_deduplicationCache.TryGetValue(key, out existing))
            {
                if (nowTicks - existing < windowTicks)
                {
                    return true;
                }
            }

            _deduplicationCache[key] = nowTicks;

            // Periodic cleanup: remove expired entries when cache grows large
            if (_deduplicationCache.Count > 10000)
            {
                CleanDeduplicationCache(nowTicks, windowTicks);
            }

            return false;
        }

        private void CleanDeduplicationCache(long nowTicks, long windowTicks)
        {
            foreach (var kvp in _deduplicationCache)
            {
                if (nowTicks - kvp.Value >= windowTicks)
                {
                    long removed;
                    _deduplicationCache.TryRemove(kvp.Key, out removed);
                }
            }
        }


        public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            try
            {
                Stopwatch sw = Stopwatch.StartNew();
                while (_queue.Count > 0
                    && !cancellationToken.IsCancellationRequested
                    && sw.Elapsed < TimeSpan.FromSeconds(10))
                {
                    Thread.Sleep(5);
                }

                lock (_fileLock)
                {
                    foreach (OpenFile of in _openFiles.Values)
                    {
                        try { of.Writer.Flush(); } catch { /* ignore */ }
                    }
                }

                return Task.FromResult(0);
            }
            catch
            {
                return Task.FromResult(0); 
            }
        }


        public override async ValueTask DisposeAsync()
        {
            if (!IsEnabled)
            {
                return;
            }

            IsEnabled = false;

            _queue.CompleteAdding();
            _cts.Cancel();

            _writerThread.Join(2000);
            _compressionSignal.Release();
            _compressionThread.Join(2000);

            lock (_fileLock)
            {
                foreach (var outputFile in _openFiles.Values)
                {
                    if (_outputFormat == FileOutputFormat.Xml && outputFile.HasXmlHeader)
                    {
                        try
                        {
                            outputFile.Writer.WriteLine("</logs>");
                        }
                        catch { }
                    }

                    outputFile.Dispose();
                }

                _openFiles.Clear();
            }

            _cts.Dispose();
            _compressionSignal.Dispose();
            _queue.Dispose();

            await base.DisposeAsync().ConfigureAwait(false);
        }
        private void WriterThreadBody()
        {
            try
            {
                while (!_queue.IsCompleted)
                {
                    LogEvent e;
                    try
                    {
                        e = _queue.Take(_cts.Token);
                    }
                    catch (OperationCanceledException) { break; }
                    catch (InvalidOperationException) { break; }

                    WriteLogEvent(e);

                    // Drain additional items
                    int extra = 0;
                    LogEvent next;
                    while (extra < _batchSize && _queue.TryTake(out next))
                    {
                        WriteLogEvent(next);
                        extra++;
                    }
                }
            }
            catch (Exception ex)
            {
                WriteToConsoleError("[FileFlow] Writer thread error: " + ex.Message);
            }
            finally
            {
                // Drain remaining events before the thread exits
                LogEvent remaining;
                while (_queue.TryTake(out remaining))
                {
                    WriteLogEvent(remaining);
                }

                // Final flush
                lock (_fileLock)
                {
                    foreach (OpenFile of in _openFiles.Values)
                    {
                        try { of.Writer.Flush(); } catch { /* ignore */ }
                    }
                }
            }
        }

        private void WriteToConsoleError(string text)
        {
            if (IgnoreConsoleErrors)
            {
                return;
            }

            try
            {
                Console.Error.WriteLine(text);
            }
            catch
            {
                // Logging must never bring down the application.
            }
        }

        private void WriteLogEvent(LogEvent log)
        {
            long size = 0;
            try
            {
                size = EstimateSize(log);
            }
            catch
            {
                size = 512;
            }

            Interlocked.Add(ref _currentMemoryBytes, -size);

            string line;
            try
            {
                StringBuilder sb = StringBuilderPool.Rent();
                try
                {
                    switch (_outputFormat)
                    {
                        case FileOutputFormat.Json:
                            FormatJson(log, sb, false);
                            break;
                        case FileOutputFormat.StructuredJson:
                            FormatJson(log, sb, true);
                            break;
                        case FileOutputFormat.Xml:
                            FormatXml(log, sb);
                            break;
                        case FileOutputFormat.Csv:
                            FormatCsv(log, sb);
                            break;
                        default:
                            FormatText(log, sb);
                            break;
                    }
                    line = sb.ToString();
                }
                finally
                {
                    StringBuilderPool.Return(sb);
                }
            }
            catch (Exception ex)
            {
                try
                {
                    line = "[FileFlow] Format error: " + ex.Message
                         + " | Original level=" + log.Level
                         + " message=" + (log.Message.Length > 0 ? log.Message.ToString() : "(empty)");
                }
                catch
                {
                    line = "[FileFlow] Format error (unrecoverable)";
                }
            }

            try
            {
                string path = GenerateFilePath(GetCurrentDate(), log);

                lock (_fileLock)
                {
                    try
                    {
                        EnsureFileOpen(path, log);

                        if (ShouldRotate(path, line.Length))
                        {
                            string archived = RotateFile(path);
                            EnsureFileOpen(path, log);
                            if (archived != null)
                            {
                                Action<string> onRotate = _onRotate;
                                if (onRotate != null)
                                {
                                    try { onRotate(archived); }
                                    catch { /* Do nothing */ }
                                }
                            }
                        }

                        OpenFile of;
                        if (_openFiles.TryGetValue(path, out of))
                        {
                            of.Writer.WriteLine(line);
                            of.Size += line.Length + Environment.NewLine.Length;
                        }
                    }
                    catch (Exception ex)
                    {
                        _lastError = ex;
                        Interlocked.Increment(ref _totalErrors);
                        Interlocked.Exchange(ref _lastErrorTimestamp, DateTime.UtcNow.Ticks);
                        try
                        {
                            WriteToConsoleError("[FileFlow] Write error for '" + path + "': " + ex.Message);
                        }
                        catch { /* Do nothing */ }
                    }
                }

                Interlocked.Add(ref _totalBytesWritten, line.Length + 1);

                // Auto-flush on error/critical
                if (_autoFlushOnError && (log.Level >= LogLevel.Error))
                {
                    lock (_fileLock)
                    {
                        OpenFile autoFlushOf;
                        if (_openFiles.TryGetValue(path, out autoFlushOf))
                        {
                            try { autoFlushOf.Writer.Flush(); } catch { /* ignore */ }
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                try
                {
                    WriteToConsoleError("[FileFlow] WriteLogEvent error: " + ex.Message);
                }
                catch { /* Do nothing */ }
            }

            // Fan-out to secondary writers
            try
            {
                if (_secondaryWriters != null)
                {
                    lock (_secondaryWritersLock)
                    {
                        foreach (Action<LogEvent, string> writer in _secondaryWriters)
                        {
                            try { writer(log, line); }
                            catch
                            {
                                // Do nothing
                            }
                        }
                    }
                }
            }
            catch
            {
                // Do nothing
            }
        }


        private void FormatText(LogEvent log, StringBuilder sb)
        {
            foreach (Action<LogEvent, StringBuilder> action in _compiledTemplate)
            {
                action(log, sb);
            }
        }


        private void FormatJson(LogEvent log, StringBuilder sb, bool structured)
        {
            sb.Append('{');

            // Timestamp
            sb.Append("\"timestamp\":\"");
            AppendJsonEscaped(LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb);
            sb.Append("\",");

            // Level
            sb.Append("\"level\":\"");
            string lvlStr;
            sb.Append(LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString());
            sb.Append("\",");

            if (structured)
            {
                // Monotonic hex correlation ID
                int cid = Interlocked.Increment(ref _correlationSeed);
                sb.Append("\"correlationId\":\"");
                sb.Append(cid.ToString("x8"));
                sb.Append("\",");

                sb.Append("\"host\":\"");
                AppendJsonEscaped(CachedMachineName, sb);
                sb.Append("\",");

                sb.Append("\"pid\":");
                sb.Append(CachedPid);
                sb.Append(',');

                // Distributed tracing
                if (log.TraceId != default(ActivityTraceId))
                {
                    sb.Append("\"traceId\":\"");
                    sb.Append(log.TraceId.ToHexString());
                    sb.Append("\",");
                }

                if (log.SpanId != default(ActivitySpanId))
                {
                    sb.Append("\"spanId\":\"");
                    sb.Append(log.SpanId.ToHexString());
                    sb.Append("\",");
                }

                sb.Append("\"threadId\":");
                sb.Append(log.ThreadId);
                sb.Append(',');
            }

            // Category
            sb.Append("\"category\":\"");
            if (!string.IsNullOrEmpty(log.Category))
            {
                AppendJsonEscaped(log.Category, sb);
            }

            sb.Append("\",");

            // Message
            sb.Append("\"message\":\"");
            if (log.Message.Length > 0)
            {
                AppendJsonEscaped(log.Message.ToString(), sb);
            }

            sb.Append('"');

            // Exception
            if (log.Exception != null)
            {
                sb.Append(",\"exception\":\"");
                AppendJsonEscaped(log.Exception.ToString(), sb);
                sb.Append('"');
            }

            // Properties (enrichers + event properties)
            bool hasEnrichers = _enrichers.Count > 0;
            bool hasProps = log.Properties.Count > 0;
            if (hasEnrichers || hasProps)
            {
                sb.Append(",\"properties\":{");
                bool first = true;

                foreach (KeyValuePair<string, Func<LogEvent, object>> kv in _enrichers)
                {
                    if (!first)
                    {
                        sb.Append(',');
                    }

                    first = false;
                    sb.Append('"');
                    AppendJsonEscaped(kv.Key, sb);
                    sb.Append("\":\"");
                    object val = kv.Value(log);
                    AppendJsonEscaped(val != null ? val.ToString() : "null", sb);
                    sb.Append('"');
                }

                foreach (var property in log.Properties)
                {
                    if (!first)
                    {
                        sb.Append(',');
                    }

                    first = false;
                    sb.Append('"');
                    AppendJsonEscaped(property.Key, sb);
                    sb.Append("\":");
                    if (property.Value == null)
                    {
                        sb.Append("null");
                    }
                    else
                    {
                        sb.Append('"');
                        AppendJsonEscaped(property.Value.ToString(), sb);
                        sb.Append('"');
                    }
                }

                sb.Append('}');
            }

            sb.Append('}');
        }

        private static void AppendJsonEscaped(string value, StringBuilder sb)
        {
            if (value == null)
            {
                return;
            }

            foreach (char c in value)
            {
                switch (c)
                {
                    case '"': sb.Append("\\\""); break;
                    case '\\': sb.Append("\\\\"); break;
                    case '\b': sb.Append("\\b"); break;
                    case '\f': sb.Append("\\f"); break;
                    case '\n': sb.Append("\\n"); break;
                    case '\r': sb.Append("\\r"); break;
                    case '\t': sb.Append("\\t"); break;
                    default:
                        if (char.IsControl(c))
                        {
                            sb.Append("\\u");
                            sb.Append(((int)c).ToString("x4"));
                        }
                        else
                        {
                            sb.Append(c);
                        }

                        break;
                }
            }
        }

        private void RepairXmlIfNeeded(string path)
        {
            try
            {
                if (!File.Exists(path))
                {
                    return;
                }

                const string footer = "</logs>";
                const int tailSize = 512;

                using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
                {
                    if (fs.Length < footer.Length)
                    {
                        return;
                    }

                    int readSize = (int)Math.Min(tailSize, fs.Length);
                    fs.Seek(-readSize, SeekOrigin.End);

                    byte[] buffer = new byte[readSize];
                    fs.Read(buffer, 0, readSize);

                    string tail = Encoding.UTF8.GetString(buffer);

                    if (!tail.Contains(footer))
                    {
                        fs.Seek(0, SeekOrigin.End);
                        using (var sw = new StreamWriter(fs, Encoding.UTF8, 1024, true))
                        {
                            sw.WriteLine();
                            sw.WriteLine(footer);
                            sw.Flush();
                        }
                    }
                }
            }
            catch
            {
                // Never throw during logging
            }
        }

        private void FormatXml(LogEvent log, StringBuilder sb)
        {
            sb.Append("<log>");

            string lvlStr;
            AppendXmlElement("timestamp", LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb);
            AppendXmlElement("level", LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString(), sb);

            if (!string.IsNullOrEmpty(log.Category))
            {
                AppendXmlElement("category", log.Category, sb);
            }

            sb.Append("<message>");
            if (log.Message.Length > 0)
            {
                AppendXmlEscaped(log.Message.ToString(), sb);
            }

            sb.Append("</message>");

            if (log.Exception != null)
            {
                AppendXmlElement("exception", log.Exception.ToString(), sb);
            }

            bool hasEnrichers = _enrichers.Count > 0;
            bool hasProps = log.Properties.Count > 0;
            if (hasEnrichers || hasProps)
            {
                sb.Append("<properties>");

                foreach (KeyValuePair<string, Func<LogEvent, object>> kv in _enrichers)
                {
                    sb.Append("<property key=\"");
                    AppendXmlEscaped(kv.Key, sb);
                    sb.Append("\">");
                    object val = kv.Value(log);
                    AppendXmlEscaped(val != null ? val.ToString() : "null", sb);
                    sb.Append("</property>");
                }

                foreach (var property in log.Properties)
                {
                    if (string.IsNullOrEmpty(property.Key))
                    {
                        continue;
                    }

                    sb.Append("<property key=\"");
                    AppendXmlEscaped(property.Key, sb);
                    sb.Append("\">");
                    if (property.Value != null)
                    {
                        AppendXmlEscaped(property.Value.ToString(), sb);
                    }
                    else
                    {
                        sb.Append("null");
                    }

                    sb.Append("</property>");
                }

                sb.Append("</properties>");
            }

            sb.Append("</log>");
        }

        private static void AppendXmlElement(string tag, string content, StringBuilder sb)
        {
            sb.Append('<').Append(tag).Append('>');
            AppendXmlEscaped(content, sb);
            sb.Append("</").Append(tag).Append('>');
        }

        private static void AppendXmlEscaped(string value, StringBuilder sb)
        {
            if (value == null)
            {
                return;
            }

            foreach (char c in value)
            {
                switch (c)
                {
                    case '&': sb.Append("&amp;"); break;
                    case '<': sb.Append("&lt;"); break;
                    case '>': sb.Append("&gt;"); break;
                    case '"': sb.Append("&quot;"); break;
                    case '\'': sb.Append("&apos;"); break;
                    default:
                        if (char.IsControl(c) && c != '\r' && c != '\n' && c != '\t')
                        {
                            sb.Append('?');
                        }
                        else
                        {
                            sb.Append(c);
                        }

                        break;
                }
            }
        }


        private void FormatCsv(LogEvent log, StringBuilder sb)
        {
            string lvlStr;

            AppendCsvField(LogEvent.GetDateTime(log.Timestamp).ToString("O"), sb);
            sb.Append(',');
            AppendCsvField(LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString(), sb);
            sb.Append(',');
            AppendCsvField(log.Category ?? string.Empty, sb);
            sb.Append(',');
            AppendCsvField(log.Message.Length > 0 ? log.Message.ToString() : string.Empty, sb);
            sb.Append(',');
            AppendCsvField(log.Exception != null ? log.Exception.ToString() : string.Empty, sb);
            sb.Append(',');

            // Properties column: key=value; key=value
            sb.Append('"');
            bool first = true;
            foreach (KeyValuePair<string, Func<LogEvent, object>> kv in _enrichers)
            {
                if (!first)
                {
                    sb.Append("; ");
                }

                first = false;
                AppendCsvInner(kv.Key, sb);
                sb.Append('=');
                object val = kv.Value(log);
                AppendCsvInner(val != null ? val.ToString() : "null", sb);
            }
            foreach (var property in log.Properties)
            {
                if (!first)
                {
                    sb.Append("; ");
                }

                first = false;
                AppendCsvInner(property.Key ?? string.Empty, sb);
                sb.Append('=');
                AppendCsvInner(property.Value != null ? property.Value.ToString() : "null", sb);
            }
            sb.Append('"');
        }

        private static void AppendCsvField(string value, StringBuilder sb)
        {
            if (value == null)
            {
                value = string.Empty;
            }

            bool needsQuote = value.IndexOfAny(CsvSpecialChars) >= 0;
            if (needsQuote)
            {
                sb.Append('"');
            }

            AppendCsvInner(value, sb);
            if (needsQuote)
            {
                sb.Append('"');
            }
        }

        private static void AppendCsvInner(string value, StringBuilder sb)
        {
            if (value == null)
            {
                return;
            }

            foreach (char c in value)
            {
                if (c == '"')
                {
                    // RFC 4180: escape quote by doubling
                    sb.Append('"');
                }

                sb.Append(c);
            }
        }

        private void EnsureFileOpen(string path, LogEvent logEvent)
        {
            try
            {
                OpenFile existing;
                if (_openFiles.TryGetValue(path, out existing))
                {
                    try
                    {
                        if (File.Exists(path))
                        {
                            return;
                        }
                    }
                    catch
                    {
                        // File.Exists failed (permissions, etc.) – assume file is gone
                    }

                    // File was deleted or check failed; dispose stale handle
                    try { existing.Dispose(); } catch { /* ignore */ }
                    _openFiles.Remove(path);
                }

                FileStream fs = null;
                StreamWriter sw = null;
                try
                {
                    string dir = Path.GetDirectoryName(path);
                    if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
                    {
                        Directory.CreateDirectory(dir);
                    }

                    fs = new FileStream(
                        path,
                        FileMode.Append,
                        FileAccess.Write,
                        FileShare.ReadWrite | FileShare.Delete,
                        FileBufferSize,
                        FileOptions.SequentialScan);

                    sw = new StreamWriter(fs, Encoding.UTF8, WriterBufferSize);
                    sw.AutoFlush = false;

                    OpenFile of = new OpenFile(fs, sw, GetCurrentDate());
                    _openFiles[path] = of;

                    // From here on, resources are owned by _openFiles; clear locals
                    // so the finally block does not double-dispose.
                    fs = null;
                    sw = null;

                    if (_outputFormat == FileOutputFormat.Csv && of.Stream.Length == 0)
                    {
                        try
                        {
                            of.Writer.Write(CsvHeader);
                            of.HasCsvHeader = true;
                        }
                        catch (Exception ex)
                        {
                            WriteToConsoleError("[FileFlow] CSV header write error for '" + path + "': " + ex.Message);
                        }
                    }

                    if (_outputFormat == FileOutputFormat.Xml)
                    {
                        try
                        {
                            if (of.Stream.Length == 0)
                            {
                                of.Writer.WriteLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
                                of.Writer.WriteLine("<logs>");
                                of.HasXmlHeader = true;
                            }
                            else
                            {
                                of.Writer.Flush();
                                RepairXmlIfNeeded(path);
                                of.HasXmlHeader = true;
                            }
                        }
                        catch (Exception ex)
                        {
                            WriteToConsoleError("[FileFlow] XML header write error for '" + path + "': " + ex.Message);
                        }
                    }
                }
                catch (Exception ex)
                {
                    // Clean up partially-allocated resources that were never
                    // handed off to an OpenFile entry.
                    try { sw?.Dispose(); } catch { /* ignore */ }
                    try { fs?.Dispose(); } catch { /* ignore */ }

                    // Remove any entry that may be in a broken state
                    OpenFile broken;
                    if (_openFiles.TryGetValue(path, out broken))
                    {
                        try { broken.Dispose(); } catch { /* ignore */ }
                        _openFiles.Remove(path);
                    }

                    WriteToConsoleError("[FileFlow] Failed to open '" + path + "': " + ex.Message);
                }
            }
            catch (Exception ex)
            {
                // Outermost safety net – never let this method take down the process
                try
                {
                    WriteToConsoleError("[FileFlow] EnsureFileOpen unhandled error: " + ex.Message);
                }
                catch { /* ignore */ }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private bool ShouldRotate(string path, long additionalBytes)
        {
            OpenFile of;
            if (!_openFiles.TryGetValue(path, out of))
            {
                return false;
            }

            return of.Size + additionalBytes > _maxFileSize
                || of.Date.Date != GetCurrentDate()
                || Interlocked.Read(ref _currentMemoryBytes) > (long)(_maxMemoryBytes * 0.9);
        }

        private string RotateFile(string path)
        {
            OpenFile of;
            if (_openFiles.TryGetValue(path, out of))
            {
                if (_outputFormat == FileOutputFormat.Xml && of.HasXmlHeader)
                {
                    try
                    {
                        of.Writer.WriteLine("</logs>");
                        of.Writer.Flush();
                    }
                    catch { }
                }

                of.Dispose();
                _openFiles.Remove(path);
            }

            if (!File.Exists(path))
            {
                return null;
            }

            string archived = ArchiveFile(path);
            Interlocked.Increment(ref _totalRotations);
            return archived;
        }

        private string ArchiveFile(string filePath)
        {
            try
            {
                string dir = Path.GetDirectoryName(filePath);
                string fileName = Path.GetFileName(filePath);
                int extIdx = fileName.LastIndexOf(_fileExtension, StringComparison.OrdinalIgnoreCase);
                string baseName = extIdx >= 0 ? fileName.Substring(0, extIdx) : fileName;
                int maxFiles = _retention.MaxRolledFiles > 0 ? _retention.MaxRolledFiles : 999;

                for (int i = maxFiles - 1; i >= 1; i--)
                {
                    string src = Path.Combine(dir, baseName + "_" + i + _fileExtension);
                    string srcGz = src + ".gz";
                    string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension);
                    string dstGz = dst + ".gz";

                    // Move .gz variant if it exists
                    if (File.Exists(srcGz))
                    {
                        if (File.Exists(dstGz))
                        {
                            File.Delete(dstGz);
                        }
                        File.Move(srcGz, dstGz);
                    }

                    if (!File.Exists(src))
                    {
                        continue;
                    }

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

                string archive = Path.Combine(dir, baseName + "_1" + _fileExtension);
                if (File.Exists(archive))
                {
                    File.Delete(archive);
                }

                File.Move(filePath, archive);

                // Queue the archived file for compression
                if (_compressionFormat != CompressionFormat.None)
                {
                    _compressionQueue.Enqueue(archive);
                    _compressionSignal.Release(1);
                }

                return archive;
            }
            catch (Exception ex)
            {
                WriteToConsoleError("[FileFlow] Archive error '" + filePath + "': " + ex.Message);
                return null;
            }
        }

        private void CompressionThreadBody()
        {
            while (true)
            {
                try
                {
                    _compressionSignal.Wait(_cts.Token);
                }
                catch (OperationCanceledException)
                {
                    DrainCompressionQueue();
                    return;
                }

                DrainCompressionQueue();
            }
        }

        private void DrainCompressionQueue()
        {
            string path;
            while (_compressionQueue.TryDequeue(out path))
            {
                try { CompressFile(path); }
                catch (Exception ex)
                {
                    WriteToConsoleError("[FileFlow] Compress error '" + path + "': " + ex.Message);
                }
            }
        }

        private void CompressFile(string path)
        {
            if (_compressionFormat == CompressionFormat.None || !File.Exists(path))
            {
                return;
            }

            string outPath = path + ".gz";

            if (File.Exists(outPath))
            {
                string ts = DateTime.UtcNow.ToString("yyMMdd_HHmmss_fff");
                File.Move(outPath, path + "_" + ts + ".gz");
            }

            const int bufSize = 65536;
            byte[] buffer = new byte[bufSize];

            using (FileStream src = File.OpenRead(path))
            using (FileStream dst = File.Create(outPath))
            using (GZipStream gz = new GZipStream(dst, CompressionLevel.Optimal))
            {
                int read;
                while ((read = src.Read(buffer, 0, bufSize)) > 0)
                {
                    gz.Write(buffer, 0, read);
                }
            }

            // Delete the original uncompressed file after successful compression
            try { File.Delete(path); } catch { /* ignore */ }

            EnforceCompressedFileLimit();
        }

        private void EnforceCompressedFileLimit()
        {
            if (_retention.MaxCompressedFiles <= 0)
            {
                return;
            }

            try
            {
                DirectoryInfo dir = new DirectoryInfo(_directory);
                if (!dir.Exists)
                {
                    return;
                }

                FileInfo[] compressedFiles = dir.GetFiles("*" + _fileExtension + ".gz")
                    .OrderByDescending(f => f.LastWriteTimeUtc)
                    .ToArray();

                for (int i = _retention.MaxCompressedFiles; i < compressedFiles.Length; i++)
                {
                    try { compressedFiles[i].Delete(); } catch { /* ignore */ }
                }
            }
            catch (Exception ex)
            {
                WriteToConsoleError("[FileFlow] Compressed retention error: " + ex.Message);
            }
        }

        private void PeriodicFlushLoop()
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                try
                {
                    long mem = Interlocked.Read(ref _currentMemoryBytes);
                    int delay = mem > (long)(_maxMemoryBytes * 0.8)
                        ? 250
                        : (int)_flushInterval.TotalMilliseconds;

                    if (_cts.Token.WaitHandle.WaitOne(delay))
                    {
                        break;
                    }

                    lock (_fileLock)
                    {
                        foreach (OpenFile of in _openFiles.Values)
                        {
                            try { of.Writer.Flush(); } catch { /* ignore */ }
                        }
                    }
                }
                catch (ThreadInterruptedException) { break; }
                catch (Exception ex)
                {
                    WriteToConsoleError("[FileFlow] Flush error: " + ex.Message);
                }
            }
        }

        private void RetentionLoop()
        {
            while (!_cts.Token.IsCancellationRequested)
            {
                if (_cts.Token.WaitHandle.WaitOne(TimeSpan.FromMinutes(15)))
                {
                    break;
                }

                ApplyRetention();
            }
        }

        private void ApplyRetention()
        {
            try
            {
                DirectoryInfo dir = new DirectoryInfo(_directory);
                if (!dir.Exists)
                {
                    return;
                }

                FileInfo[] files = dir.GetFiles("*" + _fileExtension)
                    .Concat(dir.GetFiles("*" + _fileExtension + ".gz"))
                    .OrderByDescending(f => f.LastWriteTimeUtc)
                    .ToArray();

                long totalBytes = 0;
                int kept = 0;
                int keptCompressed = 0;

                foreach (FileInfo f in files)
                {
                    bool isCompressed = f.Extension.Equals(".gz", StringComparison.OrdinalIgnoreCase);
                    bool tooOld = _retention.MaxAgeDays > 0
                                 && (DateTime.UtcNow - f.LastWriteTimeUtc).TotalDays > _retention.MaxAgeDays;
                    bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles;
                    bool tooManyCompressed = isCompressed
                        && _retention.MaxCompressedFiles > 0
                        && keptCompressed >= _retention.MaxCompressedFiles;
                    bool tooLarge = _retention.MaxTotalArchiveBytes > 0
                                 && totalBytes + f.Length > _retention.MaxTotalArchiveBytes;
                    bool directoryTooLarge = totalBytes + f.Length > _maxDirectorySize;

                    if (tooOld || tooMany || tooManyCompressed || tooLarge || directoryTooLarge)
                    {
                        try { f.Delete(); } catch { /* ignore */ }
                    }
                    else
                    {
                        totalBytes += f.Length;
                        kept++;
                        if (isCompressed)
                        {
                            keptCompressed++;
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                WriteToConsoleError("[FileFlow] Retention error: " + ex.Message);
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private DateTime GetCurrentDate()
        {
            return _timestampMode == TimestampMode.Local ? DateTime.Now.Date : DateTime.UtcNow.Date;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private string GenerateFilePath(DateTime date, LogEvent log)
        {
            string prefix = _filePrefix;

            if (_useCategoryRouting && !string.IsNullOrEmpty(log.Category))
            {
                prefix += "_" + log.Category;
            }

            if (_logLevelsForSeparateFiles.Count > 0 && _logLevelsForSeparateFiles.Contains(log.Level))
            {
                string lvlStr;
                prefix += "_" + (LevelStrings.TryGetValue(log.Level, out lvlStr) ? lvlStr : log.Level.ToString());
            }

            return Path.Combine(
                _directory,
                string.Concat(prefix, "_", CachedMachineName, "_", date.ToString(_dateFormat), _fileExtension));
        }

        private void SetFileExtension(FileOutputFormat fmt)
        {
            switch (fmt)
            {
                case FileOutputFormat.Json:
                case FileOutputFormat.StructuredJson:
                    _fileExtension = ".json"; break;
                case FileOutputFormat.Xml:
                    _fileExtension = ".xml"; break;
                case FileOutputFormat.Csv:
                    _fileExtension = ".csv"; break;
                default:
                    _fileExtension = ".log"; break;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static long EstimateSize(LogEvent log)
        {
            long s = 200L
                   + log.Message.Length * 2
                   + (log.Category != null ? log.Category.Length * 2 : 0)
                   + log.Properties.Count * 40;
            if (log.Exception != null)
            {
                s += 2048; // Avoid calling Exception.ToString() just for size estimation
            }

            return s;
        }

        private void CompileTemplate(string template)
        {
            _compiledTemplate = new List<Action<LogEvent, StringBuilder>>();
            int pos = 0;

            while (pos < template.Length)
            {
                int open = template.IndexOf('{', pos);
                if (open < 0)
                {
                    string lit = template.Substring(pos);
                    _compiledTemplate.Add((_, sb) => sb.Append(lit));
                    break;
                }

                if (open > pos)
                {
                    string lit = template.Substring(pos, open - pos);
                    _compiledTemplate.Add((_, sb) => sb.Append(lit));
                }

                int close = template.IndexOf('}', open);
                if (close < 0)
                {
                    string lit = template.Substring(open);
                    _compiledTemplate.Add((_, sb) => sb.Append(lit));
                    break;
                }

                string token = template.Substring(open + 1, close - (open + 1));
                _compiledTemplate.Add(ResolveToken(token));
                pos = close + 1;
            }
        }

        private Action<LogEvent, StringBuilder> ResolveToken(string token)
        {
            switch (token.ToLowerInvariant())
            {
                case "ts":
                    return (log, sb) =>
                        sb.Append(LogEvent.GetDateTime(log.Timestamp).ToString("yyyy-MM-dd HH:mm:ss.fff"));
                case "tz":
                    return (log, sb) =>
                        sb.Append(_timestampMode == TimestampMode.Local
                            ? TimeZoneInfo.Local.StandardName : "UTC");
                case "host":
                    return (log, sb) => sb.Append(CachedMachineName);
                case "category":
                    return (log, sb) => { if (log.Category != null) { sb.Append(log.Category); } };
                case "thread":
                    return (log, sb) => sb.Append(Thread.CurrentThread.ManagedThreadId);
                case "logtype":
                    return (log, sb) =>
                    {
                        string s;
                        sb.Append(LevelStrings.TryGetValue(log.Level, out s) ? s : log.Level.ToString());
                    };
                case "message":
                    return (log, sb) =>
                    {
                        if (log.Message.Length > 0)
                        {
                            sb.Append(log.Message.ToString());
                        }
                    };
                case "exception":
                    return (log, sb) =>
                    {
                        if (log.Exception != null)
                        {
                            sb.Append(log.Exception.ToString());
                        }
                    };
                case "props":
                    return (log, sb) => AppendProperties(log, sb);
                case "newline":
                    return (log, sb) => sb.AppendLine();
                case "pid":
                    return (log, sb) => sb.Append(CachedPid);
                case "traceid":
                    return (log, sb) =>
                    {
                        if (log.TraceId != default(ActivityTraceId))
                        {
                            sb.Append(log.TraceId.ToHexString());
                        }
                    };
                case "spanid":
                    return (log, sb) =>
                    {
                        if (log.SpanId != default(ActivitySpanId))
                        {
                            sb.Append(log.SpanId.ToHexString());
                        }
                    };
                default:
                    return BuildCustomOrLiteralToken(token);
            }
        }

        private Action<LogEvent, StringBuilder> BuildCustomOrLiteralToken(string token)
        {
            string name = token;
            return (log, sb) =>
            {
                Action<LogEvent, StringBuilder> custom;
                if (_customTokens.TryGetValue(name, out custom))
                {
                    custom(log, sb);
                }
                else
                {
                    sb.Append('{').Append(name).Append('}');
                }
            };
        }

        private void AppendProperties(LogEvent log, StringBuilder sb)
        {
            var scopeProps = _scopeProperties.Value;
            bool hasEnrichers = _enrichers.Count > 0;
            bool hasProps = log.Properties.Count > 0;
            bool hasScope = scopeProps != null && scopeProps.Count > 0;
            if (!hasEnrichers && !hasProps && !hasScope)
            {
                return;
            }

            sb.Append(" {");
            bool first = true;

            foreach (KeyValuePair<string, Func<LogEvent, object>> kv in _enrichers)
            {
                if (!first)
                {
                    sb.Append(", ");
                }

                first = false;
                object val = kv.Value(log);
                sb.Append(kv.Key).Append('=').Append(val != null ? val.ToString() : "null");
            }

            foreach (var property in log.Properties)
            {
                if (!first)
                {
                    sb.Append(", ");
                }

                first = false;
                sb.Append(property.Key).Append('=')
                  .Append(property.Value != null ? property.Value.ToString() : "null");
            }

            if (hasScope)
            {
                foreach (var kv in scopeProps)
                {
                    if (!first)
                    {
                        sb.Append(", ");
                    }

                    first = false;
                    sb.Append(kv.Key).Append('=')
                      .Append(kv.Value != null ? kv.Value.ToString() : "null");
                }
            }

            sb.Append('}');
        }
    }
}