// #region Dashboards.HubModel [C:4] [TYPE Model] [SEMANTICS dashboard, hub, grid, coordinator, screen-model]
// @ingroup Dashboards
// @BRIEF Coordinator model for the dashboard hub page — delegates to 3 sub-models (Filters, Selection, GitActions).
// @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).
// @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 handleAction(dash, action)      — Opens migrate or backup modal for a dashboard.
// @ACTION handleBulkBackup()              — Executes bulk backup.
// @ACTION handleTaskStatusClick(dash)     — Opens task drawer for a dashboard's last task.
// @ACTION navigateToDashboardDetail(dash) — Navigates to dashboard detail page.
// @ACTION toggleValidationPopover(id, ev) — Opens/closes validation status popover.
// @ACTION applyGridTransforms()           — Re-applies sort/filter + selection + git hydration.
// @ACTION loadDashboardSearchOptions()    — Fetches searchable dashboard options.
// @ACTION fetchValidationStatuses()       — Fetches batch validation statuses.
// @ACTION refreshDashboardGitState(id)    — Refreshes a single dashboard's git state.
// @ACTION hydrateVisibleGitStatusesBatch() — Fetches git statuses for pending dashboards.
// @RELATION DEPENDS_ON -> [ApiModule]
// @RELATION CALLS -> [Dashboards.FiltersModel]
// @RELATION CALLS -> [Dashboards.SelectionModel]
// @RELATION CALLS -> [Dashboards.GitActionsModel]
// @RELATION BINDS_TO -> [environmentContextStore]
// @RELATION CALLS -> [CotLogger]

