// #region DashboardHubModel [C:4] [TYPE Model] [SEMANTICS dashboard, hub, grid, filter, pagination, git, screen-model]
// @ingroup Models
// @BRIEF State model for the dashboard hub page — all atoms, invariants, and actions in one class.
// @REJECTED Inline $state + handler functions in +page.svelte was rejected because it scattered 35+ atoms and 40+ functions across 2400+ lines, making testing impossible without DOM render and violating INV_7 (>400 lines, CC >10).
// @INVARIANT Changing filter (column, search, multi-select) resets pagination to page 1.
// @INVARIANT Changing source environment resets all dashboard, filter, selection, and git state.
// @INVARIANT Selection never contains IDs not present in allDashboards (cleaned on load).
// @INVARIANT dryRunResult is always null when selectedDashboardIds changes (handled by $effect in page).
// @INVARIANT Model is 592 lines, >40 public methods. Ref: ADR-0010 (Model Decomposition Gate).
//   Split targets: DashboardFiltersModel / DashboardSelectionModel / DashboardGitActionsModel.
// @STATE loading   — API call in progress.
// @STATE loaded    — Dashboard grid with status badges rendered.
// @STATE empty     — Query returned zero results.
// @STATE error     — API call failed, error message available.
// @ACTION loadDashboards()               — Fetches dashboards for current env/page/search/filters.
// @ACTION setPage(n)                     — Navigates to page n.
// @ACTION setSort(column)                — Toggles sort on column.
// @ACTION toggleFilterValue(col,val,chk) — Adds/removes filter value.
// @ACTION clearColumnFilter(col)         — Resets one column filter.
// @ACTION handleGitInit(dashboard)       — Initializes git repo.
// @ACTION handleGitSync(dashboard)       — Syncs git repo.
// @ACTION handleGitCommit(dashboard)     — Commits changes.
// @ACTION handleGitPull(dashboard)       — Pulls from remote.
// @ACTION handleGitPush(dashboard)       — Pushes to remote.
// @ACTION handleBulkBackup()             — Executes bulk backup.
// @RELATION DEPENDS_ON -> [ApiModule]
// @RELATION CALLS -> [gitService]
// @RELATION BINDS_TO -> [environmentContextStore]
// @RELATION CALLS -> [CotLogger]

import { SvelteSet, SvelteDate } from "svelte/reactivity";
import { goto } from "$app/navigation";
import { ROUTES } from "$lib/routes.js";
import { log } from "$lib/cot-logger";
import { api } from "$lib/api.js";
import { gitService } from "../../services/gitService.js";
import { addToast } from "$lib/toasts.svelte.js";
import { openDrawerForTask, openDrawerForTaskIfPreferred } from "$lib/stores/taskDrawer.svelte.js";
import { MigrationModel } from "./MigrationModel.svelte.ts";
import { t } from "$lib/i18n/index.svelte.js";
import { normalizeTaskStatus, normalizeValidationStatus, normalizeOwners, formatDate } from "../../routes/dashboards/dashboard-helpers.js";

// ═══ Types ═══════════════════════════════════════════════════════

type SortDir = "asc" | "desc";
type FilterColumn = "title" | "git_status" | "validation_status" | "changed_on" | "actor";
type ColumnFilters = Record<FilterColumn, Set<string>>;
type ColumnFilterSearch = Record<FilterColumn, string>;

interface DashboardRow {
  id: string; title: string; slug: string;
  changedOn: string | null; changedOnLabel: string;
  owners: string[]; actorLabel: string;
  git: { status: string; branch: string | null; hasRepo: boolean | null; hasChangesForCommit: boolean };
  lastTask: { status: string | null; validationStatus: string; id: string } | null;
  actions: string[];
}

interface GitState { status: string; branch: string | null; hasRepo: boolean; hasChangesForCommit: boolean }

// ═══ Model ═══════════════════════════════════════════════════════

export class DashboardHubModel {
  // ── Core state ────────────────────────────────────────────────
  selectedEnv: string | null = $state(null);
  allDashboards: DashboardRow[] = $state([]);
  dashboards: DashboardRow[] = $state([]);
  filteredDashboards: DashboardRow[] = $state([]);
  isLoading: boolean = $state(true);
  error: string | null = $state(null);

  // ── Pagination ────────────────────────────────────────────────
  currentPage: number = $state(1);
  pageSize: number = $state(10);
  totalPages: number = $state(1);
  total: number = $state(0);

