/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { I18NService } from '@core';
import { ALAIN_I18N_TOKEN, MenuService } from '@delon/theme';
import { NzModalService } from 'ng-zorro-antd/modal';
import { ModalButtonOptions } from 'ng-zorro-antd/modal/modal-types';
import { NzNotificationService } from 'ng-zorro-antd/notification';
import { NzUploadChangeParam } from 'ng-zorro-antd/upload';
import { finalize } from 'rxjs/operators';

import { Monitor } from '../../../pojo/Monitor';
import { MemoryStorageService } from '../../../service/memory-storage.service';
import { MonitorService } from '../../../service/monitor.service';
import { findDeepestSelected, renderLabelColor } from '../../../shared/utils/common-util';

@Component({
  selector: 'app-monitor-list',
  templateUrl: './monitor-list.component.html',
  styleUrls: ['./monitor-list.component.less']
})
export class MonitorListComponent implements OnInit, OnDestroy {
  constructor(
    private route: ActivatedRoute,
    private router: Router,
    private modal: NzModalService,
    private notifySvc: NzNotificationService,
    private monitorSvc: MonitorService,
    private storageSvc: MemoryStorageService,
    private menuService: MenuService,
    @Inject(ALAIN_I18N_TOKEN) private i18nSvc: I18NService
  ) {}

  isDefaultListMenu!: boolean;
  app!: string | undefined;
  labels!: string | undefined;
  pageIndex: number = 1;
  pageSize: number = 10;
  pageSizeOptions: number[] = [10, 20, 50];
  total: number = 0;
  monitors!: Monitor[];
  tableLoading: boolean = true;
  checkedMonitorIds = new Set<number>();
  isSwitchExportTypeModalVisible = false;
  exportJsonButtonLoading = false;
  exportExcelButtonLoading = false;
  filterContent!: string;
  filterStatus: number = 9;
  // app type search filter
  appSwitchModalVisible = false;
  appSwitchModalVisibleType = 0;
  appSearchOrigin: any[] = [];
  appSearchLoading = false;
  intervalId: any;
  // save the current sorting status
  currentSortField: string | null = null;
  currentSortOrder: string | null = null;

  private previousMonitors: Monitor[] = [];
  private readonly GRACE_PERIOD_MS = 5000;

  switchExportTypeModalFooter: ModalButtonOptions[] = [
    { label: this.i18nSvc.fanyi('common.button.cancel'), type: 'default', onClick: () => (this.isSwitchExportTypeModalVisible = false) }
  ];

  ngOnInit(): void {
    this.menuService.change.subscribe(menus => {
      this.isDefaultListMenu = findDeepestSelected(menus).link === '/monitors';
    });
    this.route.queryParamMap.subscribe(paramMap => {
      let appStr = paramMap.get('app');
      let labelsStr = paramMap.get('labels');
      if (labelsStr != null) {
        this.labels = labelsStr;
      } else {
        this.labels = undefined;
      }
      if (appStr != null) {
        this.app = appStr;
      } else {
        this.app = undefined;
      }
      this.pageIndex = 1;
      this.pageSize = 10;
      this.checkedMonitorIds = new Set<number>();
      this.tableLoading = true;
      this.loadMonitorTable();
    });
    // Set up an interval to refresh the table every 2 minutes
    this.intervalId = setInterval(() => {
      this.sync();
    }, 120000); // 120000 ms = 2 minutes
  }

  ngOnDestroy(): void {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }

