// frontend/src/lib/models/MigrationModel.svelte.ts
// #region Migration.Model [C:4] [TYPE Model] [SEMANTICS migration,dashboard,dry-run,environment,screen-model]
// @defgroup Migration Migration wizard — multi-step screen model for dashboard migration.
// @INVARIANT Changing source environment resets dashboard selection to empty.
// @INVARIANT Changing source environment resets databases, mappings, and suggestions.
// @INVARIANT Changing source environment clears dry-run result.
// @INVARIANT Migration execution blocked unless source≠target and ≥1 dashboard selected.
// @INVARIANT Dry-run must complete before advancing to execution step.
// @INVARIANT Password prompt only appears for AWAITING_INPUT tasks with type "database_password".
// @INVARIANT DECOMPOSITION GATE (was 457 lines). Split: WizardModel (step nav), ExecutorModel (dry-run/exec/password).
// @STATE idle — Initial state, environments loading.
// @STATE ready — Environments loaded, awaiting user configuration.
// @STATE loading — Fetching dashboards, databases, or dry-run in progress.
// @STATE review — Dry-run completed, awaiting user decision.
// @STATE executing — Migration task submitted, task runner active.
// @STATE error — API call failed, error message available.
// @ACTION loadEnvironments() — Fetches environments, pre-fills source from active context.
// @ACTION selectSourceEnv(envId) — Sets source, triggers dashboard/database/mapping reset.
// @ACTION selectTargetEnv(envId) — Sets target environment.
// @ACTION toggleDashboard(id) — Toggles dashboard selection.
// @ACTION setReplaceDb(bool) — Toggles database replacement option.
// @ACTION setFixCrossFilters(bool) — Toggles cross-filter fixing.
// @ACTION fetchDatabases() — Loads source/target databases, mappings, and AI suggestions.
// @ACTION saveMapping(sourceUuid, targetUuid) — Saves a database mapping pair.
// @ACTION calculateDryRun() — Delegates to Migration.ExecutorModel.
// @ACTION executeMigration() — Delegates to Migration.ExecutorModel.
// @ACTION resumeMigration(passwords) — Delegates to Migration.ExecutorModel.
// @ACTION goToStep(n) — Delegates to Migration.WizardModel.
// @ACTION clearError() — Delegates to Migration.WizardModel.
// @ACTION openLogViewer(task) — Opens the log viewer modal for a task.
// @ACTION closeLogViewer() — Closes the log viewer modal.
// @ACTION toggleTaskHistory() — Toggles task history panel visibility.
// @ATOM showMigrateModal — Visibility of migration modal (used by dashboards page).
// @RELATION DEPENDS_ON -> [EXT:frontend:api]
// @RELATION BINDS_TO -> [selectedTask]
// @RELATION BINDS_TO -> [environmentContextStore]
// @RELATION CALLS -> [EXT:frontend:resumeTask]
// @RELATION DISPATCHES -> [Migration.WizardModel]
// @RELATION DISPATCHES -> [Migration.ExecutorModel]
// @RATIONALE Svelte 5 class-based model with $state atoms and $derived computed values. Extracted from MigrationPage/+page.svelte to enable L1 testing without DOM render. Decomposed into sub-models per @INVARIANT DECOMPOSITION GATE (was 457 lines, now ~290).
// @REJECTED Inline $state + handler functions in MigrationPage/+page.svelte rejected — scatters business logic across event handlers, makes testing impossible without DOM, violates RSM pattern.

import { api } from "$lib/api.js";
import { selectedTask } from "$lib/stores.svelte.js";
import { environmentContextStore } from "$lib/stores/environmentContext.svelte.js";
import { t } from "$lib/i18n/index.svelte.js";
import { WizardState } from "./Migration.WizardModel.svelte.ts";
import { MigrationExecutor } from "./Migration.ExecutorModel.svelte.ts";
import type { DryRunResult, DashboardSelection } from "./Migration.ExecutorModel.svelte.ts";

