using System.Globalization;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery;

namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging;

/// <summary>
/// Processes event log queries locally against SQLite.
/// Supports filtering by event_type, time range, instance_id, severity,
/// and keyword search (LIKE on message and source).
/// Uses keyset pagination with continuation token (last event ID).
/// </summary>
public class EventLogQueryService : IEventLogQueryService
{
    private readonly SiteEventLogger _eventLogger;
    private readonly SiteEventLogOptions _options;
    private readonly ILogger<EventLogQueryService> _logger;

    /// <summary>Initializes a new instance of <see cref="EventLogQueryService"/>.</summary>
    /// <param name="eventLogger">The concrete event logger providing lock-guarded database access.</param>
    /// <param name="options">Site event log options (page size and other query settings).</param>
    /// <param name="logger">Logger instance.</param>
    public EventLogQueryService(
        SiteEventLogger eventLogger,
        IOptions<SiteEventLogOptions> options,
        ILogger<EventLogQueryService> logger)
    {
        // Depend on the concrete recorder directly: queries must funnel database
        // access through its lock-guarded WithConnection. Taking ISiteEventLogger and
        // downcasting would throw InvalidCastException for any other implementation.
        _eventLogger = eventLogger;
        _options = options.Value;
        _logger = logger;
    }

    /// <summary>
    /// Escapes the SQL <c>LIKE</c> metacharacters (<c>\</c>, <c>%</c>, <c>_</c>) in a
    /// user-supplied keyword so it is matched as a literal substring. Used together
    /// with a <c>LIKE ... ESCAPE '\'</c> clause.
    /// </summary>
    private static string EscapeLikePattern(string input)
    {
        return input
            .Replace("\\", "\\\\")
            .Replace("%", "\\%")
            .Replace("_", "\\_");
    }

    /// <inheritdoc />
    public EventLogQueryResponse ExecuteQuery(EventLogQueryRequest request)
    {
        try
        {
            // SiteEventLogging-017: clamp caller-supplied PageSize to a hard upper
            // bound so a central client sending int.MaxValue can't force the query
            // to materialise the entire log into a single list while holding the
            // shared write lock. Silent clamp — misconfigured clients still get a
            // usable response.
            var requestedSize = request.PageSize > 0 ? request.PageSize : _options.QueryPageSize;
            var pageSize = Math.Min(requestedSize, _options.MaxQueryPageSize);

            var whereClauses = new List<string>();
            var parameters = new List<SqliteParameter>();

            // Keyset pagination: only return events with id > continuation token
            if (request.ContinuationToken.HasValue)
            {
                whereClauses.Add("id > $afterId");
                parameters.Add(new SqliteParameter("$afterId", request.ContinuationToken.Value));
            }

            if (request.From.HasValue)
            {
                whereClauses.Add("timestamp >= $from");
                parameters.Add(new SqliteParameter("$from", request.From.Value.ToString("o")));
            }

            if (request.To.HasValue)
            {
                whereClauses.Add("timestamp <= $to");
                parameters.Add(new SqliteParameter("$to", request.To.Value.ToString("o")));
            }

            if (!string.IsNullOrWhiteSpace(request.EventType))
            {
                whereClauses.Add("event_type = $eventType");
                parameters.Add(new SqliteParameter("$eventType", request.EventType));
            }

            if (!string.IsNullOrWhiteSpace(request.Severity))
            {
                whereClauses.Add("severity = $severity");
                parameters.Add(new SqliteParameter("$severity", request.Severity));
            }

            if (!string.IsNullOrWhiteSpace(request.InstanceId))
            {
                whereClauses.Add("instance_id = $instanceId");
                parameters.Add(new SqliteParameter("$instanceId", request.InstanceId));
            }

            if (!string.IsNullOrWhiteSpace(request.KeywordFilter))
            {
                // Keyword search is a literal substring match. The LIKE
                // metacharacters % and _ (and the escape char itself) must be
                // escaped so identifiers such as "store_and_forward" or a literal
                // "%" are not misinterpreted as wildcards (SiteEventLogging-013).
                var escaped = EscapeLikePattern(request.KeywordFilter);
                whereClauses.Add(
                    "(message LIKE $keyword ESCAPE '\\' OR source LIKE $keyword ESCAPE '\\')");
                parameters.Add(new SqliteParameter("$keyword", $"%{escaped}%"));
            }

            var whereClause = whereClauses.Count > 0
                ? "WHERE " + string.Join(" AND ", whereClauses)
                : "";

            // Run the read against the shared connection under the logger's write
            // lock — the connection is not thread-safe and is also used by the
            // recorder and the purge service on other threads.
            var entries = _eventLogger.WithConnection(connection =>
            {
                using var cmd = connection.CreateCommand();

                // Fetch pageSize + 1 to determine if there are more results
                cmd.CommandText = $"""
                    SELECT id, timestamp, event_type, severity, instance_id, source, message, details
                    FROM site_events
                    {whereClause}
                    ORDER BY id ASC
                    LIMIT $limit
                    """;
                cmd.Parameters.AddWithValue("$limit", pageSize + 1);
                foreach (var p in parameters)
                    cmd.Parameters.Add(p);

                var rows = new List<EventLogEntry>();
                using var reader = cmd.ExecuteReader();
                while (reader.Read())
                {
                    rows.Add(new EventLogEntry(
                        Id: reader.GetInt64(0),
                        // Parse with explicit invariant culture and round-trip style
                        // (SiteEventLogging-021). Stored values are ISO 8601 "o" UTC
                        // (see SiteEventLogger.LogEventAsync), and the recorder's
                        // emitted offset is always +00:00; AssumeUniversal +
                        // AdjustToUniversal guarantees the parsed value is UTC and
                        // does not depend on the host's CurrentCulture.
                        Timestamp: DateTimeOffset.Parse(
                            reader.GetString(1),
                            CultureInfo.InvariantCulture,
                            DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal),
                        EventType: reader.GetString(2),
                        Severity: reader.GetString(3),
                        InstanceId: reader.IsDBNull(4) ? null : reader.GetString(4),
                        Source: reader.GetString(5),
                        Message: reader.GetString(6),
                        Details: reader.IsDBNull(7) ? null : reader.GetString(7)));
                }

                return rows;
            });

            var hasMore = entries.Count > pageSize;
            if (hasMore)
            {
                entries.RemoveAt(entries.Count - 1);
            }

            var continuationToken = entries.Count > 0 ? entries[^1].Id : (long?)null;

            return new EventLogQueryResponse(
                CorrelationId: request.CorrelationId,
                SiteId: request.SiteId,
                Entries: entries,
                ContinuationToken: continuationToken,
                HasMore: hasMore,
                Success: true,
                ErrorMessage: null,
                Timestamp: DateTimeOffset.UtcNow);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to execute event log query: {CorrelationId}", request.CorrelationId);
            return new EventLogQueryResponse(
                CorrelationId: request.CorrelationId,
                SiteId: request.SiteId,
                Entries: [],
                ContinuationToken: null,
                HasMore: false,
                Success: false,
                ErrorMessage: ex.Message,
                Timestamp: DateTimeOffset.UtcNow);
        }
    }
}
