// LOGPile Frontend Application

document.addEventListener('DOMContentLoaded', () => {
    initSourceType();
    initApiSource();
    initUpload();
    initConvertMode();
    initAuditViewer();
    loadParsersInfo();
    loadSupportedFileTypes();
});

let sourceType = 'archive';
let convertFiles = [];
let isConvertRunning = false;
let convertDuplicates = [];
const CONVERT_MAX_FILES_PER_BATCH = 1000;
let supportedUploadExtensions = null;
let supportedConvertExtensions = null;
let apiConnectPayload = null;
let collectionJob = null;
let collectionJobPollTimer = null;
let collectionJobLogCounter = 0;
let apiPortTouchedByUser = false;
let isAutoUpdatingApiPort = false;
let apiProbeResult = null;
let apiPowerDecisionTimer = null;

function initAuditViewer() {
    const frame = document.getElementById('audit-viewer-frame');
    if (!frame) {
        return;
    }

    frame.addEventListener('load', () => {
        resizeAuditViewerFrame();
        try {
            const win = frame.contentWindow;
            if (win) {
                win.setTimeout(resizeAuditViewerFrame, 50);
                win.setTimeout(resizeAuditViewerFrame, 250);
            }
        } catch (err) {
            console.error('Failed to schedule viewer resize:', err);
        }
    });

    window.addEventListener('resize', () => {
        resizeAuditViewerFrame();
    });
}

function initSourceType() {
    const sourceButtons = document.querySelectorAll('.source-switch-btn');
    sourceButtons.forEach(button => {
        button.addEventListener('click', () => {
            setSourceType(button.dataset.sourceType);
        });
    });

    setSourceType(sourceType);
}

function setSourceType(nextType) {
    if (nextType === 'api') {
        sourceType = 'api';
    } else if (nextType === 'convert') {
        sourceType = 'convert';
    } else {
        sourceType = 'archive';
    }

    document.querySelectorAll('.source-switch-btn').forEach(button => {
        button.classList.toggle('active', button.dataset.sourceType === sourceType);
    });

    const archiveContent = document.getElementById('archive-source-content');
    const apiSourceContent = document.getElementById('api-source-content');
    const convertSourceContent = document.getElementById('convert-source-content');
    archiveContent.classList.toggle('hidden', sourceType !== 'archive');
    apiSourceContent.classList.toggle('hidden', sourceType !== 'api');
    if (convertSourceContent) {
        convertSourceContent.classList.toggle('hidden', sourceType !== 'convert');
    }
}

function initApiSource() {
    const apiForm = document.getElementById('api-connect-form');
    if (!apiForm) {
        return;
    }

    const cancelJobButton = document.getElementById('cancel-job-btn');
    const skipHungButton = document.getElementById('skip-hung-btn');
    const connectButton = document.getElementById('api-connect-btn');
    const collectButton = document.getElementById('api-collect-btn');
    const fieldNames = ['host', 'port', 'username', 'password'];

    apiForm.addEventListener('submit', (event) => {
        event.preventDefault();
        if (apiProbeResult && apiProbeResult.reachable) {
            startCollectionWithOptions();
        } else {
            startApiProbe();
        }
    });

    if (cancelJobButton) {
        cancelJobButton.addEventListener('click', () => {
            cancelCollectionJob();
        });
    }
    if (skipHungButton) {
        skipHungButton.addEventListener('click', () => {
            skipHungCollectionJob();
        });
    }
    if (connectButton) {
        connectButton.addEventListener('click', () => {
            startApiProbe();
        });
    }
    if (collectButton) {
        collectButton.addEventListener('click', () => {
            startCollectionWithOptions();
        });
    }
    fieldNames.forEach((fieldName) => {
        const field = apiForm.elements.namedItem(fieldName);
        if (!field) {
            return;
        }

        const eventName = field.tagName.toLowerCase() === 'select' ? 'change' : 'input';
        field.addEventListener(eventName, () => {
            if (fieldName === 'port') {
                handleApiPortInput(field.value);
            }

            const { errors } = validateCollectForm();
            renderFormErrors(errors);
            clearApiConnectStatus();
            resetApiProbeState();

            if (collectionJob && isCollectionJobTerminal(collectionJob.status)) {
                resetCollectionJobState();
            }
        });
    });

    applyRedfishDefaultPort();
    renderCollectionJob();
}


function startApiProbe() {
    const { isValid, payload, errors } = validateCollectForm();
    renderFormErrors(errors);
    if (!isValid) {
        renderApiConnectStatus(false);
        resetApiProbeState();
        return;
    }

    apiConnectPayload = payload;
    resetApiProbeState();
    setApiFormBlocked(true);
    renderApiConnectStatus(true);

    fetch('/api/collect/probe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
    })
        .then(async (response) => {
            const body = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(body.error || 'Проверка подключения не удалась');
            }

            apiProbeResult = body;
            renderApiProbeState();
        })
        .catch((err) => {
            resetApiProbeState();
            renderApiConnectStatus(false);
            const status = document.getElementById('api-connect-status');
            if (status) {
                status.textContent = err.message || 'Проверка подключения не удалась';
                status.className = 'api-connect-status error';
            }
        })
        .finally(() => {
            if (!collectionJob || isCollectionJobTerminal(collectionJob.status)) {
                setApiFormBlocked(false);
            }
        });
}

function startCollectionWithOptions() {
    const { isValid, payload, errors } = validateCollectForm();
    renderFormErrors(errors);
    if (!isValid) {
        renderApiConnectStatus(false);
        return;
    }

    if (!apiProbeResult || !apiProbeResult.reachable) {
        const status = document.getElementById('api-connect-status');
        if (status) {
            status.textContent = 'Сначала выполните проверку подключения.';
            status.className = 'api-connect-status error';
        }
        return;
    }

    const debugPayloads = document.getElementById('api-debug-payloads');
    payload.debug_payloads = debugPayloads ? debugPayloads.checked : false;
    startCollectionJob(payload);
}

function renderApiProbeState() {
    const connectButton = document.getElementById('api-connect-btn');
    const probeOptions = document.getElementById('api-probe-options');
    const status = document.getElementById('api-connect-status');
    if (!connectButton || !probeOptions || !status) {
        return;
    }

    if (!apiProbeResult || !apiProbeResult.reachable) {
        status.textContent = 'Проверка подключения не пройдена.';
        status.className = 'api-connect-status error';
        probeOptions.classList.add('hidden');
        connectButton.textContent = 'Подключиться';
        return;
    }

    const hostOn = apiProbeResult.host_powered_on;

    if (hostOn) {
        status.textContent = apiProbeResult.message || 'Связь с BMC есть, host включён.';
        status.className = 'api-connect-status success';
    } else {
        status.textContent = apiProbeResult.message || 'Связь с BMC есть, host выключен.';
        status.className = 'api-connect-status warning';
    }

    probeOptions.classList.remove('hidden');

    const hostOffWarning = document.getElementById('api-host-off-warning');
    if (hostOffWarning) {
        if (hostOn) {
            hostOffWarning.classList.add('hidden');
        } else {
            hostOffWarning.classList.remove('hidden');
        }
    }

    connectButton.textContent = 'Переподключиться';
}

function resetApiProbeState() {
    apiProbeResult = null;
    clearApiPowerDecisionTimer();
    const connectButton = document.getElementById('api-connect-btn');
    const probeOptions = document.getElementById('api-probe-options');
    if (connectButton) {
        connectButton.textContent = 'Подключиться';
    }
    if (probeOptions) {
        probeOptions.classList.add('hidden');
    }
}

function clearApiPowerDecisionTimer() {
    if (!apiPowerDecisionTimer) {
        return;
    }
    window.clearInterval(apiPowerDecisionTimer);
    apiPowerDecisionTimer = null;
}

function validateCollectForm() {
    const host = getApiValue('host');
    const portRaw = getApiValue('port');
    const username = getApiValue('username');
    const password = getApiValue('password');

    const errors = {};

    if (!host) {
        errors.host = 'Укажите host.';
    }

    const port = Number(portRaw);
    const isPortInteger = Number.isInteger(port);
    if (!portRaw) {
        errors.port = 'Укажите порт.';
    } else if (!isPortInteger || port < 1 || port > 65535) {
        errors.port = 'Порт должен быть от 1 до 65535.';
    }

    if (!username) {
        errors.username = 'Укажите username.';
    }

    if (!password) {
        errors.password = 'Введите пароль.';
    }

    if (Object.keys(errors).length > 0) {
        return { isValid: false, errors, payload: null };
    }

    // TODO: UI для выбора протокола вернем, когда откроем IPMI коннектор.
    const payload = {
        host,
        protocol: 'redfish',
        port,
        username,
        auth_type: 'password',
        tls_mode: 'insecure',
        password
    };

    return { isValid: true, errors: {}, payload };
}

function renderFormErrors(errors) {
    const apiForm = document.getElementById('api-connect-form');
    const summary = document.getElementById('api-form-errors');
    if (!apiForm || !summary) {
        return;
    }

    const errorFields = ['host', 'port', 'username', 'password'];
    errorFields.forEach((fieldName) => {
        const errorNode = apiForm.querySelector(`[data-error-for="${fieldName}"]`);
        if (!errorNode) {
            return;
        }

        const fieldWrapper = errorNode.closest('.api-form-field');
        const message = errors[fieldName] || '';
        errorNode.textContent = message;
        if (fieldWrapper) {
            fieldWrapper.classList.toggle('has-error', Boolean(message));
        }
    });

    const messages = Object.values(errors);
    if (messages.length === 0) {
        summary.innerHTML = '';
        summary.classList.add('hidden');
        return;
    }

    summary.classList.remove('hidden');
    summary.innerHTML = `<strong>Исправьте ошибки в форме:</strong><ul>${messages.map(msg => `<li>${escapeHtml(msg)}</li>`).join('')}</ul>`;
}

function renderApiConnectStatus(isValid) {
    const status = document.getElementById('api-connect-status');
    if (!status) {
        return;
    }

    if (!isValid) {
        status.textContent = 'Форма не отправлена: есть ошибки.';
        status.className = 'api-connect-status error';
        return;
    }

    status.textContent = 'Подключение...';
    status.className = 'api-connect-status info';
}

function clearApiConnectStatus() {
    const status = document.getElementById('api-connect-status');
    if (!status) {
        return;
    }

    status.textContent = '';
    status.className = 'api-connect-status';
}

function startCollectionJob(payload) {
    clearApiPowerDecisionTimer();
    resetCollectionJobState();
    setApiFormBlocked(true);

    fetch('/api/collect', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
    })
        .then(async (response) => {
            const body = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(body.error || 'Не удалось запустить задачу');
            }

            collectionJob = {
                id: body.job_id,
                status: normalizeJobStatus(body.status || 'queued'),
                progress: 0,
                currentPhase: '',
                etaSeconds: null,
                logs: [],
                activeModules: [],
                moduleScores: [],
                debugInfo: null,
                payload
            };
            appendJobLog(body.message || 'Задача поставлена в очередь');
            renderCollectionJob();

            collectionJobPollTimer = window.setInterval(() => {
                pollCollectionJobStatus();
            }, 1200);
        })
        .catch((err) => {
            setApiFormBlocked(false);
            clearApiConnectStatus();
            renderApiConnectStatus(false);
            const status = document.getElementById('api-connect-status');
            if (status) {
                status.textContent = err.message || 'Ошибка запуска задачи';
                status.className = 'api-connect-status error';
            }
        });
}

