<!-- [DEF:GitManager:Component] -->
<!--
@SEMANTICS: git, manager, dashboard, version_control, tabs, workflow
@PURPOSE:   Центральный UI управления Git с фокусом на рабочий поток аналитика (commit -> promote -> deploy).
@LAYER:     Component
@RELATION:  USES -> BranchSelector
@RELATION:  USES -> DeploymentModal
@RELATION:  CALLS -> gitService
-->

<script>
    // [SECTION: IMPORTS]
    import { onMount } from 'svelte';
    import { gitService } from '../../services/gitService';
    import { api } from '../../lib/api.js';
    import { addToast as toast } from '../../lib/toasts.js';
    import { t } from '../../lib/i18n';
    import { Button, Card, PageHeader, Select, Input } from '../../lib/ui';
    import BranchSelector from './BranchSelector.svelte';
    import DeploymentModal from './DeploymentModal.svelte';
    import ConflictResolver from './ConflictResolver.svelte';
    // [/SECTION]

    // [SECTION: PROPS]
    let {
        dashboardId,
        envId = null,
        dashboardTitle = '',
        show = $bindable(false),
    } = $props();
    // [/SECTION]

    // [SECTION: STATE]
    let currentBranch = $state('main');
    let showDeployModal = $state(false);
    let loading = $state(false);
    let initialized = $state(false);
    let checkingStatus = $state(true);

    let configs = $state([]);
    let selectedConfigId = $state('');
    let remoteUrl = $state('');
    let creatingRemoteRepo = $state(false);

    let activeTab = $state('workspace');
    let showAdvancedPromote = $state(false);
    let promoting = $state(false);
    let promoteFromBranch = $state('dev');
    let promoteToBranch = $state('preprod');
    let promoteMode = $state('mr');
    let promoteReason = $state('');
    let currentEnvStage = $state('');
    let preferredDeployTargetStage = $state('');

    let commitMessage = $state('');
    let committing = $state(false);
    let generatingMessage = $state(false);
    let workspaceStatus = $state(null);
    let workspaceDiff = $state('');
    let workspaceLoading = $state(false);
    let isPulling = $state(false);
    let isPushing = $state(false);
    let autoPushAfterCommit = $state(true);
    let repositoryProvider = $state('');
    let repositoryBindingRemoteUrl = $state('');
    let repositoryConfigUrl = $state('');
    let showUnfinishedMergeDialog = $state(false);
    let unfinishedMergeContext = $state(null);
    let copyingUnfinishedMergeCommands = $state(false);
    let mergeRecoveryLoading = $state(false);
    let mergeConflicts = $state([]);
    let showConflictResolver = $state(false);
    let mergeResolveInProgress = $state(false);
    let mergeAbortInProgress = $state(false);
    let mergeContinueInProgress = $state(false);
    // [/SECTION]

    const hasWorkspaceChanges = $derived.by(() => {
        if (!workspaceStatus) return false;
        const files = [
            ...(workspaceStatus.staged_files || []),
            ...(workspaceStatus.modified_files || []),
            ...(workspaceStatus.untracked_files || []),
        ];
        return files.length > 0;
    });

    const changedFilesCount = $derived.by(() => {
        if (!workspaceStatus) return 0;
        return [
            ...(workspaceStatus.staged_files || []),
            ...(workspaceStatus.modified_files || []),
            ...(workspaceStatus.untracked_files || []),
        ].length;
    });

    // [DEF:normalizeEnvStage:Function]
    /**
     * @purpose Normalize environment stage with legacy fallback.
     * @post Returns DEV/PREPROD/PROD.
     */
    function normalizeEnvStage(env) {
        if (env?.is_production) return 'PROD';
        const stage = String(env?.stage || '').trim().toUpperCase();
        if (stage === 'PROD' || stage === 'PREPROD') return stage;
        return 'DEV';
    }
    // [/DEF:normalizeEnvStage:Function]

    // [DEF:stageBadgeClass:Function]
    /**
     * @purpose Return visual class for environment stage badges.
     * @post Stage badge is color-coded by risk level.
     */
    function stageBadgeClass(stage) {
        if (stage === 'PROD') return 'bg-red-100 text-red-800 border-red-200';
        if (stage === 'PREPROD') return 'bg-amber-100 text-amber-800 border-amber-200';
        return 'bg-blue-100 text-blue-800 border-blue-200';
    }
    // [/DEF:stageBadgeClass:Function]

    // [DEF:resolveCurrentEnvironmentId:Function]
    /**
     * @purpose Resolve active environment id for current dashboard session.
     * @post Returns env id from prop or selected_env_id in localStorage.
     */
    function resolveCurrentEnvironmentId() {
        if (envId) return String(envId);
        if (typeof window === 'undefined') return null;
        return localStorage.getItem('selected_env_id');
    }
    // [/DEF:resolveCurrentEnvironmentId:Function]

    // [DEF:applyGitflowStageDefaults:Function]
    /**
     * @purpose Apply GitFlow defaults by current environment stage.
     * @post Promote branches and preferred deploy target stage are set.
     */
    function applyGitflowStageDefaults(stage) {
        const normalizedStage = String(stage || '').toUpperCase();
        if (normalizedStage === 'DEV') {
            promoteFromBranch = 'dev';
            promoteToBranch = 'preprod';
            preferredDeployTargetStage = 'PREPROD';
            return;
        }
        if (normalizedStage === 'PREPROD') {
            promoteFromBranch = 'preprod';
            promoteToBranch = 'main';
            preferredDeployTargetStage = 'PROD';
            return;
        }
        promoteFromBranch = 'main';
        promoteToBranch = 'main';
        preferredDeployTargetStage = '';
    }
    // [/DEF:applyGitflowStageDefaults:Function]

    // [DEF:loadCurrentEnvironmentStage:Function]
    /**
     * @purpose Detect current environment stage and bind GitFlow defaults.
     * @post currentEnvStage and promote/deploy defaults are refreshed.
     */
    async function loadCurrentEnvironmentStage() {
        try {
            const currentEnvId = resolveCurrentEnvironmentId();
            if (!currentEnvId) return;
            const environments = await api.getEnvironmentsList();
            const currentEnv = (environments || []).find((item) => item.id === currentEnvId);
            if (!currentEnv) return;
            currentEnvStage = normalizeEnvStage(currentEnv);
            applyGitflowStageDefaults(currentEnvStage);
        } catch (e) {
            console.error(`[GitManager][Coherence:Failed] Failed to resolve environment stage: ${e.message}`);
        }
    }
    // [/DEF:loadCurrentEnvironmentStage:Function]

    // [DEF:isNumericDashboardRef:Function]
    /**
     * @purpose Checks whether current dashboard reference is numeric ID.
     * @post Returns true when dashboardId is digits-only.
     */
    function isNumericDashboardRef() {
        return /^\d+$/.test(String(dashboardId || '').trim());
    }
    // [/DEF:isNumericDashboardRef:Function]

    // [DEF:checkStatus:Function]
    /**
     * @purpose Verify repository initialization for dashboard slug.
     * @post initialized status and config list are updated.
     */
    async function checkStatus() {
        if (isNumericDashboardRef()) {
            checkingStatus = false;
            initialized = false;
            toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');
            return;
        }
        checkingStatus = true;
        try {
            await gitService.getBranches(dashboardId, envId);
            initialized = true;
            await loadWorkspace();
        } catch (_e) {
            initialized = false;
            configs = await gitService.getConfigs();
            const defaultConfig = resolveDefaultConfig(configs);
            if (defaultConfig?.id) selectedConfigId = defaultConfig.id;
        } finally {
            checkingStatus = false;
        }
    }
    // [/DEF:checkStatus:Function]

    // [DEF:loadWorkspace:Function]
    /**
     * @purpose Load current git status and combined diff for workspace tab.
     * @post workspace status/diff are refreshed.
     */
    async function loadWorkspace() {
        if (!initialized) return;
        workspaceLoading = true;
        try {
            workspaceStatus = await gitService.getStatus(dashboardId, envId);
            const stagedDiff = await gitService.getDiff(dashboardId, null, true, envId);
            const unstagedDiff = await gitService.getDiff(dashboardId, null, false, envId);
            workspaceDiff = [stagedDiff, unstagedDiff].filter(Boolean).join('\n\n');
            currentBranch = workspaceStatus?.current_branch || currentBranch;
        } catch (e) {
            toast(e.message || 'Не удалось загрузить изменения', 'error');
        } finally {
            workspaceLoading = false;
        }
    }
    // [/DEF:loadWorkspace:Function]

    // [DEF:handleSync:Function]
    /**
     * @purpose Sync latest dashboard config from Superset to git workspace.
     * @post Workspace status is refreshed.
     */
    async function handleSync() {
        if (isNumericDashboardRef()) {
            toast('GitManager requires dashboard slug. Numeric ID is forbidden.', 'error');
            return;
        }
        loading = true;
        try {
            const sourceEnvId = localStorage.getItem('selected_env_id');
            await gitService.sync(dashboardId, sourceEnvId, envId);
            toast($t.git?.sync_success || 'Состояние дашборда синхронизировано с Git', 'success');
            await loadWorkspace();
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            loading = false;
        }
    }
    // [/DEF:handleSync:Function]

    // [DEF:handleGenerateMessage:Function]
    /**
     * @purpose Generate commit message from diff using LLM.
     * @post commitMessage gets generated content.
     */
    async function handleGenerateMessage() {
        generatingMessage = true;
        try {
            const data = await api.postApi(
                `/git/repositories/${encodeURIComponent(String(dashboardId))}/generate-message${envId ? `?env_id=${encodeURIComponent(String(envId))}` : ''}`,
            );
            commitMessage = data?.message || '';
            toast($t.git?.commit_message_generated || 'Сообщение для коммита сгенерировано', 'success');
        } catch (e) {
            toast(e.message || $t.git?.commit_message_failed || 'Не удалось сгенерировать сообщение', 'error');
        } finally {
            generatingMessage = false;
        }
    }
    // [/DEF:handleGenerateMessage:Function]

    // [DEF:handleCommit:Function]
    /**
     * @purpose Commit current workspace changes with provided message.
     * @post Changes are committed and workspace is refreshed.
     */
    async function handleCommit() {
        if (!commitMessage || !hasWorkspaceChanges) return;
        committing = true;
        try {
            await gitService.commit(dashboardId, commitMessage, [], envId);
            if (autoPushAfterCommit) {
                await gitService.push(dashboardId, envId);
                toast($t.git?.commit_and_push_success || 'Коммит создан и отправлен в remote', 'success');
            } else {
                toast($t.git?.commit_success || 'Коммит успешно создан', 'success');
            }
            commitMessage = '';
            await loadWorkspace();
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            committing = false;
        }
    }
    // [/DEF:handleCommit:Function]

    // [DEF:handlePull:Function]
    /**
     * @purpose Pull remote changes for current branch.
     * @post Workspace status is refreshed.
     */
    function tryParseJsonObject(value) {
        const source = String(value || '').trim();
        if (!source.startsWith('{') || !source.endsWith('}')) return null;
        try {
            const parsed = JSON.parse(source);
            return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
        } catch (_e) {
            return null;
        }
    }

    function extractUnfinishedMergeContext(error) {
        if (!error || Number(error?.status) !== 409) return null;
        const parsedMessage = tryParseJsonObject(error?.message);
        const detail = error?.detail && typeof error.detail === 'object' ? error.detail : null;
        const payload = detail || parsedMessage;
        if (!payload || payload.error_code !== 'GIT_UNFINISHED_MERGE') return null;

        const commands = Array.isArray(payload.manual_commands)
            ? payload.manual_commands.filter(Boolean).map((item) => String(item))
            : [];

        const nextSteps = Array.isArray(payload.next_steps)
            ? payload.next_steps.filter(Boolean).map((item) => String(item))
            : [];

        return {
            message: String(payload.message || ''),
            repositoryPath: String(payload.repository_path || ''),
            gitDir: String(payload.git_dir || ''),
            currentBranch: String(payload.current_branch || ''),
            mergeHead: String(payload.merge_head || ''),
            mergeMessagePreview: String(payload.merge_message_preview || ''),
            nextSteps,
            commands,
        };
    }

    function openUnfinishedMergeDialogFromError(error) {
        const context = extractUnfinishedMergeContext(error);
        if (!context) return false;
        unfinishedMergeContext = context;
        showUnfinishedMergeDialog = true;
        return true;
    }

    async function loadMergeRecoveryState() {
        mergeRecoveryLoading = true;
        try {
            const status = await gitService.getMergeStatus(dashboardId, envId);
            if (!status?.has_unfinished_merge) {
                closeUnfinishedMergeDialog();
                return;
            }
            unfinishedMergeContext = {
                ...(unfinishedMergeContext || {}),
                message: unfinishedMergeContext?.message || ($t.git?.unfinished_merge?.default_message || ''),
                repositoryPath: String(status.repository_path || unfinishedMergeContext?.repositoryPath || ''),
                gitDir: String(status.git_dir || unfinishedMergeContext?.gitDir || ''),
                currentBranch: String(status.current_branch || unfinishedMergeContext?.currentBranch || ''),
                mergeHead: String(status.merge_head || unfinishedMergeContext?.mergeHead || ''),
                mergeMessagePreview: String(status.merge_message_preview || unfinishedMergeContext?.mergeMessagePreview || ''),
                nextSteps: Array.isArray(unfinishedMergeContext?.nextSteps) ? unfinishedMergeContext.nextSteps : [],
                commands: Array.isArray(unfinishedMergeContext?.commands) ? unfinishedMergeContext.commands : [],
                conflictsCount: Number(status.conflicts_count || 0),
            };
        } catch (e) {
            toast(e.message || ($t.git?.unfinished_merge?.load_status_failed || 'Failed to load merge status'), 'error');
        } finally {
            mergeRecoveryLoading = false;
        }
    }

    function closeUnfinishedMergeDialog() {
        showUnfinishedMergeDialog = false;
        unfinishedMergeContext = null;
        mergeConflicts = [];
        showConflictResolver = false;
    }

    async function handleOpenConflictResolver() {
        mergeRecoveryLoading = true;
        try {
            mergeConflicts = await gitService.getMergeConflicts(dashboardId, envId);
            if (!Array.isArray(mergeConflicts) || mergeConflicts.length === 0) {
                toast($t.git?.unfinished_merge?.no_conflicts || 'No unresolved conflicts were found', 'info');
                return;
            }
            showConflictResolver = true;
        } catch (e) {
            toast(e.message || ($t.git?.unfinished_merge?.load_conflicts_failed || 'Failed to load merge conflicts'), 'error');
        } finally {
            mergeRecoveryLoading = false;
        }
    }

    async function handleResolveConflicts(event) {
        const detail = event?.detail || {};
        const resolutions = Object.entries(detail).map(([file_path, resolution]) => ({
            file_path,
            resolution,
        }));
        if (!resolutions.length) {
            toast($t.git?.unfinished_merge?.resolve_empty || 'No conflict resolutions selected', 'warning');
            return;
        }

        mergeResolveInProgress = true;
        try {
            await gitService.resolveMergeConflicts(dashboardId, resolutions, envId);
            toast($t.git?.unfinished_merge?.resolve_success || 'Conflicts were resolved and staged', 'success');
            showConflictResolver = false;
            await loadMergeRecoveryState();
            await loadWorkspace();
        } catch (e) {
            toast(e.message || ($t.git?.unfinished_merge?.resolve_failed || 'Failed to resolve conflicts'), 'error');
        } finally {
            mergeResolveInProgress = false;
        }
    }

    async function handleAbortUnfinishedMerge() {
        mergeAbortInProgress = true;
        try {
            await gitService.abortMerge(dashboardId, envId);
            toast($t.git?.unfinished_merge?.abort_success || 'Merge was aborted', 'success');
            closeUnfinishedMergeDialog();
            await loadWorkspace();
        } catch (e) {
            toast(e.message || ($t.git?.unfinished_merge?.abort_failed || 'Failed to abort merge'), 'error');
        } finally {
            mergeAbortInProgress = false;
        }
    }

    async function handleContinueUnfinishedMerge() {
        mergeContinueInProgress = true;
        try {
            await gitService.continueMerge(dashboardId, '', envId);
            toast($t.git?.unfinished_merge?.continue_success || 'Merge commit created successfully', 'success');
            closeUnfinishedMergeDialog();
            await loadWorkspace();
        } catch (e) {
            toast(e.message || ($t.git?.unfinished_merge?.continue_failed || 'Failed to continue merge'), 'error');
            await loadMergeRecoveryState();
        } finally {
            mergeContinueInProgress = false;
        }
    }

    function getUnfinishedMergeCommandsText() {
        if (!unfinishedMergeContext?.commands?.length) return '';
        return unfinishedMergeContext.commands.join('\n');
    }

    async function handleCopyUnfinishedMergeCommands() {
        const commandsText = getUnfinishedMergeCommandsText();
        if (!commandsText) {
            toast($t.git?.unfinished_merge?.copy_empty || 'Команды для копирования отсутствуют', 'warning');
            return;
        }
        copyingUnfinishedMergeCommands = true;
        try {
            if (typeof navigator === 'undefined' || !navigator?.clipboard?.writeText) {
                throw new Error('Clipboard API unavailable');
            }
            await navigator.clipboard.writeText(commandsText);
            toast($t.git?.unfinished_merge?.copy_success || 'Команды скопированы в буфер обмена', 'success');
        } catch (_e) {
            toast($t.git?.unfinished_merge?.copy_failed || 'Не удалось скопировать команды', 'error');
        } finally {
            copyingUnfinishedMergeCommands = false;
        }
    }

    async function handlePull() {
        isPulling = true;
        try {
            await gitService.pull(dashboardId, envId);
            toast($t.git?.pull_success || 'Изменения получены из Git', 'success');
            await loadWorkspace();
        } catch (e) {
            const handledByDialog = openUnfinishedMergeDialogFromError(e);
            if (handledByDialog) {
                await loadMergeRecoveryState();
            } else {
                toast(e.message, 'error');
            }
        } finally {
            isPulling = false;
        }
    }
    // [/DEF:handlePull:Function]

    // [DEF:handlePush:Function]
    /**
     * @purpose Push local commits to remote repository.
     * @post Workspace status is refreshed.
     */
    async function handlePush() {
        isPushing = true;
        try {
            await gitService.push(dashboardId, envId);
            toast($t.git?.push_success || 'Изменения отправлены в Git', 'success');
            await loadWorkspace();
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            isPushing = false;
        }
    }
    // [/DEF:handlePush:Function]

    // [DEF:handlePromote:Function]
    /**
     * @purpose Promote branch to the next stage via MR or unsafe direct mode.
     * @post Promotion result is shown to user.
     */
    async function handlePromote() {
        const fromBranch = String(promoteFromBranch || '').trim();
        const toBranch = String(promoteToBranch || '').trim();
        if (!fromBranch || !toBranch || fromBranch === toBranch) {
            toast('Выберите разные исходную и целевую ветки', 'error');
            return;
        }
        if (promoteMode === 'direct' && !String(promoteReason || '').trim()) {
            toast('Для небезопасного прямого переноса укажите причину', 'error');
            return;
        }

        promoting = true;
        try {
            const response = await gitService.promote(
                dashboardId,
                {
                    from_branch: fromBranch,
                    to_branch: toBranch,
                    mode: promoteMode,
                    title: `Promote ${fromBranch} -> ${toBranch}: ${dashboardTitle || dashboardId}`,
                    description: promoteMode === 'direct'
                        ? `Unsafe direct promote requested.\nReason: ${promoteReason}`
                        : undefined,
                    reason: promoteMode === 'direct' ? promoteReason : undefined,
                },
                envId,
            );

            if (promoteMode === 'direct') {
                toast('Прямой перенос выполнен. Нарушение политики записано в логи.', 'warning');
            } else {
                if (response?.url) {
                    window.open(response.url, '_blank', 'noopener,noreferrer');
                }
                toast('Merge Request создан на Git сервере', 'success');
            }
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            promoting = false;
        }
    }
    // [/DEF:handlePromote:Function]

    // [DEF:openDeployModal:Function]
    /**
     * @purpose Open deploy modal with extra confirmation for PROD context.
     * @post Deploy modal is shown when confirmation passes.
     */
    function openDeployModal() {
        if (currentEnvStage === 'PROD') {
            const expected = String(dashboardId);
            const confirmation = prompt(`Подтвердите деплой в PROD. Введите slug дашборда: ${expected}`);
            if (String(confirmation || '').trim() !== expected) {
                toast('Подтверждение PROD не пройдено. Деплой отменен.', 'error');
                return;
            }
        }
        showDeployModal = true;
    }
    // [/DEF:openDeployModal:Function]

    // [DEF:getSelectedConfig:Function]
    /**
     * @purpose Return currently selected git server config.
     * @post Config object or null.
     */
    function getSelectedConfig() {
        return configs.find((item) => item.id === selectedConfigId) || null;
    }
    // [/DEF:getSelectedConfig:Function]

    // [DEF:resolveDefaultConfig:Function]
    /**
     * @purpose Resolve default git config for current session.
     * @post Returns config by selected -> is_default -> CONNECTED -> first.
     */
    function resolveDefaultConfig(configList) {
        if (!Array.isArray(configList) || configList.length === 0) return null;
        const selected = configList.find((item) => item.id === selectedConfigId);
        if (selected) return selected;
        const explicitDefault = configList.find((item) => item?.is_default);
        if (explicitDefault) return explicitDefault;
        const connected = configList.find((item) => item?.status === 'CONNECTED');
        return connected || configList[0];
    }
    // [/DEF:resolveDefaultConfig:Function]

    // [DEF:resolvePushProviderLabel:Function]
    /**
     * @purpose Resolve lower-case provider label for auto-push checkbox.
     * @post Returns provider label, fallback "git".
     */
    function resolvePushProviderLabel() {
        const selectedConfig = getSelectedConfig() || resolveDefaultConfig(configs);
        const provider = String(selectedConfig?.provider || repositoryProvider || '')
            .trim()
            .toLowerCase();
        return provider || 'git';
    }
    // [/DEF:resolvePushProviderLabel:Function]

    // [DEF:extractHttpHost:Function]
    /**
     * @purpose Extract comparable host[:port] from URL string.
     * @post Returns lowercase host token or empty string.
     */
    function extractHttpHost(urlValue) {
        const normalized = String(urlValue || '').trim();
        if (!normalized) return '';
        try {
            const parsed = new URL(normalized);
            if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return '';
            return String(parsed.host || '').toLowerCase();
        } catch (_e) {
            return '';
        }
    }
    // [/DEF:extractHttpHost:Function]

    // [DEF:buildSuggestedRepoName:Function]
    /**
     * @purpose Build deterministic repository name from dashboard title/id.
     * @post Returns kebab-case repository name.
     */
    function buildSuggestedRepoName() {
        const source = (dashboardTitle || `dashboard-${dashboardId}`).toLowerCase();
        const slug = source
            .replace(/[^a-z0-9]+/g, '-')
            .replace(/^-+|-+$/g, '')
            .slice(0, 48);
        return `${slug || 'dashboard'}-${dashboardId}`;
    }
    // [/DEF:buildSuggestedRepoName:Function]

    // [DEF:handleCreateRemoteRepo:Function]
    /**
     * @purpose Create remote repository on selected Git provider.
     * @post remoteUrl is filled from provider response.
     */
    async function handleCreateRemoteRepo() {
        const config = getSelectedConfig() || resolveDefaultConfig(configs);
        if (!config) {
            toast($t.git?.init_validation_error || 'Сначала выберите Git сервер', 'error');
            return;
        }
        if (!selectedConfigId && config.id) selectedConfigId = config.id;
        const suggestedName = buildSuggestedRepoName();
        const inputName = prompt(`Repository name for ${config.provider}:`, suggestedName);
        const repoName = String(inputName || '').trim();
        if (!repoName) return;

        creatingRemoteRepo = true;
        try {
            const repo = await gitService.createRemoteRepository(config.id, {
                name: repoName,
                private: true,
                description: `Superset dashboard ${dashboardId}: ${dashboardTitle || repoName}`,
                auto_init: true,
                default_branch: 'main',
            });
            const resolvedRemoteUrl = repo?.clone_url || repo?.html_url || '';
            if (!resolvedRemoteUrl) {
                throw new Error('Remote repository created, but URL is empty');
            }
            remoteUrl = resolvedRemoteUrl;
            toast(`Repository created on ${config.provider}`, 'success');
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            creatingRemoteRepo = false;
        }
    }
    // [/DEF:handleCreateRemoteRepo:Function]

    // [DEF:handleInit:Function]
    /**
     * @purpose Initialize git repository for dashboard.
     * @post Repository is linked and workspace is loaded.
     */
    async function handleInit() {
        if (!selectedConfigId || !remoteUrl) {
            toast($t.git?.init_validation_error || 'Заполните все поля', 'error');
            return;
        }
        loading = true;
        try {
            await gitService.initRepository(dashboardId, selectedConfigId, remoteUrl, envId);
            toast($t.git?.init_success || 'Репозиторий инициализирован', 'success');
            initialized = true;
            const selectedConfig = getSelectedConfig();
            repositoryProvider = selectedConfig?.provider || repositoryProvider;
            await loadWorkspace();
        } catch (e) {
            toast(e.message, 'error');
        } finally {
            loading = false;
        }
    }
    // [/DEF:handleInit:Function]

    // [DEF:closeModal:Function]
    /**
     * @purpose Close git manager modal.
     * @post show=false.
     */
    function closeModal() {
        show = false;
    }
    // [/DEF:closeModal:Function]

    // [DEF:handleBackdropClick:Function]
    /**
     * @purpose Close modal on backdrop click.
     * @post show=false if clicked on overlay.
     */
    function handleBackdropClick(event) {
        if (event.target === event.currentTarget) {
            closeModal();
        }
    }
    // [/DEF:handleBackdropClick:Function]

    const originHost = $derived.by(() => extractHttpHost(repositoryBindingRemoteUrl));
    const configHost = $derived.by(() => extractHttpHost(repositoryConfigUrl));
    const hasOriginConfigMismatch = $derived.by(() => {
        if (!originHost || !configHost) return false;
        return originHost !== configHost;
    });

    $effect(() => {
        const selectedConfig = getSelectedConfig() || resolveDefaultConfig(configs);
        repositoryConfigUrl = selectedConfig?.url || '';
    });

    onMount(async () => {
        try {
            configs = await gitService.getConfigs();
            const defaultConfig = resolveDefaultConfig(configs);
            if (defaultConfig?.id) selectedConfigId = defaultConfig.id;
        } catch (_e) {
            configs = [];
        }

        await Promise.all([checkStatus(), loadCurrentEnvironmentStage()]);

        if (initialized) {
            try {
                const binding = await gitService.getRepositoryBinding(dashboardId, envId);
                repositoryProvider = binding?.provider || '';
                repositoryBindingRemoteUrl = binding?.remote_url || '';
                if (binding?.config_id) selectedConfigId = String(binding.config_id);
            } catch (_e) {
                repositoryProvider = '';
                repositoryBindingRemoteUrl = '';
            }
        }
    });
