// #region TranslationJobModel [C:5] [TYPE Model] [SEMANTICS translate,job,config,wizard,datasource,target,run,schedule,model]
// @ingroup Models
// @BRIEF Full screen model for translation job config wizard — 5 tabs: Config, Preview, Target, Run, Schedule.
// @INVARIANT Job must be saved before Target or Schedule tabs are accessible.
// @INVARIANT configValid gates Preview and Run availability.
// @INVARIANT schemaValidated gates Run execution — target schema must be checked before first run.
// @INVARIANT Environment change reloads databases and resets targetDatabaseId.
// @STATE idle/loading/configured/saving/validation_error/datasource_unavailable
// @ACTION loadInitialData() — Loads environments, LLM providers, dictionaries, and if editing, job data.
// @ACTION saveJob() — PUT/POST job configuration.
// @ACTION handleTriggerRun(full) — Starts translation run.
// @RELATION DEPENDS_ON -> [ApiModule]
// @RELATION CALLS -> [CotLogger]
// @INVARIANT DECOMPOSITION GATE: 50+ atoms, ~400 lines. Model serves as single source of truth for all form state.
// @RATIONALE Methods are NOT arrow class fields — they rely on `this` to mutate state. When passed as props
//             to child components (e.g. onTriggerRun={m.handleTriggerRun}), the binding is lost and `this`
//             becomes `undefined`, throwing "Cannot set properties of undefined". Consumers MUST wrap
//             in arrow functions: onTriggerRun={(full) => m.handleTriggerRun(full)}.
// @REJECTED Converting methods to arrow class fields rejected — it would conflict with the Svelte 5
//            `$state` rune initialization order for non-primitive state atoms in the constructor.
// @REJECTED includeSourceReference ($state at line 52) has no backend column or schema field.
//            It is a UI-only checkbox; the value always resets to `true` on page load.
//            The backend TranslateJobCreate/Update/Response schemas lack `include_source_reference`.
//            Requires a DB migration + Pydantic schema update to persist. Not implemented as of 2026-06-03.
import { api } from '$lib/api.js';
import { addToast } from '$lib/toasts.svelte.js';
import { _, getT } from '$lib/i18n/index.svelte.js';
import { triggerRun, fetchRunHistory, cancelRun, fetchDatasourceColumns, fetchDatasources } from '$lib/api/translate.js';
import { startTranslationRun, resetTranslationRun, translationRunStore } from '$lib/stores/translationRun.svelte.js';

type UxState = 'idle' | 'loading' | 'configured' | 'saving' | 'validation_error' | 'datasource_unavailable';

export class TranslationJobModel {
  // ── Context ───────────────────────────────────────────────────
  uxState: UxState = $state('idle');
  isNewJob: boolean = $state(false);
  jobId: string = $state('');
  existingJob: Record<string, unknown> | null = $state(null);
  error: string | null = $state(null);

  // ── Config form ───────────────────────────────────────────────
  name: string = $state('');
  description: string = $state('');
  sourceDatasourceId: string = $state('');
  sourceTable: string = $state('');
  targetSchema: string = $state('');
  targetTable: string = $state('');
  sourceKeyCols: string[] = $state([]);
  targetKeyCols: string[] = $state([]);
  translationColumn: string = $state('');
  targetColumn: string = $state('');
  targetLanguageColumn: string = $state('');
  targetSourceColumn: string = $state('');
  targetSourceLanguageColumn: string = $state('');
  contextColumns: string[] = $state([]);
  targetLanguages: string[] = $state(['en']);
  includeSourceReference: boolean = $state(true);
  providerId: string = $state('');
  batchSize: number = $state(50);
  disableReasoning: boolean = $state(false);
  upsertStrategy: string = $state('MERGE');
  insertMethod: string = $state('sqllab');
  connectionId: string | null = $state(null);
  dictionaryIds: string[] = $state([]);
  status: string = $state('DRAFT');