  // ── Selection ─────────────────────────────────────────────────
  selectedIds: Set<string> = $state(new SvelteSet());
  isAllSelected: boolean = $state(false);
  isAllVisibleSelected: boolean = $state(false);

  // ── Search ────────────────────────────────────────────────────
  searchQuery: string = $state("");
  filterDashboardIds: string[] = $state([]);
  searchableDashboardOptions: Array<{id: string; name: string; subtitle: string; hint: string}> = $state([]);
  searchableDashboardLoading: boolean = $state(false);
  sortColumn: FilterColumn = $state("title");
  sortDirection: SortDir = $state("asc");
  openFilterColumn: FilterColumn | null = $state(null);
  filterDropdownPosition: { left: number; top: number } = $state({ left: 0, top: 0 });
  columnFilterSearch: ColumnFilterSearch = $state({ title: "", git_status: "", validation_status: "", changed_on: "", actor: "" });
  columnFilters: ColumnFilters = $state({ title: new SvelteSet(), git_status: new SvelteSet(), validation_status: new SvelteSet(), changed_on: new SvelteSet(), actor: new SvelteSet() });

  // ── Validation popover ────────────────────────────────────────
  validationStatuses: Record<string, any> = $state({});
  openValidationPopover: string | null = $state(null);
  validationPopoverPosition: { left: number; top: number } = $state({ left: 0, top: 0 });
  validationStatusLoading: boolean = $state(false);

  // ── Bulk modals ───────────────────────────────────────────────
  showMigrateModal: boolean = $state(false);
  showBackupModal: boolean = $state(false);
  backupSchedule: string = $state("");
  isSubmittingBackup: boolean = $state(false);
  isEditingMappings: boolean = $state(false);
  readonly bulkMigrateModel = new MigrationModel();

  // ── Git ───────────────────────────────────────────────────────
  openActionDropdown: string | null = $state(null);
  gitBusyIds: Set<string> = $state(new SvelteSet());
  gitResolvedIds: Set<string> = $state(new SvelteSet());
  gitLoadingIds: Set<string> = $state(new SvelteSet());
  cachedGitConfigs: any[] = $state([]);
  dashboardsLoadSeq: number = $state(0);
  lastLoadedEnvId: string | null = $state(null);
  serverTotal: number = $state(0);
  serverTotalPages: number = $state(1);
  profileFilterOverrideShowAll: boolean = $state(false);
  effectiveProfileFilter: any = $state(null);

  // ── Derived (store-dependent — handled in page due to legacy store) ──────

  // ═══ Data loading ══════════════════════════════════════════════

  async loadDashboards(): Promise<void> {
    if (!this.selectedEnv) return;
    const requestSeq = this.dashboardsLoadSeq + 1;
    this.dashboardsLoadSeq = requestSeq;
    this.isLoading = true;
    this.error = null;
    try {
      const firstResponse = await api.getDashboards(this.selectedEnv, {
        page: this.currentPage, page_size: this.pageSize,
        search: this.searchQuery.trim() || undefined,
        page_context: "dashboards_main",
        apply_profile_default: true,
        override_show_all: this.profileFilterOverrideShowAll,
        filters: {
          title: Array.from(this.columnFilters.title || []),
          git_status: Array.from(this.columnFilters.git_status || []),
          changed_on: Array.from(this.columnFilters.changed_on || []),
          actor: Array.from(this.columnFilters.actor || []),
        },
      });
      if (requestSeq !== this.dashboardsLoadSeq) return;
      const rawDashboards = firstResponse?.dashboards || [];
      this.serverTotal = Number(firstResponse?.total || 0);
      this.serverTotalPages = Math.max(1, Number(firstResponse?.total_pages || 1));
      this.effectiveProfileFilter = firstResponse?.effective_profile_filter || null;
      this.profileFilterOverrideShowAll = Boolean(firstResponse?.effective_profile_filter?.override_show_all);
      this.gitResolvedIds = new SvelteSet();
      this.gitLoadingIds = new SvelteSet();

      this.allDashboards = rawDashboards.map((d: any) => {
        const owners = normalizeOwners(d.owners);
        const hasGitStatus = !!d.git_status;
        return {
          id: d.id, title: d.title, slug: d.slug,
          changedOn: d.last_modified || null,
          changedOnLabel: formatDate(d.last_modified),
          owners,
          actorLabel: owners.length > 0 ? owners.join(", ") : "-",
          git: hasGitStatus ? {
            status: d.git_status?.sync_status?.toLowerCase() || "no_repo",
            branch: d.git_status?.branch || null,
            hasRepo: d.git_status?.has_repo === true,
            hasChangesForCommit: d.git_status?.has_changes_for_commit === true,
          } : { status: "pending", branch: null, hasRepo: null, hasChangesForCommit: false },
          lastTask: d.last_task ? {
            status: normalizeTaskStatus(d.last_task.status),
            validationStatus: normalizeValidationStatus(d.last_task.validation_status),
            id: d.last_task.task_id,
          } : null,
          actions: ["migrate", "backup"],
        };
      });

      this.selectedIds = new SvelteSet(
        Array.from(this.selectedIds).filter(id => this.allDashboards.some(d => d.id === id))
      );
      this.applyGridTransforms();
      this.updateSelectionState();
    } catch (err: any) {
      if (requestSeq !== this.dashboardsLoadSeq) return;
      this.error = err.message || t.dashboard?.load_failed;
      log('DashboardHub', 'EXPLORE', 'Load dashboards Error', {}, err?.message || 'unknown');
    } finally {
      if (requestSeq === this.dashboardsLoadSeq) this.isLoading = false;
    }
  }