function pollCollectionJobStatus() {
    if (!collectionJob || isCollectionJobTerminal(collectionJob.status)) {
        clearCollectionJobPolling();
        return;
    }

    fetch(`/api/collect/${encodeURIComponent(collectionJob.id)}`)
        .then(async (response) => {
            const body = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(body.error || 'Не удалось получить статус задачи');
            }

            const prevStatus = collectionJob.status;
            collectionJob.status = normalizeJobStatus(body.status || collectionJob.status);
            collectionJob.progress = Number.isFinite(body.progress) ? body.progress : collectionJob.progress;
            collectionJob.currentPhase = body.current_phase || collectionJob.currentPhase || '';
            collectionJob.etaSeconds = Number.isFinite(body.eta_seconds) ? body.eta_seconds : collectionJob.etaSeconds;
            collectionJob.error = body.error || '';
            collectionJob.activeModules = Array.isArray(body.active_modules) ? body.active_modules : collectionJob.activeModules;
            collectionJob.moduleScores = Array.isArray(body.module_scores) ? body.module_scores : collectionJob.moduleScores;
            collectionJob.debugInfo = body.debug_info || collectionJob.debugInfo || null;
            syncServerLogs(body.logs);
            renderCollectionJob();

            if (isCollectionJobTerminal(collectionJob.status)) {
                clearCollectionJobPolling();
                if (collectionJob.status === 'success') {
                    loadDataFromStatus();
                } else if (collectionJob.status === 'failed' && collectionJob.error) {
                    appendJobLog(`Ошибка: ${collectionJob.error}`);
                    renderCollectionJob();
                }
            } else if (prevStatus !== collectionJob.status && collectionJob.status === 'running') {
                renderCollectionJob();
            }
        })
        .catch((err) => {
            appendJobLog(`Ошибка статуса: ${err.message}`);
            renderCollectionJob();
            clearCollectionJobPolling();
            setApiFormBlocked(false);
        });
}

function skipHungCollectionJob() {
    if (!collectionJob || isCollectionJobTerminal(collectionJob.status)) {
        return;
    }
    const btn = document.getElementById('skip-hung-btn');
    if (btn) {
        btn.disabled = true;
        btn.textContent = 'Пропуск...';
    }
    fetch(`/api/collect/${encodeURIComponent(collectionJob.id)}/skip`, {
        method: 'POST'
    })
        .then(async (response) => {
            const body = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(body.error || 'Не удалось пропустить зависшие запросы');
            }
            syncServerLogs(body.logs);
            renderCollectionJob();
        })
        .catch((err) => {
            appendJobLog(`Ошибка пропуска: ${err.message}`);
            if (btn) {
                btn.disabled = false;
                btn.textContent = 'Пропустить зависшие';
            }
            renderCollectionJob();
        });
}

function cancelCollectionJob() {
    if (!collectionJob || isCollectionJobTerminal(collectionJob.status)) {
        return;
    }
    fetch(`/api/collect/${encodeURIComponent(collectionJob.id)}/cancel`, {
        method: 'POST'
    })
        .then(async (response) => {
            const body = await response.json().catch(() => ({}));
            if (!response.ok) {
                throw new Error(body.error || 'Не удалось отменить задачу');
            }
            collectionJob.status = normalizeJobStatus(body.status || 'canceled');
            collectionJob.progress = Number.isFinite(body.progress) ? body.progress : collectionJob.progress;
            syncServerLogs(body.logs);
            clearCollectionJobPolling();
            renderCollectionJob();
        })
        .catch((err) => {
            appendJobLog(`Ошибка отмены: ${err.message}`);
            renderCollectionJob();
        });
}

function appendJobLog(message) {
    if (!collectionJob) {
        return;
    }

    const parsed = parseServerLogLine(message);
    if (isCollectLogNoise(parsed.message)) {
        // Still count toward log length so syncServerLogs offset stays correct,
        // but mark as hidden so renderCollectionJob skips it.
        collectionJob.logs.push({
            id: ++collectionJobLogCounter,
            time: parsed.time || new Date().toLocaleTimeString('ru-RU', { hour12: false }),
            message: parsed.message,
            hidden: true
        });
        return;
    }

    collectionJob.logs.push({
        id: ++collectionJobLogCounter,
        time: parsed.time || new Date().toLocaleTimeString('ru-RU', { hour12: false }),
        message: humanizeCollectLogMessage(parsed.message)
    });
}

// Transform technical log messages into human-readable form for the UI.
// The original messages are preserved in collect.log / raw_export.
function humanizeCollectLogMessage(msg) {
    // "Redfish snapshot: документов=520, ETA≈16s, корни=Chassis(294), Systems(114), последний=/redfish/v1/..."
    // → "Snapshot: /Chassis/Self/PCIeDevices/00_34_04"
    let m = msg.match(/snapshot:\s+документов=\d+[^,]*,.*последний=(\S+)/i);
    if (m) {
        const path = m[1].replace(/^\.\.\./, '').replace(/^\/redfish\/v1/, '') || m[1];
        return `Snapshot: ${path}`;
    }

    // "Redfish snapshot: собрано N документов"
    m = msg.match(/snapshot:\s+собрано\s+(\d+)\s+документов/i);
    if (m) {
        return `Snapshot: итого ${m[1]} документов`;
    }

    // "Redfish: plan-B завершен за 30s (targets=18, recovered=0)"
    m = msg.match(/plan-B завершен за ([^(]+)\(targets=(\d+),\s*recovered=(\d+)\)/i);
    if (m) {
        const recovered = parseInt(m[3], 10);
        const suffix = recovered > 0 ? `, восстановлено ${m[3]}` : '';
        return `Plan-B: завершен за ${m[1].trim()}${suffix}`;
    }

    // "Redfish: prefetch критичных endpoint (адаптивно 9/72)..."
    m = msg.match(/prefetch критичных endpoint[^(]*\(([^)]+)\)/i);
    if (m) {
        return `Prefetch критичных endpoint (${m[1]})`;
    }

    // Strip "Redfish: " / "Redfish snapshot: " prefix — redundant in context
    return msg.replace(/^Redfish(?:\s+snapshot)?:\s+/i, '');
}

function renderCollectionJob() {
    const jobStatusBlock = document.getElementById('api-job-status');
    const jobIdValue = document.getElementById('job-id-value');
    const statusValue = document.getElementById('job-status-value');
    const progressValue = document.getElementById('job-progress-value');
    const etaValue = document.getElementById('job-eta-value');
    const progressBar = document.getElementById('job-progress-bar');
    const activeModulesBlock = document.getElementById('job-active-modules');
    const activeModulesList = document.getElementById('job-active-modules-list');
    const debugInfoBlock = document.getElementById('job-debug-info');
    const debugSummary = document.getElementById('job-debug-summary');
    const phaseTelemetryNode = document.getElementById('job-phase-telemetry');
    const logsList = document.getElementById('job-logs-list');
    const cancelButton = document.getElementById('cancel-job-btn');
    if (!jobStatusBlock || !jobIdValue || !statusValue || !progressValue || !etaValue || !progressBar || !activeModulesBlock || !activeModulesList || !debugInfoBlock || !debugSummary || !phaseTelemetryNode || !logsList || !cancelButton) {
        return;
    }

    if (!collectionJob) {
        jobStatusBlock.classList.add('hidden');
        setApiFormBlocked(false);
        return;
    }

    jobStatusBlock.classList.remove('hidden');
    jobIdValue.textContent = collectionJob.id;
    statusValue.textContent = collectionJob.status;
    statusValue.className = `job-status-badge status-${collectionJob.status.toLowerCase()}`;
    const isTerminal = isCollectionJobTerminal(collectionJob.status);
    const terminalMessage = {
        success: 'Сбор завершен',
        failed: 'Сбор завершился ошибкой',
        canceled: 'Сбор отменен'
    }[collectionJob.status];
    const activity = isTerminal ? terminalMessage : latestCollectionActivityMessage();
    const eta = isTerminal ? '-' : latestCollectionETA();
    const progressPercent = Math.max(0, Math.min(100, Number(collectionJob.progress) || 0));

    progressValue.textContent = activity;
    etaValue.textContent = eta;
    progressBar.style.width = `${progressPercent}%`;
    progressBar.textContent = `${progressPercent}%`;
    renderJobActiveModules(activeModulesBlock, activeModulesList);
    renderJobDebugInfo(debugInfoBlock, debugSummary, phaseTelemetryNode);

    logsList.innerHTML = [...collectionJob.logs].reverse()
        .filter((log) => !log.hidden)
        .map((log) => (
            `<li><span class="log-time">${escapeHtml(log.time)}</span><span class="log-message">${escapeHtml(log.message)}</span></li>`
        )).join('');

    cancelButton.disabled = isTerminal;

    const skipBtn = document.getElementById('skip-hung-btn');
    if (skipBtn) {
        const isCollecting = !isTerminal && collectionJob.status === 'running';
        if (isCollecting) {
            skipBtn.classList.remove('hidden');
        } else {
            skipBtn.classList.add('hidden');
            skipBtn.disabled = false;
            skipBtn.textContent = 'Пропустить зависшие';
        }
    }

    setApiFormBlocked(!isTerminal);
}

function latestCollectionActivityMessage() {
    if (collectionJob && collectionJob.currentPhase) {
        return humanizeCollectionPhase(collectionJob.currentPhase);
    }
    if (!collectionJob || !Array.isArray(collectionJob.logs) || collectionJob.logs.length === 0) {
        return 'Сбор данных...';
    }
    const last = String(collectionJob.logs[collectionJob.logs.length - 1].message || '').trim();
    if (!last) {
        return 'Сбор данных...';
    }
    // Job logs already contain server timestamp prefix. Show concise step text in progress label.
    const cleaned = last.replace(/^\d{4}-\d{2}-\d{2}T[^\s]+\s+/, '').trim();
    if (!cleaned) {
        return 'Сбор данных...';
    }
    return cleaned.replace(/\s*[,(]?\s*ETA[^,;)]*/i, '').trim() || 'Сбор данных...';
}

function latestCollectionETA() {
    if (collectionJob && Number.isFinite(collectionJob.etaSeconds) && collectionJob.etaSeconds > 0) {
        return formatDurationSeconds(collectionJob.etaSeconds);
    }
    if (!collectionJob || !Array.isArray(collectionJob.logs) || collectionJob.logs.length === 0) {
        return '-';
    }
    const last = String(collectionJob.logs[collectionJob.logs.length - 1].message || '').trim();
    const cleaned = last.replace(/^\d{4}-\d{2}-\d{2}T[^\s]+\s+/, '').trim();
    if (!cleaned) {
        return '-';
    }
    const match = cleaned.match(/ETA[^,;)]*/i);
    if (!match) {
        return '-';
    }
    const eta = match[0].replace(/^ETA\s*[:=~≈-]?\s*/i, '').trim();
    return eta || '-';
}

