// frontend/src/lib/models/GitManagerModel.svelte.ts
// #region GitManagerModel [C:4] [TYPE Model] [SEMANTICS git,workspace,screen-model,state,repository]
// @BRIEF State model for the Git workspace panel — declares atoms, invariants, and actions.
// @INVARIANT Loading states are mutually exclusive per operation:
//   checkingStatus, loading, workspaceLoading, committing, isPulling, isPushing,
//   generatingMessage, promoting, mergeResolveInProgress, mergeAbortInProgress,
//   mergeContinueInProgress, mergeRecoveryLoading, creatingRemoteRepo.
//   At most one of these loading flags is true at any time.
// @INVARIANT Git error is always cleared before any new operation (clearError before try).
// @INVARIANT Repository binding (configId, provider, remoteUrl) is loaded only after initialized=true.
// @STATE uninitialized   — No repository bound to dashboard.
// @STATE initialized     — Repository exists, tab workspace/release/operations available.
// @STATE checking        — Initial status check in progress.
// @STATE error           — Persistent error banner visible with message and dismiss action.
// @ACTION checkStatus()              — Checks if repository is initialized; loads workspace on success.
// @ACTION loadWorkspace()            — Loads workspace status and diff.
// @ACTION handleSync()               — Synchronizes dashboard state with Git.
// @ACTION handleGenerateMessage()    — Generates AI commit message from diff.
// @ACTION handleCommit()             — Stages and commits workspace changes.
// @ACTION handlePromote(...)         — Promotes changes between branches.
// @ACTION handlePull()               — Pulls from remote.
// @ACTION handlePush()               — Pushes to remote.
// @ACTION openDeployModal()          — Opens deployment target modal.
// @ACTION handleCreateRemoteRepo()   — Creates remote repository on provider.
// @ACTION handleInit()               — Initializes local repository from remote URL.
// @ACTION loadMergeRecoveryState()   — Loads merge recovery state.
// @ACTION closeUnfinishedMergeDialog() — Closes merge dialog and resets state.
// @ACTION handleOpenConflictResolver() — Loads conflicts and opens resolver.
// @ACTION handleResolveConflicts(event) — Applies conflict resolutions.
// @ACTION handleAbortUnfinishedMerge()  — Aborts in-progress merge.
// @ACTION handleContinueUnfinishedMerge() — Finalizes merge commit.
// @ACTION handleCopyUnfinishedMergeCommands() — Copies commands to clipboard.
// @ACTION clearGitError()            — Clears the persistent error banner.
// @ACTION initialize(dashboardId, envId, dashboardTitle) — Bootstraps state on mount.
// @RELATION DEPENDS_ON -> [EXT:frontend:gitService]
// @RELATION DEPENDS_ON -> [EXT:frontend:api]
// @RELATION DEPENDS_ON -> [GitUtils]
// @RELATION DEPENDS_ON -> [ToastsModule]
// @REJECTED getState/setState proxy pattern rejected — fragile, untyped, hard to test.
//   Real $state atoms with class methods enable L1 testing without DOM render.

import { gitService } from '../../services/gitService.js';
import { api } from '$lib/api.js';
import { addToast } from '$lib/toasts.svelte.js';
import {
  isNumericDashboardRef,
  resolveDefaultConfig,
  extractUnfinishedMergeContext,
  buildSuggestedRepoName,
  resolveCurrentEnvironmentId,
  normalizeEnvStage,
  applyGitflowStageDefaults,
  resolvePushProviderLabel,
  extractHttpHost,
} from '../../services/git-utils.js';
import { getT } from '$lib/i18n/index.svelte.js';

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

interface GitConfig {
  id: string | number;
  provider?: string;
  url?: string;
  [key: string]: unknown;
}

interface WorkspaceStatus {
  current_branch?: string;
  staged_files?: string[];
  modified_files?: string[];
  untracked_files?: string[];
  is_dirty?: boolean;
  [key: string]: unknown;
}

interface GitErrorPayload {
  message: string;
  status: number;
  error_code: string | null;
  files: unknown[];
  next_steps: string[];
  detail: Record<string, unknown> | null;
  errorType?: string;
}