  // ── Datasource & env ──────────────────────────────────────────
  environmentId: string = $state('');
  environments: Record<string, unknown>[] = $state([]);
  datasourceId: string = $state('');
  datasourceSearch: string = $state('');
  availableColumns: Record<string, unknown>[] = $state([]);
  virtualColumns: Record<string, unknown>[] = $state([]);
  databaseDialect: string = $state('');
  llmProviders: Record<string, unknown>[] = $state([]);
  availableDictionaries: Record<string, unknown>[] = $state([]);
  targetDatabaseId: string = $state('');
  databases: Record<string, unknown>[] = $state([]);
  databasesLoading: boolean = $state(false);

  // ── Tab state ─────────────────────────────────────────────────
  activeTab: string = $state('config');
  tabs: { id: string; label: string }[] = $derived.by(() => {
    const base = [
      { id: 'config', label: getT()?.translate?.config?.basic_info || 'Configuration' },
      { id: 'preview', label: getT()?.translate?.preview?.title || 'Preview' },
      { id: 'run', label: getT()?.translate?.config?.run_translation || 'Run' },
    ];
    if (!this.isNewJob && this.existingJob) {
      base.splice(2, 0, { id: 'target', label: getT()?.translate?.config?.target_table_tab || 'Target Table' });
      base.push({ id: 'schedule', label: getT()?.translate?.schedule?.tab_label || 'Schedule' });
    }
    return base;
  });

  // ── Derived ───────────────────────────────────────────────────
  columnList = $derived(this.availableColumns);
  logicalColumns = $derived(this.availableColumns.filter((c: Record<string, unknown>) => c.is_physical !== false));
  virtualColumnNames = $derived(this.virtualColumns);
  isSaving: boolean = $state(false);
  isDirty: boolean = $state(false);
  validationErrors: Record<string, unknown> = $state({});
  warnings: string[] = $state([]);

  configValid = $derived(
    !!this.translationColumn && !!this.datasourceId && this.targetLanguages.length > 0 && !!this.providerId
  );
  pageTitle = $derived.by(() => {
    if (this.isNewJob) return getT()?.translate?.config?.new_title || 'New Translation Job';
    const jobName = this.existingJob?.name || this.name;
    const base = getT()?.translate?.config?.edit_title || 'Edit Translation Job';
    return jobName ? `${jobName} | ${base}` : base;
  });

  // ── Run state ─────────────────────────────────────────────────
  currentRunId = $derived(translationRunStore.value?.runId || null);
  completedRuns: Record<string, unknown>[] = $state([]);
  expandedRunIds: string[] = $state([]);
  isRunning: boolean = $state(false);
  isFullRun: boolean = $state(false);
  runError: string = $state('');
  showPageBulkReplace: boolean = $state(false);
  runComplete: boolean = $state(false);
  schemaValidated: boolean = $state(false);

  // ── Actions: Run ─────────────────────────────────────────────

  async handleTriggerRun(full = false): Promise<void> {
    this.isRunning = true;
    this.runComplete = false;
    this.isFullRun = full;
    this.runError = '';
    try {
      // Auto-save any unsaved config changes before triggering a run
      if (!this.isNewJob) {
        await this.saveJob();
      }
      const run = await triggerRun(this.jobId, full);
      startTranslationRun(run.id, { jobId: this.jobId, isFullRun: full, onComplete: this._onRunComplete.bind(this) });
      addToast(full ? 'Полный перевод запущен (все строки)' : _('translate.config.run_started'), 'success');
    } catch (err: unknown) {
      this.runError = err instanceof Error ? err.message : _('translate.config.run_failed');
      this.isRunning = false;
    }
  }

  private _onRunComplete(statusData: Record<string, unknown>): void {
    if (this.runComplete) return;
    const finishedRunId = this.currentRunId;
    this.isRunning = false;
    this.runComplete = true;
    this.expandedRunIds = [];
    if (statusData?.status !== 'CANCELLED') this.loadRunHistory();
    addToast(`${_('translate.run.run_id')} ${finishedRunId}`, 'info');
  }