  async loadDashboardSearchOptions(query: string = ''): Promise<void> {
    if (!this.selectedEnv) return;
    this.searchableDashboardLoading = true;
    try {
      const response = await api.getDashboards(this.selectedEnv, {
        search: query || undefined, page_size: 100, page_context: 'other',
        apply_profile_default: false, override_show_all: true,
      });
      this.searchableDashboardOptions = (response?.dashboards || []).map((d: any) => ({
        id: d.id, name: d.title, subtitle: d.slug || '',
        hint: d.last_modified ? formatDate(d.last_modified) : '',
      }));
    } catch { this.searchableDashboardOptions = []; }
    finally { this.searchableDashboardLoading = false; }
  }

  async fetchValidationStatuses(): Promise<void> {
    if (!this.selectedEnv || this.dashboards.length === 0) return;
    this.validationStatusLoading = true;
    try {
      const ids = this.dashboards.map(d => d.id).filter(Boolean);
      if (ids.length === 0) return;
      const result = await api.getValidationStatusBatch(this.selectedEnv, ids.join(","));
      if (result && typeof result === "object") this.validationStatuses = result;
    } catch { /* graceful no-op */ }
    finally { this.validationStatusLoading = false; }
  }

  // ═══ Pagination ════════════════════════════════════════════════

  setPage(page: number): void {
    if (page === this.currentPage) return;
    this.currentPage = page;
    void this.loadDashboards();
  }

  setPageSize(event: Event): void {
    this.pageSize = parseInt((event.target as HTMLSelectElement).value);
    this.currentPage = 1;
    void this.loadDashboards();
  }

  // ═══ Profile filter ════════════════════════════════════════════

  handleTemporaryShowAll(): void {
    if (this.profileFilterOverrideShowAll) return;
    this.profileFilterOverrideShowAll = true;
    this.currentPage = 1;
    void this.loadDashboards();
  }

  handleRestoreProfileFilter(): void {
    if (!this.profileFilterOverrideShowAll) return;
    this.profileFilterOverrideShowAll = false;
    this.currentPage = 1;
    void this.loadDashboards();
  }

  // ═══ Selection ═════════════════════════════════════════════════

  updateSelectionState(): void {
    this.isAllSelected = this.selectedIds.size === this.filteredDashboards.length && this.filteredDashboards.length > 0;
    this.isAllVisibleSelected = this.selectedIds.size === this.dashboards.length && this.dashboards.length > 0;
  }

  replaceSelectedIds(next: Set<string>): void { this.selectedIds = next; this.updateSelectionState(); }
  clearSelectedIds(): void { this.replaceSelectedIds(new SvelteSet()); }

  handleCheckboxChange(dashboard: DashboardRow, event: Event): void {
    const next = new SvelteSet(this.selectedIds);
    (event.target as HTMLInputElement).checked ? next.add(dashboard.id) : next.delete(dashboard.id);
    this.replaceSelectedIds(next);
  }