interface UnfinishedMergeContext {
  message?: string;
  repositoryPath?: string;
  gitDir?: string;
  currentBranch?: string;
  mergeHead?: string;
  mergeMessagePreview?: string;
  nextSteps?: string[];
  commands?: string[];
  conflictsCount?: number;
  [key: string]: unknown;
}

interface MergeConflict {
  file_path: string;
  [key: string]: unknown;
}

interface RepositoryBinding {
  provider?: string;
  remote_url?: string;
  config_id?: string | number;
  [key: string]: unknown;
}

interface PromotePayload {
  from_branch: string;
  to_branch: string;
  mode: string;
  title: string;
  description?: string;
  reason?: string;
}

interface PromoteResponse {
  url?: string;
  [key: string]: unknown;
}

interface MergeStatus {
  has_unfinished_merge?: boolean;
  repository_path?: string;
  git_dir?: string;
  current_branch?: string;
  merge_head?: string;
  merge_message_preview?: string;
  conflicts_count?: number;
  [key: string]: unknown;
}

interface CreateRepoPayload {
  name: string;
  private: boolean;
  description: string;
  auto_init: boolean;
  default_branch: string;
}

interface CreateRepoResult {
  clone_url?: string;
  html_url?: string;
  [key: string]: unknown;
}

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

interface NumericDashboardRefCheck {
  isNumericDashboardRef: typeof isNumericDashboardRef;
  resolveEnvId: () => string | null;
  normalizeEnvStageFn: typeof normalizeEnvStage;
  applyGitflowStageDefaultsFn: typeof applyGitflowStageDefaults;
}

interface GitflowStageDefaults {
  promoteFromBranch?: string;
  promoteToBranch?: string;
  preferredDeployTargetStage?: string;
}

// #region buildGitErrorFromException [C:2] [TYPE Function]
// @BRIEF Extract structured gitError payload from a caught exception for modal error banner.
// @PRE  e is an Error object with optional .status, .detail, .error_code properties.
// @POST Returns { message, status, error_code, files, next_steps, detail } for state.
function buildGitErrorFromException(e: unknown): GitErrorPayload {
  if (!e) return { message: 'Неизвестная ошибка Git', status: 500, error_code: null, files: [], next_steps: [], detail: null };
  const err = e as Record<string, unknown>;
  const detail = err?.detail || (typeof err?.detail === 'string' ? { message: err.detail } : null) || null;
  const detailObj = typeof detail === 'object' ? detail as Record<string, unknown> : null;
  return {
    message: (detailObj?.message as string) || (detailObj?.message_en as string) || (err.message as string) || 'Произошла ошибка Git',
    status: (err?.status as number) || 500,
    error_code: (err?.error_code as string) || (detailObj?.error_code as string) || null,
    files: (detailObj?.files as unknown[]) || [],
    next_steps: (detailObj?.next_steps as string[]) || (detailObj?.next_steps_en as string[]) || [],
    detail: detailObj,
  };
}
// #endregion buildGitErrorFromException

/** short alias for getT() */
function tt(): Record<string, unknown> {
  return getT();
}

export class GitManagerModel {
  // ── Props (synced from component) ────────────────────────────
  dashboardId: string = $state('');
  /** Raw envId from props (null = not set). resolvedEnvId adds localStorage fallback. */
  envId: string | null = $state(null);
  dashboardTitle: string = $state('');

  // ── Branch & Status ─────────────────────────────────────────
  initialized: boolean = $state(false);
  loading: boolean = $state(false);
  checkingStatus: boolean = $state(true);
  currentBranch: string = $state('main');
  activeTab: string = $state('workspace');

  // ── Configs & Remote ────────────────────────────────────────
  configs: GitConfig[] = $state([]);
  selectedConfigId: string = $state('');
  remoteUrl: string = $state('');
  creatingRemoteRepo: boolean = $state(false);
  repositoryProvider: string = $state('');
  repositoryBindingRemoteUrl: string = $state('');
  repositoryConfigUrl: string = $state('');

