using System.Globalization;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Audit;

/// <summary>
/// Code-behind for the central Audit Log page. Wires up <c>AuditFilterBar</c>
/// and <c>AuditResultsGrid</c>: the page owns the
/// active <see cref="AuditLogQueryFilter"/> and re-pushes a fresh instance to the
/// grid on every Apply (the grid uses reference identity as its "reload"
/// trigger). Row clicks land in <see cref="HandleRowSelected"/>, which pins
/// the selected row and opens the drilldown drawer.
///
/// <para>
/// Adds query-string drill-in parsing so other pages can
/// deep-link to a pre-filtered Audit Log: <c>?correlationId=</c>, <c>?target=</c>,
/// <c>?actor=</c>, <c>?site=</c>, <c>?channel=</c>, <c>?kind=</c>, and the UI-only
/// <c>?instance=</c> are read on initialization. This extends
/// with <c>?status=</c> so the Health-dashboard Audit error-rate tile can
/// drill in to <c>?status=Failed</c>. The ExecutionId follow-up adds
/// <c>?executionId=</c> for the "View this execution" drill-in, and the
/// ParentExecutionId follow-up adds <c>?parentExecutionId=</c> for the
/// "View parent execution" drill-in. When any param is present we allocate a
/// fresh <see cref="AuditLogQueryFilter"/> and assign it to
/// <see cref="_currentFilter"/>, which kicks the results grid into auto-load
/// without the user clicking Apply. Unknown values (e.g. an invalid enum name)
/// are silently dropped — the page still renders, just without that constraint.
/// </para>
///
/// <para>
/// Query-string filters are re-applied on every <see cref="NavigationManager.LocationChanged"/>,
/// not just on init. The drilldown drawer's "View this/parent execution" actions
/// navigate to <c>/audit/log?executionId=…</c> while the user is ALREADY on this
/// routed page — Blazor treats that as a same-component navigation, so
/// <see cref="OnInitialized"/> does not re-run. Without the
/// <see cref="NavigationManager.LocationChanged"/> subscription the URL would
/// change but <see cref="_currentFilter"/> would stay stale and the grid would
/// never reload to the new drill-in. The subscription is disposed via
/// <see cref="IDisposable"/>.
/// </para>
/// </summary>
public partial class AuditLogPage : IDisposable
{
    [Inject] private NavigationManager Navigation { get; set; } = null!;

    /// <summary>
    /// KPI-history facade for the Trends panel — fetched per metric,
    /// per window. Best-effort: a failed series fetch degrades that one chart to
    /// the unavailable placeholder and never breaks the audit query UI.
    /// </summary>
    [Inject] private IKpiHistoryQueryService KpiHistory { get; set; } = null!;

    /// <summary>Logger for the best-effort Trends panel — a degraded series fetch
    /// is logged at warning level so the silent fallback is still observable.</summary>
    [Inject] private ILogger<AuditLogPage> Logger { get; set; } = null!;

    private AuditLogQueryFilter? _currentFilter;
    private AuditEventView? _selectedEvent;
    private bool _drawerOpen;
    private string? _initialInstanceSearch;

