using Microsoft.AspNetCore.Components;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;

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

/// <summary>
/// Filter bar for the central Audit Log page. Owns the
/// <see cref="AuditQueryModel"/> binding state and renders the filter controls
/// — Channel as a single-select (one channel at a time, so the Kind options
/// narrow to it cleanly); Kind / Status / Site as compact
/// <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared.MultiSelectDropdown{TValue}"/>
/// controls; plus the time range, free-text searches and the Errors-only
/// toggle — and publishes an <see cref="AuditLogQueryFilter"/> via
/// <see cref="OnFilterChanged"/> when the user clicks Apply. The selected
/// dimensions map through to the filter's list fields; see
/// <see cref="AuditQueryModel"/> for the Errors-only and time-range rules.
/// </summary>
public partial class AuditFilterBar
{
    private readonly AuditQueryModel _model = new();
    private List<Site> _sites = new();

    /// <summary>Channel options — the full enum, fixed for the component's lifetime.</summary>
    private static readonly IReadOnlyList<AuditChannel> _channels = Enum.GetValues<AuditChannel>();

    /// <summary>Status options — the full enum, fixed for the component's lifetime.</summary>
    private static readonly IReadOnlyList<AuditStatus> _statuses = Enum.GetValues<AuditStatus>();

    /// <summary>Site identifiers in display order; rebuilt once when sites load.</summary>
    private IReadOnlyList<string> _siteIds = Array.Empty<string>();

    /// <summary>
    /// Distinct <c>SourceNode</c> identifiers in display order; populated once
    /// when the filter bar initialises from the cached
    /// <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Services.IAuditLogQueryService.GetDistinctSourceNodesAsync"/>
    /// snapshot (60s TTL). Failure is non-fatal — the dropdown falls back to
    /// "No nodes available", mirroring the site loader.
    /// </summary>
    private IReadOnlyList<string> _sourceNodes = Array.Empty<string>();

    /// <summary>
    /// Raised when the user clicks Apply. Carries the
    /// <see cref="AuditLogQueryFilter"/> the parent page hands to
    /// <see cref="ZB.MOM.WW.ScadaBridge.CentralUI.Services.IAuditLogQueryService"/>.
    /// </summary>
    [Parameter] public EventCallback<AuditLogQueryFilter> OnFilterChanged { get; set; }

    /// <summary>
    /// Test seam: overriding "now" is needed to make the time-range collapse tests
    /// stable in unit suites. Production callers leave this null and the model
    /// uses <see cref="DateTime.UtcNow"/>.
    /// </summary>
    [Parameter] public Func<DateTime>? NowUtcProvider { get; set; }

    /// <summary>
    /// When set on first render,
    /// pre-populates the Instance free-text input. Instance is UI-only — the
    /// repository filter contract has no instance column — so this flows in
    /// through a separate parameter rather than the <see cref="AuditLogQueryFilter"/>
    /// the parent page passes to the grid.
    /// </summary>
    [Parameter] public string? InitialInstanceSearch { get; set; }

    /// <inheritdoc />
    protected override async Task OnInitializedAsync()
    {
        // One-shot prefill from a drill-in deep link. Subsequent parameter changes
        // do NOT overwrite user input — the field is owned by the operator after
        // first render.
        if (!string.IsNullOrWhiteSpace(InitialInstanceSearch))
        {
            _model.InstanceSearch = InitialInstanceSearch.Trim();
        }

        // Populate the Site dropdown at component init. Failure is non-fatal — the
        // dropdown just shows "No sites available." Sites are listed by Name to
        // match operator expectations from the Notification Report.
        try
        {
            var sites = await SiteRepository.GetAllSitesAsync();
            _sites = sites.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase).ToList();
        }
        catch
        {
            // Swallowed: filter bar still renders without the Site options. The page
            // surfaces site-load errors elsewhere (the grid query path).
            _sites = new();
        }

        _siteIds = _sites.Select(s => s.SiteIdentifier).ToArray();