  // ── Commit ──────────────────────────────────────────────────
  commitMessage: string = $state('');
  committing: boolean = $state(false);
  generatingMessage: boolean = $state(false);
  autoPushAfterCommit: boolean = $state(true);

  // ── Workspace ───────────────────────────────────────────────
  workspaceStatus: WorkspaceStatus | null = $state(null);
  workspaceDiff: string = $state('');
  workspaceLoading: boolean = $state(false);

  // ── Pull / Push ─────────────────────────────────────────────
  isPulling: boolean = $state(false);
  isPushing: boolean = $state(false);

  // ── Promote ─────────────────────────────────────────────────
  showAdvancedPromote: boolean = $state(false);
  promoting: boolean = $state(false);
  promoteFromBranch: string = $state('dev');
  promoteToBranch: string = $state('preprod');
  promoteMode: string = $state('mr');
  promoteReason: string = $state('');

  // ── Environment ─────────────────────────────────────────────
  currentEnvStage: string = $state('');
  preferredDeployTargetStage: string = $state('');

  // ── Deploy ──────────────────────────────────────────────────
  showDeployModal: boolean = $state(false);

  // ── Merge Recovery ──────────────────────────────────────────
  showUnfinishedMergeDialog: boolean = $state(false);
  unfinishedMergeContext: UnfinishedMergeContext | null = $state(null);
  copyingUnfinishedMergeCommands: boolean = $state(false);
  mergeRecoveryLoading: boolean = $state(false);
  mergeConflicts: MergeConflict[] = $state([]);
  showConflictResolver: boolean = $state(false);
  mergeResolveInProgress: boolean = $state(false);
  mergeAbortInProgress: boolean = $state(false);
  mergeContinueInProgress: boolean = $state(false);

  // ── Error Banner ────────────────────────────────────────────
  gitError: GitErrorPayload | null = $state(null);
  gitErrorType: string = $state('error');

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

  /** Resolved envId with localStorage fallback. */
  resolvedEnvId: string | null = $derived(resolveCurrentEnvironmentId(this.envId));

  /** True when there are any workspace changes (staged, modified, or untracked). */
  hasWorkspaceChanges: boolean = $derived.by(() => {
    if (!this.workspaceStatus) return false;
    const w = this.workspaceStatus;
    return [...(w.staged_files || []), ...(w.modified_files || []), ...(w.untracked_files || [])].length > 0;
  });

  /** Total count of changed files (staged + modified + untracked). */
  changedFilesCount: number = $derived.by(() => {
    if (!this.workspaceStatus) return 0;
    const w = this.workspaceStatus;
    return [...(w.staged_files || []), ...(w.modified_files || []), ...(w.untracked_files || [])].length;
  });

  /** Lower-case provider label for auto-push checkbox text. */
  pushProviderLabel: string = $derived(resolvePushProviderLabel(this.configs, this.selectedConfigId, this.repositoryProvider));

  /** Origin host from repository binding URL. */
  originHost: string = $derived(extractHttpHost(this.repositoryBindingRemoteUrl));

  /** Config host from default config URL. Synced from component via model.repositoryConfigUrl. */
  configHost: string = $derived(extractHttpHost(this.repositoryConfigUrl));

  /** True when origin and config hosts differ (server mismatch warning). */
  hasOriginConfigMismatch: boolean = $derived(!!(this.originHost && this.configHost && this.originHost !== this.configHost));

  constructor({ dashboardId = '', envId = null, dashboardTitle = '' }: { dashboardId?: string; envId?: string | null; dashboardTitle?: string } = {}) {
    this.dashboardId = dashboardId;
    this.envId = envId;
    this.dashboardTitle = dashboardTitle;
  }

  // ── Helpers ──────────────────────────────────────────────────

  /** Clear the persistent error banner. */
  clearGitError(): void {
    this.gitError = null;
    this.gitErrorType = 'error';
  }

