using System.Text.Json;
using YMhut.Box.Core.App;

namespace YMhut.Box.Core.Logging;

public sealed class JsonFileLogService : ILogService
{
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
    private readonly SemaphoreSlim _gate = new(1, 1);

    public JsonFileLogService(AppPaths paths)
    {
        paths.EnsureCreated();
        LogPath = Path.Combine(paths.Logs, "app-log.jsonl");
    }

    public event EventHandler<LogEntry>? EntryWritten;

    public string LogPath { get; }

    public async Task WriteAsync(
        string level,
        string category,
        string message,
        string? detail = null,
        CancellationToken cancellationToken = default)
    {
        var entry = new LogEntry(DateTimeOffset.Now, level, category, message, detail);
        var line = JsonSerializer.Serialize(entry, JsonOptions);

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
            await File.AppendAllTextAsync(LogPath, line + Environment.NewLine, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }

        EntryWritten?.Invoke(this, entry);
    }

    public async Task<IReadOnlyList<LogEntry>> ReadAsync(
        string? level = null,
        string? category = null,
        int take = 500,
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(LogPath))
        {
            return [];
        }

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var entries = new List<LogEntry>();
            foreach (var line in await File.ReadAllLinesAsync(LogPath, cancellationToken).ConfigureAwait(false))
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                try
                {
                    var entry = JsonSerializer.Deserialize<LogEntry>(line, JsonOptions);
                    if (entry is not null)
                    {
                        entries.Add(entry);
                    }
                }
                catch (JsonException)
                {
                }
            }

            return entries
                .Where(entry => string.IsNullOrWhiteSpace(level) || string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase))
                .Where(entry => string.IsNullOrWhiteSpace(category) || string.Equals(entry.Category, category, StringComparison.OrdinalIgnoreCase))
                .OrderByDescending(entry => entry.Timestamp)
                .Take(Math.Clamp(take, 1, 5000))
                .ToList();
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<IReadOnlyList<LogEntry>> ReadByDateAsync(
        DateOnly date,
        string? level = null,
        string? query = null,
        int take = 0,
        CancellationToken cancellationToken = default)
    {
        var filtered = await ReadByDateFilteredAsync(date, level, query, cancellationToken).ConfigureAwait(false);
        return take > 0 ? filtered.Take(take).ToList() : filtered.ToList();
    }

    public async Task<IReadOnlyList<LogEntry>> ReadByDatePageAsync(
        DateOnly date,
        string? level = null,
        string? query = null,
        int skip = 0,
        int take = 100,
        CancellationToken cancellationToken = default)
    {
        var filtered = await ReadByDateFilteredAsync(date, level, query, cancellationToken).ConfigureAwait(false);
        return filtered
            .Skip(Math.Max(0, skip))
            .Take(Math.Clamp(take, 1, 5000))
            .ToList();
    }

    private async Task<IReadOnlyList<LogEntry>> ReadByDateFilteredAsync(
        DateOnly date,
        string? level,
        string? query,
        CancellationToken cancellationToken)
    {
        var entries = await ReadAllEntriesAsync(cancellationToken).ConfigureAwait(false);
        var normalizedQuery = query?.Trim() ?? string.Empty;
        return entries
            .Where(entry => DateOnly.FromDateTime(entry.Timestamp.LocalDateTime) == date)
            .Where(entry => string.IsNullOrWhiteSpace(level) || string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase))
            .Where(entry => string.IsNullOrWhiteSpace(normalizedQuery)
                || entry.Message.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase)
                || entry.Category.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase)
                || (entry.Detail?.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase) ?? false))
            .OrderByDescending(entry => entry.Timestamp)
            .ToList();
    }

    private async Task<IReadOnlyList<LogEntry>> ReadAllEntriesAsync(CancellationToken cancellationToken)
    {
        if (!File.Exists(LogPath))
        {
            return [];
        }

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var entries = new List<LogEntry>();
            foreach (var line in await File.ReadAllLinesAsync(LogPath, cancellationToken).ConfigureAwait(false))
            {
                if (string.IsNullOrWhiteSpace(line))
                {
                    continue;
                }

                try
                {
                    var entry = JsonSerializer.Deserialize<LogEntry>(line, JsonOptions);
                    if (entry is not null)
                    {
                        entries.Add(entry);
                    }
                }
                catch (JsonException)
                {
                }
            }

            return entries;
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task CleanupAsync(int retentionCount, CancellationToken cancellationToken = default)
    {
        if (!File.Exists(LogPath))
        {
            return;
        }

        var keep = Math.Max(0, retentionCount);
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var lines = await File.ReadAllLinesAsync(LogPath, cancellationToken).ConfigureAwait(false);
            var retained = keep == 0 ? [] : lines.TakeLast(keep).ToArray();
            await File.WriteAllLinesAsync(LogPath, retained, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearAllAsync(CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (File.Exists(LogPath))
            {
                File.Delete(LogPath);
            }
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearTodayAsync(CancellationToken cancellationToken = default)
    {
        if (!File.Exists(LogPath))
        {
            return;
        }

        var today = DateTimeOffset.Now.Date;
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var retained = new List<string>();
            foreach (var line in await File.ReadAllLinesAsync(LogPath, cancellationToken).ConfigureAwait(false))
            {
                try
                {
                    var entry = JsonSerializer.Deserialize<LogEntry>(line, JsonOptions);
                    if (entry is null || entry.Timestamp.Date != today)
                    {
                        retained.Add(line);
                    }
                }
                catch (JsonException)
                {
                    retained.Add(line);
                }
            }

            await File.WriteAllLinesAsync(LogPath, retained, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearFilteredTodayAsync(
        string? level = null,
        string? query = null,
        CancellationToken cancellationToken = default)
    {
        if (!File.Exists(LogPath))
        {
            return;
        }

        var today = DateTimeOffset.Now.Date;
        var normalizedQuery = query?.Trim() ?? string.Empty;
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            var retained = new List<string>();
            foreach (var line in await File.ReadAllLinesAsync(LogPath, cancellationToken).ConfigureAwait(false))
            {
                try
                {
                    var entry = JsonSerializer.Deserialize<LogEntry>(line, JsonOptions);
                    if (entry is null || !Matches(entry))
                    {
                        retained.Add(line);
                    }
                }
                catch (JsonException)
                {
                    retained.Add(line);
                }
            }

            await File.WriteAllLinesAsync(LogPath, retained, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }

        bool Matches(LogEntry entry)
        {
            if (entry.Timestamp.Date != today)
            {
                return false;
            }

            if (!string.IsNullOrWhiteSpace(level) && !string.Equals(entry.Level, level, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            return string.IsNullOrWhiteSpace(normalizedQuery)
                || entry.Message.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase)
                || entry.Category.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase)
                || (entry.Detail?.Contains(normalizedQuery, StringComparison.OrdinalIgnoreCase) ?? false);
        }
    }
}
