using System.Threading.Channels;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace ScadaLink.SiteEventLogging;

/// <summary>
/// Records operational events to a local SQLite database.
/// Only the active node generates events. Not replicated to standby.
/// On failover, the new active node starts a fresh log.
/// </summary>
/// <remarks>
/// <para>
/// A single <see cref="SqliteConnection"/> is owned here and is NOT thread-safe.
/// All access — recording, querying, purging — must be funnelled through
/// <see cref="WithConnection"/>, which serialises callers on a shared lock.
/// </para>
/// <para>
/// Event recording is offloaded to a dedicated background writer thread (fed by an
/// unbounded <see cref="Channel{T}"/>). <see cref="LogEventAsync"/> only validates
/// its arguments and enqueues, so callers — typically Akka actor threads on hot
/// paths — never block on disk I/O or on contention for the write lock. The
/// returned <see cref="Task"/> completes once the event is durably persisted and
/// faults if the write fails, so failures are observable rather than swallowed.
/// </para>
/// </remarks>
public class SiteEventLogger : ISiteEventLogger, IDisposable
{
    private readonly SqliteConnection _connection;
    private readonly ILogger<SiteEventLogger> _logger;
    private readonly object _writeLock = new();
    private readonly Channel<PendingEvent> _writeQueue;
    private readonly Task _writerLoop;
    private long _failedWriteCount;
    private bool _disposed;

    public SiteEventLogger(
        IOptions<SiteEventLogOptions> options,
        ILogger<SiteEventLogger> logger,
        string? connectionStringOverride = null)
    {
        _logger = logger;

        var connectionString = connectionStringOverride
            ?? $"Data Source={options.Value.DatabasePath};Cache=Shared";
        _connection = new SqliteConnection(connectionString);
        _connection.Open();

        InitializeSchema();

        _writeQueue = Channel.CreateUnbounded<PendingEvent>(new UnboundedChannelOptions
        {
            SingleReader = true,
            SingleWriter = false,
        });
        _writerLoop = Task.Run(ProcessWriteQueueAsync);
    }

    /// <summary>
    /// Number of event writes that have failed (SQLite error, disk full, etc.)
    /// since this logger was created. Surfaced so Health Monitoring can detect a
    /// logging outage instead of relying on a local log line nobody is watching.
    /// </summary>
    public long FailedWriteCount => Interlocked.Read(ref _failedWriteCount);

    /// <summary>
    /// Runs <paramref name="action"/> against the shared connection while holding the
    /// write lock, so purge / query / record callers on different threads never use
    /// the non-thread-safe <see cref="SqliteConnection"/> concurrently.
    /// Returns <see langword="false"/> without invoking the action if the logger has
    /// been disposed.
    /// </summary>
    internal bool WithConnection(Action<SqliteConnection> action)
    {
        ArgumentNullException.ThrowIfNull(action);
        lock (_writeLock)
        {
            if (_disposed) return false;
            action(_connection);
            return true;
        }
    }

    /// <summary>
    /// Runs <paramref name="func"/> against the shared connection while holding the
    /// write lock. Throws <see cref="ObjectDisposedException"/> if the logger has
    /// been disposed (callers that need a result cannot proceed without the database).
    /// </summary>
    internal T WithConnection<T>(Func<SqliteConnection, T> func)
    {
        ArgumentNullException.ThrowIfNull(func);
        lock (_writeLock)
        {
            ObjectDisposedException.ThrowIf(_disposed, this);
            return func(_connection);
        }
    }

    private void InitializeSchema()
    {
        // auto_vacuum must be set before any table is created for it to take effect
        // on a fresh database. With INCREMENTAL mode, PRAGMA incremental_vacuum can
        // later reclaim free pages so the storage-cap purge can shrink the file.
        using (var pragmaCmd = _connection.CreateCommand())
        {
            pragmaCmd.CommandText = "PRAGMA auto_vacuum = INCREMENTAL";
            pragmaCmd.ExecuteNonQuery();
        }

        using var cmd = _connection.CreateCommand();
        cmd.CommandText = """
            CREATE TABLE IF NOT EXISTS site_events (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                timestamp TEXT NOT NULL,
                event_type TEXT NOT NULL,
                severity TEXT NOT NULL,
                instance_id TEXT,
                source TEXT NOT NULL,
                message TEXT NOT NULL,
                details TEXT
            );
            CREATE INDEX IF NOT EXISTS idx_events_timestamp ON site_events(timestamp);
            CREATE INDEX IF NOT EXISTS idx_events_type ON site_events(event_type);
            CREATE INDEX IF NOT EXISTS idx_events_instance ON site_events(instance_id);
            """;
        cmd.ExecuteNonQuery();
    }