import { SvelteSet } 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 { 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";
import { DashboardsFiltersModel } from "./Dashboards.FiltersModel.svelte.ts";
import { DashboardsSelectionModel } from "./Dashboards.SelectionModel.svelte.ts";
import { DashboardsGitActionsModel } from "./Dashboards.GitActionsModel.svelte.ts";

// ── Type re-exports (for importing convenience) ──────────────────
export type { DashboardRow, FilterColumn, SortDir, ColumnFilters } from "./Dashboards.FiltersModel.svelte.ts";
export type { GitState } from "./Dashboards.GitActionsModel.svelte.ts";

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

export class DashboardHubModel {
  // ── Sub-models ─────────────────────────────────────────────────
  readonly filters = new DashboardsFiltersModel();
  readonly selection = new DashboardsSelectionModel();
  readonly gitActions = new DashboardsGitActionsModel();

  // ── Core state ────────────────────────────────────────────────
  selectedEnv: string | null = $state(null);
  allDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]);
  /** Larger dataset for filter options (loaded via loadDashboardSearchOptions, up to 100) */
  filterOptionsDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]);
  dashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]);
  filteredDashboards: import("./Dashboards.FiltersModel.svelte.ts").DashboardRow[] = $state([]);
  isLoading: boolean = $state(true);
  error: string | null = $state(null);

  // ── Validation popover ────────────────────────────────────────
  validationStatuses: Record<string, any> = $state({});
  /** Version counter bumped after each fetchValidationStatuses — triggers grid re-render */
  validationVersion: number = $state(0);
  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();

  // ── Server metadata ───────────────────────────────────────────
  dashboardsLoadSeq: number = $state(0);
  lastLoadedEnvId: string | null = $state(null);
  serverTotal: number = $state(0);
  serverTotalPages: number = $state(1);

  constructor() {
    // Wire up git state callback: when git state changes on any dashboard,
    // propagate to the parent's dashboard arrays for reactivity.
    this.gitActions.onGitStateUpdate = (id: string, nextGit: any) => {
      const merge = (col: any[]) => col.map((d: any) => d.id === id ? { ...d, git: { ...d.git, ...nextGit } } : d);
      this.allDashboards = merge(this.allDashboards) as any;
      this.filteredDashboards = merge(this.filteredDashboards) as any;
      this.dashboards = merge(this.dashboards) as any;
    };
  }

  // ══ Delegated atoms (backward-compat access) ══════════════════

  // ── Filters model delegation ──────────────────────────────────
  get currentPage() { return this.filters.currentPage; }
  set currentPage(v) { this.filters.currentPage = v; }
  get pageSize() { return this.filters.pageSize; }
  set pageSize(v) { this.filters.pageSize = v; }
  get totalPages() { return this.filters.totalPages; }
  set totalPages(v) { this.filters.totalPages = v; }
  get total() { return this.filters.total; }
  set total(v) { this.filters.total = v; }
  get searchQuery() { return this.filters.searchQuery; }
  set searchQuery(v) { this.filters.searchQuery = v; }
  get filterDashboardIds() { return this.filters.filterDashboardIds; }
  set filterDashboardIds(v) { this.filters.filterDashboardIds = v; }
  get searchableDashboardOptions() { return this.filters.searchableDashboardOptions; }
  set searchableDashboardOptions(v) { this.filters.searchableDashboardOptions = v; }
  get searchableDashboardLoading() { return this.filters.searchableDashboardLoading; }
  set searchableDashboardLoading(v) { this.filters.searchableDashboardLoading = v; }
  get sortColumn() { return this.filters.sortColumn; }
  set sortColumn(v) { this.filters.sortColumn = v; }
  get sortDirection() { return this.filters.sortDirection; }
  set sortDirection(v) { this.filters.sortDirection = v; }
  get openFilterColumn() { return this.filters.openFilterColumn; }
  set openFilterColumn(v) { this.filters.openFilterColumn = v; }
  get filterDropdownPosition() { return this.filters.filterDropdownPosition; }
  set filterDropdownPosition(v) { this.filters.filterDropdownPosition = v; }
  get columnFilterSearch() { return this.filters.columnFilterSearch; }
  set columnFilterSearch(v) { this.filters.columnFilterSearch = v; }
  get columnFilters() { return this.filters.columnFilters; }
  set columnFilters(v) { this.filters.columnFilters = v; }
  get profileFilterOverrideShowAll() { return this.filters.profileFilterOverrideShowAll; }
  set profileFilterOverrideShowAll(v) { this.filters.profileFilterOverrideShowAll = v; }
  get effectiveProfileFilter() { return this.filters.effectiveProfileFilter; }
  set effectiveProfileFilter(v) { this.filters.effectiveProfileFilter = v; }

  // ── Selection model delegation ────────────────────────────────
  get selectedIds() { return this.selection.selectedIds; }
  set selectedIds(v) { this.selection.selectedIds = v; }
  get isAllSelected() { return this.selection.isAllSelected; }
  set isAllSelected(v) { this.selection.isAllSelected = v; }
  get isAllVisibleSelected() { return this.selection.isAllVisibleSelected; }
  set isAllVisibleSelected(v) { this.selection.isAllVisibleSelected = v; }

  // ── Git actions model delegation ──────────────────────────────
  get openActionDropdown() { return this.gitActions.openActionDropdown; }
  set openActionDropdown(v) { this.gitActions.openActionDropdown = v; }
  get gitBusyIds() { return this.gitActions.gitBusyIds; }
  set gitBusyIds(v) { this.gitActions.gitBusyIds = v; }
  get gitResolvedIds() { return this.gitActions.gitResolvedIds; }
  set gitResolvedIds(v) { this.gitActions.gitResolvedIds = v; }
  get gitLoadingIds() { return this.gitActions.gitLoadingIds; }
  set gitLoadingIds(v) { this.gitActions.gitLoadingIds = v; }
  get cachedGitConfigs() { return this.gitActions.cachedGitConfigs; }
  set cachedGitConfigs(v) { this.gitActions.cachedGitConfigs = v; }

  // ══ Method delegation (backward-compat) ═══════════════════════

  // ── From FiltersModel ─────────────────────────────────────────

  setPage(page: number): void {
    this.filters.setPage(page);
    void this.loadDashboards();
  }

  setPageSize(event: Event): void {
    this.filters.setPageSize(event);
    void this.loadDashboards();
  }

  handleTemporaryShowAll(): void {
    this.filters.handleTemporaryShowAll();
    void this.loadDashboards();
  }

  handleRestoreProfileFilter(): void {
    this.filters.handleRestoreProfileFilter();
    void this.loadDashboards();
  }

  getValidationLabelForCell(dashboard: any): string {
    return this.filters.getValidationLabelForCell(dashboard, this.validationStatuses);
  }

  getColumnCellValue(dashboard: any, column: any): string {
    return this.filters.getColumnCellValue(dashboard, column, this.validationStatuses);
  }

  getFilterOptions(column: any): string[] {
    const data = this.filterOptionsDashboards.length > this.allDashboards.length
      ? this.filterOptionsDashboards : this.allDashboards;
    return this.filters.getFilterOptions(column, data as any, this.validationStatuses);
  }

  getVisibleFilterOptions(column: any): string[] {
    const data = this.filterOptionsDashboards.length > this.allDashboards.length
      ? this.filterOptionsDashboards : this.allDashboards;
    return this.filters.getVisibleFilterOptions(column, data as any, this.validationStatuses);
  }

  getFilterOptionLabel(column: any, rawValue: string): string {
    return this.filters.getFilterOptionLabel(column, rawValue);
  }

  toggleFilterDropdown(column: any, event: Event | undefined, panelWidth?: number): void {
    this.filters.toggleFilterDropdown(column, event, panelWidth);
  }

  toggleFilterValue(column: any, value: string, checked: boolean): void {
    this.filters.toggleFilterValue(column, value, checked);
    void this.loadDashboards();
  }

  clearColumnFilter(column: any): void {
    this.filters.clearColumnFilter(column);
    void this.loadDashboards();
  }

  selectAllColumnFilterValues(column: any): void {
    this.filters.selectAllColumnFilterValues(column, this.allDashboards as any, this.validationStatuses);
    void this.loadDashboards();
  }

  updateColumnFilterSearch(column: any, value: string): void {
    this.filters.updateColumnFilterSearch(column, value);
  }

  hasColumnFilter(column: any): boolean {
    return this.filters.hasColumnFilter(column);
  }

  getSortValue(dashboard: any, column: any): string | number {
    const gitSummary = this.gitActions.getGitSummaryLabel(dashboard);
    return this.filters.getSortValue(dashboard, column, gitSummary);
  }

  handleSort(column: any): void {
    this.filters.handleSort(column);
    this.applyGridTransforms();
  }

  // ── From SelectionModel ───────────────────────────────────────

  updateSelectionState(): void {
    this.selection.updateSelectionState(this.filteredDashboards as any, this.dashboards as any);
  }

  replaceSelectedIds(next: Set<string>): void {
    // Goes through parent's getter/setter for backward compat (test overrides selectedIds with defineProperty)
    this.selectedIds = next;
    this.updateSelectionState();
  }
  clearSelectedIds(): void { this.selection.clearSelectedIds(); }

  handleCheckboxChange(dashboard: any, event: Event): void {
    this.selection.handleCheckboxChange(dashboard, event);
  }

  handleSelectAll(): void {
    this.selection.handleSelectAll(this.filteredDashboards as any);
  }

  handleSelectVisible(): void {
    this.selection.handleSelectVisible(this.dashboards as any);
  }

  // ── From GitActionsModel ──────────────────────────────────────

  getGitSummaryLabel(dashboard: any): string {
    return this.gitActions.getGitSummaryLabel(dashboard);
  }

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

  setGitBusy(dashboardId: string, busy: boolean): void {
    this.gitActions.setGitBusy(dashboardId, busy);
  }

  async ensureGitConfigs(): Promise<any[]> { return this.gitActions.ensureGitConfigs(); }

  /** Backward-compat: applies git state update to all dashboard arrays. */
  updateDashboardGitState(dashboardId: string, nextGit: any): void {
    this.gitActions.onGitStateUpdate?.(dashboardId, nextGit);
  }

  normalizeRepositoryStatusPayload(status: any): any {
    return this.gitActions.normalizeRepositoryStatusPayload(status);
  }

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

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

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

  async handleGitInit(dashboard: any): Promise<void> {
    await this.gitActions.handleGitInit(dashboard);
  }

  async handleGitSync(dashboard: any): Promise<void> {
    await this.gitActions.handleGitSync(dashboard, this.selectedEnv);
  }

  async handleGitCommit(dashboard: any): Promise<void> {
    await this.gitActions.handleGitCommit(dashboard, this.selectedEnv);
  }

  async handleGitPull(dashboard: any): Promise<void> {
    await this.gitActions.handleGitPull(dashboard, this.selectedEnv);
  }

  async handleGitPush(dashboard: any): Promise<void> {
    await this.gitActions.handleGitPush(dashboard, this.selectedEnv);
  }

  toggleActionDropdown(id: string | null, event: Event): void {
    this.gitActions.toggleActionDropdown(id, event);
  }
  closeActionDropdown(): void { this.gitActions.closeActionDropdown(); }

  // ══ Data loading (parent-owned) ═══════════════════════════════

  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.gitActions.gitResolvedIds = new SvelteSet();
      this.gitActions.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: "no_repo", 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"],
        } as any;
      });

      this.selectedIds = new SvelteSet(
        Array.from(this.selectedIds).filter(id => this.allDashboards.some((d: any) => d.id === id))
      );
      this.applyGridTransforms();
    } 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,
      });
      const raw = response?.dashboards || [];
      this.searchableDashboardOptions = raw.map((d: any) => ({
        id: d.id, name: d.title, subtitle: d.slug || '',
        hint: d.last_modified ? formatDate(d.last_modified) : '',
      }));
      // Also store for filter options (lightweight DashboardRow mapping)
      this.filterOptionsDashboards = raw.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: "no_repo", branch: null, hasRepo: null, hasChangesForCommit: false },
        } as any;
      });
    } catch {
      this.searchableDashboardOptions = [];
      console.warn('[DashboardHub] loadDashboardSearchOptions failed');
    }
    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: any) => 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; this.validationVersion++; }
    } catch { /* graceful no-op */ }
    finally { this.validationStatusLoading = false; }
  }

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

  toggleValidationPopover(dashboardId: string, trigger: HTMLElement): void {
    if (this.openValidationPopover === dashboardId) { this.closeValidationPopover(); return; }
    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;
  }

  closeValidationPopover(): void {
    this.openValidationPopover = null;
  }

  // ══ Grid transforms (coordinator) ════════════════════════════

  applyGridTransforms(): void {
    const result = this.filters.applyGridTransforms(this.allDashboards as any, this.serverTotal, this.serverTotalPages);
    this.filteredDashboards = result.filteredDashboards as any;
    this.dashboards = result.dashboards as any;
    this.total = result.total;
    this.totalPages = result.totalPages;
    this.selection.updateSelectionState(this.filteredDashboards as any, this.dashboards as any);
    void this.hydrateVisibleGitStatusesBatch().catch(() => {});
    void this.fetchValidationStatuses();
  }

  // ══ Actions (parent-owned) ════════════════════════════════════

  async handleAction(dashboard: any, 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.selection.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: any): void {
    if (dashboard.lastTask?.id) {
      log('DashboardHub', 'REASON', 'Open task drawer for task', { taskId: dashboard.lastTask.id });
      openDrawerForTask(dashboard.lastTask.id);
    }
  }

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