using Microsoft.Data.Sqlite;
using YMhut.Box.Core.App;

namespace YMhut.Box.Core.Logging;

public sealed class SqliteLogService : ILogService
{
    private readonly SemaphoreSlim _gate = new(1, 1);
    private bool _initialized;

    public SqliteLogService(AppPaths paths)
    {
        paths.EnsureCreated();
        LogPath = AppDatabasePaths.ResolveMainDatabasePath(paths);
    }

    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);
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                INSERT INTO logs(timestamp, level, category, message, detail)
                VALUES ($timestamp, $level, $category, $message, $detail);
                """;
            command.Parameters.AddWithValue("$timestamp", entry.Timestamp.ToString("O"));
            command.Parameters.AddWithValue("$level", entry.Level);
            command.Parameters.AddWithValue("$category", entry.Category);
            command.Parameters.AddWithValue("$message", entry.Message);
            command.Parameters.AddWithValue("$detail", (object?)entry.Detail ?? DBNull.Value);
            await command.ExecuteNonQueryAsync(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)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                SELECT timestamp, level, category, message, detail
                FROM logs
                WHERE ($level = '' OR level = $level)
                  AND ($category = '' OR category = $category)
                ORDER BY timestamp DESC
                LIMIT $take;
                """;
            command.Parameters.AddWithValue("$level", level ?? string.Empty);
            command.Parameters.AddWithValue("$category", category ?? string.Empty);
            command.Parameters.AddWithValue("$take", Math.Clamp(take, 1, 5000));

            var entries = new List<LogEntry>();
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                entries.Add(new LogEntry(
                    DateTimeOffset.TryParse(reader.GetString(0), out var timestamp) ? timestamp : DateTimeOffset.Now,
                    reader.GetString(1),
                    reader.GetString(2),
                    reader.GetString(3),
                    reader.IsDBNull(4) ? null : reader.GetString(4)));
            }

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

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

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

    private async Task<IReadOnlyList<LogEntry>> ReadByDatePageCoreAsync(
        DateOnly date,
        string? level,
        string? query,
        int skip,
        int take,
        bool usePaging,
        CancellationToken cancellationToken)
    {
        var start = date.ToDateTime(TimeOnly.MinValue);
        var end = start.AddDays(1);
        var normalizedQuery = query?.Trim() ?? string.Empty;

        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = usePaging
                ? """
                  SELECT timestamp, level, category, message, detail
                  FROM logs
                  WHERE timestamp >= $start
                    AND timestamp < $end
                    AND ($level = '' OR level = $level)
                    AND (
                        $query = ''
                        OR message LIKE $like
                        OR category LIKE $like
                        OR IFNULL(detail, '') LIKE $like
                    )
                  ORDER BY timestamp DESC
                  LIMIT $take OFFSET $skip;
                  """
                : """
                  SELECT timestamp, level, category, message, detail
                  FROM logs
                  WHERE timestamp >= $start
                    AND timestamp < $end
                    AND ($level = '' OR level = $level)
                    AND (
                        $query = ''
                        OR message LIKE $like
                        OR category LIKE $like
                        OR IFNULL(detail, '') LIKE $like
                    )
                  ORDER BY timestamp DESC;
                  """;
            command.Parameters.AddWithValue("$start", start.ToString("O"));
            command.Parameters.AddWithValue("$end", end.ToString("O"));
            command.Parameters.AddWithValue("$level", level ?? string.Empty);
            command.Parameters.AddWithValue("$query", normalizedQuery);
            command.Parameters.AddWithValue("$like", $"%{normalizedQuery}%");
            if (usePaging)
            {
                command.Parameters.AddWithValue("$take", Math.Clamp(take, 1, 5000));
                command.Parameters.AddWithValue("$skip", Math.Max(0, skip));
            }

            var entries = new List<LogEntry>();
            await using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                entries.Add(new LogEntry(
                    DateTimeOffset.TryParse(reader.GetString(0), out var timestamp) ? timestamp : DateTimeOffset.Now,
                    reader.GetString(1),
                    reader.GetString(2),
                    reader.GetString(3),
                    reader.IsDBNull(4) ? null : reader.GetString(4)));
            }

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

    public async Task CleanupAsync(int retentionCount, CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            var keep = Math.Max(0, retentionCount);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = keep == 0
                ? "DELETE FROM logs;"
                : """
                  DELETE FROM logs
                  WHERE id NOT IN (
                      SELECT id FROM logs ORDER BY timestamp DESC LIMIT $keep
                  );
                  """;
            if (keep > 0)
            {
                command.Parameters.AddWithValue("$keep", keep);
            }

            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            await VacuumAsync(connection, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearAllAsync(CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = "DELETE FROM logs;";
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            await VacuumAsync(connection, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearTodayAsync(CancellationToken cancellationToken = default)
    {
        var today = DateTimeOffset.Now.Date;
        var tomorrow = today.AddDays(1);
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                DELETE FROM logs
                WHERE timestamp >= $today
                  AND timestamp < $tomorrow;
                """;
            command.Parameters.AddWithValue("$today", today.ToString("O"));
            command.Parameters.AddWithValue("$tomorrow", tomorrow.ToString("O"));
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            await VacuumAsync(connection, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task ClearFilteredTodayAsync(
        string? level = null,
        string? query = null,
        CancellationToken cancellationToken = default)
    {
        var today = DateTimeOffset.Now.Date;
        var tomorrow = today.AddDays(1);
        var normalizedQuery = query?.Trim() ?? string.Empty;
        await _gate.WaitAsync(cancellationToken).ConfigureAwait(false);
        try
        {
            await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
            await using var connection = OpenConnection();
            await using var command = connection.CreateCommand();
            command.CommandText = """
                DELETE FROM logs
                WHERE timestamp >= $today
                  AND timestamp < $tomorrow
                  AND ($level = '' OR level = $level)
                  AND (
                      $query = ''
                      OR message LIKE $like
                      OR category LIKE $like
                      OR IFNULL(detail, '') LIKE $like
                  );
                """;
            command.Parameters.AddWithValue("$today", today.ToString("O"));
            command.Parameters.AddWithValue("$tomorrow", tomorrow.ToString("O"));
            command.Parameters.AddWithValue("$level", level ?? string.Empty);
            command.Parameters.AddWithValue("$query", normalizedQuery);
            command.Parameters.AddWithValue("$like", $"%{normalizedQuery}%");
            await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
            await VacuumAsync(connection, cancellationToken).ConfigureAwait(false);
        }
        finally
        {
            _gate.Release();
        }
    }

    private async Task EnsureInitializedAsync(CancellationToken cancellationToken)
    {
        if (_initialized)
        {
            return;
        }

        Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
        await using var connection = OpenConnection();
        await using var command = connection.CreateCommand();
        command.CommandText = """
            PRAGMA journal_mode = WAL;
            PRAGMA synchronous = NORMAL;
            PRAGMA busy_timeout = 5000;
            CREATE TABLE IF NOT EXISTS logs (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                level TEXT NOT NULL,
                category TEXT NOT NULL,
                message TEXT NOT NULL,
                detail TEXT NULL
            );
            CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp);
            CREATE INDEX IF NOT EXISTS idx_logs_timestamp_level ON logs(timestamp DESC, level);
            CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level);
            CREATE INDEX IF NOT EXISTS idx_logs_category ON logs(category);
            """;
        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
        _initialized = true;
    }

    private SqliteConnection OpenConnection()
    {
        var connection = new SqliteConnection($"Data Source={LogPath};Pooling=True");
        connection.Open();
        using var command = connection.CreateCommand();
        command.CommandText = """
            PRAGMA busy_timeout = 5000;
            PRAGMA synchronous = NORMAL;
            """;
        command.ExecuteNonQuery();
        return connection;
    }

    private static async Task VacuumAsync(SqliteConnection connection, CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.CommandText = "VACUUM;";
        await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
    }
}