    public Task LogEventAsync(
        string eventType,
        string severity,
        string? instanceId,
        string source,
        string message,
        string? details = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(eventType);
        ArgumentException.ThrowIfNullOrWhiteSpace(severity);
        ArgumentException.ThrowIfNullOrWhiteSpace(source);
        ArgumentException.ThrowIfNullOrWhiteSpace(message);

        var pending = new PendingEvent(
            DateTimeOffset.UtcNow.ToString("o"),
            eventType,
            severity,
            instanceId,
            source,
            message,
            details);

        // Enqueue only — the actual SQLite write happens on the background writer
        // thread so the caller (an Akka actor thread on a hot path) never blocks
        // on disk I/O or on contention for the write lock.
        if (!_writeQueue.Writer.TryWrite(pending))
        {
            // The channel is unbounded, so the only way TryWrite fails is that the
            // writer has been completed (logger disposed). Drop silently — there is
            // nowhere to persist the event.
            pending.Completion.TrySetResult();
        }

        return pending.Completion.Task;
    }

    private async Task ProcessWriteQueueAsync()
    {
        await foreach (var pending in _writeQueue.Reader.ReadAllAsync().ConfigureAwait(false))
        {
            try
            {
                var written = WithConnection(connection =>
                {
                    using var cmd = connection.CreateCommand();
                    cmd.CommandText = """
                        INSERT INTO site_events (timestamp, event_type, severity, instance_id, source, message, details)
                        VALUES ($timestamp, $event_type, $severity, $instance_id, $source, $message, $details)
                        """;
                    cmd.Parameters.AddWithValue("$timestamp", pending.Timestamp);
                    cmd.Parameters.AddWithValue("$event_type", pending.EventType);
                    cmd.Parameters.AddWithValue("$severity", pending.Severity);
                    cmd.Parameters.AddWithValue("$instance_id", (object?)pending.InstanceId ?? DBNull.Value);
                    cmd.Parameters.AddWithValue("$source", pending.Source);
                    cmd.Parameters.AddWithValue("$message", pending.Message);
                    cmd.Parameters.AddWithValue("$details", (object?)pending.Details ?? DBNull.Value);
                    cmd.ExecuteNonQuery();
                });

                // WithConnection returns false only when the logger has been
                // disposed; the event simply cannot be persisted in that case.
                pending.Completion.TrySetResult();
            }
            catch (Exception ex)
            {
                // SiteEventLogging-008: a write failure must be observable. Count it
                // (Health Monitoring reads FailedWriteCount) and fault the caller's
                // Task instead of silently discarding the exception.
                Interlocked.Increment(ref _failedWriteCount);
                _logger.LogError(ex, "Failed to record event: {EventType} from {Source}",
                    pending.EventType, pending.Source);
                pending.Completion.TrySetException(ex);
            }
        }
    }

    public void Dispose()
    {
        Task? writerLoop = null;
        lock (_writeLock)
        {
            if (_disposed) return;
            _disposed = true;
            // Stop accepting new events and let the writer loop drain.
            _writeQueue.Writer.TryComplete();
            writerLoop = _writerLoop;
        }

        // Wait for the writer loop to finish outside the lock — the loop itself
        // acquires the lock for each write.
        try
        {
            writerLoop?.Wait(TimeSpan.FromSeconds(5));
        }
        catch (AggregateException)
        {
            // A faulted writer loop has already been logged per event; nothing more
            // to do during disposal.
        }

        lock (_writeLock)
        {
            _connection.Dispose();
        }
    }

    /// <summary>An event awaiting persistence by the background writer.</summary>
    private sealed record PendingEvent(
        string Timestamp,
        string EventType,
        string Severity,
        string? InstanceId,
        string Source,
        string Message,
        string? Details)
    {
        public TaskCompletionSource Completion { get; } =
            new(TaskCreationOptions.RunContinuationsAsynchronously);
    }
}