// ── Types ─────────────────────────────────────────────────────

export type ScreenState = "idle" | "ready" | "loading" | "review" | "executing" | "error";

interface Environment {
  id: string;
  name: string;
  [key: string]: unknown;
}

interface Dashboard {
  id: string;
  [key: string]: unknown;
}

interface Database {
  uuid: string;
  database_name: string;
  [key: string]: unknown;
}

interface Mapping {
  id?: number;
  source_db_uuid?: string;
  target_db_uuid?: string;
  source_db_name?: string;
  target_db_name?: string;
  [key: string]: unknown;
}

interface LogViewerTask {
  id: string;
  status: string;
  [key: string]: unknown;
}

export class MigrationModel {
  // ── Sub-models (instantiated after $state fields, before $derived) ─

  wizard: WizardState = new WizardState(this as unknown as MigrationModel);
  executor: MigrationExecutor = new MigrationExecutor(this as unknown as MigrationModel);

  // ── Atoms (reactive state) ──────────────────────────────────────

  // Environments
  environments: Environment[] = $state([]);
  sourceEnvId: string = $state("");
  targetEnvId: string = $state("");

  // Migration options
  replaceDb: boolean = $state(false);
  fixCrossFilters: boolean = $state(true);

  // Dashboards
  dashboards: Dashboard[] = $state([]);

  // Database mappings
  sourceDatabases: Database[] = $state([]);
  targetDatabases: Database[] = $state([]);
  mappings: Mapping[] = $state([]);
  suggestions: Record<string, unknown>[] = $state([]);

  // Loading & error
  loading: boolean = $state(true);
  error: string = $state("");
  fetchingDbs: boolean = $state(false);

  // Task history panel
  showTaskHistory: boolean = $state(false);

  // Log viewer modal
  showLogViewer: boolean = $state(false);
  logViewerTaskId: string | null = $state(null);
  logViewerTaskStatus: string | null = $state(null);

  // Migration modal visibility (used by dashboards page)
  showMigrateModal: boolean = $state(false);

  // Store refs (read-only reactive bindings)
  selectedTaskStore = selectedTask;
  envContextStore = environmentContextStore;

  // ── Proxy Getters / Setters (delegate to sub-models) ─────────────

  get currentStep(): number { return this.wizard.currentStep; }
  set currentStep(v: number) { this.wizard.currentStep = v; }

  get selectedDashboardIds(): string[] { return this.executor.selectedDashboardIds; }
  set selectedDashboardIds(v: string[]) { this.executor.selectedDashboardIds = v; }

  get dryRunResult(): DryRunResult | null { return this.executor.dryRunResult; }
  set dryRunResult(v: DryRunResult | null) { this.executor.dryRunResult = v; }

  get dryRunLoading(): boolean { return this.executor.dryRunLoading; }
  set dryRunLoading(v: boolean) { this.executor.dryRunLoading = v; }

  get isCalculating(): boolean { return this.executor.isCalculating; }

  get showPasswordPrompt(): boolean { return this.executor.showPasswordPrompt; }
  set showPasswordPrompt(v: boolean) { this.executor.showPasswordPrompt = v; }

  get passwordPromptDatabases(): string[] { return this.executor.passwordPromptDatabases; }
  set passwordPromptDatabases(v: string[]) { this.executor.passwordPromptDatabases = v; }

  get passwordPromptErrorMessage(): string { return this.executor.passwordPromptErrorMessage; }
  set passwordPromptErrorMessage(v: string) { this.executor.passwordPromptErrorMessage = v; }

  // ── Derived ─────────────────────────────────────────────────────

  /** Whether each wizard step is ready to proceed. */
  stepReady = $derived<Record<number, boolean>>({
    1: this.sourceEnvId !== "" && this.targetEnvId !== "" && this.sourceEnvId !== this.targetEnvId,
    2: this.selectedDashboardIds.length > 0,
    3: true, // review is always ready if we got here
  });