function isCollectionJobTerminal(status) {
    return ['success', 'failed', 'canceled'].includes(normalizeJobStatus(status));
}

function setApiFormBlocked(shouldBlock) {
    const apiForm = document.getElementById('api-connect-form');
    if (!apiForm) {
        return;
    }

    apiForm.classList.toggle('is-disabled', shouldBlock);
    Array.from(apiForm.elements).forEach((field) => {
        field.disabled = shouldBlock;
    });
}

function clearCollectionJobPolling() {
    if (!collectionJobPollTimer) {
        return;
    }

    window.clearInterval(collectionJobPollTimer);
    collectionJobPollTimer = null;
}

function resetCollectionJobState() {
    clearCollectionJobPolling();
    collectionJob = null;
    renderCollectionJob();
}

function syncServerLogs(logs) {
    if (!collectionJob || !Array.isArray(logs)) {
        return;
    }
    if (logs.length <= collectionJob.logs.length) {
        return;
    }

    const from = collectionJob.logs.length;
    for (let i = from; i < logs.length; i += 1) {
        appendJobLog(logs[i]);
    }
}

// Patterns for log lines that are internal debug noise and should not be shown in the UI.
const _collectLogNoisePatterns = [
    /plan-B \(\d+\/\d+/,                          // individual plan-B step lines
    /plan-B топ веток/,
    /snapshot: heartbeat/,
    /snapshot: post-probe коллекций \(/,
    /snapshot: топ веток/,
    /prefetch завершен/,
    /cooldown перед повторным добором/,
    /Redfish telemetry:/,
    /redfish-postprobe-metrics:/,
    /redfish-prefetch-metrics:/,
    /redfish-collect:/,
    /redfish-profile-plan:/,
    /redfish replay:/,
];

function isCollectLogNoise(message) {
    return _collectLogNoisePatterns.some((re) => re.test(message));
}

// Strip the server-side RFC3339Nano timestamp prefix from a log line and return {time, message}.
function parseServerLogLine(raw) {
    const m = String(raw).match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s+(.*)/s);
    if (!m) {
        return { time: null, message: String(raw).trim() };
    }
    const d = new Date(m[1]);
    const time = isNaN(d) ? null : d.toLocaleTimeString('ru-RU', { hour12: false });
    return { time, message: m[2].trim() };
}

function normalizeJobStatus(status) {
    return String(status || '').trim().toLowerCase();
}

function humanizeCollectionPhase(phase) {
    const value = String(phase || '').trim().toLowerCase();
    return {
        discovery: 'Discovery',
        snapshot: 'Snapshot',
        snapshot_postprobe_nvme: 'Snapshot NVMe post-probe',
        snapshot_postprobe_collections: 'Snapshot collection post-probe',
        prefetch: 'Prefetch critical endpoints',
        critical_plan_b: 'Critical plan-B',
        profile_plan_b: 'Profile plan-B'
    }[value] || value || 'Сбор данных...';
}

function formatDurationSeconds(totalSeconds) {
    const seconds = Math.max(0, Math.round(Number(totalSeconds) || 0));
    if (seconds <= 0) {
        return '-';
    }
    const minutes = Math.floor(seconds / 60);
    const remainingSeconds = seconds % 60;
    if (minutes === 0) {
        return `${remainingSeconds}s`;
    }
    if (remainingSeconds === 0) {
        return `${minutes}m`;
    }
    return `${minutes}m ${remainingSeconds}s`;
}

function renderJobActiveModules(activeModulesBlock, activeModulesList) {
    const activeModules = collectionJob && Array.isArray(collectionJob.activeModules) ? collectionJob.activeModules : [];
    if (activeModules.length === 0) {
        activeModulesBlock.classList.add('hidden');
        activeModulesList.innerHTML = '';
        return;
    }

    activeModulesBlock.classList.remove('hidden');
    activeModulesList.innerHTML = activeModules.map((module) => {
        const score = Number.isFinite(module.score) ? module.score : 0;
        return `<span class="job-module-chip" title="${escapeHtml(moduleTitle(module))}">
            <span class="job-module-chip-name">${escapeHtml(module.name || '-')}</span>
            <span class="job-module-chip-score">${escapeHtml(String(score))}</span>
        </span>`;
    }).join('');
}

function renderJobDebugInfo(debugInfoBlock, debugSummary, phaseTelemetryNode) {
    const debug = collectionJob && collectionJob.debugInfo ? collectionJob.debugInfo : null;
    if (!debug) {
        debugInfoBlock.classList.add('hidden');
        debugSummary.innerHTML = '';
        phaseTelemetryNode.innerHTML = '';
        return;
    }

    debugInfoBlock.classList.remove('hidden');
    const throttled = debug.adaptive_throttled ? 'on' : 'off';
    const prefetchEnabled = typeof debug.prefetch_enabled === 'boolean' ? String(debug.prefetch_enabled) : 'auto';
    debugSummary.innerHTML = `adaptive_throttling=<strong>${escapeHtml(throttled)}</strong>, snapshot_workers=<strong>${escapeHtml(String(debug.snapshot_workers || 0))}</strong>, prefetch_workers=<strong>${escapeHtml(String(debug.prefetch_workers || 0))}</strong>, prefetch_enabled=<strong>${escapeHtml(prefetchEnabled)}</strong>`;

    const phases = Array.isArray(debug.phase_telemetry) ? debug.phase_telemetry : [];
    if (phases.length === 0) {
        phaseTelemetryNode.innerHTML = '';
        return;
    }
    phaseTelemetryNode.innerHTML = phases.map((item) => (
        `<div class="job-phase-row">
            <span class="job-phase-name">${escapeHtml(humanizeCollectionPhase(item.phase || ''))}</span>
            <span class="job-phase-metric">req=${escapeHtml(String(item.requests || 0))}</span>
            <span class="job-phase-metric">err=${escapeHtml(String(item.errors || 0))}</span>
            <span class="job-phase-metric">avg=${escapeHtml(String(item.avg_ms || 0))}ms</span>
            <span class="job-phase-metric">p95=${escapeHtml(String(item.p95_ms || 0))}ms</span>
        </div>`
    )).join('');
}

function moduleTitle(activeModule) {
    const name = String(activeModule && activeModule.name || '').trim();
    const scores = collectionJob && Array.isArray(collectionJob.moduleScores) ? collectionJob.moduleScores : [];
    const full = scores.find((item) => String(item && item.name || '').trim() === name);
    if (!full) {
        return name;
    }
    const state = full.active ? 'active' : 'inactive';
    return `${name}: score=${Number.isFinite(full.score) ? full.score : 0}, priority=${Number.isFinite(full.priority) ? full.priority : 0}, ${state}`;
}

async function loadDataFromStatus() {
    try {
        const response = await fetch('/api/status');
        const payload = await response.json();
        if (!payload.loaded) {
            return;
        }
        const vendor = payload.vendor || payload.protocol || '';
        const filename = payload.filename || (payload.protocol && payload.target_host
            ? `${payload.protocol}://${payload.target_host}`
            : '');
        await loadData(vendor, filename);
    } catch (err) {
        console.error('Failed to load data after collect:', err);
    }
}

function applyRedfishDefaultPort() {
    const apiForm = document.getElementById('api-connect-form');
    if (!apiForm) {
        return;
    }

    const portField = apiForm.elements.namedItem('port');
    if (!portField || typeof portField.value !== 'string') {
        return;
    }

    const currentValue = portField.value.trim();
    if (apiPortTouchedByUser && currentValue !== '') {
        return;
    }

    isAutoUpdatingApiPort = true;
    portField.value = '443';
    isAutoUpdatingApiPort = false;
}

function handleApiPortInput(value) {
    if (isAutoUpdatingApiPort) {
        return;
    }
    apiPortTouchedByUser = value.trim() !== '';
}

function getApiValue(fieldName) {
    const apiForm = document.getElementById('api-connect-form');
    if (!apiForm) {
        return '';
    }

    const field = apiForm.elements.namedItem(fieldName);
    if (!field || typeof field.value !== 'string') {
        return '';
    }
    return field.value.trim();
}

// Load and display available parsers
async function loadParsersInfo() {
    try {
        const response = await fetch('/api/parsers');
        const data = await response.json();
        const container = document.getElementById('parsers-info');

        if (data.parsers && data.parsers.length > 0) {
            let html = '<p class="parsers-title">Подключенные парсеры:</p><div class="parsers-list">';
            data.parsers.forEach(p => {
                html += `<span class="parser-chip">
                    <span class="parser-chip-name">${escapeHtml(p.name)}</span>
                    <span class="parser-chip-version">v${escapeHtml(p.version)}</span>
                </span>`;
            });
            html += '</div>';
            container.innerHTML = html;
        }
    } catch (err) {
        console.error('Failed to load parsers info:', err);
    }
}

// Upload handling
function initUpload() {
    const dropZone = document.getElementById('drop-zone');
    const fileInput = document.getElementById('file-input');

    dropZone.addEventListener('dragover', (e) => {
        e.preventDefault();
        dropZone.classList.add('dragover');
    });

    dropZone.addEventListener('dragleave', () => {
        dropZone.classList.remove('dragover');
    });

    dropZone.addEventListener('drop', (e) => {
        e.preventDefault();
        dropZone.classList.remove('dragover');
        const files = e.dataTransfer.files;
        if (files.length > 0) {
            uploadFile(files[0]);
        }
    });

    fileInput.addEventListener('change', () => {
        if (fileInput.files.length > 0) {
            uploadFile(fileInput.files[0]);
        }
    });
}

async function uploadFile(file) {
    const status = document.getElementById('upload-status');
    status.textContent = 'Загрузка и анализ...';
    status.className = '';

    const formData = new FormData();
    formData.append('archive', file);

    try {
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData
        });

        const result = await response.json();

        if (response.ok) {
            status.innerHTML = `<strong>${escapeHtml(result.vendor)}</strong><br>` +
                `${result.stats.sensors} сенсоров, ${result.stats.fru} компонентов, ${result.stats.events} событий`;
            status.className = 'success';
            loadData(result.vendor, result.filename);
        } else {
            status.textContent = result.error || 'Ошибка загрузки';
            status.className = 'error';
        }
    } catch (err) {
        status.textContent = 'Ошибка соединения';
        status.className = 'error';
    }
}

function initConvertMode() {
    const folderInput = document.getElementById('convert-folder-input');
    const runButton = document.getElementById('convert-run-btn');
    if (!folderInput || !runButton) {
        return;
    }

    folderInput.addEventListener('change', async () => {
        const raw = Array.from(folderInput.files || []).filter(file => file && file.name);
        const summary = document.getElementById('convert-folder-summary');
        if (summary) {
            summary.textContent = 'Проверка дубликатов…';
            summary.className = 'api-connect-status';
        }
        const { unique, duplicates } = await deduplicateConvertFiles(raw);
        convertFiles = unique;
        convertDuplicates = duplicates;
        renderConvertSummary();
    });

    runButton.addEventListener('click', async () => {
        await runConvertBatch();
    });
    renderConvertSummary();
}