</script>

<!-- [SECTION: TEMPLATE] -->
{#if show}
    <div
        class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
        onclick={handleBackdropClick}
        onkeydown={(event) => {
            if (event.key === 'Escape') closeModal();
        }}
        role="button"
        tabindex="0"
        aria-label={$t.common?.close || 'Close'}
    >
        <div
            class="relative max-h-[90vh] w-full max-w-6xl overflow-y-auto rounded-lg bg-white p-6 shadow-2xl"
            role="dialog"
            aria-modal="true"
            aria-label={`${$t.git?.management || 'Управление Git'}: ${dashboardTitle}`}
        >
            <button
                type="button"
                onclick={closeModal}
                class="absolute right-4 top-4 z-10 rounded p-1 text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-700"
                aria-label={$t.common?.close || 'Close'}
            >
                <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                </svg>
            </button>

            <PageHeader title={`${$t.git?.management || 'Управление Git'}: ${dashboardTitle}`}>
                <div slot="subtitle" class="text-sm text-gray-500">slug: {dashboardId}</div>
            </PageHeader>

            {#if checkingStatus}
                <div class="flex justify-center py-12">
                    <div class="h-8 w-8 animate-spin rounded-full border-b-2 border-blue-600"></div>
                </div>
            {:else if !initialized}
                <div class="mx-auto max-w-md py-8">
                    <Card>
                        <p class="mb-6 text-sm text-gray-600">
                            {$t.git?.not_linked || 'Этот дашборд еще не привязан к Git-репозиторию.'}
                        </p>

                        <div class="space-y-6">
                            <Select
                                label={$t.git?.server || 'Git сервер'}
                                bind:value={selectedConfigId}
                                options={configs.map(c => ({ value: c.id, label: `${c.name} (${c.provider})` }))}
                            />
                            {#if configs.length === 0}
                                <p class="-mt-4 text-xs text-red-500">{$t.git?.no_servers_configured || 'Нет сконфигурированных Git серверов'}</p>
                            {/if}

                            <Input
                                label={$t.git?.remote_url || 'Remote URL'}
                                bind:value={remoteUrl}
                                placeholder={$t.git?.remote_url_placeholder}
                            />
                            <Button
                                variant="secondary"
                                onclick={handleCreateRemoteRepo}
                                disabled={creatingRemoteRepo || configs.length === 0 || !selectedConfigId}
                                isLoading={creatingRemoteRepo}
                                class="w-full"
                            >
                                Create repo
                            </Button>

                            <Button
                                onclick={handleInit}
                                disabled={loading || configs.length === 0 || creatingRemoteRepo}
                                isLoading={loading}
                                class="w-full"
                            >
                                {$t.git?.init_repo || 'Инициализировать Git-репозиторий'}
                            </Button>
                        </div>
                    </Card>
                </div>
            {:else}
                <div class="space-y-4">
                    {#if hasOriginConfigMismatch}
                        <div class="rounded-lg border border-amber-300 bg-amber-50 p-3 text-sm text-amber-800">
                            <div class="font-semibold">Git server mismatch detected</div>
                            <div class="mt-1">
                                Configured server host: <code>{configHost}</code>, repository origin host: <code>{originHost}</code>.
                                Next push will auto-realign origin host to configured server.
                            </div>
                        </div>
                    {/if}
                    <div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2">
                        <div class="flex items-center gap-2">
                            <span class={`inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-semibold ${stageBadgeClass(currentEnvStage)}`}>
                                {currentEnvStage || 'DEV'}
                            </span>
                            <span class="text-xs text-slate-600">Текущая ветка: <strong>{currentBranch}</strong></span>
                        </div>
                        <div class="w-full max-w-sm">
                            <BranchSelector {dashboardId} {envId} bind:currentBranch />
                        </div>
                    </div>

                    <div class="flex flex-wrap items-center gap-2 border-b border-slate-200 pb-2">
                        <button
                            class={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeTab === 'workspace' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}
                            onclick={() => (activeTab = 'workspace')}
                        >
                            📝 Фиксация изменений
                        </button>
                        <button
                            class={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeTab === 'release' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}
                            onclick={() => (activeTab = 'release')}
                        >
                            🚀 Релиз
                        </button>
                        <button
                            class={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${activeTab === 'operations' ? 'bg-slate-900 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200'}`}
                            onclick={() => (activeTab = 'operations')}
                        >
                            ⚙️ Серверные операции
                        </button>
                    </div>

                    {#if activeTab === 'workspace'}
                        <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
                            <div class="space-y-4">
                                <Button
                                    variant="secondary"
                                    onclick={handleSync}
                                    disabled={loading}
                                    isLoading={loading}
                                    class="w-full"
                                >
                                    🔄 Синхронизировать из Superset
                                </Button>

                                <div class="rounded-lg border border-slate-200 p-3">
                                    <div class="mb-2 flex items-center justify-between">
                                        <h3 class="text-sm font-semibold text-slate-700">Сообщение коммита</h3>
                                        <button
                                            class="inline-flex items-center text-xs text-blue-600 hover:text-blue-800 disabled:opacity-50"
                                            onclick={handleGenerateMessage}
                                            disabled={generatingMessage || workspaceLoading}
                                        >
                                            ✨ Сгенерировать LLM
                                        </button>
                                    </div>
                                    <textarea
                                        bind:value={commitMessage}
                                        class={`h-36 w-full resize-none rounded border p-2 outline-none focus:ring-2 focus:ring-blue-500 ${generatingMessage ? 'animate-pulse bg-slate-100' : ''}`}
                                        placeholder="Опишите изменения..."
                                    ></textarea>
                                </div>

                                <div class="rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs text-slate-600">
                                    Файлов с изменениями: {changedFilesCount}
                                </div>

                                <Button
                                    onclick={handleCommit}
                                    disabled={committing || workspaceLoading || !commitMessage || !hasWorkspaceChanges}
                                    isLoading={committing}
                                    class="w-full"
                                >
                                    Зафиксировать (Commit)
                                </Button>
                                <label class="flex items-center gap-2 text-xs text-slate-600">
                                    <input type="checkbox" bind:checked={autoPushAfterCommit} />
                                    {$t.git?.auto_push_after_commit || 'Сделать push после commit в'} {resolvePushProviderLabel()}
                                </label>
                            </div>

                            <div class="flex min-h-[420px] flex-col overflow-hidden rounded-lg border border-slate-200 bg-slate-50">
                                <div class="border-b bg-slate-100 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-slate-600">
                                    Diff (изменения)
                                </div>
                                <div class="flex-1 overflow-auto p-3">
                                    {#if workspaceLoading}
                                        <div class="space-y-2">
                                            {#each Array(6) as _}
                                                <div class="h-4 animate-pulse rounded bg-slate-200"></div>
                                            {/each}
                                        </div>
                                    {:else if workspaceDiff}
                                        <pre class="whitespace-pre-wrap font-mono text-xs text-slate-800">{workspaceDiff}</pre>
                                    {:else}
                                        <div class="flex h-full items-center justify-center text-sm text-slate-500">
                                            Нет изменений для коммита
                                        </div>
                                    {/if}
                                </div>
                            </div>
                        </div>
                    {:else if activeTab === 'release'}
                        <div class="space-y-4">
                            <div class="rounded-lg border border-slate-200 bg-slate-50 p-4">
                                <div class="mb-3 text-sm font-semibold text-slate-700">Текущий статус пайплайна</div>
                                <div class="flex flex-wrap items-center gap-2 text-sm">
                                    <span class={`rounded-full border px-3 py-1 font-semibold ${stageBadgeClass('DEV')}`}>DEV (dev)</span>
                                    <span class="text-slate-400">➔</span>
                                    <span class={`rounded-full border px-3 py-1 font-semibold ${stageBadgeClass('PREPROD')}`}>PREPROD (preprod)</span>
                                    <span class="text-slate-400">➔</span>
                                    <span class={`rounded-full border px-3 py-1 font-semibold ${stageBadgeClass('PROD')}`}>PROD (main)</span>
                                </div>
                                {#if preferredDeployTargetStage}
                                    <p class="mt-2 text-xs text-slate-600">
                                        Следующий шаг по GitFlow: <strong>{promoteFromBranch} ➔ {promoteToBranch}</strong>
                                    </p>
                                {/if}
                            </div>

                            <Button
                                onclick={handlePromote}
                                disabled={promoting || !promoteFromBranch || !promoteToBranch || promoteFromBranch === promoteToBranch}
                                isLoading={promoting}
                                class={`w-full ${promoteMode === 'direct' ? 'bg-red-600 hover:bg-red-700 focus-visible:ring-red-500' : ''}`}
                            >
                                {promoteMode === 'direct'
                                    ? `Прямой перенос ${promoteFromBranch} ➔ ${promoteToBranch} (unsafe)`
                                    : `Создать Merge Request (${promoteFromBranch} ➔ ${promoteToBranch})`}
                            </Button>

                            <button
                                class="text-sm text-slate-600 hover:text-slate-900"
                                onclick={() => (showAdvancedPromote = !showAdvancedPromote)}
                            >
                                {showAdvancedPromote ? '▴ Скрыть advanced settings' : '▾ Advanced settings'}
                            </button>

                            {#if showAdvancedPromote}
                                <div class="space-y-3 rounded-lg border border-slate-200 p-3">
                                    <div class="grid grid-cols-1 gap-2 md:grid-cols-2">
                                        <Input
                                            label="From branch"
                                            bind:value={promoteFromBranch}
                                            placeholder="dev"
                                        />
                                        <Input
                                            label="To branch"
                                            bind:value={promoteToBranch}
                                            placeholder="preprod"
                                        />
                                    </div>
                                    <Select
                                        label="Promotion mode"
                                        bind:value={promoteMode}
                                        options={[
                                            { value: 'mr', label: 'Create MR/PR (Safe)' },
                                            { value: 'direct', label: 'Direct merge without MR (Unsafe)' },
                                        ]}
                                    />
                                    {#if promoteMode === 'direct'}
                                        <div class="rounded-lg border border-red-300 bg-red-50 p-3 text-sm text-red-800">
                                            <div class="font-semibold">Внимание: прямой перенос без MR</div>
                                            <div class="mt-1">Это обходит процесс аппрува и записывается в audit лог.</div>
                                        </div>
                                        <Input
                                            label="Причина (обязательно)"
                                            bind:value={promoteReason}
                                            placeholder="Почему bypass MR?"
                                        />
                                    {/if}
                                </div>
                            {/if}
                        </div>
                    {:else}
                        <div class="grid grid-cols-1 gap-3 md:grid-cols-3">
                            <Button
                                variant="ghost"
                                onclick={handlePull}
                                disabled={isPulling}
                                isLoading={isPulling}
                                class="border border-gray-200"
                            >
                                ⬇ Pull
                            </Button>
                            <Button
                                variant="ghost"
                                onclick={handlePush}
                                disabled={isPushing}
                                isLoading={isPushing}
                                class="border border-gray-200"
                            >
                                ⬆ Push{#if workspaceStatus?.ahead_count > 0} ({workspaceStatus.ahead_count}){/if}
                            </Button>
                            <Button
                                variant="primary"
                                onclick={openDeployModal}
                                class={`w-full ${currentEnvStage === 'PROD' ? 'bg-red-600 hover:bg-red-700 focus-visible:ring-red-500' : 'bg-green-600 hover:bg-green-700 focus-visible:ring-green-500'}`}
                            >
                                🚀 Deploy
                            </Button>
                        </div>
                    {/if}
                </div>
            {/if}
        </div>

        {#if showUnfinishedMergeDialog && unfinishedMergeContext}
            <div
                class="absolute inset-0 z-20 flex items-center justify-center bg-black bg-opacity-40 p-4"
                role="dialog"
                aria-modal="true"
                aria-label={$t.git?.unfinished_merge?.title || 'Repository has an unfinished merge'}
            >
                <div class="max-h-[85vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-amber-300 bg-white p-5 shadow-2xl">
                    <div class="mb-3 text-lg font-semibold text-amber-900">
                        {$t.git?.unfinished_merge?.title || 'Repository has an unfinished merge'}
                    </div>
                    <p class="mb-4 text-sm text-slate-700">
                        {unfinishedMergeContext.message || ($t.git?.unfinished_merge?.default_message || 'An unfinished merge was detected in this repository.')}
                    </p>

                    <div class="grid grid-cols-1 gap-2 text-sm md:grid-cols-2">
                        <div class="rounded border border-slate-200 bg-slate-50 p-2">
                            <div class="text-xs text-slate-500">{$t.git?.unfinished_merge?.repository_path || 'Repository path'}</div>
                            <div class="break-all font-mono text-slate-800">{unfinishedMergeContext.repositoryPath || '—'}</div>
                        </div>
                        <div class="rounded border border-slate-200 bg-slate-50 p-2">
                            <div class="text-xs text-slate-500">{$t.git?.unfinished_merge?.branch || 'Current branch'}</div>
                            <div class="font-mono text-slate-800">{unfinishedMergeContext.currentBranch || '—'}</div>
                        </div>
                        {#if unfinishedMergeContext.mergeHead}
                            <div class="rounded border border-slate-200 bg-slate-50 p-2 md:col-span-2">
                                <div class="text-xs text-slate-500">MERGE_HEAD</div>
                                <div class="break-all font-mono text-slate-800">{unfinishedMergeContext.mergeHead}</div>
                            </div>
                        {/if}
                    </div>

                    {#if unfinishedMergeContext.nextSteps?.length}
                        <div class="mt-4">
                            <div class="mb-1 text-sm font-semibold text-slate-700">
                                {$t.git?.unfinished_merge?.next_steps || 'Recommended steps'}
                            </div>
                            <ol class="list-decimal space-y-1 pl-5 text-sm text-slate-700">
                                {#each unfinishedMergeContext.nextSteps as step}
                                    <li>{step}</li>
                                {/each}
                            </ol>
                        </div>
                    {/if}

                    {#if unfinishedMergeContext.commands?.length}
                        <div class="mt-4">
                            <div class="mb-1 text-sm font-semibold text-slate-700">
                                {$t.git?.unfinished_merge?.manual_commands || 'Manual recovery commands'}
                            </div>
                            <pre class="overflow-auto rounded border border-slate-200 bg-slate-50 p-3 text-xs text-slate-800">{unfinishedMergeContext.commands.join('\n')}</pre>
                        </div>
                    {/if}

                    <div class="mt-5 flex flex-wrap justify-end gap-2">
                        <Button
                            variant="ghost"
                            onclick={loadMergeRecoveryState}
                            disabled={mergeRecoveryLoading}
                            isLoading={mergeRecoveryLoading}
                        >
                            {$t.common?.refresh || 'Refresh'}
                        </Button>
                        <Button
                            variant="secondary"
                            onclick={handleCopyUnfinishedMergeCommands}
                            disabled={copyingUnfinishedMergeCommands}
                            isLoading={copyingUnfinishedMergeCommands}
                        >
                            {$t.git?.unfinished_merge?.copy_commands || 'Copy commands'}
                        </Button>
                        <Button
                            variant="ghost"
                            onclick={handleOpenConflictResolver}
                            disabled={mergeRecoveryLoading || mergeResolveInProgress}
                            isLoading={mergeResolveInProgress}
                        >
                            {$t.git?.unfinished_merge?.open_resolver || 'Open conflict resolver'}
                        </Button>
                        <Button
                            variant="ghost"
                            onclick={handleAbortUnfinishedMerge}
                            disabled={mergeAbortInProgress}
                            isLoading={mergeAbortInProgress}
                            class="border border-red-300 text-red-700 hover:bg-red-50"
                        >
                            {$t.git?.unfinished_merge?.abort_merge || 'Abort merge'}
                        </Button>
                        <Button
                            onclick={handleContinueUnfinishedMerge}
                            disabled={mergeContinueInProgress}
                            isLoading={mergeContinueInProgress}
                        >
                            {$t.git?.unfinished_merge?.continue_merge || 'Continue merge'}
                        </Button>
                        <Button onclick={closeUnfinishedMergeDialog}>
                            {$t.common?.close || 'Close'}
                        </Button>
                    </div>
                </div>
            </div>
        {/if}
    </div>
{/if}

<ConflictResolver
    conflicts={mergeConflicts}
    bind:show={showConflictResolver}
    onresolve={handleResolveConflicts}
/>

<DeploymentModal
    {dashboardId}
    {envId}
    preferredTargetStage={preferredDeployTargetStage}
    bind:show={showDeployModal}
/>
<!-- [/SECTION] -->

<!-- [/DEF:GitManager:Component] -->