  /** Human-readable screen state derived from atoms. */
  screenState: ScreenState = $derived(
    this.loading ? "idle"
    : this.error ? "error"
    : this.dryRunLoading || this.fetchingDbs ? "loading"
    : this.dryRunResult ? "review"
    : this.currentStep >= 4 ? "executing"
    : "ready"
  );

  /** Whether environments are distinct and valid. */
  envsReady: boolean = $derived(this.stepReady[1]);

  /** Whether migration can be executed. */
  canExecute: boolean = $derived(this.dryRunResult != null && this.stepReady[2]);

  // ── Delegating Methods ──────────────────────────────────────────

  goToStep(step: number): void { this.wizard.goToStep(step); }
  clearError(): void { this.wizard.clearError(); }
  async calculateDryRun(): Promise<void> { return this.executor.calculateDryRun(); }
  async executeMigration(endpoint: string = "/migration/execute"): Promise<void> {
    return this.executor.executeMigration(endpoint);
  }
  async resumeMigration(passwords: Record<string, string>): Promise<void> {
    return this.executor.resumeMigration(passwords);
  }
  checkPasswordPrompt(): void { this.executor.checkPasswordPrompt(); }

  // ── Core Actions ────────────────────────────────────────────────

  /** Load environments list. Pre-fills source from active context. */
  async loadEnvironments(): Promise<void> {
    this.loading = true;
    this.error = "";
    try {
      this.environments = await api.getEnvironmentsList();
      const activeEnvId = this.envContextStore.current?.selectedEnvId;
      if (activeEnvId && this.environments.some((env: Environment) => env.id === activeEnvId)) {
        this.sourceEnvId = activeEnvId;
      }
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : "Failed to load environments";
    } finally {
      this.loading = false;
    }
  }

  /**
   * Select source environment. Triggers dashboard refetch.
   * @INVARIANT Resets dashboard selection, databases, mappings, and dry-run result.
   */
  selectSourceEnv(envId: string): void {
    if (envId === this.sourceEnvId) return;
    this.sourceEnvId = envId;
    // @INVARIANT: reset dependent selections
    this.selectedDashboardIds = [];
    this.dashboards = [];
    this.sourceDatabases = [];
    this.targetDatabases = [];
    this.mappings = [];
    this.suggestions = [];
    this.dryRunResult = null;
    if (envId) this._fetchDashboards(envId);
  }

  /** Select target environment. Resets dry-run result. */
  selectTargetEnv(envId: string): void {
    if (envId === this.targetEnvId) return;
    this.targetEnvId = envId;
    this.dryRunResult = null;
  }

  /** Toggle database replacement option. Resets databases when toggling on. */
  setReplaceDb(val: boolean): void {
    this.replaceDb = val;
    if (!val) {
      this.sourceDatabases = [];
      this.targetDatabases = [];
      this.mappings = [];
      this.suggestions = [];
    }
    this.dryRunResult = null;
  }

  /** Toggle cross-filter fixing. */
  setFixCrossFilters(val: boolean): void {
    this.fixCrossFilters = val;
    this.dryRunResult = null;
  }

  /** Toggle a dashboard in the selection set. */
  toggleDashboard(id: string): void {
    if (this.selectedDashboardIds.includes(id)) {
      this.selectedDashboardIds = this.selectedDashboardIds.filter((i) => i !== id);
    } else {
      this.selectedDashboardIds = [...this.selectedDashboardIds, id];
    }
    this.dryRunResult = null;
  }

  /** Select all dashboards. */
  selectAllDashboards(): void {
    this.selectedDashboardIds = this.dashboards.map((d) => d.id);
    this.dryRunResult = null;
  }

  /** Deselect all dashboards. */
  deselectAllDashboards(): void {
    this.selectedDashboardIds = [];
    this.dryRunResult = null;
  }