  async loadRunHistory(): Promise<void> {
    try {
      const data = await fetchRunHistory(this.jobId, { page_size: 5 });
      this.completedRuns = ((data as { items?: Record<string, unknown>[] })?.items || []).filter(
        (run: Record<string, unknown>) => run && typeof run.id === 'string' && (run.id as string).trim().length > 0
      );
      this.expandedRunIds = this.expandedRunIds.filter((runId) => this.completedRuns.some((run: Record<string, unknown>) => run.id === runId));
    } catch { this.completedRuns = []; this.expandedRunIds = []; }
  }

  toggleRunDetails(runId: string): void {
    if (!runId) return;
    if (this.expandedRunIds.includes(runId)) this.expandedRunIds = this.expandedRunIds.filter((id) => id !== runId);
    else this.expandedRunIds = [...this.expandedRunIds, runId];
  }

  async handleRetryRun(): Promise<void> {
    if (this.currentRunId) { try { await cancelRun(this.currentRunId); } catch { /* ignore */ } }
    resetTranslationRun();
    await this.handleTriggerRun();
  }

  async handleRetryInsert(): Promise<void> {
    if (this.currentRunId) {
      try {
        await api.postApi(`/translate/runs/${this.currentRunId}/retry-insert`, {});
        addToast(_('translate.config.insert_retry_started'), 'success');
      } catch (err: unknown) { addToast(err instanceof Error ? err.message : _('translate.config.insert_retry_failed'), 'error'); }
    }
  }

  // ── Actions: Data loading ────────────────────────────────────

  async loadInitialData(): Promise<void> {
    this.uxState = 'loading';
    try {
      const [providers, dicts] = await Promise.all([
        api.requestApi<unknown>('/llm/providers').catch(() => ({ providers: [] })),
        api.requestApi('/translate/dictionaries?page_size=100').catch(() => ({ items: [] })),
      ]);
      this.llmProviders = (
        Array.isArray(providers)
          ? providers
          : ((providers as { providers?: unknown[] })?.providers
            ?? (providers as { results?: unknown[] })?.results
            ?? [])
      ) as Record<string, unknown>[];
      this.availableDictionaries = ((dicts as { items?: unknown[] })?.items || (dicts as { results?: unknown[] })?.results || []) as Record<string, unknown>[];

      if (!this.isNewJob) {
        const job = await api.requestApi(`/translate/jobs/${this.jobId}`) as Record<string, unknown>;
        this.existingJob = job;
        this.name = (job.name as string) || '';
        this.description = (job.description as string) || '';
        this.sourceDatasourceId = (job.source_datasource_id as string) || '';
        this.sourceTable = (job.source_table as string) || '';
        this.translationColumn = (job.translation_column as string) || '';
        this.targetColumn = (job.target_column as string) || '';
        this.targetLanguageColumn = (job.target_language_column as string) || '';
        this.targetSourceColumn = (job.target_source_column as string) || '';
        this.targetSourceLanguageColumn = (job.target_source_language_column as string) || '';
        this.targetLanguages = (job.target_languages as string[]) || ['en'];
        this.providerId = (job.provider_id as string) || '';
        this.batchSize = (job.batch_size as number) || 50;
        this.sourceKeyCols = (job.source_key_cols as string[]) || [];
        this.targetKeyCols = (job.target_key_cols as string[]) || [];
        this.contextColumns = (job.context_columns as string[]) || [];
        this.dictionaryIds = (job.dictionary_ids as string[]) || [];
        this.upsertStrategy = (job.upsert_strategy as string) || 'MERGE';
        this.insertMethod = (job.insert_method as string) || 'sqllab';
        this.connectionId = (job.connection_id as string) || null;
        this.disableReasoning = (job.disable_reasoning as boolean) ?? false;
        this.databaseDialect = (job.database_dialect as string) || '';
        this.datasourceId = (job.source_datasource_id as string) || '';
        this.status = (job.status as string) || 'DRAFT';
        this.targetSchema = (job.target_schema as string) || '';
        this.targetTable = (job.target_table as string) || '';
        this.targetDatabaseId = (job.target_database_id as string) || '';
        this.environmentId = (job.environment_id as string) || '';
        if (this.environmentId) {
          this.loadDatabases();
          this.loadDatasourceColumns();
          // Look up datasource name to populate search input on page load
          if (this.datasourceId) {
            try {
              const list = await fetchDatasources<Record<string, unknown>[]>(this.environmentId, '');
              const ds = (Array.isArray(list) ? list : []).find((d) => String(d.id) === String(this.datasourceId));
              if (ds) {
                this.datasourceSearch = `${ds.table_name} (${ds.database_name} · ${ds.database_dialect})`;
              }
            } catch { /* lookup is best-effort */ }
          }
        }
        this.loadRunHistory();
      }
      this.uxState = 'configured';
    } catch (err: unknown) {
      this.error = err instanceof Error ? err.message : 'Failed to load data';
      this.uxState = 'idle';
    }
  }