function renderConvertSummary() {
    const summary = document.getElementById('convert-folder-summary');
    if (!summary) {
        return;
    }

    if (convertFiles.length === 0) {
        summary.textContent = 'Выберите папку с файлами, включая вложенные каталоги.';
        summary.className = 'api-connect-status';
        return;
    }

    const selectedFiles = convertFiles.filter(file => file && file.name);
    const supportedFiles = selectedFiles.filter(file => isSupportedConvertFileName(file.webkitRelativePath || file.name));
    const skippedCount = selectedFiles.length - supportedFiles.length;
    const previewCount = 5;
    const previewFiles = supportedFiles.slice(0, previewCount).map(file => escapeHtml(file.webkitRelativePath || file.name));
    const remaining = supportedFiles.length - previewFiles.length;
    const previewText = previewFiles.length > 0 ? `Примеры: ${previewFiles.join(', ')}` : '';
    const skippedText = skippedCount > 0 ? ` Пропущено неподдерживаемых: ${skippedCount}.` : '';
    const batchCount = Math.ceil(supportedFiles.length / CONVERT_MAX_FILES_PER_BATCH);
    const batchesText = batchCount > 1 ? ` Будет ${batchCount} прохода(ов) по ${CONVERT_MAX_FILES_PER_BATCH} файлов.` : '';

    let dupText = '';
    if (convertDuplicates.length > 0) {
        const names = convertDuplicates.map(d => escapeHtml(d.name)).join(', ');
        const reasons = convertDuplicates.map(d => d.reason === 'hash' ? 'одинаковое содержимое' : 'одинаковое имя');
        const uniqueReasons = [...new Set(reasons)].join(', ');
        dupText = ` <span style="color:#c0392b">⚠ Пропущено дубликатов: ${convertDuplicates.length} (${uniqueReasons}): ${names}.</span>`;
    }
    summary.innerHTML = `<strong>${supportedFiles.length}</strong> файлов готовы к конвертации.${previewText ? ` ${previewText}` : ''}${remaining > 0 ? ` и ещё ${remaining}` : ''}.${skippedText}${batchesText}${dupText}`;
    summary.className = 'api-connect-status';
}

async function runConvertBatch() {
    const runButton = document.getElementById('convert-run-btn');
    if (!runButton || isConvertRunning) {
        return;
    }
    if (convertFiles.length === 0) {
        renderConvertStatus('Нет файлов для конвертации', 'error');
        return;
    }

    const selectedFiles = convertFiles.filter(file => file && file.name);
    const supportedFiles = selectedFiles.filter(file => isSupportedConvertFileName(file.webkitRelativePath || file.name));
    if (supportedFiles.length === 0) {
        renderConvertStatus('В выбранной папке нет файлов поддерживаемого типа', 'error');
        return;
    }
    const batches = chunkFiles(supportedFiles, CONVERT_MAX_FILES_PER_BATCH);

    isConvertRunning = true;
    runButton.disabled = true;
    renderConvertProgress(0, 'Подготовка загрузки...');
    renderConvertStatus(`Выполняю пакетную конвертацию (${batches.length} проходов)...`, 'info');

    try {
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const passSummaries = [];

        for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
            const batchFiles = batches[batchIdx];
            const pass = batchIdx + 1;
            const passLabel = `Проход ${pass}/${batches.length}`;
            const passStart = Math.round((batchIdx / batches.length) * 100);
            const passEnd = Math.round(((batchIdx + 1) / batches.length) * 100);

            const formData = new FormData();
            batchFiles.forEach(file => {
                const relativePath = file.webkitRelativePath || file.name || 'file';
                formData.append('files[]', file, relativePath);
            });

            const startResponse = await uploadConvertBatch(formData, (percent) => {
                const clamped = Math.max(0, Math.min(100, Number(percent) || 0));
                const uploadPhase = passStart + Math.round((passEnd - passStart) * 0.3 * (clamped / 100));
                renderConvertProgress(uploadPhase, `${passLabel}: загрузка ${clamped}%`);
            });

            if (!startResponse.ok) {
                const errorPayload = parseConvertErrorPayload(startResponse.bodyText);
                hideConvertProgress();
                renderConvertStatus(`${passLabel}: ${errorPayload.error || 'пакетная конвертация завершилась с ошибкой'}`, 'error');
                return;
            }

            if (!startResponse.jobId) {
                hideConvertProgress();
                renderConvertStatus(`${passLabel}: сервер не вернул идентификатор задачи`, 'error');
                return;
            }

            await waitForConvertJob(startResponse.jobId, (statusPayload) => {
                const serverProgress = Math.max(0, Math.min(100, Number(statusPayload.progress || 0)));
                const phase = 0.3 + 0.7 * (serverProgress / 100);
                const combined = passStart + Math.round((passEnd - passStart) * phase);
                renderConvertProgress(combined, `${passLabel}: конвертация ${serverProgress}%`);
            });

            const downloadResponse = await downloadConvertArchive(startResponse.jobId);
            if (!downloadResponse.ok) {
                const errorPayload = parseConvertErrorPayload(downloadResponse.bodyText);
                hideConvertProgress();
                renderConvertStatus(`${passLabel}: ${errorPayload.error || 'не удалось скачать результат'}`, 'error');
                return;
            }

            const suffix = batches.length > 1 ? `-part${pass}` : '';
            downloadBlob(downloadResponse.blob, `logpile-convert-${timestamp}${suffix}.zip`);
            passSummaries.push(downloadResponse.summaryHeader || `${passLabel}: завершено`);
        }

        hideConvertProgress();
        renderConvertStatus(passSummaries.join(' | '), 'success');
    } catch (err) {
        hideConvertProgress();
        renderConvertStatus('Ошибка соединения при конвертации', 'error');
    } finally {
        isConvertRunning = false;
        runButton.disabled = false;
    }
}

function chunkFiles(files, chunkSize) {
    const safeChunkSize = Math.max(1, Number(chunkSize) || 1);
    const chunks = [];
    for (let i = 0; i < files.length; i += safeChunkSize) {
        chunks.push(files.slice(i, i + safeChunkSize));
    }
    return chunks;
}

function uploadConvertBatch(formData, onUploadPercent) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/api/convert');
        xhr.responseType = 'text';

        xhr.upload.addEventListener('progress', (event) => {
            if (!event.lengthComputable) {
                return;
            }
            const percent = Math.max(0, Math.min(100, Math.round((event.loaded / event.total) * 100)));
            onUploadPercent(percent);
        });

        xhr.addEventListener('load', () => {
            if (xhr.status >= 200 && xhr.status < 300) {
                let body = {};
                try {
                    body = JSON.parse(xhr.responseText || '{}');
                } catch (err) {
                    body = {};
                }
                resolve({
                    ok: true,
                    status: xhr.status,
                    jobId: body.job_id || ''
                });
                return;
            }
            resolve({
                ok: false,
                status: xhr.status,
                bodyText: xhr.responseText || ''
            });
        });

        xhr.addEventListener('error', () => {
            reject(new Error('network'));
        });

        xhr.send(formData);
    });
}

async function waitForConvertJob(jobId, onProgress) {
    while (true) {
        const response = await fetch(`/api/convert/${encodeURIComponent(jobId)}`);
        const payload = await response.json().catch(() => ({}));
        if (!response.ok) {
            throw new Error(payload.error || 'Не удалось получить статус конвертации');
        }

        if (onProgress) {
            onProgress(payload);
        }

        const status = String(payload.status || '').toLowerCase();
        if (status === 'success') {
            return payload;
        }
        if (status === 'failed' || status === 'canceled') {
            throw new Error(payload.error || 'Конвертация завершилась ошибкой');
        }

        await delay(900);
    }
}

async function downloadConvertArchive(jobId) {
    const response = await fetch(`/api/convert/${encodeURIComponent(jobId)}/download`);
    if (!response.ok) {
        return {
            ok: false,
            bodyText: await response.text().catch(() => '')
        };
    }
    return {
        ok: true,
        blob: await response.blob(),
        summaryHeader: response.headers.get('X-Convert-Summary') || ''
    };
}

function delay(ms) {
    return new Promise((resolve) => {
        window.setTimeout(resolve, ms);
    });
}

function parseConvertErrorPayload(bodyText) {
    if (!bodyText) {
        return {};
    }
    try {
        return JSON.parse(bodyText);
    } catch (err) {
        return {};
    }
}

async function deduplicateConvertFiles(files) {
    // First pass: deduplicate by basename
    const seenNames = new Map(); // name -> index in unique
    const unique = [];
    const duplicates = [];
    for (const file of files) {
        if (seenNames.has(file.name)) {
            duplicates.push({ name: file.webkitRelativePath || file.name, reason: 'name' });
        } else {
            seenNames.set(file.name, unique.length);
            unique.push(file);
        }
    }
    // Second pass: deduplicate by SHA-256 hash
    const seenHashes = new Map(); // hash -> file.name
    const hashUnique = [];
    for (const file of unique) {
        try {
            const buf = await file.arrayBuffer();
            const hashBuf = await crypto.subtle.digest('SHA-256', buf);
            const hash = Array.from(new Uint8Array(hashBuf)).map(b => b.toString(16).padStart(2, '0')).join('');
            if (seenHashes.has(hash)) {
                duplicates.push({ name: file.webkitRelativePath || file.name, reason: 'hash' });
            } else {
                seenHashes.set(hash, file.name);
                hashUnique.push(file);
            }
        } catch (_) {
            hashUnique.push(file);
        }
    }
    return { unique: hashUnique, duplicates };
}

function isSupportedConvertFileName(filename) {
    const name = String(filename || '').trim().toLowerCase();
    if (!name) {
        return false;
    }
    if (Array.isArray(supportedConvertExtensions) && supportedConvertExtensions.length > 0) {
        return supportedConvertExtensions.some(ext => name.endsWith(ext));
    }
    return true;
}

async function loadSupportedFileTypes() {
    try {
        const response = await fetch('/api/file-types');
        const payload = await response.json();
        if (!response.ok) {
            return;
        }
        if (Array.isArray(payload.upload_extensions)) {
            supportedUploadExtensions = payload.upload_extensions
                .map(ext => String(ext || '').trim().toLowerCase())
                .filter(Boolean);
        }
        if (Array.isArray(payload.convert_extensions)) {
            supportedConvertExtensions = payload.convert_extensions
                .map(ext => String(ext || '').trim().toLowerCase())
                .filter(Boolean);
        }
        applyUploadAcceptExtensions();
        renderConvertSummary();
    } catch (err) {
        // Keep permissive fallback if endpoint is temporarily unavailable.
    }
}

function applyUploadAcceptExtensions() {
    const fileInput = document.getElementById('file-input');
    if (!fileInput || !Array.isArray(supportedUploadExtensions) || supportedUploadExtensions.length === 0) {
        return;
    }
    fileInput.setAttribute('accept', supportedUploadExtensions.join(','));
}