    /// <inheritdoc />
    protected override void OnInitialized()
    {
        ApplyQueryStringFilters();
        Navigation.LocationChanged += HandleLocationChanged;
    }

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        // The Trends panel is open by default, so load its series on first render.
        // Done in OnInitializedAsync (not OnInitialized) so the per-metric awaits
        // do not block the synchronous query-string drill-in parsing above.
        if (_trendsOpen)
        {
            await LoadTrendsAsync();
        }
    }

    /// <summary>
    /// Re-applies the query-string drill-in filters when the URL changes while
    /// this page stays routed (e.g. the drawer's "View parent execution" action
    /// navigates to <c>/audit/log?executionId=…</c>). Reassigning
    /// <see cref="_currentFilter"/> to a fresh instance is what kicks the results
    /// grid into reloading; we also close the drawer so the operator sees the
    /// newly filtered grid. The body is marshalled through
    /// <see cref="ComponentBase.InvokeAsync(Action)"/> because
    /// <see cref="NavigationManager.LocationChanged"/> can fire off the renderer's
    /// synchronization context.
    /// </summary>
    private void HandleLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        _ = InvokeAsync(() =>
        {
            ApplyQueryStringFilters();
            _drawerOpen = false;
            StateHasChanged();
        });
    }

    /// <summary>Unsubscribes from navigation events to prevent memory leaks when the component is removed.</summary>
    public void Dispose()
    {
        Navigation.LocationChanged -= HandleLocationChanged;
    }

    private void ApplyQueryStringFilters()
    {
        var uri = Navigation.ToAbsoluteUri(Navigation.Uri);
        var query = QueryHelpers.ParseQuery(uri.Query);

        // A paramless navigation (e.g. clicking the "Audit Log" nav link while
        // already here) intentionally preserves the last applied filter rather
        // than clearing the grid: this method is a drill-in mechanism and every
        // drill-in carries query params. The operator clears via the filter bar.
        if (query.Count == 0)
        {
            return;
        }

        Guid? correlationId = null;
        if (query.TryGetValue("correlationId", out var corrValues)
            && Guid.TryParse(corrValues.ToString(), out var parsedCorr))
        {
            correlationId = parsedCorr;
        }

        // ?executionId= is the "View this execution" drill-in target — the
        // universal per-run correlation value. Lax-parsed like ?correlationId=:
        // an unparseable value is silently dropped (no constraint).
        Guid? executionId = null;
        if (query.TryGetValue("executionId", out var execValues)
            && Guid.TryParse(execValues.ToString(), out var parsedExec))
        {
            executionId = parsedExec;
        }

        // ?parentExecutionId= constrains to runs spawned by a given execution.
        // Lax-parsed like ?executionId=: an unparseable value is silently dropped.
        Guid? parentExecutionId = null;
        if (query.TryGetValue("parentExecutionId", out var parentExecValues)
            && Guid.TryParse(parentExecValues.ToString(), out var parsedParentExec))
        {
            parentExecutionId = parsedParentExec;
        }

        string? target = null;
        if (query.TryGetValue("target", out var targetValues))
        {
            var v = targetValues.ToString();
            if (!string.IsNullOrWhiteSpace(v))
            {
                target = v.Trim();
            }
        }

        string? actor = null;
        if (query.TryGetValue("actor", out var actorValues))
        {
            var v = actorValues.ToString();
            if (!string.IsNullOrWhiteSpace(v))
            {
                actor = v.Trim();
            }
        }

        // site/channel/kind/status accept repeated params for symmetry with the
        // multi-value export URL — a single ?site=/?channel=/?kind=/?status=
        // drill-in still works (one-element list). Unknown enum names are silently
        // dropped. The lax-parse contract is shared with the two export endpoints
        // via AuditQueryParamParsers so all three surfaces stay in lockstep.
        IReadOnlyList<string>? sites = AuditQueryParamParsers.ParseStringList(Raw(query, "site"));

        IReadOnlyList<AuditChannel>? channels =
            AuditQueryParamParsers.ParseEnumList<AuditChannel>(Raw(query, "channel"));

        // ?kind= is honored for symmetry with BuildExportUrl, which emits a kind=
        // param — a kind drill-in deep link must round-trip back into the filter.
        IReadOnlyList<AuditKind>? kinds =
            AuditQueryParamParsers.ParseEnumList<AuditKind>(Raw(query, "kind"));

        // The Health-dashboard Audit error-rate tile drills in
        // with ?status=Failed (and operators may craft URLs with Parked/Discarded).
        // Unknown values are silently dropped — the page still renders without
        // the constraint.
        IReadOnlyList<AuditStatus>? statuses =
            AuditQueryParamParsers.ParseEnumList<AuditStatus>(Raw(query, "status"));

        // Instance is UI-only — the filter contract has no matching column, so we
        // pass it as a separate seam to the filter bar.
        if (query.TryGetValue("instance", out var instanceValues))
        {
            var v = instanceValues.ToString();
            if (!string.IsNullOrWhiteSpace(v))
            {
                _initialInstanceSearch = v.Trim();
            }
        }

        // If ANY filter-shaped param was provided, allocate the filter so the grid
        // auto-loads. Pure ?instance= deep links (UI-only) do not trigger auto-load
        // because the filter contract has no instance column — the user still needs
        // to refine + Apply for those.
        if (correlationId is null && executionId is null && parentExecutionId is null
            && target is null && actor is null
            && sites is null && channels is null && kinds is null && statuses is null)
        {
            return;
        }

        _currentFilter = new AuditLogQueryFilter(
            Channels: channels,
            Kinds: kinds,
            Statuses: statuses,
            SourceSiteIds: sites,
            Target: target,
            Actor: actor,
            CorrelationId: correlationId,
            ExecutionId: executionId,
            ParentExecutionId: parentExecutionId);
    }

    /// <summary>
    /// Extracts the raw repeated values for one query-string key, returning
    /// <c>null</c> when the key is absent so the shared
    /// <see cref="AuditQueryParamParsers"/> sees the same absent-vs-present
    /// distinction the ASP.NET <c>IQueryCollection</c> callers do.
    /// <c>StringValues</c> is itself an <c>IEnumerable&lt;string?&gt;</c>.
    /// </summary>
    private static IEnumerable<string?>? Raw(
        Dictionary<string, Microsoft.Extensions.Primitives.StringValues> query, string key) =>
        query.TryGetValue(key, out var values) ? (IEnumerable<string?>)values : null;

    private void HandleFilterChanged(AuditLogQueryFilter filter)
    {
        // Always reassign — the grid keys reloads on reference change, so even a
        // chip-for-chip identical filter must allocate a fresh instance.
        _currentFilter = filter;
    }

    private void HandleRowSelected(AuditEventView row)
    {
        // A grid row click hands us the full AuditEvent. We pin it as
        // the selected row and open the drilldown drawer — the drawer is fully
        // presentational so we do not need to refetch the row.
        _selectedEvent = row;
        _drawerOpen = true;
    }

    private void HandleDrawerClose()
    {
        // We deliberately keep _selectedEvent set so re-opening (e.g. via the
        // grid) shows the same row instantly without a re-render flicker.
        _drawerOpen = false;
    }

    // ─────────────────────────────────────────────────────────────────────────
    // Audit Log trend charts.
    //
    // A best-effort Trends panel that sits above the audit query UI: one
    // KpiTrendChart per AuditLog global metric, over a 24h (default) or 7d
    // window. Each series is fetched independently and a failure degrades only
    // that chart (IsAvailable=false + short message) — the audit query UI is
    // never affected. Re-queries on init (panel open by default), on the window
    // toggle, and when the panel is re-shown after being collapsed.
    // ─────────────────────────────────────────────────────────────────────────

    /// <summary>Whether the Trends panel is expanded (open by default).</summary>
    private bool _trendsOpen = true;

    /// <summary>
    /// True while a window's series are being (re)fetched — disables the 24h/7d
    /// window toggle buttons so a mid-flight click cannot stack overlapping loads.
    /// </summary>
    private bool _trendsLoading;

    /// <summary>Active window in hours — 24 (default) or 168 (7 days).</summary>
    private int _windowHours = 24;

    /// <summary>One rendered chart's state: the fetched series + availability.</summary>
    private readonly record struct TrendSeries(
        IReadOnlyList<KpiSeriesPoint>? Points, bool IsAvailable, string? ErrorMessage);

    /// <summary>The metrics rendered in the panel, in display order.</summary>
    private static readonly (string Metric, string Title, string? Unit)[] TrendMetrics =
    {
        (KpiMetrics.AuditLog.TotalEventsLastHour, "Events / hour", null),
        (KpiMetrics.AuditLog.ErrorEventsLastHour, "Error events / hour", null),
        (KpiMetrics.AuditLog.BacklogTotal, "Backlog", null),
    };

    /// <summary>Per-metric series state, keyed by the metric name.</summary>
    private readonly Dictionary<string, TrendSeries> _trendSeries = new();

    /// <summary>
    /// Fetches every Trends metric for the current window. Each
    /// <see cref="IKpiHistoryQueryService.GetSeriesAsync"/> call is wrapped so a
    /// failure surfaces as an unavailable chart rather than throwing out of the
    /// page — audit-log functionality must never depend on KPI history being up.
    /// </summary>
    private async Task LoadTrendsAsync()
    {
        _trendsLoading = true;
        try
        {
            var toUtc = DateTime.UtcNow;
            var fromUtc = toUtc - TimeSpan.FromHours(_windowHours);

            foreach (var (metric, _, _) in TrendMetrics)
            {
                try
                {
                    var points = await KpiHistory.GetSeriesAsync(
                        KpiSources.AuditLog, metric, KpiScopes.Global, scopeKey: null,
                        fromUtc, toUtc);
                    _trendSeries[metric] = new TrendSeries(points, IsAvailable: true, ErrorMessage: null);
                }
                catch (Exception ex)
                {
                    // Best-effort: degrade this chart only, keep the rest of the page
                    // alive — but log the failure so the silent fallback is observable.
                    Logger.LogWarning(ex, "Failed to load Audit Log KPI trend series for metric {Metric}.", metric);
                    _trendSeries[metric] = new TrendSeries(
                        Points: null, IsAvailable: false, ErrorMessage: "Trend data unavailable.");
                }
            }
        }
        finally
        {
            _trendsLoading = false;
        }
    }

    /// <summary>Returns the rendered state for a metric, defaulting to available-empty.</summary>
    private TrendSeries SeriesFor(string metric) =>
        _trendSeries.TryGetValue(metric, out var s) ? s : new TrendSeries(null, true, null);

    /// <summary>Expands/collapses the panel; re-queries when it is (re)shown.</summary>
    private async Task ToggleTrendsAsync()
    {
        _trendsOpen = !_trendsOpen;
        if (_trendsOpen)
        {
            await LoadTrendsAsync();
        }
    }

    /// <summary>Switches the window (24h ↔ 7d) and re-queries every series.</summary>
    private async Task SetWindowAsync(int hours)
    {
        if (_windowHours == hours)
        {
            return;
        }

        _windowHours = hours;
        await LoadTrendsAsync();
    }

    /// <summary>
    /// URL the Export-CSV link points at. Renders the most
    /// recently applied filter as query-string params so the server-side
    /// streaming endpoint reproduces the user's current view. With no filter
    /// applied yet, returns the bare endpoint — i.e. an unconstrained export.
    /// </summary>
    /// <remarks>
    /// Built here rather than in markup so the per-row test coverage can
    /// exercise the URL composition without booting the full Blazor renderer.
    /// </remarks>
    internal string ExportUrl => BuildExportUrl(_currentFilter);

    /// <summary>
    /// Builds the CSV export URL for the given filter, encoding all active filter dimensions as query parameters.
    /// </summary>
    /// <param name="filter">Currently applied filter; null returns the bare export endpoint.</param>
    /// <returns>The relative URL with encoded filter dimensions as query parameters.</returns>
    internal static string BuildExportUrl(AuditLogQueryFilter? filter)
    {
        const string basePath = "/api/centralui/audit/export";
        if (filter is null)
        {
            return basePath;
        }

        // No capacity hint: the dimensions are multi-value, so the part count is
        // unbounded by the number of filter fields.
        var parts = new List<KeyValuePair<string, string?>>();
        // The filter dimensions are multi-value end-to-end. Emit ONE
        // repeated query-string key per selected value (channel=A&channel=B); the
        // export endpoint's ParseFilter reads the full repeated set.
        if (filter.Channels is { Count: > 0 } channels)
        {
            foreach (var channel in channels)
            {
                parts.Add(new("channel", channel.ToString()));
            }
        }
        if (filter.Kinds is { Count: > 0 } kinds)
        {
            foreach (var kind in kinds)
            {
                parts.Add(new("kind", kind.ToString()));
            }
        }
        if (filter.Statuses is { Count: > 0 } statuses)
        {
            foreach (var status in statuses)
            {
                parts.Add(new("status", status.ToString()));
            }
        }
        if (filter.SourceSiteIds is { Count: > 0 } sourceSiteIds)
        {
            foreach (var site in sourceSiteIds)
            {
                if (!string.IsNullOrWhiteSpace(site))
                {
                    parts.Add(new("site", site));
                }
            }
        }
        if (!string.IsNullOrWhiteSpace(filter.Target))
        {
            parts.Add(new("target", filter.Target));
        }
        if (!string.IsNullOrWhiteSpace(filter.Actor))
        {
            parts.Add(new("actor", filter.Actor));
        }
        if (filter.CorrelationId is { } corr)
        {
            parts.Add(new("correlationId", corr.ToString()));
        }
        if (filter.ExecutionId is { } exec)
        {
            parts.Add(new("executionId", exec.ToString()));
        }
        if (filter.ParentExecutionId is { } parentExec)
        {
            parts.Add(new("parentExecutionId", parentExec.ToString()));
        }
        if (filter.FromUtc is { } from)
        {
            parts.Add(new("from", from.ToString("O", CultureInfo.InvariantCulture)));
        }
        if (filter.ToUtc is { } to)
        {
            parts.Add(new("to", to.ToString("O", CultureInfo.InvariantCulture)));
        }

        if (parts.Count == 0)
        {
            return basePath;
        }

        return QueryHelpers.AddQueryString(basePath, parts);
    }
}