  async loadDatabases(): Promise<void> {
    if (!this.environmentId) return;
    this.databasesLoading = true;
    try {
      const res = await api.getEnvironmentDatabases(this.environmentId) as Record<string, unknown>[];
      this.databases = (Array.isArray(res) ? res : []) as Record<string, unknown>[];
    } catch { this.databases = []; }
    finally { this.databasesLoading = false; }
  }

  async loadDatasourceColumns(): Promise<void> {
    if (!this.datasourceId) return;
    try {
      const res = await fetchDatasourceColumns<{ columns?: Record<string, unknown>[]; virtual?: Record<string, unknown>[] }>(this.datasourceId, this.environmentId);
      this.availableColumns = (res?.columns || []) as Record<string, unknown>[];
      this.virtualColumns = (res?.virtual || []) as Record<string, unknown>[];
    } catch { this.availableColumns = []; this.virtualColumns = []; }
  }

  handleEnvChange(envId: string): void {
    this.environmentId = envId;
    this.targetDatabaseId = '';
    this.loadDatabases();
  }

  // ── Actions: Save ────────────────────────────────────────────

  markDirty(): void {
    this.isDirty = true;
  }

  async saveJob(): Promise<void> {
    this.uxState = 'saving';
    this.validationErrors = {};
    this.isDirty = false;
    try {
      const payload = {
        name: this.name,
        description: this.description,
        source_datasource_id: this.sourceDatasourceId || this.datasourceId,
        source_table: this.sourceTable,
        translation_column: this.translationColumn,
        target_column: this.targetColumn || undefined,
        target_languages: this.targetLanguages,
        provider_id: this.providerId || undefined,
        batch_size: this.batchSize,
        source_key_cols: this.sourceKeyCols,
        target_key_cols: this.targetKeyCols,
        context_columns: this.contextColumns,
        dictionary_ids: this.dictionaryIds,
        upsert_strategy: this.upsertStrategy,
        insert_method: this.insertMethod,
        connection_id: this.connectionId || undefined,
        disable_reasoning: this.disableReasoning,
        database_dialect: this.databaseDialect || undefined,
        target_schema: this.targetSchema || undefined,
        target_table: this.targetTable || undefined,
        target_database_id: this.targetDatabaseId || undefined,
        environment_id: this.environmentId || undefined,
        target_language_column: this.targetLanguageColumn || undefined,
        target_source_column: this.targetSourceColumn || undefined,
        target_source_language_column: this.targetSourceLanguageColumn || undefined,
        status: this.status,
      };
      const url = this.isNewJob ? '/translate/jobs' : `/translate/jobs/${this.jobId}`;
      const method: string = this.isNewJob ? 'POST' : 'PUT';
      const resp = await api.requestApi<{ id?: string }>(url, method, payload);
      if (resp?.id && this.isNewJob) {
        this.jobId = resp.id;
        this.isNewJob = false;
        this.existingJob = { id: resp.id, ...payload };
      }
      addToast(getT()?.translate?.config?.saved || 'Job saved', 'success');
      this.uxState = 'configured';
    } catch (err: unknown) {
      this.error = err instanceof Error ? err.message : 'Failed to save';
      addToast(this.error, 'error');
      this.uxState = 'validation_error';
    }
  }
}
// #endregion TranslationJobModel
