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

namespace YMhut.Box.Core.Logging;

public sealed class ResilientLogService(ILogService primary, AppPaths paths) : ILogService
{
    private readonly SemaphoreSlim _fallbackGate = new(1, 1);

    public event EventHandler<LogEntry>? EntryWritten;

    public string LogPath => primary.LogPath;

    private string FallbackPath => Path.Combine(paths.Logs, "app-log-fallback.jsonl");

    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);
        try
        {
            await primary.WriteAsync(level, category, message, detail, cancellationToken).ConfigureAwait(false);
            EntryWritten?.Invoke(this, entry);
        }
        catch (Exception exception)
        {
            var fallbackEntry = entry with
            {
                Detail = string.Join(Environment.NewLine, new[]
                {
                    detail,
                    $"SQLite logging degraded: {exception.GetType().Name}: {Sanitize(exception.Message)}"
                }.Where(value => !string.IsNullOrWhiteSpace(value)))
            };
            await WriteFallbackAsync(fallbackEntry, cancellationToken).ConfigureAwait(false);
            EntryWritten?.Invoke(this, fallbackEntry);
        }
    }

    public async Task<IReadOnlyList<LogEntry>> ReadAsync(
        string? level = null,
        string? category = null,
        int take = 500,
        CancellationToken cancellationToken = default)
    {
        try
        {
            return await primary.ReadAsync(level, category, take, cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            return (await ReadFallbackAsync(cancellationToken).ConfigureAwait(false))
                .Where(entry => string.IsNullOrWhiteSpace(level) || entry.Level == level)
                .Where(entry => string.IsNullOrWhiteSpace(category) || entry.Category == category)
                .OrderByDescending(entry => entry.Timestamp)
                .Take(Math.Clamp(take, 1, 5000))
                .ToArray();
        }
    }

    public async Task<IReadOnlyList<LogEntry>> ReadByDateAsync(
        DateOnly date,
        string? level = null,
        string? query = null,
        int take = 0,
        CancellationToken cancellationToken = default)
    {
        try
        {
            return await primary.ReadByDateAsync(date, level, query, take, cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            var entries = FilterFallbackByDate(await ReadFallbackAsync(cancellationToken).ConfigureAwait(false), date, level, query);
            return take > 0 ? entries.Take(take).ToArray() : entries.ToArray();
        }
    }

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

    public async Task CleanupAsync(int retentionCount, CancellationToken cancellationToken = default)
    {
        try
        {
            await primary.CleanupAsync(retentionCount, cancellationToken).ConfigureAwait(false);
        }
        catch
        {
            await TrimFallbackAsync(retentionCount, cancellationToken).ConfigureAwait(false);
        }
    }

    public async Task ClearAllAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            await primary.ClearAllAsync(cancellationToken).ConfigureAwait(false);
        }
        catch
        {
        }

        await ClearFallbackAsync(_ => true, cancellationToken).ConfigureAwait(false);
    }

    public async Task ClearTodayAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            await primary.ClearTodayAsync(cancellationToken).ConfigureAwait(false);
        }
        catch
        {
        }

        var today = DateOnly.FromDateTime(DateTime.Now);
        await ClearFallbackAsync(entry => DateOnly.FromDateTime(entry.Timestamp.LocalDateTime) == today, cancellationToken).ConfigureAwait(false);
    }

    public async Task ClearFilteredTodayAsync(
        string? level = null,
        string? query = null,
        CancellationToken cancellationToken = default)
    {
        try
        {
            await primary.ClearFilteredTodayAsync(level, query, cancellationToken).ConfigureAwait(false);
        }
        catch
        {
        }

        var today = DateOnly.FromDateTime(DateTime.Now);
        await ClearFallbackAsync(entry =>
            DateOnly.FromDateTime(entry.Timestamp.LocalDateTime) == today &&
            (string.IsNullOrWhiteSpace(level) || entry.Level == level) &&
            Matches(entry, query), cancellationToken).ConfigureAwait(false);
    }

    private async Task WriteFallbackAsync(LogEntry entry, CancellationToken cancellationToken)
    {
        await _fallbackGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Directory.CreateDirectory(paths.Logs);
            var json = JsonSerializer.Serialize(entry);
            await File.AppendAllTextAsync(FallbackPath, json + Environment.NewLine, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _fallbackGate.Release();
        }
    }

    private async Task<IReadOnlyList<LogEntry>> ReadFallbackAsync(CancellationToken cancellationToken)
    {
        await _fallbackGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            if (!File.Exists(FallbackPath))
            {
                return [];
            }

            var entries = new List<LogEntry>();
            foreach (var line in await File.ReadAllLinesAsync(FallbackPath, cancellationToken).ConfigureAwait(false))
            {
                try
                {
                    var entry = JsonSerializer.Deserialize<LogEntry>(line);
                    if (entry is not null)
                    {
                        entries.Add(entry);
                    }
                }
                catch (JsonException)
                {
                }
            }

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

    private async Task TrimFallbackAsync(int retentionCount, CancellationToken cancellationToken)
    {
        var keep = Math.Max(0, retentionCount);
        var entries = (await ReadFallbackAsync(cancellationToken).ConfigureAwait(false))
            .OrderByDescending(entry => entry.Timestamp)
            .Take(keep)
            .Reverse()
            .ToArray();
        await RewriteFallbackAsync(entries, cancellationToken).ConfigureAwait(false);
    }

    private async Task ClearFallbackAsync(Func<LogEntry, bool> predicate, CancellationToken cancellationToken)
    {
        var entries = (await ReadFallbackAsync(cancellationToken).ConfigureAwait(false))
            .Where(entry => !predicate(entry))
            .ToArray();
        await RewriteFallbackAsync(entries, cancellationToken).ConfigureAwait(false);
    }

    private async Task RewriteFallbackAsync(IReadOnlyList<LogEntry> entries, CancellationToken cancellationToken)
    {
        await _fallbackGate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            Directory.CreateDirectory(paths.Logs);
            var lines = entries.Select(entry => JsonSerializer.Serialize(entry)).ToArray();
            await File.WriteAllLinesAsync(FallbackPath, lines, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _fallbackGate.Release();
        }
    }

    private static IReadOnlyList<LogEntry> FilterFallbackByDate(IReadOnlyList<LogEntry> entries, DateOnly date, string? level, string? query)
    {
        return entries
            .Where(entry => DateOnly.FromDateTime(entry.Timestamp.LocalDateTime) == date)
            .Where(entry => string.IsNullOrWhiteSpace(level) || entry.Level == level)
            .Where(entry => Matches(entry, query))
            .OrderByDescending(entry => entry.Timestamp)
            .ToArray();
    }

    private static bool Matches(LogEntry entry, string? query)
    {
        if (string.IsNullOrWhiteSpace(query))
        {
            return true;
        }

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

    private static string Sanitize(string value)
        => string.IsNullOrWhiteSpace(value) ? string.Empty : value.Length > 240 ? value[..240] : value;
}