  /** @private Set the persistent error banner + show persistent toast. */
  _setGitError(e: unknown, errorType?: string): void {
    const err = buildGitErrorFromException(e);
    err.errorType = errorType || (err.status >= 500 ? 'error' : err.status === 409 ? 'warning' : 'error');
    this.gitError = err;
    this.gitErrorType = err.errorType;
    addToast(err.message, err.errorType === 'warning' ? 'warning' : 'error', 0);
  }

  /** @private Convenient access to i18n store value for translations. */
  get _t(): Record<string, unknown> {
    return tt();
  }

  /** @private Collect current scalar + helper snapshot for handler context. */
  _snapshot(): NumericDashboardRefCheck {
    return {
      isNumericDashboardRef,
      resolveEnvId: () => resolveCurrentEnvironmentId(this.envId),
      normalizeEnvStageFn: normalizeEnvStage,
      applyGitflowStageDefaultsFn: applyGitflowStageDefaults,
    };
  }

  // ── Initialization ───────────────────────────────────────────

  /**
   * Bootstrap state on mount: load configs, check status, resolve env stage,
   * and load repository binding if initialized.
   */
  async initialize(): Promise<void> {
    this.loading = true;
    try {
      this.configs = await gitService.getConfigs();
      const dc = resolveDefaultConfig(this.configs, this.selectedConfigId);
      if (dc?.id) this.selectedConfigId = String(dc.id);
    } catch (_e: unknown) {
      this.configs = [];
    }
    this.loading = false;
    await Promise.all([this.checkStatus(), this.loadCurrentEnvironmentStage()]);
    if (this.initialized) {
      try {
        const binding: RepositoryBinding = await gitService.getRepositoryBinding(this.dashboardId, this.resolvedEnvId);
        this.repositoryProvider = binding?.provider || '';
        this.repositoryBindingRemoteUrl = binding?.remote_url || '';
        if (binding?.config_id) this.selectedConfigId = String(binding.config_id);
      } catch (_e: unknown) {
        this.repositoryProvider = '';
        this.repositoryBindingRemoteUrl = '';
      }
    }
  }

  // ── Environment Stage ────────────────────────────────────────

  /**
   * Resolve current environment stage (DEV/PREPROD/PROD) and apply GitFlow defaults.
   * @SIDE_EFFECT Fetches /environments from API.
   */
  async loadCurrentEnvironmentStage(): Promise<void> {
    const currentEnvId = resolveCurrentEnvironmentId(this.envId);
    if (!currentEnvId) return;
    try {
      const environments: EnvironmentItem[] = await api.getEnvironmentsList();
      const currentEnv = (environments || []).find((item: EnvironmentItem) => item.id === currentEnvId);
      if (!currentEnv) return;
      const stage = normalizeEnvStage(currentEnv);
      this.currentEnvStage = stage;
      const defaults: GitflowStageDefaults = applyGitflowStageDefaults(stage);
      if (defaults.promoteFromBranch !== undefined) this.promoteFromBranch = defaults.promoteFromBranch;
      if (defaults.promoteToBranch !== undefined) this.promoteToBranch = defaults.promoteToBranch;
      if (defaults.preferredDeployTargetStage !== undefined) this.preferredDeployTargetStage = defaults.preferredDeployTargetStage;
    } catch (e: unknown) {
      console.error(`[GitManagerModel][loadCurrentEnvironmentStage] Failed: ${e instanceof Error ? e.message : String(e)}`);
    }
  }

  // ── Check Status ─────────────────────────────────────────────

  /**
   * Check if Git repository is initialized for the dashboard.
   * @POST initialized=true if repository found and workspace loaded.
   */
  async checkStatus(): Promise<void> {
    if (isNumericDashboardRef(this.dashboardId)) {
      this.checkingStatus = false;
      this.initialized = false;
      addToast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');
      return;
    }
    this.checkingStatus = true;
    if (!this.resolvedEnvId) {
      this.initialized = false;
      this.checkingStatus = false;
      return;
    }
    try {
      await gitService.getBranches(this.dashboardId, this.resolvedEnvId);
      this.initialized = true;
      await this.loadWorkspace();
    } catch (_e: unknown) {
      this.initialized = false;
    } finally {
      this.checkingStatus = false;
    }
  }