  /** Fetch databases, mappings, and AI suggestions for current env pair. */
  async fetchDatabases(): Promise<void> {
    if (!this.sourceEnvId || !this.targetEnvId) return;
    this.fetchingDbs = true;
    this.error = "";
    try {
      const [src, tgt, maps, sugs] = await Promise.all([
        api.requestApi(`/environments/${this.sourceEnvId}/databases`),
        api.requestApi(`/environments/${this.targetEnvId}/databases`),
        api.requestApi(`/mappings?source_env_id=${this.sourceEnvId}&target_env_id=${this.targetEnvId}`),
        api.postApi("/mappings/suggest", {
          source_env_id: this.sourceEnvId,
          target_env_id: this.targetEnvId,
        }),
      ]);
      this.sourceDatabases = src;
      this.targetDatabases = tgt;
      this.mappings = maps;
      this.suggestions = sugs;
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : "Failed to fetch databases";
    } finally {
      this.fetchingDbs = false;
    }
  }

  /** Save a database mapping between source and target. */
  async saveMapping(sourceUuid: string, targetUuid: string): Promise<void> {
    const sDb = this.sourceDatabases.find((d: Database) => d.uuid === sourceUuid);
    const tDb = this.targetDatabases.find((d: Database) => d.uuid === targetUuid);
    if (!sDb || !tDb) return;
    try {
      const savedMapping = await api.postApi("/mappings", {
        source_env_id: this.sourceEnvId,
        target_env_id: this.targetEnvId,
        source_db_uuid: sourceUuid,
        target_db_uuid: targetUuid,
        source_db_name: sDb.database_name,
        target_db_name: tDb.database_name,
      });
      this.mappings = [
        ...this.mappings.filter((m: Mapping) => m.source_db_uuid !== sourceUuid),
        savedMapping,
      ];
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : "Failed to save mapping";
    }
  }

  /** Open log viewer for a task. */
  openLogViewer(task: LogViewerTask): void {
    this.logViewerTaskId = task.id;
    this.logViewerTaskStatus = task.status;
    this.showLogViewer = true;
  }

  /** Close log viewer. */
  closeLogViewer(): void {
    this.showLogViewer = false;
    this.logViewerTaskId = null;
    this.logViewerTaskStatus = null;
  }

  /** Toggle task history panel. */
  toggleTaskHistory(): void {
    this.showTaskHistory = !this.showTaskHistory;
  }

  // ── Private ─────────────────────────────────────────────────────

  /** Fetch dashboards for the given environment. Resets selection. */
  async _fetchDashboards(envId: string): Promise<void> {
    try {
      this.dashboards = await api.requestApi(`/environments/${envId}/dashboards`);
      this.selectedDashboardIds = [];   // @INVARIANT: reset on env change
    } catch (e: unknown) {
      this.error = e instanceof Error ? e.message : "Failed to fetch dashboards";
      this.dashboards = [];
    }
  }

  /** Build the DashboardSelection payload for API calls. */
  _buildSelection(): DashboardSelection {
    return {
      selected_ids: this.selectedDashboardIds,
      source_env_id: this.sourceEnvId,
      target_env_id: this.targetEnvId,
      replace_db_config: this.replaceDb,
      fix_cross_filters: this.fixCrossFilters,
    };
  }

  /** Validate preconditions for migration/dry-run. Returns false and sets error if invalid. */
  _validatePreconditions(): boolean {
    if (!this.sourceEnvId || !this.targetEnvId) {
      this.error = t.migration?.select_both_envs || "Please select both source and target environments.";
      return false;
    }
    if (this.sourceEnvId === this.targetEnvId) {
      this.error = t.migration?.different_envs || "Source and target environments must be different.";
      return false;
    }
    if (this.selectedDashboardIds.length === 0) {
      this.error = t.migration?.select_dashboards || "Please select at least one dashboard to migrate.";
      return false;
    }
    return true;
  }
}
// #endregion Migration.Model