    this.clearGraceTimers();
  }

  onAppChanged(): void {
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: { ...this.route.snapshot.queryParams, app: this.app },
      queryParamsHandling: 'merge'
    });
  }

  onFilterSearchMonitors() {
    this.tableLoading = true;
    this.pageIndex = 1;
    let filter$ = this.monitorSvc
      .searchMonitors(this.app, this.labels, this.filterContent, this.filterStatus, this.pageIndex - 1, this.pageSize)
      .subscribe(
        message => {
          filter$.unsubscribe();
          this.tableLoading = false;
          // Filtering changes the result set, so the previous selection no longer applies.
          this.checkedMonitorIds.clear();
          if (message.code === 0) {
            let page = message.data;
            this.monitors = page.content;
            this.pageIndex = page.number + 1;
            this.total = page.totalElements;
          } else {
            console.warn(message.msg);
          }
        },
        error => {
          this.tableLoading = false;
          filter$.unsubscribe();
          console.error(error.msg);
        }
      );
  }

  sync() {
    // Reconcile only on auto-refresh so externally removed monitors fade out instead of blinking away.
    this.loadMonitorTable(this.currentSortField, this.currentSortOrder, true);
  }

  getAppIconName(app: string | undefined): string {
    let hierarchy: any[] = this.storageSvc.getData('hierarchy');
    let find = hierarchy.find((item: { category: string; value: string }) => {
      return item.value == app;
    });
    if (find == undefined) {
      return this.i18nSvc.fanyi('monitor.icon.center');
    }
    let icon = this.i18nSvc.fanyi(`monitor.icon.${find.category}`);
    if (icon == `monitor.icon.${find.category}`) {
      return this.i18nSvc.fanyi('monitor.icon.center');
    }
    return icon;
  }

  loadMonitorTable(sortField?: string | null, sortOrder?: string | null, reconcile: boolean = false) {
    this.tableLoading = true;
    let monitorInit$ = this.monitorSvc
      .searchMonitors(this.app, this.labels, this.filterContent, this.filterStatus, this.pageIndex - 1, this.pageSize, sortField, sortOrder)
      .subscribe(
        message => {
          this.tableLoading = false;
          // Keep the selection across reloads (pagination, auto-refresh, post-CRUD) so multi-page checks persist (#4164).
          if (message.code === 0) {
            let page = message.data;
            this.monitors = reconcile ? this.reconcileMonitorStates(page.content) : this.resetMonitorStates(page.content);
            this.pageIndex = page.number + 1;
            this.total = page.totalElements;
          } else {
            console.warn(message.msg);
          }
          monitorInit$.unsubscribe();
        },
        error => {
          this.tableLoading = false;
          monitorInit$.unsubscribe();
        }
      );
  }
  changeMonitorTable(sortField?: string | null, sortOrder?: string | null) {
    this.tableLoading = true;
    let monitorInit$ = this.monitorSvc
      .searchMonitors(this.app, this.labels, this.filterContent, this.filterStatus, this.pageIndex - 1, this.pageSize, sortField, sortOrder)
      .subscribe(
        message => {
          this.tableLoading = false;
          // Keep the selection across pagination so checks made on other pages persist (#4164).
          if (message.code === 0) {
            let page = message.data;
            // Pagination changes the result set, so reconcile here would flag the previous page as "disappeared" (#4156).
            this.monitors = this.resetMonitorStates(page.content);
            this.pageIndex = page.number + 1;
            this.total = page.totalElements;
          } else {
            console.warn(message.msg);
          }
          monitorInit$.unsubscribe();
        },
        error => {
          this.tableLoading = false;
          monitorInit$.unsubscribe();
        }
      );
  }

  onEditOneMonitor(monitorId: number) {
    if (monitorId == null) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-edit'), '');
      return;
    }
    this.router.navigateByUrl(`/monitors/${monitorId}/edit`);
  }

  onDeleteOneMonitor(monitorId: number) {
    let monitors = new Set<number>();
    monitors.add(monitorId);
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.delete'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.deleteMonitors(monitors)
    });
  }

  onDeleteMonitors() {
    if (this.checkedMonitorIds == null || this.checkedMonitorIds.size === 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), '');
      return;
    }
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.delete-batch'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.deleteMonitors(this.checkedMonitorIds)
    });
  }

  onExportMonitors() {
    if (this.checkedMonitorIds == null || this.checkedMonitorIds.size == 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-export'), '');
      return;
    }
    this.isSwitchExportTypeModalVisible = true;
  }

  onExportAllMonitors() {
    this.isSwitchExportTypeModalVisible = true;
  }

  onImportMonitors(info: NzUploadChangeParam): void {
    console.log(info.type);
    if (info.type === 'start') {
      this.notifySvc.info(
        this.i18nSvc.fanyi('common.notice'),
        this.i18nSvc.fanyi('common.notify.import-submitted', { taskName: info.file.name })
      );
    }
    if (info.type === 'success' && info.file.response) {
      this.tableLoading = true;
      const message = info.file.response;
      if (message.code === 0) {
        this.loadMonitorTable();
      } else {
        this.tableLoading = false;
      }
    }
  }

  deleteMonitors(monitors: Set<number>) {
    if (monitors == null || monitors.size == 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-delete'), '');
      return;
    }
    this.tableLoading = true;
    const deleteMonitors$ = this.monitorSvc.deleteMonitors(monitors).subscribe(
      message => {
        deleteMonitors$.unsubscribe();
        if (message.code === 0) {
          this.notifySvc.success(this.i18nSvc.fanyi('common.notify.delete-success'), '');
          monitors.forEach(id => this.checkedMonitorIds.delete(id));
          this.updatePageIndex(monitors.size);
          this.loadMonitorTable();
        } else {
          this.tableLoading = false;
          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), message.msg);
        }
      },
      error => {
        this.tableLoading = false;
        deleteMonitors$.unsubscribe();
        this.notifySvc.error(this.i18nSvc.fanyi('common.notify.delete-fail'), error.msg);
      }
    );
    // delete grafana dashboard
    for (let monitorId of monitors) {
      this.deleteGrafanaDashboard(monitorId);
    }
  }

  updatePageIndex(delSize: number) {
    const lastPage = Math.max(1, Math.ceil((this.total - delSize) / this.pageSize));
    this.pageIndex = this.pageIndex > lastPage ? lastPage : this.pageIndex;
  }

  exportMonitors(type: string) {
    if (this.checkedMonitorIds == null || this.checkedMonitorIds.size == 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-export'), '');
      return;
    }
    switch (type) {
      case 'JSON':
        this.exportJsonButtonLoading = true;
        break;
      case 'EXCEL':
        this.exportExcelButtonLoading = true;
        break;
    }
    const exportMonitors$ = this.monitorSvc
      .exportMonitors(this.checkedMonitorIds, type)
      .pipe(
        finalize(() => {
          this.exportExcelButtonLoading = false;
          this.exportJsonButtonLoading = false;
          exportMonitors$.unsubscribe();
        })
      )
      .subscribe(
        response => {
          const message = response.body!;
          if (message.type == 'application/json') {
            this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), '');
          } else {
            const blob = new Blob([message], { type: response.headers.get('Content-Type')! });
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.download = response.headers.get('Content-Disposition')!.split(';')[1].split('filename=')[1];
            a.href = url;
            a.click();
            window.URL.revokeObjectURL(url);
            this.isSwitchExportTypeModalVisible = false;
          }
        },
        error => {
          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), error.msg);
        }
      );
  }

  exportAllMonitors(type: string) {
    switch (type) {
      case 'JSON':
        this.exportJsonButtonLoading = true;
        break;
      case 'EXCEL':
        this.exportExcelButtonLoading = true;
        break;
    }
    const exportAllMonitors$ = this.monitorSvc
      .exportAllMonitors(type)
      .pipe(
        finalize(() => {
          this.exportExcelButtonLoading = false;
          this.exportJsonButtonLoading = false;
          exportAllMonitors$.unsubscribe();
        })
      )
      .subscribe(
        response => {
          const message = response.body!;
          if (message.type == 'application/json') {
            this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), '');
          } else {
            const blob = new Blob([message], { type: response.headers.get('Content-Type')! });
            const url = window.URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.download = response.headers.get('Content-Disposition')!.split(';')[1].split('filename=')[1];
            a.href = url;
            a.click();
            window.URL.revokeObjectURL(url);
            this.isSwitchExportTypeModalVisible = false;
          }
        },
        error => {
          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.export-fail'), error.msg);
        }
      );
  }

  onCancelManageMonitors() {
    if (this.checkedMonitorIds == null || this.checkedMonitorIds.size === 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-cancel'), '');
      return;
    }
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.cancel-batch'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.cancelManageMonitors(this.checkedMonitorIds)
    });
  }

  onCancelManageOneMonitor(monitorId: number) {
    let monitors = new Set<number>();
    monitors.add(monitorId);
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.cancel'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.cancelManageMonitors(monitors)
    });
  }

  cancelManageMonitors(monitors: Set<number>) {
    this.tableLoading = true;
    const cancelManage$ = this.monitorSvc.cancelManageMonitors(monitors).subscribe(
      message => {
        cancelManage$.unsubscribe();
        if (message.code === 0) {
          this.notifySvc.success(this.i18nSvc.fanyi('common.notify.cancel-success'), '');
          monitors.forEach(id => this.checkedMonitorIds.delete(id));
          this.loadMonitorTable();
        } else {
          this.tableLoading = false;
          if (message.code === 3) {
            this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
            this.loadMonitorTable();
          } else {
            this.notifySvc.error(this.i18nSvc.fanyi('common.notify.cancel-fail'), message.msg);
          }
        }
      },
      error => {
        this.tableLoading = false;
        cancelManage$.unsubscribe();
        if (error.status === 404) {
          this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
          this.loadMonitorTable();
        } else {
          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.cancel-fail'), error.msg);
        }
      }
    );
  }

  onEnableManageMonitors() {
    if (this.checkedMonitorIds == null || this.checkedMonitorIds.size === 0) {
      this.notifySvc.warning(this.i18nSvc.fanyi('common.notify.no-select-enable'), '');
      return;
    }
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.enable-batch'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.enableManageMonitors(this.checkedMonitorIds)
    });
  }

  onEnableManageOneMonitor(monitorId: number) {
    let monitors = new Set<number>();
    monitors.add(monitorId);
    this.modal.confirm({
      nzTitle: this.i18nSvc.fanyi('common.confirm.enable'),
      nzOkText: this.i18nSvc.fanyi('common.button.ok'),
      nzCancelText: this.i18nSvc.fanyi('common.button.cancel'),
      nzOkDanger: true,
      nzOkType: 'primary',
      nzClosable: false,
      nzOnOk: () => this.enableManageMonitors(monitors)
    });
  }

  enableManageMonitors(monitors: Set<number>) {
    this.tableLoading = true;
    const enableManage$ = this.monitorSvc.enableManageMonitors(monitors).subscribe(
      message => {
        enableManage$.unsubscribe();
        if (message.code === 0) {
          this.notifySvc.success(this.i18nSvc.fanyi('common.notify.enable-success'), '');
          monitors.forEach(id => this.checkedMonitorIds.delete(id));
          this.loadMonitorTable();
        } else {
          this.tableLoading = false;
          if (message.code === 3) {
            this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
            this.loadMonitorTable();
          } else {
            this.notifySvc.error(this.i18nSvc.fanyi('common.notify.enable-fail'), message.msg);
          }
        }
      },
      error => {
        this.tableLoading = false;
        enableManage$.unsubscribe();
        // 检查是否是404错误
        if (error.status === 404) {
          this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
          this.loadMonitorTable();
        } else {
          this.notifySvc.error(this.i18nSvc.fanyi('common.notify.enable-fail'), error.msg);
        }
      }
    );
  }

  // begin: List multiple choice paging
  // Reflects only the current page: checked when every selectable monitor on this page is selected.
  get checkedAll(): boolean {
    const selectable = this.monitors?.filter(monitor => !this.isMonitorDisabled(monitor)) ?? [];
    return selectable.length > 0 && selectable.every(monitor => this.checkedMonitorIds.has(monitor.id));
  }

  onAllChecked(checked: boolean) {
    // Only toggle the current page's selectable monitors, leaving selections on other pages untouched (#4164).
    this.monitors
      ?.filter(monitor => !this.isMonitorDisabled(monitor))
      .forEach(monitor => {
        if (checked) {
          this.checkedMonitorIds.add(monitor.id);
        } else {
          this.checkedMonitorIds.delete(monitor.id);
        }
      });
  }

  // Clear the whole cross-page selection in one click, so users aren't forced to page back to deselect (#4164).
  clearSelection() {
    this.checkedMonitorIds.clear();
  }

  onItemChecked(monitorId: number, checked: boolean) {
    if (checked) {
      this.checkedMonitorIds.add(monitorId);
    } else {
      this.checkedMonitorIds.delete(monitorId);
    }
  }

  // end: List multiple choice paging

  notifyCopySuccess() {
    this.notifySvc.success(this.i18nSvc.fanyi('common.notify.copy-success'), '');
  }

  onPageIndexChange(pageIndex: number) {
    this.pageIndex = pageIndex;
    this.changeMonitorTable(this.currentSortField, this.currentSortOrder);
  }

  onPageSizeChange(pageSize: number) {
    this.pageSize = pageSize;
    this.pageIndex = 1;
    this.changeMonitorTable(this.currentSortField, this.currentSortOrder);
  }

  // begin: app type search filter

  onSearchAppClicked() {
    this.appSwitchModalVisibleType = 1;
    this.onAppSwitchModalOpen();
  }

  onAppSwitchModalOpen() {
    this.appSwitchModalVisible = true;
    this.appSearchLoading = true;
    let appMenus: Record<string, any> = {};
    let hierarchy: any[] = this.storageSvc.getData('hierarchy');
    hierarchy.forEach((app: any) => {
      if (app.category == '__system__') {
        return;
      }
      let menus = appMenus[app.category];
      app.categoryLabel = this.i18nSvc.fanyi(`menu.monitor.${app.category}`);
      if (app.categoryLabel == `menu.monitor.${app.category}`) {
        app.categoryLabel = app.category.toUpperCase();
      }
      if (menus == undefined) {
        menus = { label: app.categoryLabel, child: [app] };
      } else {
        menus.child.push(app);
      }
      appMenus[app.category] = menus;
    });
    this.appSearchOrigin = Object.entries(appMenus);
    this.appSearchOrigin.sort((a, b) => {
      return b[1].length - a[1].length;
    });
    this.appSearchLoading = false;
  }

  onAppSwitchModalCancel() {
    this.appSwitchModalVisible = false;
    this.appSwitchModalVisibleType = 0;
  }

  gotoMonitorAddDetail(app: string) {
    if (this.appSwitchModalVisibleType === 1) {
      this.app = app;
      this.onAppChanged();
      this.onAppSwitchModalCancel();
    } else {
      this.router.navigateByUrl(`/monitors/new?app=${app}`);
    }
  }

  // end: app type search filter

  deleteGrafanaDashboard(monitorId: number) {
    this.monitorSvc.deleteGrafanaDashboard(monitorId).subscribe(
      message => {
        if (message.code === 0) {
          console.log('delete grafana dashboard success');
        } else {
          console.warn(message.msg);
        }
      },
      error => {
        console.error(error.msg);
      }
    );
  }

  protected readonly getLabelColor = renderLabelColor;

  copyMonitor(monitorId: number) {
    this.monitorSvc.copyMonitor(monitorId).subscribe(
      message => {
        if (message.code === 0) {
          this.notifySvc.success(this.i18nSvc.fanyi('monitor.copy.success'), '');
          this.loadMonitorTable();
        } else {
          if (message.code === 3) {
            this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
            this.loadMonitorTable();
          } else {
            this.notifySvc.error(this.i18nSvc.fanyi('monitor.copy.failed'), message.msg);
          }
        }
      },
      error => {
        if (error.status === 404) {
          this.notifySvc.warning(this.i18nSvc.fanyi('monitor.item.unavailable.refresh'), '');
          this.loadMonitorTable();
        } else {
          this.notifySvc.error(this.i18nSvc.fanyi('monitor.copy.failed'), error.msg);
        }
      }
    );
  }

  // Replace the list as-is for result-set swaps (load, pagination, filter, post-CRUD reload), without keeping stale rows.
  private resetMonitorStates(newMonitors: Monitor[]): Monitor[] {
    this.clearGraceTimers();
    const processedMonitors = newMonitors.map(monitor => ({
      ...monitor,
      _displayStatus: 'ACTIVE' as const
    }));
    this.previousMonitors = [...processedMonitors];
    return processedMonitors;
  }

  private clearGraceTimers(): void {
    this.previousMonitors?.forEach(monitor => {
      if (monitor._graceTimer) {
        clearTimeout(monitor._graceTimer);
      }
    });
  }

  private reconcileMonitorStates(newMonitors: Monitor[]): Monitor[] {
    if (!this.previousMonitors || this.previousMonitors.length === 0) {
      const processedMonitors = newMonitors.map(monitor => ({
        ...monitor,
        _displayStatus: 'ACTIVE' as const
      }));
      this.previousMonitors = [...processedMonitors];
      return processedMonitors;
    }
    const newMonitorMap = new Map(newMonitors.map(m => [m.id, m]));
    const previousMonitorMap = new Map(this.previousMonitors.map(m => [m.id, m]));
    const reconciledMonitors: Monitor[] = [];

    newMonitors.forEach(newMonitor => {
      const previousMonitor = previousMonitorMap.get(newMonitor.id);
      if (previousMonitor) {
        if (previousMonitor._graceTimer) {
          clearTimeout(previousMonitor._graceTimer);
        }
        reconciledMonitors.push({
          ...newMonitor,
          _displayStatus: 'ACTIVE' as const
        });
      } else {
        reconciledMonitors.push({
          ...newMonitor,
          _displayStatus: 'ACTIVE' as const
        });
      }
    });

    this.previousMonitors.forEach(previousMonitor => {
      if (!newMonitorMap.has(previousMonitor.id)) {
        if (previousMonitor._displayStatus === 'DISAPPEARED') {
          reconciledMonitors.push(previousMonitor);
        } else {
          const disappearedMonitor = {
            ...previousMonitor,
            _displayStatus: 'DISAPPEARED' as const,
            _disappearTime: Date.now()
          };

          disappearedMonitor._graceTimer = setTimeout(() => {
            this.monitors = this.monitors.filter(m => m.id !== disappearedMonitor.id);
          }, this.GRACE_PERIOD_MS);

          reconciledMonitors.push(disappearedMonitor);
        }
      }
    });

    this.previousMonitors = [...reconciledMonitors];
    return reconciledMonitors;
  }

  isMonitorDisabled(monitor: Monitor): boolean {
    return monitor._displayStatus === 'DISAPPEARED';
  }

  getMonitorDisplayClass(monitor: Monitor): string {
    if (monitor._displayStatus === 'DISAPPEARED') {
      return 'monitor-disappeared';
    }
    return '';
  }
}