  // ── Workspace ────────────────────────────────────────────────

  /**
   * Load Git workspace status and diff for initialized repository.
   * @SIDE_EFFECT Fetches /status and /diff for the dashboard repository.
   */
  async loadWorkspace(): Promise<void> {
    if (!this.initialized) return;
    this.clearGitError();
    this.workspaceLoading = true;
    try {
      const ws: WorkspaceStatus = await gitService.getStatus(this.dashboardId, this.resolvedEnvId);
      const sd: string = await gitService.getDiff(this.dashboardId, null, true, this.resolvedEnvId);
      const ud: string = await gitService.getDiff(this.dashboardId, null, false, this.resolvedEnvId);
      this.workspaceStatus = ws;
      this.workspaceDiff = [sd, ud].filter(Boolean).join('\n\n');
      this.currentBranch = ws?.current_branch || this.currentBranch;
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.workspaceLoading = false;
    }
  }

  // ── Sync ─────────────────────────────────────────────────────

  /**
   * Synchronize local dashboard state with Git repository.
   * @SIDE_EFFECT POSTs to /git/repositories/{ref}/sync.
   */
  async handleSync(): Promise<void> {
    if (isNumericDashboardRef(this.dashboardId)) {
      addToast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');
      return;
    }
    this.clearGitError();
    this.loading = true;
    try {
      const sourceEnvId = this.resolvedEnvId || localStorage.getItem('selected_env_id');
      await gitService.sync(this.dashboardId, sourceEnvId, this.resolvedEnvId);
      addToast((this._t?.git as Record<string, unknown>)?.sync_success as string || 'Состояние дашборда синхронизировано с Git', 'success');
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.loading = false;
    }
  }

  // ── Generate Message ─────────────────────────────────────────

  /**
   * Generate AI commit message from dashboard diff.
   * @SIDE_EFFECT POSTs to /git/repositories/{ref}/generate-message.
   */
  async handleGenerateMessage(): Promise<void> {
    this.clearGitError();
    this.generatingMessage = true;
    try {
      const data: { message?: string } = await api.postApi(
        `/git/repositories/${encodeURIComponent(String(this.dashboardId))}/generate-message${
          this.resolvedEnvId ? `?env_id=${encodeURIComponent(String(this.resolvedEnvId))}` : ''
        }`,
        undefined,
        { suppressToast: true },
      );
      this.commitMessage = data?.message || '';
      addToast((this._t?.git as Record<string, unknown>)?.commit_message_generated as string || 'Сообщение для коммита сгенерировано', 'success');
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.generatingMessage = false;
    }
  }

  // ── Commit ───────────────────────────────────────────────────

  /**
   * Stage and commit workspace changes, optionally push.
   * @PRE  commitMessage is non-empty; hasWorkspaceChanges is true.
   */
  async handleCommit(): Promise<void> {
    if (!this.commitMessage || !this.hasWorkspaceChanges) return;
    this.clearGitError();
    this.committing = true;
    try {
      await gitService.commit(this.dashboardId, this.commitMessage, [], this.resolvedEnvId);
      if (this.autoPushAfterCommit) {
        await gitService.push(this.dashboardId, this.resolvedEnvId);
        addToast((this._t?.git as Record<string, unknown>)?.commit_and_push_success as string || 'Коммит создан и отправлен в remote', 'success');
      } else {
        addToast((this._t?.git as Record<string, unknown>)?.commit_success as string || 'Коммит успешно создан', 'success');
      }
      this.commitMessage = '';
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.committing = false;
    }
  }

  // ── Promote ──────────────────────────────────────────────────