function renderConvertStatus(message, status) {
    const statusNode = document.getElementById('convert-status');
    if (!statusNode) {
        return;
    }

    statusNode.textContent = message || '';
    statusNode.className = 'api-connect-status';
    if (status === 'success') {
        statusNode.classList.add('success');
    } else if (status === 'error') {
        statusNode.classList.add('error');
    } else if (status === 'info') {
        statusNode.classList.add('info');
    }
}

function renderConvertProgress(percent, label) {
    const wrap = document.getElementById('convert-progress');
    const bar = document.getElementById('convert-progress-bar');
    const value = document.getElementById('convert-progress-value');
    const text = document.getElementById('convert-progress-label');
    if (!wrap || !bar || !value || !text) {
        return;
    }

    const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
    wrap.classList.remove('hidden');
    bar.style.width = `${safePercent}%`;
    value.textContent = `${safePercent}%`;
    text.textContent = label || 'Выполняется...';
}

function hideConvertProgress() {
    const wrap = document.getElementById('convert-progress');
    if (!wrap) {
        return;
    }
    wrap.classList.add('hidden');
}

function downloadBlob(blob, filename) {
    const url = URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.style.display = 'none';
    link.href = url;
    link.download = filename;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
    window.setTimeout(() => {
        URL.revokeObjectURL(url);
    }, 3000);
}

// Tab navigation
function initTabs() {
    const tabs = document.querySelectorAll('.tab');
    tabs.forEach(tab => {
        tab.addEventListener('click', () => {
            tabs.forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
            tab.classList.add('active');
            document.getElementById(tab.dataset.tab).classList.add('active');
        });
    });
}

// Filters
function initFilters() {
    document.getElementById('sensor-filter').addEventListener('change', (e) => {
        filterSensors(e.target.value);
    });
    document.getElementById('severity-filter').addEventListener('change', (e) => {
        filterEvents(e.target.value);
    });
    document.getElementById('serial-filter').addEventListener('change', (e) => {
        filterSerials(e.target.value);
    });
}

let allSensors = [];
let allEvents = [];
let allSerials = [];
let allParseErrors = [];

let currentVendor = '';
let auditViewerNonce = 0;

// Load data from API
async function loadData(vendor, filename) {
    currentVendor = vendor || '';
    document.getElementById('upload-section').classList.add('hidden');
    document.getElementById('data-section').classList.remove('hidden');
    document.getElementById('clear-btn').classList.remove('hidden');
    document.getElementById('header-raw-btn').classList.remove('hidden');
    document.getElementById('header-reanimator-btn').classList.remove('hidden');
    document.getElementById('header-log-meta').classList.remove('hidden');

    // Update vendor badge if exists (legacy support)
    const vendorBadge = document.getElementById('vendor-badge');
    if (vendorBadge && currentVendor) {
        vendorBadge.textContent = currentVendor;
        vendorBadge.classList.remove('hidden');
    }

    loadAuditViewer();
}

function loadAuditViewer() {
    const frame = document.getElementById('audit-viewer-frame');
    if (!frame) {
        return;
    }
    auditViewerNonce += 1;
    frame.style.height = '60vh';
    frame.src = `/chart/current?ts=${auditViewerNonce}`;
}

function resizeAuditViewerFrame() {
    const frame = document.getElementById('audit-viewer-frame');
    if (!frame) {
        return;
    }

    try {
        const doc = frame.contentDocument || (frame.contentWindow && frame.contentWindow.document);
        if (!doc || !doc.documentElement || !doc.body) {
            return;
        }

        const nextHeight = Math.max(
            doc.documentElement.scrollHeight,
            doc.body.scrollHeight,
            640
        );
        frame.style.height = `${nextHeight}px`;
    } catch (err) {
        console.error('Failed to resize audit viewer frame:', err);
    }
}

async function loadConfig() {
    try {
        const response = await fetch('/api/config');
        const config = await response.json();
        renderConfig(config);
    } catch (err) {
        console.error('Failed to load config:', err);
    }
}