  handleSelectAll(): void {
    if (this.isAllSelected) { this.clearSelectedIds(); return; }
    const next = new SvelteSet(this.selectedIds);
    this.filteredDashboards.forEach(d => next.add(d.id));
    this.replaceSelectedIds(next);
  }

  handleSelectVisible(): void {
    const next = new SvelteSet(this.selectedIds);
    if (this.isAllVisibleSelected) { this.dashboards.forEach(d => next.delete(d.id)); }
    else { this.dashboards.forEach(d => next.add(d.id)); }
    this.replaceSelectedIds(next);
  }

  // ═══ Column filters ═══════════════════════════════════════════

  getValidationLabelForCell(dashboard: DashboardRow): string {
    const vs = this.validationStatuses[dashboard.id];
    return (!vs || !vs.status) ? "unknown" : String(vs.status).toLowerCase();
  }

  getColumnCellValue(dashboard: DashboardRow, column: FilterColumn): string {
    if (column === "title") return dashboard.title || "-";
    if (column === "git_status") return String(dashboard.git?.status || "pending").toLowerCase();
    if (column === "validation_status") return this.getValidationLabelForCell(dashboard);
    if (column === "changed_on") return dashboard.changedOn ? String(dashboard.changedOn).slice(0, 10) : "-";
    if (column === "actor") return dashboard.actorLabel || "-";
    return "-";
  }

  getFilterOptions(column: FilterColumn): string[] {
    return Array.from(new SvelteSet(
      this.allDashboards.map(d => this.getColumnCellValue(d, column)).filter(Boolean)
    )).sort((a, b) => a.localeCompare(b, "ru"));
  }

  getVisibleFilterOptions(column: FilterColumn): string[] {
    const searchText = (this.columnFilterSearch[column] || "").toLowerCase();
    return this.getFilterOptions(column).filter(v => v.toLowerCase().includes(searchText));
  }

  toggleFilterDropdown(column: FilterColumn, event: Event | undefined, panelWidth = 256): void {
    event?.stopPropagation();
    if (this.openFilterColumn === column) { this.openFilterColumn = null; return; }
    const trigger = (event as any)?.currentTarget;
    if (trigger?.getBoundingClientRect) {
      const rect = trigger.getBoundingClientRect();
      const vw = typeof window !== "undefined" ? window.innerWidth : 1920;
      this.filterDropdownPosition = { left: Math.max(8, Math.min(rect.left, vw - panelWidth - 8)), top: rect.bottom + 8 };
    }
    this.openFilterColumn = column;
  }

  toggleFilterValue(column: FilterColumn, value: string, checked: boolean): void {
    const next = new SvelteSet(this.columnFilters[column]);
    checked ? next.add(value) : next.delete(value);
    this.columnFilters = { ...this.columnFilters, [column]: next };
    this.currentPage = 1;
    void this.loadDashboards();
  }

  clearColumnFilter(column: FilterColumn): void {
    this.columnFilters = { ...this.columnFilters, [column]: new SvelteSet() };
    this.currentPage = 1;
    void this.loadDashboards();
  }

  selectAllColumnFilterValues(column: FilterColumn): void {
    this.columnFilters = { ...this.columnFilters, [column]: new SvelteSet(this.getVisibleFilterOptions(column)) };
    this.currentPage = 1;
    void this.loadDashboards();
  }

  updateColumnFilterSearch(column: FilterColumn, value: string): void {
    this.columnFilterSearch = { ...this.columnFilterSearch, [column]: value };
  }

  hasColumnFilter(column: FilterColumn): boolean { return this.columnFilters[column]?.size > 0; }

  // ═══ Sort ══════════════════════════════════════════════════════

  getSortValue(dashboard: DashboardRow, column: FilterColumn): string | number {
    if (column === "title") return (dashboard.title || "").toLowerCase();
    if (column === "git_status") return (this.getGitSummaryLabel(dashboard) || "").toLowerCase();
    if (column === "validation_status") return (this.getValidationLabelForCell(dashboard) || "").toLowerCase();
    if (column === "changed_on") return dashboard.changedOn ? new SvelteDate(dashboard.changedOn).getTime() : 0;
    if (column === "actor") return (dashboard.actorLabel || "").toLowerCase();
    return "";
  }