  /**
   * Promote changes between branches via MR or direct merge.
   * @SIDE_EFFECT Opens MR URL in new tab (MR mode).
   */
  async handlePromote(): Promise<void> {
    if (!this.promoteFromBranch || !this.promoteToBranch || this.promoteFromBranch === this.promoteToBranch) {
      addToast('Выберите разные исходную и целевую ветки', 'error');
      return;
    }
    if (this.promoteMode === 'direct' && !String(this.promoteReason || '').trim()) {
      addToast('Для небезопасного прямого переноса укажите причину', 'error');
      return;
    }
    this.clearGitError();
    this.promoting = true;
    try {
      const response: PromoteResponse = await gitService.promote(
        this.dashboardId,
        {
          from_branch: this.promoteFromBranch,
          to_branch: this.promoteToBranch,
          mode: this.promoteMode,
          title: `Promote ${this.promoteFromBranch} -> ${this.promoteToBranch}: ${this.dashboardTitle || this.dashboardId}`,
          description: this.promoteMode === 'direct' ? `Unsafe direct promote.\nReason: ${this.promoteReason}` : undefined,
          reason: this.promoteMode === 'direct' ? this.promoteReason : undefined,
        } satisfies PromotePayload,
        this.resolvedEnvId,
      );
      if (this.promoteMode === 'direct') {
        addToast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning');
      } else {
        if (response?.url) window.open(response.url, '_blank', 'noopener,noreferrer');
        addToast('Merge Request создан на Git сервере', 'success');
      }
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.promoting = false;
    }
  }

  // ── Pull ─────────────────────────────────────────────────────

  /**
   * Pull changes from remote repository.
   * @SIDE_EFFECT POSTs to /pull; opens merge dialog on GIT_UNFINISHED_MERGE.
   */
  async handlePull(): Promise<void> {
    this.clearGitError();
    this.isPulling = true;
    try {
      await gitService.pull(this.dashboardId, this.resolvedEnvId);
      addToast((this._t?.git as Record<string, unknown>)?.pull_success as string || 'Изменения получены из Git', 'success');
      await this.loadWorkspace();
    } catch (e: unknown) {
      const handled = this.openUnfinishedMergeDialogFromError(e);
      if (handled) {
        await this.loadMergeRecoveryState();
      } else {
        this._setGitError(e);
      }
    } finally {
      this.isPulling = false;
    }
  }

  // ── Push ─────────────────────────────────────────────────────

  /**
   * Push local commits to remote repository.
   * @SIDE_EFFECT POSTs to /push.
   */
  async handlePush(): Promise<void> {
    this.clearGitError();
    this.isPushing = true;
    try {
      await gitService.push(this.dashboardId, this.resolvedEnvId);
      addToast((this._t?.git as Record<string, unknown>)?.push_success as string || 'Изменения отправлены в Git', 'success');
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.isPushing = false;
    }
  }

  // ── Deploy Modal ─────────────────────────────────────────────

