using System.Collections.Concurrent;
using System.IO;
using System.Text;
using System.Text.Json;

namespace NexusShell.Services.Diagnostics;

public sealed class WorkspaceEventLog : IAsyncDisposable
{
    private const int MaxQueueSize = 4096;
    private const int MaxFileBytes = 5 * 1024 * 1024;
    private const int FlushBatchSize = 32;

    private readonly ConcurrentQueue<DiagnosticLogEntry> _queue = new();
    private readonly SemaphoreSlim _signal = new(0);
    private readonly CancellationTokenSource _cts = new();
    private readonly Task _writerTask;
    private readonly object _fileLock = new();
    private string? _sessionDirectory;
    private string? _eventsPath;
    private int _droppedCount;

    public WorkspaceEventLog()
    {
        _writerTask = Task.Run(WriterLoopAsync);
    }

    public int PendingCount => _queue.Count;
    public int DroppedCount => _droppedCount;
    public string? EventsFilePath => _eventsPath;

    public void BindSession(string sessionDirectory)
    {
        Directory.CreateDirectory(sessionDirectory);
        _sessionDirectory = sessionDirectory;
        _eventsPath = Path.Combine(sessionDirectory, "events.jsonl");
    }

    public void Enqueue(DiagnosticLogEntry entry)
    {
        if (_queue.Count >= MaxQueueSize)
        {
            Interlocked.Increment(ref _droppedCount);
            return;
        }

        _queue.Enqueue(entry);
        try
        {
            _signal.Release();
        }
        catch (ObjectDisposedException)
        {
            // Shutting down.
        }
    }

    public async Task FlushAsync(CancellationToken cancellationToken = default)
    {
        for (var i = 0; i < 100 && (_queue.Count > 0 || PendingCount > 0); i++)
        {
            try
            {
                _signal.Release();
            }
            catch (ObjectDisposedException)
            {
                break;
            }

            await Task.Delay(25, cancellationToken);
        }
    }

    public IReadOnlyList<DiagnosticLogEntry> ReadRecentEntries(int maxCount = 100)
    {
        if (_eventsPath is null || !File.Exists(_eventsPath))
        {
            return [];
        }

        try
        {
            var lines = File.ReadLines(_eventsPath).TakeLast(maxCount);
            var entries = new List<DiagnosticLogEntry>();
            foreach (var line in lines)
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                try
                {
                    var doc = JsonSerializer.Deserialize<PersistedEntry>(line);
                    if (doc is not null)
                    {
                        entries.Add(new DiagnosticLogEntry(
                            doc.TimestampUtc,
                            doc.Category,
                            doc.EventName,
                            doc.Message,
                            doc.Properties));
                    }
                }
                catch
                {
                    // Skip malformed lines.
                }
            }

            return entries;
        }
        catch
        {
            return [];
        }
    }

    private async Task WriterLoopAsync()
    {
        var batch = new List<DiagnosticLogEntry>(FlushBatchSize);
        while (!_cts.IsCancellationRequested)
        {
            try
            {
                await _signal.WaitAsync(TimeSpan.FromMilliseconds(500), _cts.Token);
            }
            catch (OperationCanceledException)
            {
                break;
            }

            while (_queue.TryDequeue(out var entry))
            {
                batch.Add(entry);
                if (batch.Count >= FlushBatchSize)
                {
                    WriteBatch(batch);
                    batch.Clear();
                }
            }

            if (batch.Count > 0)
            {
                WriteBatch(batch);
                batch.Clear();
            }
        }

        while (_queue.TryDequeue(out var remaining))
        {
            batch.Add(remaining);
        }

        if (batch.Count > 0)
        {
            WriteBatch(batch);
        }
    }

    private void WriteBatch(IReadOnlyList<DiagnosticLogEntry> batch)
    {
        if (_eventsPath is null || batch.Count == 0)
        {
            return;
        }

        lock (_fileLock)
        {
            RotateIfNeeded(_eventsPath);
            using var stream = new FileStream(_eventsPath, FileMode.Append, FileAccess.Write, FileShare.Read);
            using var writer = new StreamWriter(stream, Encoding.UTF8);
            foreach (var entry in batch)
            {
                var props = LogRedactor.RedactProperties(entry.Properties);
                var persisted = new PersistedEntry
                {
                    TimestampUtc = entry.TimestampUtc,
                    Category = entry.Category,
                    EventName = entry.EventName,
                    Message = LogRedactor.Redact(entry.Message),
                    Properties = props is null ? null : new Dictionary<string, string>(props),
                };
                writer.WriteLine(JsonSerializer.Serialize(persisted));
            }
        }
    }

    private static void RotateIfNeeded(string path)
    {
        if (!File.Exists(path))
        {
            return;
        }

        var info = new FileInfo(path);
        if (info.Length < MaxFileBytes)
        {
            return;
        }

        var rotated = path + ".1";
        if (File.Exists(rotated))
        {
            File.Delete(rotated);
        }

        File.Move(path, rotated);
    }

    public async ValueTask DisposeAsync()
    {
        _cts.Cancel();
        try
        {
            await _writerTask.ConfigureAwait(false);
        }
        catch
        {
            // Best effort shutdown.
        }

        _signal.Dispose();
        _cts.Dispose();
    }

    private sealed class PersistedEntry
    {
        public DateTimeOffset TimestampUtc { get; set; }
        public DiagnosticEventCategory Category { get; set; }
        public string EventName { get; set; } = string.Empty;
        public string Message { get; set; } = string.Empty;
        public Dictionary<string, string>? Properties { get; set; }
    }
}