function renderConfig(data) {
    const container = document.getElementById('config-content');

    if (!data || Object.keys(data).length === 0) {
        container.innerHTML = '<p class="no-data">Нет данных о конфигурации</p>';
        return;
    }

    const config = data.hardware || data;
    const spec = data.specification;
    const redfishFetchErrors = Array.isArray(data.redfish_fetch_errors) ? data.redfish_fetch_errors : [];
    const devices = Array.isArray(config.devices) ? config.devices : [];
    const volumes = Array.isArray(config.volumes) ? config.volumes : [];

    const cpus = devices.filter(d => d.kind === 'cpu');
    const memory = devices.filter(d => d.kind === 'memory');
    const powerSupplies = devices.filter(d => d.kind === 'psu');
    const storage = devices.filter(d => d.kind === 'storage');
    const gpus = devices.filter(d => d.kind === 'gpu');
    const networkAdapters = devices.filter(d => d.kind === 'network');
    const inventoryRows = devices.filter(d => ['pcie', 'storage', 'gpu', 'network'].includes(d.kind));
    const pcieBalance = calculateCPUToPCIeBalance(inventoryRows, cpus);
    const pcieByCPU = new Map();
    pcieBalance.perCPU.forEach(item => {
        const idx = extractCPUIndex(item.label);
        if (idx !== null) pcieByCPU.set(idx, item.lanes);
    });
    const memoryByCPU = calculateMemoryModulesByCPU(memory);

    let html = '';

    // Server info header
    if (config.board) {
        html += `<div class="server-info">
            <div class="server-info-item"><span class="server-info-label">Модель сервера:</span> <strong>${escapeHtml(config.board.product_name || '-')}</strong></div>
            <div class="server-info-item"><span class="server-info-label">Серийный номер:</span> <code>${escapeHtml(config.board.serial_number || '-')}</code></div>
        </div>`;
    }

    // Configuration sub-tabs
    html += `<div class="config-tabs">
        <button class="config-tab active" data-config-tab="spec">Спецификация</button>
        <button class="config-tab" data-config-tab="cpu">CPU</button>
        <button class="config-tab" data-config-tab="memory">Memory</button>
        <button class="config-tab" data-config-tab="power">Power</button>
        <button class="config-tab" data-config-tab="storage">Hard Drive</button>
        <button class="config-tab" data-config-tab="gpu">GPU</button>
        <button class="config-tab" data-config-tab="network">Network</button>
        <button class="config-tab" data-config-tab="pcie">Device Inventory</button>
    </div>`;

    // Specification tab
    html += '<div class="config-tab-content active" id="config-spec">';
    const partialInventory = detectPartialRedfishInventory({
        cpus,
        memory,
        redfishFetchErrors
    });
    if (partialInventory) {
        html += `<div class="spec-section">
            <h3>Частичный инвентарь</h3>
            <p class="no-data" style="margin-top: 0;">${escapeHtml(partialInventory)}</p>
        </div>`;
    }
    if (spec && spec.length > 0) {
        html += '<div class="spec-section"><h3>Спецификация сервера</h3><ul class="spec-list">';
        spec.forEach(item => {
            html += `<li><span class="spec-category">${escapeHtml(item.category)}</span> ${escapeHtml(item.name)} <span class="spec-qty">- ${item.quantity} шт.</span></li>`;
        });
        html += '</ul></div>';
    } else {
        html += '<p class="no-data">Нет данных о спецификации</p>';
    }
    html += '</div>';

    // CPU tab
    html += '<div class="config-tab-content" id="config-cpu">';
    if (cpus.length > 0) {
        const cpuCount = cpus.length;
        const cpuModel = cpus[0].model || '-';
        const totalCores = cpus.reduce((sum, c) => sum + (c.cores || 0), 0);
        const totalThreads = cpus.reduce((sum, c) => sum + (c.threads || 0), 0);
        const balanceClass = pcieBalance.severity === 'critical'
            ? 'pcie-balance-critical'
            : (pcieBalance.severity === 'warning' ? 'pcie-balance-warning' : 'pcie-balance-ok');
        const balanceLabel = pcieBalance.severity === 'critical'
            ? 'Перевес высокий'
            : (pcieBalance.severity === 'warning' ? 'Есть перевес' : 'Распределено ровно');
        html += `<h3>Процессоры</h3>
            <div class="section-overview">
                <div class="stat-box"><span class="stat-value">${cpuCount}</span><span class="stat-label">Процессоров</span></div>
                <div class="stat-box"><span class="stat-value">${totalCores}</span><span class="stat-label">Ядер</span></div>
                <div class="stat-box"><span class="stat-value">${totalThreads}</span><span class="stat-label">Потоков</span></div>
                <div class="stat-box"><span class="stat-value">${pcieBalance.totalLanes}</span><span class="stat-label">Занято PCIe линий</span></div>
                <div class="stat-box ${balanceClass}"><span class="stat-value">${balanceLabel}</span><span class="stat-label">Баланс PCIe</span></div>
                <div class="stat-box model-box"><span class="stat-value">${escapeHtml(cpuModel)}</span><span class="stat-label">Модель</span></div>
            </div>
            <div class="pcie-balance-bars">`;
        pcieBalance.perCPU.forEach(cpu => {
            html += `<div class="pcie-balance-row">
                <span class="pcie-balance-cpu">${escapeHtml(cpu.label)}</span>
                <div class="pcie-balance-track"><div class="pcie-balance-fill ${balanceClass}" style="width:${cpu.percent}%"></div></div>
                <span class="pcie-balance-value">${cpu.lanes}</span>
            </div>`;
        });
        html += `</div>
            <table class="config-table"><thead><tr><th>Socket</th><th>Модель</th><th>Ядра</th><th>Потоки</th><th>Частота</th><th>Max Turbo</th><th>TDP</th><th>L3 Cache</th><th>PCIe линии (занято)</th><th>Модулей памяти</th><th>PPIN</th></tr></thead><tbody>`;
        cpus.forEach(cpu => {
            const socket = cpu.slot || '-';
            const cpuIdx = extractCPUIndex(socket);
            const pcieUsed = cpuIdx !== null ? (pcieByCPU.get(cpuIdx) || 0) : '-';
            const memoryModules = cpuIdx !== null ? (memoryByCPU.get(cpuIdx) || 0) : '-';
            const tdp = (cpu.details && cpu.details.tdp_w) || '-';
            const l3 = (cpu.details && cpu.details.l3_cache_kb) ? Math.round(cpu.details.l3_cache_kb / 1024) : '-';
            const ppin = (cpu.details && cpu.details.ppin) || '-';
            html += `<tr>
                <td>${escapeHtml(socket)}</td>
                <td>${escapeHtml(cpu.model || '-')}</td>
                <td>${cpu.cores || '-'}</td>
                <td>${cpu.threads || '-'}</td>
                <td>${cpu.frequency_mhz ? cpu.frequency_mhz + ' MHz' : '-'}</td>
                <td>${cpu.max_frequency_mhz ? cpu.max_frequency_mhz + ' MHz' : '-'}</td>
                <td>${tdp !== '-' ? tdp + 'W' : '-'}</td>
                <td>${l3 !== '-' ? l3 + ' MB' : '-'}</td>
                <td>${pcieUsed}</td>
                <td>${memoryModules}</td>
                <td><code>${escapeHtml(ppin)}</code></td>
            </tr>`;
        });
        html += '</tbody></table>';
    } else {
        html += '<p class="no-data">Нет данных о процессорах</p>';
    }
    html += '</div>';

    // Memory tab
    html += '<div class="config-tab-content" id="config-memory">';
    if (memory.length > 0) {
        const totalGB = memory.reduce((sum, m) => sum + (m.size_mb || 0), 0) / 1024;
        const presentCount = memory.filter(m => m.present !== false).length;
        const workingCount = memory.filter(m => (m.size_mb || 0) > 0).length;
        html += `<h3>Модули памяти</h3>
            <div class="memory-overview">
                <div class="stat-box"><span class="stat-value">${totalGB} GB</span><span class="stat-label">Всего</span></div>
                <div class="stat-box"><span class="stat-value">${presentCount}</span><span class="stat-label">Установлено</span></div>
                <div class="stat-box"><span class="stat-value">${workingCount}</span><span class="stat-label">Активно</span></div>
            </div>
            <table class="config-table memory-table"><thead><tr>
                <th>Location</th><th>Наличие</th><th>Размер</th><th>Тип</th><th>Max частота</th><th>Текущая частота</th><th>Производитель</th><th>Артикул</th><th>Серийный номер</th><th>Статус</th>
            </tr></thead><tbody>`;
        memory.forEach(mem => {
            const present = mem.present !== false ? '✓' : '-';
            const presentClass = mem.present !== false ? 'present-yes' : 'present-no';
            const sizeGB = (mem.size_mb || 0) / 1024;
            const statusClass = (mem.status === 'OK' || !mem.status) ? '' : 'status-warning';
            const rowClass = sizeGB === 0 ? 'row-warning' : '';
            html += `<tr class="${rowClass}">
                <td>${escapeHtml(mem.location || mem.slot)}</td>
                <td class="${presentClass}">${present}</td>
                <td>${sizeGB} GB</td>
                <td>${escapeHtml(mem.type || '-')}</td>
                <td>${(mem.details && mem.details.max_speed_mhz) || '-'} MHz</td>
                <td>${(mem.details && mem.details.current_speed_mhz) || mem.speed_mhz || '-'} MHz</td>
                <td>${escapeHtml(mem.manufacturer || '-')}</td>
                <td><code>${escapeHtml(mem.part_number || '-')}</code></td>
                <td><code>${escapeHtml(mem.serial_number || '-')}</code></td>
                <td class="${statusClass}">${escapeHtml(mem.status || 'OK')}</td>
            </tr>`;
        });
        html += '</tbody></table>';
    } else {
        html += '<p class="no-data">Нет данных о памяти</p>';
    }
    html += '</div>';

    // Power tab
    html += '<div class="config-tab-content" id="config-power">';
    if (powerSupplies.length > 0) {
        const psuTotal = powerSupplies.length;
        const psuPresent = powerSupplies.filter(p => p.present !== false).length;
        const psuOK = powerSupplies.filter(p => p.status === 'OK').length;
        const psuModel = powerSupplies[0].model || '-';
        const psuWattage = powerSupplies[0].wattage_w || 0;
        const psuCurrentPowerW = powerSupplies.reduce((sum, psu) => {
            if (Number.isFinite(psu.output_power_w) && psu.output_power_w > 0) {
                return sum + psu.output_power_w;
            }
            if (Number.isFinite(psu.input_power_w) && psu.input_power_w > 0) {
                return sum + psu.input_power_w;
            }
            return sum;
        }, 0);
        const psuCurrentPowerLabel = psuCurrentPowerW > 0 ? `${psuCurrentPowerW}W` : '-';
        html += `<h3>Блоки питания</h3>
            <div class="section-overview">
                <div class="stat-box"><span class="stat-value">${psuTotal}</span><span class="stat-label">Всего</span></div>
                <div class="stat-box"><span class="stat-value">${psuPresent}</span><span class="stat-label">Подключено</span></div>
                <div class="stat-box"><span class="stat-value">${psuOK}</span><span class="stat-label">Работает</span></div>
                <div class="stat-box"><span class="stat-value">${psuWattage}W</span><span class="stat-label">Мощность</span></div>
                <div class="stat-box"><span class="stat-value">${psuCurrentPowerLabel}</span><span class="stat-label">Текущая суммарная</span></div>
                <div class="stat-box model-box"><span class="stat-value">${escapeHtml(psuModel)}</span><span class="stat-label">Модель</span></div>
            </div>
            <table class="config-table"><thead><tr><th>Слот</th><th>Производитель</th><th>Модель</th><th>Мощность</th><th>Вход</th><th>Выход</th><th>Напряжение</th><th>Температура</th><th>Статус</th></tr></thead><tbody>`;
        powerSupplies.forEach(psu => {
            const statusClass = psu.status === 'OK' ? '' : 'status-warning';
            html += `<tr>
                <td>${escapeHtml(psu.slot)}</td>
                <td>${escapeHtml(psu.manufacturer || psu.vendor || '-')}</td>
                <td>${escapeHtml(psu.model || '-')}</td>
                <td>${psu.wattage_w || '-'}W</td>
                <td>${psu.input_power_w || '-'}W</td>
                <td>${psu.output_power_w || '-'}W</td>
                <td>${psu.input_voltage ? psu.input_voltage.toFixed(0) : '-'}V</td>
                <td>${psu.temperature_c || '-'}°C</td>
                <td class="${statusClass}">${escapeHtml(psu.status || '-')}</td>
            </tr>`;
        });
        html += '</tbody></table>';
    } else {
        html += '<p class="no-data">Нет данных о блоках питания</p>';
    }
    html += '</div>';

    // Storage tab
    html += '<div class="config-tab-content" id="config-storage">';
    if (storage.length > 0 || volumes.length > 0) {
        const storTotal = storage.length;
        const storHDD = storage.filter(s => s.type === 'HDD').length;
        const storSSD = storage.filter(s => s.type === 'SSD').length;
        const storNVMe = storage.filter(s => s.type === 'NVMe').length;
        const totalTB = (storage.reduce((sum, s) => sum + (s.size_gb || 0), 0) / 1000).toFixed(1);
        let typesSummary = [];
        if (storHDD > 0) typesSummary.push(`${storHDD} HDD`);
        if (storSSD > 0) typesSummary.push(`${storSSD} SSD`);
        if (storNVMe > 0) typesSummary.push(`${storNVMe} NVMe`);
        html += `<h3>Накопители</h3>
            <div class="section-overview">
                <div class="stat-box"><span class="stat-value">${storTotal}</span><span class="stat-label">Всего слотов</span></div>
                <div class="stat-box"><span class="stat-value">${storage.filter(s => s.present).length}</span><span class="stat-label">Установлено</span></div>
                <div class="stat-box"><span class="stat-value">${totalTB > 0 ? totalTB + ' TB' : '-'}</span><span class="stat-label">Объём</span></div>
                <div class="stat-box"><span class="stat-value">${volumes.length}</span><span class="stat-label">Логических томов</span></div>
                <div class="stat-box model-box"><span class="stat-value">${typesSummary.join(', ') || '-'}</span><span class="stat-label">По типам</span></div>
            </div>
            <table class="config-table"><thead><tr><th>NO.</th><th>Статус</th><th>Расположение</th><th>Backplane ID</th><th>Тип</th><th>Модель</th><th>Размер</th><th>Серийный номер</th></tr></thead><tbody>`;
        storage.forEach(s => {
            const presentIcon = s.present ? '<span style="color: #27ae60;">●</span>' : '<span style="color: #95a5a6;">○</span>';
            const presentText = s.present ? 'Present' : 'Empty';
            html += `<tr>
                <td>${escapeHtml(s.slot || '-')}</td>
                <td>${presentIcon} ${presentText}</td>
                <td>${escapeHtml(s.location || '-')}</td>
                <td>${s.details && s.details.backplane_id !== undefined ? s.details.backplane_id : '-'}</td>
                <td>${escapeHtml(s.type || '-')}</td>
                <td>${escapeHtml(s.model || '-')}</td>
                <td>${s.size_gb > 0 ? s.size_gb + ' GB' : '-'}</td>
                <td>${s.serial_number ? '<code>' + escapeHtml(s.serial_number) + '</code>' : '-'}</td>
            </tr>`;
        });
        html += '</tbody></table>';
        if (volumes.length > 0) {
            html += `<h3 style="margin-top:16px;">Логические тома (RAID/VROC)</h3>
                <table class="config-table"><thead><tr><th>ID</th><th>Имя</th><th>Контроллер</th><th>RAID</th><th>Размер</th><th>Статус</th></tr></thead><tbody>`;
            volumes.forEach(v => {
                html += `<tr>
                    <td>${escapeHtml(v.id || '-')}</td>
                    <td>${escapeHtml(v.name || '-')}</td>
                    <td>${escapeHtml(v.controller || '-')}</td>
                    <td>${escapeHtml(v.raid_level || '-')}</td>
                    <td>${v.size_gb > 0 ? `${v.size_gb} GB` : '-'}</td>
                    <td>${escapeHtml(v.status || '-')}</td>
                </tr>`;
            });
            html += '</tbody></table>';
        }
    } else {
        html += '<p class="no-data">Нет данных о накопителях</p>';
    }
    html += '</div>';

    // GPU tab
    html += '<div class="config-tab-content" id="config-gpu">';
    const gpuRows = gpus;
    if (gpuRows.length > 0) {
        const gpuCount = gpuRows.length;
        const gpuModel = gpuRows[0].model || '-';
        const gpuVendor = gpuRows[0].manufacturer || '-';
        html += `<h3>Графические процессоры</h3>
            <div class="section-overview">
                <div class="stat-box"><span class="stat-value">${gpuCount}</span><span class="stat-label">Всего GPU</span></div>
                <div class="stat-box"><span class="stat-value">${escapeHtml(gpuVendor)}</span><span class="stat-label">Производитель</span></div>
                <div class="stat-box model-box"><span class="stat-value">${escapeHtml(gpuModel)}</span><span class="stat-label">Модель</span></div>
            </div>
            <table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>BDF</th><th>PCIe</th><th>Серийный номер</th></tr></thead><tbody>`;
        gpuRows.forEach(gpu => {
            const pcieLink = formatPCIeLink(
                gpu.current_link_width || gpu.link_width,
                gpu.current_link_speed || gpu.link_speed,
                gpu.max_link_width,
                gpu.max_link_speed
            );
            html += `<tr>
                <td>${escapeHtml(gpu.slot || '-')}</td>
                <td>${escapeHtml(gpu.model || '-')}</td>
                <td>${escapeHtml(gpu.manufacturer || '-')}</td>
                <td><code>${escapeHtml(gpu.bdf || '-')}</code></td>
                <td>${pcieLink}</td>
                <td><code>${escapeHtml(gpu.serial_number || '-')}</code></td>
            </tr>`;
        });
        html += '</tbody></table>';
    } else {
        html += '<p class="no-data">Нет GPU</p>';
    }
    html += '</div>';

    // Network tab
    html += '<div class="config-tab-content" id="config-network">';
    const networkRows = networkAdapters;
    const normalizeNetworkPortCount = (value) => {
        const num = Number(value);
        if (!Number.isFinite(num) || num <= 0 || num > 256) {
            return null;
        }
        return Math.trunc(num);
    };
    if (networkRows.length > 0) {
        const nicCount = networkRows.length;
        const totalPorts = networkRows.reduce((sum, n) => sum + (normalizeNetworkPortCount(n.port_count) || 0), 0);
        const nicTypes = [...new Set(networkRows.map(n => n.port_type).filter(t => t))];
        const nicModels = [...new Set(networkRows.map(n => n.model).filter(m => m))];
        html += `<h3>Сетевые адаптеры</h3>
            <div class="section-overview">
                <div class="stat-box"><span class="stat-value">${nicCount}</span><span class="stat-label">Адаптеров</span></div>
                <div class="stat-box"><span class="stat-value">${totalPorts}</span><span class="stat-label">Портов</span></div>
                <div class="stat-box"><span class="stat-value">${nicTypes.join(', ') || '-'}</span><span class="stat-label">Тип портов</span></div>
                <div class="stat-box model-box"><span class="stat-value">${escapeHtml(nicModels.join(', ') || '-')}</span><span class="stat-label">Модели</span></div>
            </div>
            <table class="config-table"><thead><tr><th>Слот</th><th>Модель</th><th>Производитель</th><th>Порты</th><th>Тип</th><th>MAC адреса</th><th>Статус</th></tr></thead><tbody>`;
        networkRows.forEach(nic => {
            const macs = nic.mac_addresses ? nic.mac_addresses.join(', ') : '-';
            const statusClass = nic.status === 'OK' ? '' : 'status-warning';
            const displayPortCount = normalizeNetworkPortCount(nic.port_count);
            html += `<tr>
                <td>${escapeHtml(nic.location || nic.slot || '-')}</td>
                <td>${escapeHtml(nic.model || '-')}</td>
                <td>${escapeHtml(nic.manufacturer || nic.vendor || '-')}</td>
                <td>${displayPortCount ?? '-'}</td>
                <td>${escapeHtml(nic.port_type || '-')}</td>
                <td><code>${escapeHtml(macs)}</code></td>
                <td class="${statusClass}">${escapeHtml(nic.status || '-')}</td>
            </tr>`;
        });
        html += '</tbody></table>';
    } else {
        html += '<p class="no-data">Нет данных о сетевых адаптерах</p>';
    }
    html += '</div>';

    // PCIe Device Inventory tab
    html += '<div class="config-tab-content" id="config-pcie">';
    if (inventoryRows.length > 0) {
        html += '<h3>PCIe устройства</h3>';
        const groups = new Map();
        inventoryRows.forEach(p => {
            const idx = extractCPUIndex(p.slot);
            const key = idx === null ? 'other' : `cpu${idx}`;
            if (!groups.has(key)) {
                groups.set(key, {
                    idx,
                    title: idx === null ? 'Без привязки к CPU' : `CPU${idx}`,
                    lanes: 0,
                    rows: []
                });
            }
            const lanes = Number(p.link_width) > 0 ? Number(p.link_width) : (Number(p.max_link_width) > 0 ? Number(p.max_link_width) : 0);
            const group = groups.get(key);
            group.lanes += lanes;
            group.rows.push(p);
        });

        const sortedGroups = [...groups.values()].sort((a, b) => {
            if (a.idx === null) return 1;
            if (b.idx === null) return -1;
            return a.idx - b.idx;
        });

        sortedGroups.forEach(group => {
            html += `<h4 class="pcie-group-title">${escapeHtml(group.title)} · занято линий: ${group.lanes}</h4>`;
            html += '<table class="config-table"><thead><tr><th>Слот</th><th>BDF</th><th>Модель</th><th>Производитель</th><th>Vendor:Device ID</th><th>PCIe Link</th><th>Серийный номер</th><th>Прошивка</th></tr></thead><tbody>';
            group.rows.forEach(p => {
                const pcieLink = formatPCIeLink(
                    p.link_width,
                    p.link_speed,
                    p.max_link_width,
                    p.max_link_speed
                );
                const firmware = p.firmware || findPCIeFirmwareVersion(config.firmware, p);
                html += `<tr>
                    <td>${escapeHtml(p.slot || '-')}</td>
                    <td><code>${escapeHtml(p.bdf || '-')}</code></td>
                    <td>${escapeHtml(p.model || p.part_number || p.device_class || '-')}</td>
                    <td>${escapeHtml(p.manufacturer || '-')}</td>
                    <td><code>${p.vendor_id ? p.vendor_id.toString(16) : '-'}:${p.device_id ? p.device_id.toString(16) : '-'}</code></td>
                    <td>${pcieLink}</td>
                    <td><code>${escapeHtml(p.serial_number || '-')}</code></td>
                    <td><code>${escapeHtml(firmware || '-')}</code></td>
                </tr>`;
            });
            html += '</tbody></table>';
        });
    } else {
        html += '<p class="no-data">Нет данных о PCIe устройствах</p>';
    }
    html += '</div>';

    container.innerHTML = html;

    // Initialize config sub-tabs
    initConfigTabs();
}

