(() => {
    const DAY_MS = 24 * 60 * 60 * 1000;
    const TIMELINE_LEFT_WIDTH = 180;
    const TIMELINE_RIGHT_PADDING = 24;
    const TIMELINE_TOP_PADDING = 24;
    const TIMELINE_ROW_HEIGHT = 54;
    const TIMELINE_AXIS_HEIGHT = 52;
    const TIMELINE_MIN_WIDTH = 760;
    const TIMELINE_LANE_STEP = 12;

    const STATUS_COLORS = {
        ACCEPTED: '#198754',
        REJECTED: '#dc3545',
        PLANNED: '#8d99a6',
        WAITING_DECISION: '#f0ad2e',
        ISSUING: '#0d6efd',
        EXPIRED: '#495057',
        SUPERSEDED: '#c7ced6',
        START_AMBIGUOUS: '#dc3545'
    };

    const state = {
        platforms: [],
        plans: [],
        selectedNoradId: null,
        activeTab: 'table',
        loadedHistoryDays: null,
        selectedPlanId: null
    };

    const el = (id) => document.getElementById(id);

    document.addEventListener('DOMContentLoaded', () => {
        setDefaultTimelineInterval();
        bindEvents();
        reloadAll();
    });

    function bindEvents() {
        el('tgu-refresh')?.addEventListener('click', () => reloadAll());
        el('tgu-show-all')?.addEventListener('click', () => {
            state.selectedNoradId = null;
            state.selectedPlanId = null;
            renderPlatforms();
            updateSelectedPlatformText();
            reloadPlans();
        });
        el('tgu-history-days')?.addEventListener('change', () => reloadPlans());
        document.querySelectorAll('[data-tgu-tab]').forEach((button) => {
            button.addEventListener('click', () => switchTab(button.dataset.tguTab));
        });
        el('tgu-chart-show')?.addEventListener('click', () => applyTimelineInterval());
        document.querySelectorAll('[data-tgu-range-hours]').forEach((button) => {
            button.addEventListener('click', () => setQuickTimelineRange(Number(button.dataset.tguRangeHours)));
        });
        window.addEventListener('resize', () => {
            if (state.activeTab === 'chart') renderTimeline();
        });
    }

    async function reloadAll() {
        showAlert('');
        await loadPlatforms();
        await reloadPlans();
    }

    async function loadPlatforms() {
        setTableLoading('tgu-platforms-body', 4, 'Загрузка платформ...');
        try {
            const platforms = await fetchJson('/api/tgu-planning/platforms');
            state.platforms = Array.isArray(platforms) ? platforms : [];
            renderPlatforms();
            renderTimeline();
        } catch (error) {
            state.platforms = [];
            renderPlatforms();
            renderTimeline();
            showAlert(error.message || 'Не удалось загрузить платформы.', 'danger');
        }
    }

    async function reloadPlans(historyDaysOverride = null) {
        const historyDays = historyDaysOverride || historyDaysForCurrentView();
        const url = state.selectedNoradId
            ? `/api/tgu-planning/spacecraft/${encodeURIComponent(state.selectedNoradId)}/plans?historyDays=${historyDays}`
            : `/api/tgu-planning/plans?historyDays=${historyDays}`;
        setTableLoading('tgu-plans-body', 8, 'Загрузка планов...');
        setTimelineLoading('Загрузка планов.');
        el('tgu-plans-meta').textContent = state.selectedNoradId
            ? `Планы для NORAD ${state.selectedNoradId}, история ${historyDays} дн.`
            : `Все планы, история ${historyDays} дн.`;

        try {
            const plans = await fetchJson(url);
            state.loadedHistoryDays = historyDays;
            state.plans = Array.isArray(plans) ? plans : [];
            if (state.selectedPlanId && !state.plans.some((plan) => String(plan.planId) === String(state.selectedPlanId))) {
                state.selectedPlanId = null;
            }
            renderPlans();
            renderTimeline();
            renderSelectedPlanDetails();
        } catch (error) {
            state.plans = [];
            state.selectedPlanId = null;
            renderPlans();
            renderTimeline();
            renderSelectedPlanDetails();
            showAlert(error.message || 'Не удалось загрузить планы.', 'danger');
        }
    }

    function switchTab(tab) {
        if (!['table', 'chart'].includes(tab)) return;
        state.activeTab = tab;
        document.querySelectorAll('[data-tgu-tab]').forEach((button) => {
            button.classList.toggle('active', button.dataset.tguTab === tab);
        });
        el('tgu-table-panel')?.classList.toggle('d-none', tab !== 'table');
        el('tgu-chart-panel')?.classList.toggle('d-none', tab !== 'chart');
        if (tab === 'chart') {
            applyTimelineInterval();
        }
    }

    function renderPlatforms() {
        const tbody = el('tgu-platforms-body');
        if (!tbody) return;

        if (!state.platforms.length) {
            tbody.innerHTML = emptyRow(4, 'Платформы не найдены.');
            return;
        }

        tbody.innerHTML = state.platforms.map((platform) => {
            const selected = String(platform.noradId || '') === String(state.selectedNoradId || '') ? ' table-active' : '';
            const norad = platform.noradId ?? '—';
            return `
                <tr class="${selected}" data-norad-id="${escapeHtml(platform.noradId || '')}">
                    <td>${escapeHtml(platform.name || platform.businessKey || platform.id || '—')}</td>
                    <td>${escapeHtml(norad)}</td>
                    <td><span class="badge text-bg-light">${escapeHtml(platform.status || '—')}</span></td>
                    <td>${escapeHtml(platform.mission || '—')}</td>
                </tr>`;
        }).join('');

        tbody.querySelectorAll('tr[data-norad-id]').forEach((row) => {
            row.addEventListener('click', () => selectPlatform(row.dataset.noradId));
        });
    }

    function selectPlatform(noradId) {
        if (!noradId) {
            showAlert('У выбранной платформы нет NORAD ID, фильтрация планов невозможна.', 'warning');
            return;
        }
        state.selectedNoradId = noradId;
        state.selectedPlanId = null;
        renderPlatforms();
        updateSelectedPlatformText();
        reloadPlans();
    }

    function renderPlans() {
        const tbody = el('tgu-plans-body');
        if (!tbody) return;

        if (!state.plans.length) {
            tbody.innerHTML = emptyRow(8, 'Планы не найдены.');
            return;
        }

        tbody.innerHTML = state.plans.map((plan) => {
            const platform = platformBySpacecraftId(plan.spacecraftId);
            return `
                <tr>
                    <td title="${escapeHtml(plan.planId)}">${shortUuid(plan.planId)}</td>
                    <td>${escapeHtml(plan.spacecraftId || '—')}</td>
                    <td>${escapeHtml(platform?.name || '—')}</td>
                    <td>${formatDateTime(plan.startTime)}</td>
                    <td>${formatDateTime(plan.endTime)}</td>
                    <td>${escapeHtml(plan.kppId || '—')}</td>
                    <td><span class="badge text-bg-light">${escapeHtml(plan.status || '—')}</span></td>
                    <td>${planActions(plan)}</td>
                </tr>`;
        }).join('');

        tbody.querySelectorAll('button[data-decision]').forEach((button) => {
            button.addEventListener('click', () => sendDecision(
                button.dataset.planId,
                button.dataset.decision,
                button
            ));
        });
    }

    function applyTimelineInterval() {
        const interval = readTimelineInterval();
        if (!interval.valid) {
            renderTimelineError(interval.message);
            return;
        }

        const requiredHistoryDays = historyDaysForTimelineFrom(interval.from);
        if (!state.loadedHistoryDays || requiredHistoryDays > state.loadedHistoryDays) {
            reloadPlans(requiredHistoryDays);
            return;
        }

        renderTimeline();
        renderSelectedPlanDetails();
    }

    function setQuickTimelineRange(hours) {
        if (!Number.isFinite(hours) || hours <= 0) return;
        const from = startOfToday();
        const to = new Date(from.getTime() + hours * 60 * 60 * 1000);
        el('tgu-chart-from').value = toDatetimeLocalValue(from);
        el('tgu-chart-to').value = toDatetimeLocalValue(to);
        applyTimelineInterval();
    }

    function renderTimeline() {
        if (!el('tgu-timeline-content')) return;

        const interval = readTimelineInterval();
        if (!interval.valid) {
            renderTimelineError(interval.message);
            return;
        }
        clearTimelineError();

        const visiblePlans = state.plans
            .map((plan) => ({plan, start: parsePlanDate(plan.startTime), end: parsePlanDate(plan.endTime)}))
            .filter((item) => item.start && item.end && item.end.getTime() >= interval.from.getTime() && item.start.getTime() <= interval.to.getTime())
            .sort((left, right) => {
                const spacecraftCompare = String(left.plan.spacecraftId || '').localeCompare(String(right.plan.spacecraftId || ''));
                return spacecraftCompare || left.start.getTime() - right.start.getTime();
            });

        if (!visiblePlans.length) {
            el('tgu-timeline-content').innerHTML = '<div class="text-muted text-center py-4">Нет планов в выбранном интервале</div>';
            return;
        }

        const grouped = groupBySpacecraft(visiblePlans);
        const scroll = el('tgu-timeline-scroll');
        const svgWidth = Math.max(scroll?.clientWidth || TIMELINE_MIN_WIDTH, TIMELINE_MIN_WIDTH);
        const chartWidth = svgWidth - TIMELINE_LEFT_WIDTH - TIMELINE_RIGHT_PADDING;
        const svgHeight = TIMELINE_TOP_PADDING + grouped.length * TIMELINE_ROW_HEIGHT + TIMELINE_AXIS_HEIGHT;
        const axisY = TIMELINE_TOP_PADDING + grouped.length * TIMELINE_ROW_HEIGHT + 14;
        const ticks = timelineTicks(interval.from, interval.to);
        const intervalMs = interval.to.getTime() - interval.from.getTime();
        const xForTime = (time) => {
            // Координата считается от выбранного интервала, а не от фактически загруженной истории.
            return TIMELINE_LEFT_WIDTH + ((time.getTime() - interval.from.getTime()) / intervalMs) * chartWidth;
        };

        const rows = grouped.map((group, index) => {
            const rowY = TIMELINE_TOP_PADDING + index * TIMELINE_ROW_HEIGHT + 24;
            const label = spacecraftLabel(group.spacecraftId);
            const subLabel = label === String(group.spacecraftId) ? '' : String(group.spacecraftId);
            const sortedPlans = group.items.sort((left, right) => left.start.getTime() - right.start.getTime());
            const planLines = sortedPlans.map((item, planIndex) => {
                const x1 = Math.max(TIMELINE_LEFT_WIDTH, xForTime(item.start));
                const x2 = Math.min(TIMELINE_LEFT_WIDTH + chartWidth, xForTime(item.end));
                const widthAdjustedX2 = x2 - x1 < 4 ? Math.min(TIMELINE_LEFT_WIDTH + chartWidth, x1 + 4) : x2;
                const status = String(item.plan.status || '');
                const selected = String(item.plan.planId) === String(state.selectedPlanId);
                const dashArray = status === 'START_AMBIGUOUS' ? '7 5' : '';
                const planY = rowY + timelineLaneOffset(planIndex);
                return `
                    <line class="tgu-timeline-plan${selected ? ' tgu-timeline-plan-selected' : ''}"
                          x1="${x1.toFixed(2)}" y1="${planY}" x2="${widthAdjustedX2.toFixed(2)}" y2="${planY}"
                          stroke="${escapeHtml(statusColor(status))}" stroke-width="${selected ? 15 : 11}"
                          ${dashArray ? `stroke-dasharray="${dashArray}"` : ''}
                          data-plan-id="${escapeHtml(item.plan.planId)}">
                        <title>${escapeHtml(planTooltip(item.plan))}</title>
                    </line>`;
            }).join('');
            const arrows = sortedPlans.slice(0, -1).map((item, planIndex) => {
                const next = sortedPlans[planIndex + 1];
                const startX = Math.min(TIMELINE_LEFT_WIDTH + chartWidth - 8, Math.max(TIMELINE_LEFT_WIDTH + 8, xForTime(item.end) + 7));
                const endX = Math.min(TIMELINE_LEFT_WIDTH + chartWidth - 4, Math.max(TIMELINE_LEFT_WIDTH + 12, xForTime(next.start) - 7));
                const startY = rowY + timelineLaneOffset(planIndex);
                const endY = rowY + timelineLaneOffset(planIndex + 1);
                if (endX - startX >= 16) {
                    return `<line class="tgu-sequence-arrow" x1="${startX.toFixed(2)}" y1="${startY.toFixed(2)}" x2="${endX.toFixed(2)}" y2="${endY.toFixed(2)}" marker-end="url(#tgu-arrowhead)" />`;
                }
                const markerX = Math.min(TIMELINE_LEFT_WIDTH + chartWidth - 20, Math.max(TIMELINE_LEFT_WIDTH + 20, xForTime(next.start)));
                const controlY = Math.min(startY, endY) - TIMELINE_LANE_STEP;
                return `<path class="tgu-sequence-arrow" d="M ${(markerX - 12).toFixed(2)} ${startY.toFixed(2)} C ${(markerX - 2).toFixed(2)} ${controlY.toFixed(2)}, ${(markerX + 16).toFixed(2)} ${controlY.toFixed(2)}, ${(markerX + 12).toFixed(2)} ${endY.toFixed(2)}" marker-end="url(#tgu-arrowhead)" />`;
            }).join('');
            return `
                <g>
                    <line class="tgu-timeline-grid" x1="${TIMELINE_LEFT_WIDTH}" y1="${rowY + 24}" x2="${TIMELINE_LEFT_WIDTH + chartWidth}" y2="${rowY + 24}" />
                    <text class="tgu-timeline-label" x="12" y="${rowY - 4}">
                        <title>${escapeHtml(String(group.spacecraftId))}</title>${escapeHtml(label)}
                    </text>
                    ${subLabel ? `<text class="tgu-timeline-sub-label" x="12" y="${rowY + 12}">${escapeHtml(subLabel)}</text>` : ''}
                    ${arrows}
                    ${planLines}
                </g>`;
        }).join('');

        const tickMarkup = ticks.map((tick) => {
            const x = xForTime(tick);
            return `
                <g>
                    <line class="tgu-timeline-grid" x1="${x.toFixed(2)}" y1="${TIMELINE_TOP_PADDING - 8}" x2="${x.toFixed(2)}" y2="${axisY}" />
                    <line class="tgu-timeline-axis" x1="${x.toFixed(2)}" y1="${axisY}" x2="${x.toFixed(2)}" y2="${axisY + 6}" />
                    <text class="tgu-timeline-tick-label" x="${x.toFixed(2)}" y="${axisY + 22}" text-anchor="middle">${escapeHtml(formatAxisTick(tick))}</text>
                </g>`;
        }).join('');

        el('tgu-timeline-content').innerHTML = `
            <svg class="tgu-timeline-svg" viewBox="0 0 ${svgWidth} ${svgHeight}" role="img" aria-label="Последовательность планов по времени">
                <defs>
                    <marker id="tgu-arrowhead" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto" markerUnits="strokeWidth">
                        <path d="M0,0 L8,4 L0,8 Z" fill="#8ea1b5"></path>
                    </marker>
                </defs>
                <text class="tgu-timeline-label" x="${TIMELINE_LEFT_WIDTH}" y="16">Последовательность планов по времени</text>
                ${tickMarkup}
                <line class="tgu-timeline-axis" x1="${TIMELINE_LEFT_WIDTH}" y1="${axisY}" x2="${TIMELINE_LEFT_WIDTH + chartWidth}" y2="${axisY}" />
                ${rows}
            </svg>`;

        el('tgu-timeline-content').querySelectorAll('[data-plan-id]').forEach((line) => {
            line.addEventListener('click', () => {
                state.selectedPlanId = line.dataset.planId;
                renderTimeline();
                renderSelectedPlanDetails();
            });
        });
    }

    function renderSelectedPlanDetails() {
        const target = el('tgu-plan-details');
        if (!target) return;
        const plan = state.plans.find((item) => String(item.planId) === String(state.selectedPlanId));
        if (!plan) {
            target.innerHTML = '';
            return;
        }

        const platform = platformBySpacecraftId(plan.spacecraftId);
        target.innerHTML = `
            <div class="d-flex justify-content-between align-items-start flex-wrap gap-3 mb-3">
                <div>
                    <div class="catalog-section-title">Детали плана</div>
                    <div class="catalog-helper mt-1">${escapeHtml(platform?.name || plan.spacecraftId || '—')}</div>
                </div>
                ${planActions(plan)}
            </div>
            <div class="tgu-plan-details-grid">
                ${detailItem('planId', plan.planId)}
                ${detailItem('spacecraftId', plan.spacecraftId)}
                ${detailItem('startTime', formatDateTime(plan.startTime))}
                ${detailItem('endTime', formatDateTime(plan.endTime))}
                ${detailItem('kppId', plan.kppId || '—')}
                ${detailItem('status', plan.status || '—')}
            </div>`;

        target.querySelectorAll('button[data-decision]').forEach((button) => {
            button.addEventListener('click', () => sendDecision(
                button.dataset.planId,
                button.dataset.decision,
                button
            ));
        });
    }

    function renderTimelineError(message) {
        const error = el('tgu-chart-error');
        if (error) {
            error.textContent = message;
            error.classList.remove('d-none');
        }
        const target = el('tgu-timeline-content');
        if (target) target.innerHTML = '';
    }

    function clearTimelineError() {
        const error = el('tgu-chart-error');
        if (!error) return;
        error.textContent = '';
        error.classList.add('d-none');
    }

    function setTimelineLoading(message) {
        const target = el('tgu-timeline-content');
        if (target) target.innerHTML = `<div class="text-muted text-center py-4">${escapeHtml(message)}</div>`;
    }

    function planActions(plan) {
        if (plan.status !== 'WAITING_DECISION') return '<span class="text-muted">—</span>';
        const planId = escapeHtml(plan.planId);
        return `
            <div class="d-flex flex-wrap gap-2">
                <button type="button" class="btn btn-outline-success btn-sm" data-decision="ACCEPTED" data-plan-id="${planId}">
                    Принять
                </button>
                <button type="button" class="btn btn-outline-danger btn-sm" data-decision="REJECTED" data-plan-id="${planId}">
                    Отклонить
                </button>
            </div>`;
    }

    async function sendDecision(planId, decision, button) {
        let reason = decision === 'ACCEPTED' ? 'UI_TEST_ACCEPTED' : '';
        if (decision === 'REJECTED') {
            reason = window.prompt('Причина отклонения плана', 'UI_TEST_REJECTED');
            if (reason === null) return;
            reason = reason.trim();
            if (!reason) {
                showAlert('Для REJECTED укажите reason.', 'warning');
                return;
            }
        }

        setButtonLoading(button, true, 'Отправка...');
        try {
            const query = new URLSearchParams({decision, reason});
            const result = await fetchJson(`/api/tgu-planning/plans/${encodeURIComponent(planId)}/decision?${query}`, {
                method: 'POST',
                headers: {'Accept': 'application/json'},
                decisionRequest: true
            });
            const attempt = result?.attemptId ? ` attemptId=${result.attemptId}` : '';
            const message = decision === 'REJECTED'
                ? `Решение REJECTED отправлено. План может вернуться в PLANNED для повторной выдачи.${attempt}`
                : `Тестовое решение ACCEPTED отправлено.${attempt}`;
            showAlert(message, decision === 'REJECTED' ? 'warning' : 'success');
            await reloadPlans(state.loadedHistoryDays || selectedHistoryDays());
        } catch (error) {
            showAlert(error.message || 'Не удалось отправить тестовое решение по плану.', 'danger');
        } finally {
            setButtonLoading(button, false);
        }
    }

    function selectedHistoryDays() {
        const value = Number(el('tgu-history-days')?.value || 7);
        return [1, 3, 7, 14].includes(value) ? value : 7;
    }

    function historyDaysForCurrentView() {
        const selected = selectedHistoryDays();
        if (state.activeTab !== 'chart') return selected;
        const interval = readTimelineInterval();
        return interval.valid ? Math.max(selected, historyDaysForTimelineFrom(interval.from)) : selected;
    }

    function historyDaysForTimelineFrom(from) {
        const now = new Date();
        if (from.getTime() >= now.getTime()) return 1;
        return Math.max(1, Math.ceil((now.getTime() - from.getTime()) / DAY_MS));
    }

    function setDefaultTimelineInterval() {
        const from = startOfToday();
        const to = new Date(from.getTime() + DAY_MS);
        if (el('tgu-chart-from')) el('tgu-chart-from').value = toDatetimeLocalValue(from);
        if (el('tgu-chart-to')) el('tgu-chart-to').value = toDatetimeLocalValue(to);
    }

    function readTimelineInterval() {
        const from = parseDatetimeLocalValue(el('tgu-chart-from')?.value);
        const to = parseDatetimeLocalValue(el('tgu-chart-to')?.value);
        if (!from || !to) return {valid: false, message: 'Укажите from и to.'};
        if (from.getTime() >= to.getTime()) return {valid: false, message: 'from должен быть раньше to.'};
        return {valid: true, from, to};
    }

    function parseDatetimeLocalValue(value) {
        if (!value) return null;
        const date = new Date(value);
        return Number.isNaN(date.getTime()) ? null : date;
    }

    function parsePlanDate(value) {
        return PcpUtcTime.parseApiDate(value);
    }

    function startOfToday() {
        const now = new Date();
        return new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
    }

    function toDatetimeLocalValue(date) {
        return PcpUtcTime.toLocalInputValue(date);
    }

    function timelineTicks(from, to) {
        const duration = to.getTime() - from.getTime();
        const tickCount = duration <= DAY_MS ? 6 : 7;
        const ticks = [];
        for (let index = 0; index <= tickCount; index += 1) {
            ticks.push(new Date(from.getTime() + (duration * index) / tickCount));
        }
        return ticks;
    }

    function timelineLaneOffset(index) {
        return [0, -1, 0, 1][index % 4] * TIMELINE_LANE_STEP;
    }

    function groupBySpacecraft(items) {
        const groups = new Map();
        items.forEach((item) => {
            const spacecraftId = item.plan.spacecraftId || '—';
            if (!groups.has(spacecraftId)) groups.set(spacecraftId, []);
            groups.get(spacecraftId).push(item);
        });
        return Array.from(groups.entries()).map(([spacecraftId, groupItems]) => ({spacecraftId, items: groupItems}));
    }

    function spacecraftLabel(spacecraftId) {
        const platform = platformBySpacecraftId(spacecraftId);
        return platform?.name || String(spacecraftId || '—');
    }

    function statusColor(status) {
        return STATUS_COLORS[status] || '#8d99a6';
    }

    function planTooltip(plan) {
        return [
            `planId: ${plan.planId || '—'}`,
            `spacecraftId: ${plan.spacecraftId || '—'}`,
            `startTime: ${plan.startTime || '—'}`,
            `endTime: ${plan.endTime || '—'}`,
            `kppId: ${plan.kppId || '—'}`,
            `status: ${plan.status || '—'}`
        ].join('\n');
    }

    function detailItem(label, value) {
        return `
            <div>
                <div class="tgu-plan-detail-label">${escapeHtml(label)}</div>
                <div class="tgu-plan-detail-value">${escapeHtml(value)}</div>
            </div>`;
    }

    function updateSelectedPlatformText() {
        if (!state.selectedNoradId) {
            el('tgu-planning-selected-platform').textContent = 'Все планы по доступным платформам.';
            return;
        }
        const platform = platformBySpacecraftId(state.selectedNoradId);
        const name = platform?.name || 'платформа';
        el('tgu-planning-selected-platform').textContent = `${name}, NORAD ${state.selectedNoradId}.`;
    }

    function platformBySpacecraftId(spacecraftId) {
        return state.platforms.find((platform) => String(platform.noradId) === String(spacecraftId)) || null;
    }

    async function fetchJson(url, options = {}) {
        const decisionRequest = Boolean(options.decisionRequest);
        const requestOptions = {...options};
        delete requestOptions.decisionRequest;
        const response = await fetch(
            url,
            requestOptions.headers ? requestOptions : {...requestOptions, headers: {'Accept': 'application/json'}}
        );
        if (!response.ok) {
            const text = await response.text();
            throw new Error(errorMessage(response.status, text, decisionRequest));
        }
        if (response.status === 204) return null;
        const text = await response.text();
        return text ? JSON.parse(text) : null;
    }

    function errorMessage(status, text, decisionRequest) {
        if (decisionRequest && status === 409) {
            return 'План не является активным планом, ожидающим решения, или для него нет active attempt.';
        }
        if (decisionRequest && status === 404) {
            const extracted = extractError(text);
            return extracted === 'Test controller недоступен или отключён.' ? extracted : 'План не найден.';
        }
        if (decisionRequest && status >= 500) {
            const extracted = extractError(text);
            return extracted || 'Test controller недоступен или отключён.';
        }
        return extractError(text) || `HTTP ${status}`;
    }

    function extractError(text) {
        if (!text) return '';
        try {
            const json = JSON.parse(text);
            return json.reason || json.message || json.error || json.detail || text;
        } catch (_) {
            return text;
        }
    }

    function showAlert(message, type = 'danger') {
        const target = el('tgu-planning-alert');
        if (!target) return;
        if (!message) {
            target.innerHTML = '';
            return;
        }
        target.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
            ${escapeHtml(message)}
            <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
        </div>`;
    }

    function setTableLoading(bodyId, colspan, message) {
        const tbody = el(bodyId);
        if (tbody) tbody.innerHTML = emptyRow(colspan, message);
    }

    function emptyRow(colspan, message) {
        return `<tr><td colspan="${colspan}" class="text-muted text-center py-4">${escapeHtml(message)}</td></tr>`;
    }

    function shortUuid(value) {
        if (!value) return '—';
        return `${String(value).slice(0, 8)}...`;
    }

    function formatDateTime(value) {
        if (!value) return '—';
        const formatted = PcpUtcTime.formatDateTime(value, {
            year: '2-digit',
            month: '2-digit',
            day: '2-digit',
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });
        return formatted === '—' ? escapeHtml(value) : formatted;
    }

    function formatAxisTick(value) {
        return value.toLocaleString('ru-RU', {
            day: '2-digit',
            month: '2-digit',
            hour: '2-digit',
            minute: '2-digit'
        });
    }

    function setButtonLoading(button, loading, text = 'Загрузка...') {
        if (!button) return;
        if (loading) {
            button.dataset.originalText = button.textContent;
            button.textContent = text;
            button.disabled = true;
        } else {
            button.textContent = button.dataset.originalText || button.textContent;
            button.disabled = false;
        }
    }

    function escapeHtml(value) {
        return String(value ?? '')
            .replaceAll('&', '&amp;')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#039;');
    }
})();