  handleSort(column: FilterColumn): void {
    if (this.sortColumn === column) {
      this.sortDirection = this.sortDirection === "asc" ? "desc" : "asc";
    } else {
      this.sortColumn = column;
      this.sortDirection = "asc";
    }
    this.currentPage = 1;
    this.applyGridTransforms();
  }

  // ═══ Grid transforms ═══════════════════════════════════════════

  applyGridTransforms(): void {
    let next = this.allDashboards;
    if (this.filterDashboardIds.length > 0) {
      next = next.filter(d => this.filterDashboardIds.includes(d.id));
    }
    next = [...next].sort((a, b) => {
      const aV = this.getSortValue(a, this.sortColumn);
      const bV = this.getSortValue(b, this.sortColumn);
      if (aV < bV) return this.sortDirection === "asc" ? -1 : 1;
      if (aV > bV) return this.sortDirection === "asc" ? 1 : -1;
      return 0;
    });
    this.filteredDashboards = next;
    this.dashboards = this.filteredDashboards;
    this.total = this.serverTotal;
    this.totalPages = this.serverTotalPages;
    this.updateSelectionState();
    void this.hydrateVisibleGitStatusesBatch();
    void this.fetchValidationStatuses();
  }

  // ═══ Git helpers ═══════════════════════════════════════════════

  getGitSummaryLabel(dashboard: DashboardRow): string {
    if (dashboard.git?.hasRepo === null) return t.common?.loading || "Loading";
    if (!dashboard.git?.hasRepo) return t.dashboard?.status_no_repo || "No Repo";
    return dashboard.git?.hasChangesForCommit ? t.dashboard?.status_changes || "Diff" : t.dashboard?.status_no_changes || "Synced";
  }

  isGitBusy(dashboardId: string): boolean { return this.gitBusyIds.has(dashboardId); }

  setGitBusy(dashboardId: string, busy: boolean): void {
    if (busy) this.gitBusyIds.add(dashboardId); else this.gitBusyIds.delete(dashboardId);
    this.gitBusyIds = new SvelteSet(this.gitBusyIds);
  }

  async ensureGitConfigs(): Promise<any[]> {
    if (this.cachedGitConfigs.length > 0) return this.cachedGitConfigs;
    this.cachedGitConfigs = await gitService.getConfigs();
    return this.cachedGitConfigs;
  }

  updateDashboardGitState(dashboardId: string, nextGit: Partial<GitState>): void {
    const merge = (col: DashboardRow[]) => col.map(d => d.id === dashboardId ? { ...d, git: { ...d.git, ...nextGit } } : d);
    this.allDashboards = merge(this.allDashboards);
    this.filteredDashboards = merge(this.filteredDashboards);
    this.dashboards = merge(this.dashboards);
  }

  normalizeRepositoryStatusPayload(status: any): GitState {
    const syncState = String(status?.sync_state || "").toUpperCase();
    const syncStatus = String(status?.sync_status || "").toUpperCase();
    const explicitHasRepo = status?.has_repo;
    const hasRepo = explicitHasRepo === true ? true : explicitHasRepo === false ? false : !(syncState === "NO_REPO" || syncStatus === "NO_REPO");
    if (!hasRepo) return { status: "no_repo", branch: null, hasRepo: false, hasChangesForCommit: false };
    const hasChanges = syncState === "CHANGES" || syncStatus === "DIFF" || Boolean(status?.is_dirty) ||
      (status?.untracked_files?.length || 0) > 0 || (status?.modified_files?.length || 0) > 0 || (status?.staged_files?.length || 0) > 0;
    return { status: hasChanges ? "diff" : "ok", branch: status?.current_branch || status?.branch || null, hasRepo: true, hasChangesForCommit: hasChanges };
  }