        // Populate the Node dropdown alongside the Site dropdown. The service
        // caches the distinct-nodes lookup for 60s so this never costs more
        // than one DB hit per minute per circuit; on failure the dropdown
        // degrades to "No nodes available" like the site loader.
        try
        {
            var nodes = await AuditLogQueryService.GetDistinctSourceNodesAsync();
            _sourceNodes = nodes.ToArray();
        }
        catch
        {
            _sourceNodes = Array.Empty<string>();
        }
    }

    /// <summary>
    /// Single-select Channel binding for the filter bar. The Audit Log filters one
    /// channel at a time so the Kind options narrow cleanly to it; the model still
    /// stores the selection as a set (0 or 1 entry) so <see cref="AuditQueryModel.ToFilter"/>
    /// and <see cref="AuditQueryModel.VisibleKinds"/> are unchanged. <c>null</c> = all channels.
    /// </summary>
    private AuditChannel? SelectedChannel
    {
        get => _model.Channels.Count > 0 ? _model.Channels.First() : null;
        set
        {
            _model.Channels.Clear();
            if (value is { } channel)
            {
                _model.Channels.Add(channel);
            }

            OnChannelsChanged();
        }
    }

    /// <summary>
    /// Runs after the Channel selection changes. Drops any Kind selections that fell
    /// outside the new visible set — without this, changing the channel could leave
    /// stale Kind selections that no longer match any visible option.
    /// </summary>
    private void OnChannelsChanged()
    {
        var visible = _model.VisibleKinds().ToHashSet();
        _model.Kinds.RemoveWhere(k => !visible.Contains(k));
    }

    /// <summary>Display label for a site identifier — its friendly Name, id as fallback.</summary>
    private string SiteName(string siteIdentifier)
    {
        var site = _sites.FirstOrDefault(s =>
            string.Equals(s.SiteIdentifier, siteIdentifier, StringComparison.OrdinalIgnoreCase));
        return site?.Name ?? siteIdentifier;
    }

    private void ClearFilters()
    {
        _model.Channels.Clear();
        _model.Kinds.Clear();
        _model.Statuses.Clear();
        _model.SiteIdentifiers.Clear();
        _model.SourceNodes.Clear();
        _model.TimeRange = AuditTimeRangePreset.LastHour;
        _model.CustomFromUtc = null;
        _model.CustomToUtc = null;
        _model.InstanceSearch = string.Empty;
        _model.ScriptSearch = string.Empty;
        _model.TargetSearch = string.Empty;
        _model.ActorSearch = string.Empty;
        _model.ExecutionId = string.Empty;
        _model.ParentExecutionId = string.Empty;
        _model.ErrorsOnly = false;
    }

    private async Task Apply()
    {
        // <input type="datetime-local"> binds with DateTimeKind.Unspecified
        // — the value is the user's browser-local wall-clock. Tag it as Local then convert
        // to UTC before the model emits the filter, otherwise a non-UTC operator's window
        // is silently shifted by their UTC offset. Done on a swap-and-restore basis so the
        // bound inputs still show the user's local picks on the next render.
        var originalFrom = _model.CustomFromUtc;
        var originalTo = _model.CustomToUtc;
        try
        {
            _model.CustomFromUtc = LocalInputToUtc(originalFrom);
            _model.CustomToUtc = LocalInputToUtc(originalTo);
            var now = NowUtcProvider?.Invoke() ?? DateTime.UtcNow;
            var filter = _model.ToFilter(now);
            await OnFilterChanged.InvokeAsync(filter);
        }
        finally
        {
            _model.CustomFromUtc = originalFrom;
            _model.CustomToUtc = originalTo;
        }
    }

    /// <summary>
    /// Converts a value bound from <c>&lt;input type="datetime-local"&gt;</c> (which Blazor
    /// surfaces as <see cref="DateTimeKind.Unspecified"/>) into UTC. The input represents
    /// the operator's browser-local wall-clock, so we must tag it <see cref="DateTimeKind.Local"/>
    /// before <see cref="DateTime.ToUniversalTime"/> can do anything meaningful.
    /// </summary>
    private static DateTime? LocalInputToUtc(DateTime? value) =>
        value.HasValue
            ? DateTime.SpecifyKind(value.Value, DateTimeKind.Local).ToUniversalTime()
            : (DateTime?)null;

    private static string TimeRangeLabel(AuditTimeRangePreset preset) => preset switch
    {
        AuditTimeRangePreset.Last5Minutes => "now − 5 min → now",
        AuditTimeRangePreset.LastHour => "now − 1h → now",
        AuditTimeRangePreset.Last24Hours => "now − 24h → now",
        _ => "—",
    };
}
