@page "/deployment/deployments"
@using ScadaLink.Security
@using ScadaLink.Commons.Entities.Deployment
@using ScadaLink.Commons.Entities.Instances
@using ScadaLink.Commons.Interfaces.Repositories
@using ScadaLink.Commons.Types.Enums
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDeployment)]
@inject IDeploymentManagerRepository DeploymentManagerRepository
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject ScadaLink.DeploymentManager.IDeploymentStatusNotifier DeploymentStatusNotifier
@implements IDisposable

<div class="container-fluid mt-3">
    <div class="d-flex justify-content-between align-items-center mb-3">
        <h4 class="mb-0">Deployment Status</h4>
        <div class="d-flex gap-2 align-items-center">
            <button class="btn btn-outline-secondary btn-sm" @onclick="ToggleAutoRefresh"
                    aria-label="@(_autoRefresh ? "Pause auto-refresh" : "Resume auto-refresh")">
                @(_autoRefresh ? "⏸ Pause updates" : "▶ Resume updates")
            </button>
            <button class="btn btn-outline-secondary btn-sm" @onclick="LoadDataAsync" aria-label="Refresh deployments">Refresh</button>
        </div>
    </div>

    @if (_loading)
    {
        <LoadingSpinner IsLoading="true" />
    }
    else if (_errorMessage != null)
    {
        <div class="alert alert-danger">@_errorMessage</div>
    }
    else
    {
        @* Summary cards *@
        <div class="row mb-3">
            <div class="col-lg-3 col-md-6 col-12">
                <div class="card border-warning">
                    <div class="card-body text-center py-2">
                        <h4 class="mb-0 text-warning">@_records.Count(r => r.Status == DeploymentStatus.Pending)</h4>
                        <small class="text-muted">Pending</small>
                    </div>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 col-12">
                <div class="card border-info">
                    <div class="card-body text-center py-2">
                        <h4 class="mb-0 text-info">@_records.Count(r => r.Status == DeploymentStatus.InProgress)</h4>
                        <small class="text-muted">In Progress</small>
                    </div>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 col-12">
                <div class="card border-success">
                    <div class="card-body text-center py-2">
                        <h4 class="mb-0 text-success">@_records.Count(r => r.Status == DeploymentStatus.Success)</h4>
                        <small class="text-muted">Successful</small>
                    </div>
                </div>
            </div>
            <div class="col-lg-3 col-md-6 col-12">
                <div class="card border-danger">
                    <div class="card-body text-center py-2">
                        <h4 class="mb-0 text-danger">@_records.Count(r => r.Status == DeploymentStatus.Failed)</h4>
                        <small class="text-muted">Failed</small>
                    </div>
                </div>
            </div>
        </div>

        @if (_records.Count == 0)
        {
            <div class="text-center py-5 text-muted">
                <p class="mb-0">No deployments recorded.</p>
            </div>
        }
        else
        {
        <table class="table table-sm table-striped table-hover align-middle">
            <thead class="table-dark">
                <tr>
                    <th>Deployment</th>
                    <th>Instance</th>
                    <th>Status</th>
                    <th>Deployed By</th>
                    <th>Started</th>
                    <th>Completed</th>
                    <th class="text-end">Actions</th>
                </tr>
            </thead>
            <tbody>
                @foreach (var record in _pagedRecords)
                {
                    var rowId = $"deploy-row-{record.DeploymentId}";
                    var errorCollapseId = $"deploy-err-{record.DeploymentId}";
                    var isFailed = record.Status == DeploymentStatus.Failed;
                    var idShort = record.DeploymentId[..Math.Min(12, record.DeploymentId.Length)];
                    var revShort = record.RevisionHash?[..Math.Min(8, record.RevisionHash?.Length ?? 0)];
                    <tr id="@rowId" class="@GetRowClass(record.Status)">
                        <td>
                            <code class="small">@idShort@(string.IsNullOrEmpty(revShort) ? "" : $"@{revShort}")</code>
                        </td>
                        <td>@GetInstanceName(record.InstanceId)</td>
                        <td>
                            @if (isFailed)
                            {
                                <i class="bi bi-x-circle text-danger me-1" aria-hidden="true"></i>
                            }
                            <span class="badge @GetStatusBadge(record.Status)"
                                  aria-label="@($"Deployment status: {record.Status}")">
                                @record.Status
                                @if (record.Status == DeploymentStatus.InProgress)
                                {
                                    <span class="spinner-border spinner-border-sm ms-1" style="width: 0.7rem; height: 0.7rem;"
                                          role="status" aria-label="Deployment in progress"></span>
                                }
                            </span>
                        </td>
                        <td class="small">@record.DeployedBy</td>
                        <td class="small">
                            <TimestampDisplay Value="@record.DeployedAt" />
                        </td>
                        <td class="small">
                            @if (record.CompletedAt.HasValue)
                            {
                                <TimestampDisplay Value="@record.CompletedAt.Value" />
                            }
                            else
                            {
                                <span class="text-muted">—</span>
                            }
                        </td>
                        <td class="small text-end">
                            @if (isFailed && !string.IsNullOrEmpty(record.ErrorMessage))
                            {
                                <button class="btn btn-link btn-sm p-0" type="button"
                                        @onclick="() => ToggleErrorExpansion(record.DeploymentId)"
                                        aria-expanded="@(IsErrorExpanded(record.DeploymentId) ? "true" : "false")"
                                        aria-controls="@errorCollapseId">
                                    @(IsErrorExpanded(record.DeploymentId) ? "Hide error" : "View error")
                                </button>
                            }
                        </td>
                    </tr>
                    @if (isFailed && !string.IsNullOrEmpty(record.ErrorMessage) && IsErrorExpanded(record.DeploymentId))
                    {
                        <tr id="@errorCollapseId" class="table-danger">
                            <td colspan="7">
                                <div class="small">
                                    <strong>Error:</strong>
                                    <pre class="mb-0 mt-1 small" style="white-space: pre-wrap; word-break: break-word;">@record.ErrorMessage</pre>
                                </div>
                            </td>
                        </tr>
                    }
                }
            </tbody>
        </table>
        }

        @if (_totalPages > 1)
        {
            <nav>
                <ul class="pagination pagination-sm justify-content-end">
                    <li class="page-item @(_currentPage <= 1 ? "disabled" : "")">
                        <button class="page-link" @onclick="() => GoToPage(_currentPage - 1)">Previous</button>
                    </li>
                    @foreach (var page in ScadaLink.CentralUI.Components.Shared.PagerWindow.Build(_currentPage, _totalPages))
                    {
                        if (page == 0)
                        {
                            <li class="page-item disabled">
                                <span class="page-link">&hellip;</span>
                            </li>
                        }
                        else
                        {
                            var p = page;
                            <li class="page-item @(p == _currentPage ? "active" : "")">
                                <button class="page-link" @onclick="() => GoToPage(p)">@(p)</button>
                            </li>
                        }
                    }
                    <li class="page-item @(_currentPage >= _totalPages ? "disabled" : "")">
                        <button class="page-link" @onclick="() => GoToPage(_currentPage + 1)">Next</button>
                    </li>
                </ul>
            </nav>
        }
    }