function initConfigTabs() {
    const tabs = document.querySelectorAll('.config-tab');
    tabs.forEach(tab => {
        tab.addEventListener('click', () => {
            tabs.forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.config-tab-content').forEach(c => c.classList.remove('active'));
            tab.classList.add('active');
            document.getElementById('config-' + tab.dataset.configTab).classList.add('active');
        });
    });
}

async function loadFirmware() {
    try {
        const response = await fetch('/api/firmware');
        const firmware = await response.json();
        renderFirmware(firmware);
    } catch (err) {
        console.error('Failed to load firmware:', err);
    }
}

let allFirmware = [];

function renderFirmware(firmware) {
    allFirmware = firmware || [];

    // Render in Firmware tab
    const tbody = document.querySelector('#firmware-table tbody');
    tbody.innerHTML = '';

    if (!firmware || firmware.length === 0) {
        tbody.innerHTML = '<tr><td colspan="3" class="no-data">Нет данных о прошивках</td></tr>';
    } else {
        firmware.forEach(fw => {
            const row = document.createElement('tr');
            row.innerHTML = `
                <td>${escapeHtml(fw.component)}</td>
                <td>${escapeHtml(fw.model)}</td>
                <td><code>${escapeHtml(fw.version)}</code></td>
            `;
            tbody.appendChild(row);
        });
    }
}

async function loadSensors() {
    try {
        const response = await fetch('/api/sensors');
        allSensors = await response.json();
        renderSensors(allSensors);
    } catch (err) {
        console.error('Failed to load sensors:', err);
    }
}

function renderSensors(sensors) {
    const container = document.getElementById('sensors-content');

    if (!sensors || sensors.length === 0) {
        container.innerHTML = '<p class="no-data">Нет данных о сенсорах</p>';
        return;
    }

    // Group by type
    const byType = {};
    sensors.forEach(s => {
        if (!byType[s.type]) byType[s.type] = [];
        byType[s.type].push(s);
    });

    const typeNames = {
        temperature: 'Температура',
        voltage: 'Напряжение',
        power: 'Мощность',
        fan_speed: 'Вентиляторы',
        fan_status: 'Статус вентиляторов',
        psu_status: 'Статус БП',
        cpu_status: 'Статус CPU',
        storage_status: 'Статус накопителей',
        other: 'Прочее'
    };

    let html = '';
    for (const [type, items] of Object.entries(byType)) {
        html += `<div class="sensor-group" data-type="${type}">
            <h3>${typeNames[type] || type}</h3>
            <div class="sensor-grid">`;

        items.forEach(s => {
            let valueStr = '';
            let statusClass = s.status === 'ok' ? 'ok' : (s.status === 'ns' ? 'ns' : 'warn');
            const sensorName = String(s.name || '').toLowerCase();
            const isPSUVoltage = type === 'voltage' && sensorName.includes('psu') && sensorName.includes('voltage');

            if (Number.isFinite(s.value)) {
                valueStr = `${s.value} ${s.unit}`;
            } else if (s.raw_value) {
                valueStr = s.raw_value;
            } else {
                valueStr = s.status;
            }

            // Server computes PSU voltage range status; UI only reflects it.
            let extraClass = '';
            if (isPSUVoltage && s.status === 'warn') {
                extraClass = ' voltage-out-of-range';
            }

            html += `<div class="sensor-card ${statusClass}${extraClass}">
                <span class="sensor-name">${escapeHtml(s.name)}</span>
                <span class="sensor-value">${escapeHtml(valueStr)}</span>
            </div>`;
        });

        html += '</div></div>';
    }

    container.innerHTML = html;
}

function filterSensors(type) {
    if (!type) {
        renderSensors(allSensors);
        return;
    }
    const filtered = allSensors.filter(s => s.type === type);
    renderSensors(filtered);
}

async function loadSerials() {
    try {
        const response = await fetch('/api/serials');
        allSerials = await response.json();
        renderSerials(allSerials);
    } catch (err) {
        console.error('Failed to load serials:', err);
    }
}

function renderSerials(serials) {
    const tbody = document.querySelector('#serials-table tbody');
    tbody.innerHTML = '';

    if (!serials || serials.length === 0) {
        tbody.innerHTML = '<tr><td colspan="5" class="no-data">Нет серийных номеров</td></tr>';
        return;
    }

    const categoryNames = {
        'Board': 'Мат. плата',
        'CPU': 'Процессор',
        'Memory': 'Память',
        'Storage': 'Накопитель',
        'GPU': 'Видеокарта',
        'PCIe': 'PCIe',
        'Network': 'Сеть',
        'PSU': 'БП',
        'Firmware': 'Прошивка',
        'FRU': 'FRU'
    };

    serials.forEach(item => {
        // Skip items without serial number or with N/A
        if (!item.serial_number || item.serial_number === 'N/A') return;
        const row = document.createElement('tr');
        row.innerHTML = `
            <td><span class="category-badge ${item.category.toLowerCase()}">${categoryNames[item.category] || item.category}</span></td>
            <td>${escapeHtml(item.component)}</td>
            <td>${escapeHtml(item.location || '-')}</td>
            <td><code>${escapeHtml(item.serial_number)}</code></td>
            <td>${escapeHtml(item.manufacturer || '-')}</td>
        `;
        tbody.appendChild(row);
    });
}

function filterSerials(category) {
    if (!category) {
        renderSerials(allSerials);
        return;
    }
    const filtered = allSerials.filter(s => s.category === category);
    renderSerials(filtered);
}

async function loadEvents() {
    try {
        const response = await fetch('/api/events');
        allEvents = await response.json();
        renderEvents(allEvents);
    } catch (err) {
        console.error('Failed to load events:', err);
    }
}

async function loadParseErrors() {
    try {
        const response = await fetch('/api/parse-errors');
        const payload = await response.json();
        allParseErrors = Array.isArray(payload && payload.items) ? payload.items : [];
        renderParseErrors(allParseErrors);
    } catch (err) {
        console.error('Failed to load parse errors:', err);
        allParseErrors = [];
        renderParseErrors([]);
    }
}