  /**
   * Open deployment target modal with PROD confirmation prompt.
   */
  openDeployModal(): void {
    if (this.currentEnvStage === 'PROD') {
      const expected = String(this.dashboardId);
      const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);
      if (String(confirmation || '').trim() !== expected) {
        addToast('Подтверждение PROD не пройдено. Деплой отменен.', 'error');
        return;
      }
    }
    this.showDeployModal = true;
  }

  // ── Remote Repository ────────────────────────────────────────

  /**
   * Create a remote repository on the selected Git provider.
   * @SIDE_EFFECT POSTs to /git/config/{id}/repositories; prompts user for repo name.
   */
  async handleCreateRemoteRepo(): Promise<void> {
    const config = resolveDefaultConfig(this.configs, this.selectedConfigId);
    if (!config) {
      addToast((this._t?.git as Record<string, unknown>)?.init_validation_error as string || 'Сначала выберите Git сервер', 'error');
      return;
    }
    if (!this.selectedConfigId && config.id) this.selectedConfigId = String(config.id);
    const suggestedName = buildSuggestedRepoName(this.dashboardTitle, this.dashboardId);
    const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);
    const repoName = String(inputName || '').trim();
    if (!repoName) return;
    this.clearGitError();
    this.creatingRemoteRepo = true;
    try {
      const repo: CreateRepoResult = await gitService.createRemoteRepository(config.id, {
        name: repoName,
        private: true,
        description: `Superset dashboard ${this.dashboardId}: ${this.dashboardTitle || repoName}`,
        auto_init: true,
        default_branch: 'main',
      } satisfies CreateRepoPayload);
      const url = repo?.clone_url || repo?.html_url || '';
      if (!url) throw new Error('Remote repository created, but URL is empty');
      this.remoteUrl = url;
      addToast(`Repository created on ${config.provider}`, 'success');
    } catch (e: unknown) {
      if ((e as Record<string, unknown>)?.status === 409 && /already exists/i.test(String((e as Error)?.message || ''))) {
        addToast((this._t?.git as Record<string, unknown>)?.repo_already_exists as string || 'Repository already exists. Enter its URL below and click Init.', 'warning', 0);
      } else {
        this._setGitError(e, 'warning');
      }
    } finally {
      this.creatingRemoteRepo = false;
    }
  }

  // ── Init Repository ──────────────────────────────────────────

  /**
   * Initialize local Git repository for the dashboard: clone remote.
   * @SIDE_EFFECT POSTs to /git/repositories/{ref}/init.
   */
  async handleInit(): Promise<void> {
    if (!this.selectedConfigId || !this.remoteUrl) {
      addToast((this._t?.git as Record<string, unknown>)?.init_validation_error as string || 'Заполните все поля', 'error');
      return;
    }
    if (!this.resolvedEnvId && !isNumericDashboardRef(this.dashboardId)) {
      addToast('Environment must be selected to initialize Git for this dashboard.', 'error');
      return;
    }
    this.clearGitError();
    this.loading = true;
    try {
      await gitService.initRepository(this.dashboardId, this.selectedConfigId, this.remoteUrl, this.resolvedEnvId);
      addToast((this._t?.git as Record<string, unknown>)?.init_success as string || 'Репозиторий инициализирован', 'success');
      const provider = resolveDefaultConfig(this.configs, this.selectedConfigId)?.provider || '';
      this.initialized = true;
      this.repositoryProvider = provider;
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.loading = false;
    }
  }

  // ── Merge Recovery ───────────────────────────────────────────

  /**
   * Parse error for unfinished merge context and open dialog.
   * @POST Returns true and opens dialog if merge context found.
   */
  openUnfinishedMergeDialogFromError(error: unknown): boolean {
    const context = extractUnfinishedMergeContext(error);
    if (!context) return false;
    this.unfinishedMergeContext = context;
    this.showUnfinishedMergeDialog = true;
    return true;
  }

  /**
   * Load merge recovery state from backend for unfinished merge.
   * @SIDE_EFFECT GETs /merge/status from backend.
   */
  async loadMergeRecoveryState(): Promise<void> {
    this.clearGitError();
    this.mergeRecoveryLoading = true;
    try {
      const status: MergeStatus = await gitService.getMergeStatus(this.dashboardId, this.resolvedEnvId);
      if (!status?.has_unfinished_merge) {
        this.closeUnfinishedMergeDialog();
        return;
      }
      this.unfinishedMergeContext = {
        ...(this.unfinishedMergeContext || {}),
        message: this.unfinishedMergeContext?.message || ((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.default_message as string || '',
        repositoryPath: String(status.repository_path || this.unfinishedMergeContext?.repositoryPath || ''),
        gitDir: String(status.git_dir || this.unfinishedMergeContext?.gitDir || ''),
        currentBranch: String(status.current_branch || this.unfinishedMergeContext?.currentBranch || ''),
        mergeHead: String(status.merge_head || this.unfinishedMergeContext?.mergeHead || ''),
        mergeMessagePreview: String(status.merge_message_preview || ''),
        nextSteps: Array.isArray(this.unfinishedMergeContext?.nextSteps) ? this.unfinishedMergeContext.nextSteps : [],
        commands: Array.isArray(this.unfinishedMergeContext?.commands) ? this.unfinishedMergeContext.commands : [],
        conflictsCount: Number(status.conflicts_count || 0),
      };
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.mergeRecoveryLoading = false;
    }
  }

  /**
   * Close unfinished merge dialog and reset merge state.
   */
  closeUnfinishedMergeDialog(): void {
    this.showUnfinishedMergeDialog = false;
    this.unfinishedMergeContext = null;
    this.mergeConflicts = [];
    this.showConflictResolver = false;
  }

  /**
   * Load merge conflicts and open conflict resolver UI.
   * @SIDE_EFFECT GETs /merge/conflicts from backend.
   */
  async handleOpenConflictResolver(): Promise<void> {
    this.clearGitError();
    this.mergeRecoveryLoading = true;
    try {
      const mc: MergeConflict[] = await gitService.getMergeConflicts(this.dashboardId, this.resolvedEnvId);
      if (!Array.isArray(mc) || mc.length === 0) {
        addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.no_conflicts as string || 'No unresolved conflicts were found', 'info');
        return;
      }
      this.mergeConflicts = mc;
      this.showConflictResolver = true;
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.mergeRecoveryLoading = false;
    }
  }

  /**
   * Apply conflict resolutions and stage resolved files.
   * @SIDE_EFFECT POSTs resolutions to /merge/resolve.
   */
  async handleResolveConflicts(event: CustomEvent): Promise<void> {
    const detail = event?.detail || {};
    const resolutions: Array<{ file_path: string; resolution: unknown }> = Object.entries(detail).map(([fp, r]) => ({ file_path: fp, resolution: r }));
    if (!resolutions.length) {
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.resolve_empty as string || 'No conflict resolutions selected', 'warning');
      return;
    }
    this.clearGitError();
    this.mergeResolveInProgress = true;
    try {
      await gitService.resolveMergeConflicts(this.dashboardId, resolutions, this.resolvedEnvId);
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.resolve_success as string || 'Conflicts were resolved and staged', 'success');
      this.showConflictResolver = false;
      await this.loadMergeRecoveryState();
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.mergeResolveInProgress = false;
    }
  }

  /**
   * Abort current unfinished merge.
   * @SIDE_EFFECT POSTs to /merge/abort.
   */
  async handleAbortUnfinishedMerge(): Promise<void> {
    this.clearGitError();
    this.mergeAbortInProgress = true;
    try {
      await gitService.abortMerge(this.dashboardId, this.resolvedEnvId);
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.abort_success as string || 'Merge was aborted', 'success');
      this.closeUnfinishedMergeDialog();
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
    } finally {
      this.mergeAbortInProgress = false;
    }
  }

  /**
   * Finalize unfinished merge by creating merge commit.
   * @SIDE_EFFECT POSTs to /merge/continue.
   */
  async handleContinueUnfinishedMerge(): Promise<void> {
    this.clearGitError();
    this.mergeContinueInProgress = true;
    try {
      await gitService.continueMerge(this.dashboardId, '', this.resolvedEnvId);
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.continue_success as string || 'Merge commit created successfully', 'success');
      this.closeUnfinishedMergeDialog();
      await this.loadWorkspace();
    } catch (e: unknown) {
      this._setGitError(e);
      await this.loadMergeRecoveryState();
    } finally {
      this.mergeContinueInProgress = false;
    }
  }

  /**
   * Get shell commands text for manual merge recovery.
   */
  getUnfinishedMergeCommandsText(): string {
    if (!this.unfinishedMergeContext?.commands?.length) return '';
    return this.unfinishedMergeContext.commands.join('\n');
  }

  /**
   * Copy merge recovery commands to clipboard.
   */
  async handleCopyUnfinishedMergeCommands(): Promise<void> {
    const text = this.getUnfinishedMergeCommandsText();
    if (!text) {
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.copy_empty as string || 'Команды для копирования отсутствуют', 'warning');
      return;
    }
    this.copyingUnfinishedMergeCommands = true;
    try {
      await navigator.clipboard.writeText(text);
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.copy_success as string || 'Команды скопированы в буфер обмена', 'success');
    } catch (_e: unknown) {
      addToast(((this._t?.git as Record<string, unknown>)?.unfinished_merge as Record<string, unknown>)?.copy_failed as string || 'Не удалось скопировать команды', 'error');
    } finally {
      this.copyingUnfinishedMergeCommands = false;
    }
  }
}
// #endregion GitManagerModel