  async fetchDashboardGitStatusesBatch(dashboardIds: string[], force = false): Promise<void> {
    const uniqueIds = Array.from(new SvelteSet(dashboardIds));
    if (uniqueIds.length === 0) return;
    if (force) { uniqueIds.forEach(id => this.gitResolvedIds.delete(id)); this.gitResolvedIds = new SvelteSet(this.gitResolvedIds); }
    const pendingIds = uniqueIds.filter(id => !this.gitResolvedIds.has(id) && !this.gitLoadingIds.has(id));
    if (pendingIds.length === 0) return;
    pendingIds.forEach(id => this.gitLoadingIds.add(id));
    this.gitLoadingIds = new SvelteSet(this.gitLoadingIds);
    try {
      const batchResult = await gitService.getStatusesBatch(pendingIds);
      const statuses = batchResult?.statuses || {};
      pendingIds.forEach(id => {
        const st = statuses[id] ?? statuses[String(id)];
        this.updateDashboardGitState(id, st ? this.normalizeRepositoryStatusPayload(st) : { status: "no_repo", branch: null, hasRepo: false, hasChangesForCommit: false });
      });
    } catch {
      pendingIds.forEach(id => this.updateDashboardGitState(id, { status: "no_repo", branch: null, hasRepo: false, hasChangesForCommit: false }));
    } finally {
      pendingIds.forEach(id => { this.gitLoadingIds.delete(id); this.gitResolvedIds.add(id); });
      this.gitLoadingIds = new SvelteSet(this.gitLoadingIds);
      this.gitResolvedIds = new SvelteSet(this.gitResolvedIds);
    }
  }

  async refreshDashboardGitState(dashboardId: string, force = false): Promise<void> { await this.fetchDashboardGitStatusesBatch([dashboardId], force); }

  async hydrateVisibleGitStatusesBatch(): Promise<void> {
    const pending = this.dashboards.filter(d => d.git?.hasRepo === null && !this.gitResolvedIds.has(d.id) && !this.gitLoadingIds.has(d.id)).map(d => d.id);
    if (pending.length > 0) await this.fetchDashboardGitStatusesBatch(pending, false);
  }

  async handleGitInit(dashboard: DashboardRow): Promise<void> {
    this.setGitBusy(dashboard.id, true);
    try {
      const configs = await this.ensureGitConfigs();
      if (!configs.length) { addToast(t.git?.no_servers_configured || "No Git config found", "error"); return; }
      const config = configs[0];
      const defaultRemote = config?.default_repository ? `${String(config.url || "").replace(/\/$/, "")}/${config.default_repository}.git` : "";
      const remoteUrl = prompt(t.git?.remote_url || "Remote URL", defaultRemote);
      if (!remoteUrl) return;
      await gitService.initRepository(dashboard.id, config.id, remoteUrl.trim());
      addToast(t.git?.init_success || "Repository initialized", "success");
      await this.refreshDashboardGitState(dashboard.id, true);
    } catch (err: any) { addToast(err?.message || "Git init failed", "error"); }
    finally { this.setGitBusy(dashboard.id, false); }
  }

  async handleGitSync(dashboard: DashboardRow): Promise<void> {
    this.setGitBusy(dashboard.id, true);
    try { await gitService.sync(dashboard.id, this.selectedEnv || null); addToast(t.git?.sync_success || "Synced", "success"); await this.refreshDashboardGitState(dashboard.id, true); }
    catch (err: any) { addToast(err?.message || "Git sync failed", "error"); }
    finally { this.setGitBusy(dashboard.id, false); }
  }

  async handleGitCommit(dashboard: DashboardRow): Promise<void> {
    if (!dashboard.git?.hasRepo) { addToast(t.git?.not_linked || "Repository not linked", "error"); return; }
    if (!dashboard.git?.hasChangesForCommit) { addToast(t.git?.nothing_to_commit || "No changes to commit", "error"); return; }
    const message = prompt(t.git?.commit_message || "Commit message", `Update dashboard ${dashboard.title}`);
    if (!message?.trim()) return;
    this.setGitBusy(dashboard.id, true);
    try { await gitService.commit(dashboard.slug || dashboard.id, message.trim()); addToast(t.git?.commit_success || "Committed", "success"); await this.refreshDashboardGitState(dashboard.id, true); }
    catch (err: any) { addToast(err?.message || "Git commit failed", "error"); }
    finally { this.setGitBusy(dashboard.id, false); }
  }

  async handleGitPull(dashboard: DashboardRow): Promise<void> {
    if (!dashboard.git?.hasRepo) return;
    this.setGitBusy(dashboard.id, true);
    try { await gitService.pull(dashboard.slug || dashboard.id, this.selectedEnv || null); addToast(t.git?.pull_success || "Pulled", "success"); await this.refreshDashboardGitState(dashboard.id, true); }
    catch (err: any) { addToast(err?.message || "Git pull failed", "error"); }
    finally { this.setGitBusy(dashboard.id, false); }
  }