</div>

@code {
    private List<DeploymentRecord> _records = new();
    private List<DeploymentRecord> _pagedRecords = new();
    private Dictionary<int, string> _instanceNames = new();
    private bool _loading = true;
    private string? _errorMessage;
    private bool _autoRefresh = true;
    private readonly HashSet<string> _expandedErrors = new();

    private int _currentPage = 1;
    private int _totalPages;
    private const int PageSize = 25;

    // CentralUI-022: IDeploymentStatusNotifier is a process singleton that
    // raises StatusChanged on the DeploymentManager service thread. Dispose()
    // unsubscribes, but the notifier can read its subscriber list and begin
    // invoking OnDeploymentStatusChanged just before this component is disposed.
    // The handler then runs against a disposed component and InvokeAsync throws
    // ObjectDisposedException as an unobserved fire-and-forget task exception.
    // This flag (set first in Dispose()) makes a racing callback no-op, and the
    // dispatch swallows the residual ObjectDisposedException — mirroring the
    // DebugView (CentralUI-009) and ToastNotification (CentralUI-010) guards.
    private volatile bool _disposed;

    // CentralUI-006: deployment status updates are push-based, not polled.
    // DeploymentManager raises IDeploymentStatusNotifier.StatusChanged on every
    // deployment-record status write; this page subscribes to it and reloads,
    // and Blazor Server pushes the re-render to the browser over its SignalR
    // circuit — satisfying the design's "no polling required" requirement.
    // The notifier event is raised on the DeploymentManager service thread, so
    // the handler marshals onto the renderer via InvokeAsync.

    protected override async Task OnInitializedAsync()
    {
        await LoadDataAsync();
        DeploymentStatusNotifier.StatusChanged += OnDeploymentStatusChanged;
    }

    private void OnDeploymentStatusChanged(ScadaLink.DeploymentManager.DeploymentStatusChange change)
    {
        // CentralUI-022: a callback racing disposal must not touch the component.
        if (_disposed || !_autoRefresh) return;
        _ = DispatchReloadAsync();
    }

    /// <summary>
    /// Reloads the deployment table on the renderer's dispatcher, guarded
    /// against the component being disposed mid-flight (CentralUI-022):
    /// <c>InvokeAsync</c> throws <see cref="ObjectDisposedException"/> once the
    /// circuit is gone, and this handler runs fire-and-forget so that exception
    /// would otherwise go unobserved on the DeploymentManager thread.
    /// </summary>
    private async Task DispatchReloadAsync()
    {
        if (_disposed) return;
        try
        {
            await InvokeAsync(async () =>
            {
                if (_disposed) return;
                await LoadDataAsync();
                StateHasChanged();
            });
        }
        catch (ObjectDisposedException)
        {
            // Component disposed between the guard and the dispatch — ignore.
        }
    }

    private void ToggleAutoRefresh()
    {
        // When paused, incoming push notifications are ignored; "Refresh" still
        // forces a manual reload. No timer is involved either way.
        _autoRefresh = !_autoRefresh;
    }

    private bool IsErrorExpanded(string deploymentId) => _expandedErrors.Contains(deploymentId);

    private void ToggleErrorExpansion(string deploymentId)
    {
        if (!_expandedErrors.Remove(deploymentId))
        {
            _expandedErrors.Add(deploymentId);
        }
    }

    private async Task LoadDataAsync()
    {
        _loading = _records.Count == 0; // Only show loading on first load
        _errorMessage = null;
        try
        {
            // Build instance lookups first — site scoping (CentralUI-002) filters
            // deployment records by the site of their instance.
            var instances = await TemplateEngineRepository.GetAllInstancesAsync();
            _instanceNames = instances.ToDictionary(i => i.Id, i => i.UniqueName);
            var instanceSiteIds = instances.ToDictionary(i => i.Id, i => i.SiteId);

            var systemWide = await SiteScope.IsSystemWideAsync();
            var permittedSiteIds = systemWide
                ? null
                : await SiteScope.PermittedSiteIdsAsync();

            _records = (await DeploymentManagerRepository.GetAllDeploymentRecordsAsync())
                .Where(r => permittedSiteIds == null
                    || (instanceSiteIds.TryGetValue(r.InstanceId, out var sid)
                        && permittedSiteIds.Contains(sid)))
                .OrderByDescending(r => r.DeployedAt)
                .ToList();

            _totalPages = Math.Max(1, (int)Math.Ceiling(_records.Count / (double)PageSize));
            if (_currentPage > _totalPages) _currentPage = 1;
            UpdatePage();
        }
        catch (Exception ex)
        {
            _errorMessage = $"Failed to load deployments: {ex.Message}";
        }
        _loading = false;
    }

    private void GoToPage(int page)
    {
        if (page < 1 || page > _totalPages) return;
        _currentPage = page;
        UpdatePage();
    }

    private void UpdatePage()
    {
        _pagedRecords = _records
            .Skip((_currentPage - 1) * PageSize)
            .Take(PageSize)
            .ToList();
    }

    private string GetInstanceName(int instanceId) =>
        _instanceNames.GetValueOrDefault(instanceId, $"#{instanceId}");

    private static string GetStatusBadge(DeploymentStatus status) => status switch
    {
        DeploymentStatus.Pending => "bg-warning text-dark",
        DeploymentStatus.InProgress => "bg-info text-dark",
        DeploymentStatus.Success => "bg-success",
        DeploymentStatus.Failed => "bg-danger",
        _ => "bg-secondary"
    };

    private static string GetRowClass(DeploymentStatus status) => status switch
    {
        DeploymentStatus.Failed => "table-danger",
        DeploymentStatus.InProgress => "table-info",
        _ => ""
    };

    public void Dispose()
    {
        // CentralUI-022: set the guard first so a callback already in flight on
        // the DeploymentManager thread no-ops, then unsubscribe so no further
        // status change reaches this disposed component.
        _disposed = true;
        DeploymentStatusNotifier.StatusChanged -= OnDeploymentStatusChanged;
    }
}