function renderParseErrors(items) {
    const tbody = document.querySelector('#parse-errors-table tbody');
    if (!tbody) return;
    tbody.innerHTML = '';

    if (!items || items.length === 0) {
        tbody.innerHTML = '<tr><td colspan="5" class="no-data">Ошибок разбора не обнаружено</td></tr>';
        return;
    }

    items.forEach(item => {
        const row = document.createElement('tr');
        const severity = (item.severity || 'info').toLowerCase();
        const source = item.source || '-';
        const category = item.category || '-';
        const path = item.path || '-';
        const message = item.message || item.detail || '-';
        row.innerHTML = `
            <td>${escapeHtml(source)}</td>
            <td>${escapeHtml(category)}</td>
            <td><span class="severity ${escapeHtml(severity)}">${escapeHtml(severity)}</span></td>
            <td><code>${escapeHtml(path)}</code></td>
            <td>${escapeHtml(message)}</td>
        `;
        tbody.appendChild(row);
    });
}

function renderEvents(events) {
    const tbody = document.querySelector('#events-table tbody');
    tbody.innerHTML = '';

    if (!events || events.length === 0) {
        tbody.innerHTML = '<tr><td colspan="4" class="no-data">Нет событий</td></tr>';
        return;
    }

    events.forEach(event => {
        const row = document.createElement('tr');
        row.innerHTML = `
            <td>${formatDate(event.timestamp)}</td>
            <td>${escapeHtml(event.source)}</td>
            <td>${escapeHtml(event.description)}</td>
            <td><span class="severity ${event.severity}">${event.severity}</span></td>
        `;
        tbody.appendChild(row);
    });
}

function filterEvents(severity) {
    if (!severity) {
        renderEvents(allEvents);
        return;
    }
    const filtered = allEvents.filter(e => e.severity === severity);
    renderEvents(filtered);
}

// Export functions
function exportData(format) {
    window.location.href = `/api/export/${format}`;
}

// Clear data
async function clearData() {
    try {
        await fetch('/api/clear', { method: 'DELETE' });
        document.getElementById('upload-section').classList.remove('hidden');
        document.getElementById('data-section').classList.add('hidden');
        document.getElementById('clear-btn').classList.add('hidden');
        document.getElementById('header-raw-btn').classList.add('hidden');
        document.getElementById('header-reanimator-btn').classList.add('hidden');
        document.getElementById('header-log-meta').classList.add('hidden');
        document.getElementById('upload-status').textContent = '';
        allSensors = [];
        allEvents = [];
        allSerials = [];
        allParseErrors = [];
        currentVendor = '';
        const frame = document.getElementById('audit-viewer-frame');
        if (frame) {
            frame.src = 'about:blank';
        }
    } catch (err) {
        console.error('Failed to clear data:', err);
    }
}

// Restart app (reload page)
function restartApp() {
    if (confirm('Перезапустить приложение? Все загруженные данные будут потеряны.')) {
        fetch('/api/clear', { method: 'DELETE' }).then(() => {
            window.location.reload();
        });
    }
}

// Exit app (shutdown server)
async function exitApp() {
    if (confirm('Завершить работу приложения?')) {
        try {
            await fetch('/api/shutdown', { method: 'POST' });
            document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
        } catch (err) {
            // Server shutdown, connection will fail
            document.body.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;"><div style="text-align:center;"><h1>LOGPile</h1><p>Приложение завершено. Можете закрыть эту вкладку.</p></div></div>';
        }
    }
}

// Utilities
function formatDate(isoString) {
    if (!isoString) return '-';
    const date = new Date(isoString);
    return date.toLocaleString('ru-RU');
}

function escapeHtml(text) {
    if (!text) return '';
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function detectPartialRedfishInventory({ cpus, memory, redfishFetchErrors }) {
    const errors = Array.isArray(redfishFetchErrors) ? redfishFetchErrors : [];
    const paths = errors.map(item => String(item && typeof item === 'object' ? (item.path || '') : '')).filter(Boolean);
    const cpuMissing = (!Array.isArray(cpus) || cpus.length === 0) && paths.some(p => /\/Systems\/[^/]+\/Processors(\/)?$/i.test(p));
    const memMissing = (!Array.isArray(memory) || memory.length === 0) && paths.some(p => /\/Systems\/[^/]+\/Memory(\/)?$/i.test(p));
    if (!cpuMissing && !memMissing) return '';
    if (cpuMissing && memMissing) return 'Не удалось восстановить CPU и Memory: Redfish endpoint\'ы /Processors и /Memory были недоступны во время сбора.';
    if (cpuMissing) return 'CPU-инвентарь неполный: Redfish endpoint /Processors был недоступен во время сбора.';
    return 'Memory-инвентарь неполный: Redfish endpoint /Memory был недоступен во время сбора.';
}

function calculateCPUToPCIeBalance(inventoryRows, cpus) {
    const laneByCPU = new Map();
    const cpuIndexes = new Set();

    (cpus || []).forEach(cpu => {
        const idx = extractCPUIndex(cpu.slot);
        if (idx !== null) {
            cpuIndexes.add(idx);
            laneByCPU.set(idx, 0);
        }
    });

    (inventoryRows || []).forEach(dev => {
        const idx = extractCPUIndex(dev.slot);
        if (idx === null) return;

        const lanes = Number(dev.link_width) > 0
            ? Number(dev.link_width)
            : (Number(dev.max_link_width) > 0
                ? Number(dev.max_link_width)
                : (dev.bdf ? 1 : 0));
        if (lanes <= 0) return;

        if (!laneByCPU.has(idx)) laneByCPU.set(idx, 0);
        laneByCPU.set(idx, laneByCPU.get(idx) + lanes);
        cpuIndexes.add(idx);
    });

    const indexes = [...cpuIndexes].sort((a, b) => a - b);
    const values = indexes.map(i => laneByCPU.get(i) || 0);
    const totalLanes = values.reduce((a, b) => a + b, 0);
    const maxLanes = values.length ? Math.max(...values) : 0;
    const minLanes = values.length ? Math.min(...values) : 0;
    const diffRatio = totalLanes > 0 ? (maxLanes - minLanes) / totalLanes : 0;
    let severity = 'ok';
    if (values.length > 1) {
        if (diffRatio >= 0.35) severity = 'critical';
        else if (diffRatio >= 0.2) severity = 'warning';
    }

    const denominator = maxLanes > 0 ? maxLanes : 1;
    const perCPU = indexes.map(i => {
        const lanes = laneByCPU.get(i) || 0;
        return {
            label: `CPU${i}`,
            lanes,
            percent: Math.round((lanes / denominator) * 100)
        };
    });

    if (perCPU.length === 0) {
        perCPU.push({ label: 'CPU?', lanes: 0, percent: 0 });
    }

    return { totalLanes, severity, perCPU };
}

function extractCPUIndex(slot) {
    const s = String(slot || '').trim();
    if (!s) return null;
    const m = s.match(/cpu\s*([0-9]+)/i);
    if (!m) return null;
    const idx = Number(m[1]);
    return Number.isFinite(idx) ? idx : null;
}

function calculateMemoryModulesByCPU(memoryRows) {
    const out = new Map();
    (memoryRows || []).forEach(mem => {
        if (mem.present === false || (mem.size_mb || 0) <= 0) return;
        const idx = extractCPUIndex(mem.location || mem.slot);
        if (idx === null) return;
        out.set(idx, (out.get(idx) || 0) + 1);
    });
    return out;
}

function findPCIeFirmwareVersion(firmwareEntries, pcieDevice) {
    if (!Array.isArray(firmwareEntries) || !pcieDevice) return '';

    const slot = (pcieDevice.slot || '').trim().toLowerCase();
    const model = (pcieDevice.part_number || '').trim().toLowerCase();
    if (!slot && !model) return '';

    const slotPatterns = slot
        ? [
            new RegExp(`^psu\\s*${escapeRegExp(slot)}\\b`, 'i'),
            new RegExp(`^nic\\s+${escapeRegExp(slot)}\\b`, 'i'),
            new RegExp(`^gpu\\s+${escapeRegExp(slot)}\\b`, 'i'),
            new RegExp(`^nvswitch\\s+${escapeRegExp(slot)}\\b`, 'i')
        ]
        : [];

    for (const fw of firmwareEntries) {
        const name = (fw.device_name || '').trim().toLowerCase();
        const version = (fw.version || '').trim();
        if (!name || !version) continue;
        if (slot && slotPatterns.some(re => re.test(name))) return version;
        if (model && name.includes(model)) return version;
    }

    return '';
}

function escapeRegExp(value) {
    return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function formatPCIeLink(currentWidth, currentSpeed, maxWidth, maxSpeed) {
    // Helper to convert speed to generation
    function speedToGen(speed) {
        if (!speed) return '';
        const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
        if (gtMatch) {
            const gts = parseFloat(gtMatch[1]);
            if (gts >= 32) return 'Gen5';
            if (gts >= 16) return 'Gen4';
            if (gts >= 8) return 'Gen3';
            if (gts >= 5) return 'Gen2';
            if (gts >= 2.5) return 'Gen1';
        }
        return '';
    }

    // Helper to extract GT/s value for comparison
    function extractGTs(speed) {
        if (!speed) return 0;
        const gtMatch = speed.match(/(\d+\.?\d*)\s*GT/i);
        return gtMatch ? parseFloat(gtMatch[1]) : 0;
    }

    // If no data, return dash
    if (!currentWidth && !currentSpeed) return '-';

    const curGen = speedToGen(currentSpeed);
    const maxGen = speedToGen(maxSpeed);

    // Check if current is lower than max
    const widthDegraded = maxWidth && currentWidth && currentWidth < maxWidth;
    const speedDegraded = maxSpeed && currentSpeed && extractGTs(currentSpeed) < extractGTs(maxSpeed);

    // Build current link string
    const curWidthStr = currentWidth ? `x${currentWidth}` : '';
    const curLinkStr = curGen ? `${curWidthStr} ${curGen}` : `${curWidthStr} ${currentSpeed || ''}`;

    // Build max link string (if available)
    let maxLinkStr = '';
    if (maxWidth || maxSpeed) {
        const maxWidthStr = maxWidth ? `x${maxWidth}` : '';
        maxLinkStr = maxGen ? `${maxWidthStr} ${maxGen}` : `${maxWidthStr} ${maxSpeed || ''}`;
    }

    // Apply degraded class if needed
    const degradedClass = (widthDegraded || speedDegraded) ? ' class="pcie-degraded"' : '';

    // Format output: show "current" or "current / max" if max differs
    if (maxLinkStr && (widthDegraded || speedDegraded)) {
        return `<span${degradedClass}>${curLinkStr}</span> <span class="pcie-max">/ ${maxLinkStr}</span>`;
    } else if (maxLinkStr && maxLinkStr !== curLinkStr) {
        return `${curLinkStr} <span class="pcie-max">/ ${maxLinkStr}</span>`;
    } else {
        return curLinkStr;
    }
}