  async handleGitPush(dashboard: DashboardRow): Promise<void> {
    if (!dashboard.git?.hasRepo) return;
    this.setGitBusy(dashboard.id, true);
    try { await gitService.push(dashboard.slug || dashboard.id, this.selectedEnv || null); addToast(t.git?.push_success || "Pushed", "success"); await this.refreshDashboardGitState(dashboard.id, true); }
    catch (err: any) { addToast(err?.message || "Git push failed", "error"); }
    finally { this.setGitBusy(dashboard.id, false); }
  }

  // ═══ Validation popover ════════════════════════════════════════

  toggleValidationPopover(dashboardId: string, event: Event): void {
    event.stopPropagation();
    if (this.openValidationPopover === dashboardId) { this.openValidationPopover = null; return; }
    const trigger = event.currentTarget as HTMLElement;
    if (trigger?.getBoundingClientRect) {
      const rect = trigger.getBoundingClientRect();
      const vw = typeof window !== "undefined" ? window.innerWidth : 1920;
      this.validationPopoverPosition = { left: Math.max(8, Math.min(rect.left, vw - 320 - 8)), top: rect.bottom + 8 };
    }
    this.openValidationPopover = dashboardId;
  }

  // ═══ Actions ═══════════════════════════════════════════════════

  toggleActionDropdown(id: string | null, event: Event): void {
    event.stopPropagation();
    this.openActionDropdown = this.openActionDropdown === id ? null : id;
  }
  closeActionDropdown(): void { this.openActionDropdown = null; }

  async handleAction(dashboard: DashboardRow, action: string): Promise<void> {
    log('DashboardHub', 'REASON', `${action} on dashboard`, { title: dashboard.title });
    this.closeActionDropdown();
    try {
      if (action === "migrate") {
        this.replaceSelectedIds(new SvelteSet([dashboard.id]));
        this.showMigrateModal = true;
        this.isEditingMappings = false;
        this.bulkMigrateModel.sourceEnvId = this.selectedEnv ?? "";
        this.bulkMigrateModel.selectedDashboardIds = Array.from(this.selectedIds);
        this.bulkMigrateModel.targetEnvId = "";
        this.bulkMigrateModel.dryRunResult = null;
        this.bulkMigrateModel.error = "";
        this.bulkMigrateModel.sourceDatabases = [];
        this.bulkMigrateModel.targetDatabases = [];
        this.bulkMigrateModel.mappings = [];
        this.bulkMigrateModel.suggestions = [];
        this.bulkMigrateModel.currentStep = 1;
      } else if (action === "backup") {
        this.replaceSelectedIds(new SvelteSet([dashboard.id]));
        this.showBackupModal = true;
        this.backupSchedule = "";
      }
    } catch (err: any) { log('DashboardHub', 'EXPLORE', 'Action Error', {}, err?.message || 'unknown'); }
  }

  async handleBulkBackup(): Promise<void> {
    if (this.isSubmittingBackup || this.selectedIds.size === 0) return;
    this.isSubmittingBackup = true;
    try {
      const response = await api.postApi("/dashboards/backup", { env_id: this.selectedEnv, dashboard_ids: Array.from(this.selectedIds), schedule: this.backupSchedule || undefined });
      log('DashboardHub', 'REFLECT', 'Bulk backup task created', { task_id: response.task_id });
      this.showBackupModal = false;
      this.clearSelectedIds();
      if (response.task_id) {
        const taskId = typeof response.task_id === "object" ? response.task_id.id || response.task_id.task_id : response.task_id;
        openDrawerForTaskIfPreferred(taskId);
      }
    } catch (err: any) {
      log('DashboardHub', 'EXPLORE', 'Bulk backup error', {}, err?.message || 'unknown');
      alert(t.dashboard?.backup_task_failed);
    } finally { this.isSubmittingBackup = false; }
  }

  handleTaskStatusClick(dashboard: DashboardRow): void {
    if (dashboard.lastTask?.id) {
      log('DashboardHub', 'REASON', 'Open task drawer for task', { taskId: dashboard.lastTask.id });
      openDrawerForTask(dashboard.lastTask.id);
    }
  }

  navigateToDashboardDetail(dashboard: DashboardRow): void {
    const ref = dashboard?.slug || dashboard?.id;
    if (!ref || !this.selectedEnv) return;
    goto(ROUTES.dashboards.detail(ref, this.selectedEnv));
  }
}
// #endregion DashboardHubModel
