import { apiGet, apiPost, apiPut, apiPatch, apiDelete } from '../api.js';
import { canEdit, homeNavButton, showToast } from '../app.js';
import { confirmAction } from '../confirm-modal.js';
import { navigate } from '../router.js';
import {
    SOURCE_LANG,
    normalizeEntry,
    translationFor,
    targetLanguages,
    buildTranslationGroups,
    sourceLanguageLabel,
    translationListHeaderHTML,
    renderCollapsibleGroupedTranslationsHTML,
    escHtml,
} from '../translations-helpers.js';
import {
    mountConditionEditor,
    parseConditionForm,
} from '../condition-editor.js';
import { mountQuestionnairePreview } from '../questionnaire-preview.js';

const LAYOUT_TYPES = [
    { value: 'radio_question',             label: 'Radio (Single Choice)' },
    { value: 'multi_check_box_question',   label: 'Checkbox (Multiple Choice)' },
    { value: 'glass_scale_question',       label: 'Glass Scale' },
    { value: 'value_spinner',              label: 'Value Spinner' },
    { value: 'slider_question',          label: 'Slider (min–max)' },
    { value: 'date_spinner',               label: 'Date Spinner' },
    { value: 'string_spinner',             label: 'String Spinner' },
    { value: 'free_text',                  label: 'Free Text' },
    { value: 'client_coach_code_question', label: 'Client / Counselor code' },
    { value: 'last_page',                  label: 'Last Page' },
];

const OPTION_TYPES = new Set(['radio_question', 'multi_check_box_question']);

const GLASS_SCORE_LEVELS = [
    { key: 'never_glass', label: 'Never' },
    { key: 'little_glass', label: 'Little' },
    { key: 'moderate_glass', label: 'Moderate' },
    { key: 'much_glass', label: 'Much' },
    { key: 'extreme_glass', label: 'Extreme' },
];

let questionnaire = null;
let questions = [];
let retiredQuestions = [];
let isNew = false;
let activeTab = 'questions';
let allQuestionnaires = [];
let questionsByQuestionnaire = {};
let conditionEditorApi = null;

// ── Entry point ──────────────────────────────────────────────────────────

export async function editorPage(params) {
    const app = document.getElementById('app');
    isNew = !params.id || params.id === 'new';
    activeTab = 'questions';

    app.innerHTML = `
        <div class="page-header">
            <div class="page-header-start">
                ${homeNavButton()}
                <div class="page-header-titles">
                    <h1 id="editorTitle">${isNew ? 'New Questionnaire' : 'Loading...'}</h1>
                </div>
            </div>
            <div class="actions">
                <a href="#/questionnaires" class="btn">Questionnaires</a>
                ${!isNew ? `<a href="#/questionnaire/${params.id}/results" class="btn">View Results</a>` : ''}
            </div>
        </div>
        <div id="editorContent"><div class="spinner"></div></div>
    `;

    try {
        const qnList = await apiGet('questionnaires.php');
        allQuestionnaires = qnList.questionnaires || [];
    } catch {
        allQuestionnaires = [];
    }

    if (isNew) {
        questionnaire = { questionnaireID: null, name: '', version: '1.0', state: 'draft',
                          orderIndex: 0, showPoints: 0, conditionJson: '{}' };
        questions = [];
        renderEditor();
    } else {
        try {
            const quData = await apiGet(
                `questions.php?questionnaireID=${encodeURIComponent(params.id)}&includeRetired=1`
            );
            questionnaire = allQuestionnaires.find(q => q.questionnaireID === params.id);
            if (!questionnaire) {
                showToast('Questionnaire not found', 'error');
                navigate('#/questionnaires');
                return;
            }
            questionnaire.structureRevision = quData.structureRevision ?? questionnaire.structureRevision ?? 1;
            const allQs = quData.questions || [];
            questions = allQs.filter(q => !q.retired);
            retiredQuestions = allQs.filter(q => q.retired);
            document.getElementById('editorTitle').textContent = questionnaire.name;
            renderEditor();
        } catch (e) {
            showToast(e.message, 'error');
            document.getElementById('editorContent').innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
        }
    }
}

// ── Layout helpers ───────────────────────────────────────────────────────

function layoutSelectHTML(selected = 'radio_question', id = '') {
    return `<select ${id ? `id="${id}"` : ''} class="type-select">
        ${LAYOUT_TYPES.map(t =>
            `<option value="${t.value}" ${selected === t.value ? 'selected' : ''}>${t.label}</option>`
        ).join('')}
    </select>`;
}

function layoutLabel(value) {
    const t = LAYOUT_TYPES.find(t => t.value === value);
    return t ? t.label : value || 'unknown';
}

const STABLE_KEY_RE = /^[a-zA-Z][a-zA-Z0-9_]*$/;

function parseConfig(q) {
    try { return JSON.parse(q.configJson || '{}'); } catch { return {}; }
}

function questionKeyOf(q) {
    const cfg = typeof q === 'object' && q.configJson !== undefined ? parseConfig(q) : (q.config || q);
    return (q.questionKey || cfg.questionKey || '').trim();
}

function questionLocalIds() {
    return questions.map(q => {
        const parts = q.questionID.split('__');
        const localId = parts[parts.length - 1];
        return {
            questionID: q.questionID,
            localId,
            defaultText: q.defaultText,
            questionKey: questionKeyOf(q),
        };
    });
}

function branchTargetLabel(q) {
    const key = questionKeyOf(q) || q.localId;
    return `${q.localId} · ${key}`;
}

function branchTargetOptionsHTML(selected = '') {
    return questionLocalIds().map(q =>
        `<option value="${esc(q.localId)}" ${selected === q.localId ? 'selected' : ''}>${esc(branchTargetLabel(q))}</option>`
    ).join('');
}

function validateStableKey(key, label = 'Key') {
    if (!STABLE_KEY_RE.test(key)) {
        showToast(`${label} must match [a-zA-Z][a-zA-Z0-9_]*`, 'error');
        return false;
    }
    return true;
}

function optionKeyOf(o) {
    if (o.optionKey) return o.optionKey;
    const dt = (o.defaultText || '').trim();
    return STABLE_KEY_RE.test(dt) ? dt : '';
}

function optionGermanLabel(o) {
    return (o.labelGerman || '').trim() || (STABLE_KEY_RE.test(o.defaultText || '') ? '' : o.defaultText) || '';
}

function notesSectionHTML(config, prefix, questionKey) {
    const nbKey = questionKey ? `${questionKey}_note_before` : '—';
    const naKey = questionKey ? `${questionKey}_note_after` : '—';
    return `
        <div class="notes-section" style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">
            <h4 style="font-size:.85rem;margin-bottom:8px;color:var(--text-secondary)">Notes (German)</h4>
            <div class="form-row">
                <div class="form-group" style="flex:1">
                    <label>Note before question</label>
                    <input type="text" id="${prefix}_noteBefore" value="${esc(config.noteBefore || '')}" placeholder="Optional text shown above the question">
                    <span class="field-hint">Key: <code>${esc(nbKey)}</code></span>
                </div>
                <div class="form-group" style="flex:1">
                    <label>Note after question</label>
                    <input type="text" id="${prefix}_noteAfter" value="${esc(config.noteAfter || '')}" placeholder="Optional text shown below the question">
                    <span class="field-hint">Key: <code>${esc(naKey)}</code></span>
                </div>
            </div>
        </div>`;
}

function readNotesFromForm(prefix) {
    const val = id => document.getElementById(`${prefix}_${id}`)?.value?.trim() || '';
    return { noteBefore: val('noteBefore'), noteAfter: val('noteAfter') };
}

function glassSymptomsRowsFromQuestion(q) {
    if (Array.isArray(q?.glassSymptoms) && q.glassSymptoms.length) {
        return q.glassSymptoms.map(r => ({
            key: (r.key || '').trim(),
            labelDe: (r.labelDe || '').trim(),
            scoreLevels: r.scoreLevels || defaultGlassScoreLevels(),
        }));
    }
    const config = parseConfig(q);
    return (config.symptoms || []).map(key => ({
        key: String(key).trim(),
        labelDe: '',
        scoreLevels: defaultGlassScoreLevels(),
    }));
}

function defaultGlassScoreLevels() {
    return { never_glass: 0, little_glass: 1, moderate_glass: 2, much_glass: 3, extreme_glass: 4 };
}

function valueScoreRulesSectionHTML(prefix, config, scoreRules = []) {
    const min = Number(config.range?.min ?? 0);
    const max = Number(config.range?.max ?? 100);
    const step = Math.max(1, Number(config.step ?? 1));
    const lo = Math.min(min, max);
    const hi = Math.max(min, max);
    const ruleMap = {};
    for (const r of scoreRules || []) {
        ruleMap[r.levelKey] = r.points;
    }
    const values = [];
    for (let v = lo; v <= hi; v += step) values.push(v);
    if (!values.length) values.push(lo);
    const rows = values.map(v => `
        <div class="value-score-row">
            <span class="value-score-label">${v}</span>
            <input type="number" class="value-score-pts" data-value="${v}" value="${ruleMap[String(v)] ?? 0}" min="0" step="1">
        </div>`).join('');
    return `
        <div class="value-score-rules" id="${prefix}_value_scores">
            <label style="font-size:.85rem;font-weight:600;color:var(--text-secondary)">Points per value</label>
            <p class="field-hint">Set points for each selectable value (used when the questionnaire is scored).</p>
            <div class="value-score-grid">${rows}</div>
        </div>`;
}

function readValueScoreRulesFromForm(prefix) {
    const container = document.getElementById(`${prefix}_value_scores`);
    if (!container) return [];
    const rules = [];
    container.querySelectorAll('.value-score-pts').forEach(inp => {
        rules.push({
            scopeKey: '',
            levelKey: String(inp.dataset.value),
            points: parseInt(inp.value || '0', 10) || 0,
        });
    });
    return rules;
}

function wireValueScoreRules(prefix) {
    const minInp = document.getElementById(`${prefix}_rangeMin`);
    const maxInp = document.getElementById(`${prefix}_rangeMax`);
    const stepInp = document.getElementById(`${prefix}_step`);
    const rerender = () => {
        const section = document.getElementById(`${prefix}_value_scores`);
        if (!section) return;
        const config = {
            range: {
                min: parseInt(minInp?.value || '0', 10),
                max: parseInt(maxInp?.value || '100', 10),
            },
            step: parseInt(stepInp?.value || '1', 10) || 1,
        };
        const existing = readValueScoreRulesFromForm(prefix);
        section.outerHTML = valueScoreRulesSectionHTML(prefix, config, existing);
    };
    [minInp, maxInp, stepInp].forEach(el => el?.addEventListener('change', rerender));
}

function suggestSymptomKey(label) {
    let slug = (label || '').trim().toLowerCase()
        .replace(/[^a-z0-9]+/g, '_')
        .replace(/^_+|_+$/g, '');
    if (!slug) return '';
    if (!/^[a-z]/.test(slug)) slug = `sym_${slug}`;
    if (!STABLE_KEY_RE.test(slug)) slug = `sym_${slug.replace(/^[^a-z]+/, '')}`;
    return STABLE_KEY_RE.test(slug) ? slug : '';
}

function glassSymptomsSectionHTML(rows, prefix) {
    const list = rows.length ? rows : [];
    return `
        <div class="glass-symptoms-section" id="${prefix}_glass_symptoms">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
                <label style="font-size:.85rem;font-weight:600;color:var(--text-secondary)">Symptoms</label>
                <button type="button" class="btn btn-sm" data-action="add-glass-symptom">+ Add symptom</button>
            </div>
            <p class="field-hint" style="margin-bottom:10px">Each row is one table line in the app. German text is saved with the question; translate other languages on the Translations tab.</p>
            <ul class="glass-symptoms-list">
                ${list.length ? list.map((row, i) => glassSymptomRowHTML(prefix, row, i)).join('')
                    : '<li class="glass-symptoms-empty">No symptoms yet.</li>'}
            </ul>
        </div>`;
}

function glassSymptomRowHTML(prefix, row, idx) {
    const levels = row.scoreLevels || defaultGlassScoreLevels();
    const ptsRow = GLASS_SCORE_LEVELS.map(l => `
        <div class="glass-score-cell">
            <label>${esc(l.label)}</label>
            <input type="number" class="glass-score-pts" data-level="${esc(l.key)}"
                value="${Number(levels[l.key] ?? 0)}" min="0" step="1">
        </div>`).join('');
    return `
        <li class="glass-symptom-row" data-idx="${idx}">
            <div class="form-row" style="align-items:flex-end;margin-bottom:0">
                <div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
                    <label style="font-size:.75rem">Symptom key <span class="required-mark">*</span></label>
                    <input type="text" class="glass-symptom-key" value="${esc(row.key || '')}"
                        placeholder="e.g. q10_reexperience_trauma" pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off">
                </div>
                <div class="form-group" style="flex:2;margin-bottom:0">
                    <label style="font-size:.75rem">German label <span class="required-mark">*</span></label>
                    <input type="text" class="glass-symptom-label" value="${esc(row.labelDe || '')}"
                        placeholder="Text shown in the app for this row">
                </div>
                <button type="button" class="btn-icon btn-icon-danger glass-symptom-remove" title="Remove">&#x2716;</button>
            </div>
            <div class="glass-score-grid">${ptsRow}</div>
        </li>`;
}

function readGlassSymptomsFromForm(prefix) {
    const container = document.getElementById(`${prefix}_glass_symptoms`);
    if (!container) return [];
    const rows = [];
    container.querySelectorAll('.glass-symptom-row').forEach(rowEl => {
        const key = rowEl.querySelector('.glass-symptom-key')?.value?.trim() || '';
        const labelDe = rowEl.querySelector('.glass-symptom-label')?.value?.trim() || '';
        const scoreLevels = {};
        rowEl.querySelectorAll('.glass-score-pts').forEach(inp => {
            scoreLevels[inp.dataset.level] = parseInt(inp.value || '0', 10) || 0;
        });
        if (key || labelDe) rows.push({ key, labelDe, scoreLevels });
    });
    return rows;
}

function validateGlassSymptoms(rows) {
    const seen = new Set();
    for (const row of rows) {
        if (!row.key) {
            showToast('Each symptom needs a key', 'error');
            return false;
        }
        if (!validateStableKey(row.key, 'Symptom key')) return false;
        if (!row.labelDe) {
            showToast(`German label is required for symptom "${row.key}"`, 'error');
            return false;
        }
        if (seen.has(row.key)) {
            showToast(`Duplicate symptom key "${row.key}"`, 'error');
            return false;
        }
        seen.add(row.key);
    }
    return true;
}

function wireGlassSymptomsSection(prefix) {
    const container = document.getElementById(`${prefix}_glass_symptoms`);
    if (!container) return;

    const renderList = (rows) => {
        const list = container.querySelector('.glass-symptoms-list');
        if (!list) return;
        if (!rows.length) {
            list.innerHTML = '<li class="glass-symptoms-empty">No symptoms yet.</li>';
            return;
        }
        list.innerHTML = rows.map((row, i) => glassSymptomRowHTML(prefix, row, i)).join('');
        bindRowEvents();
    };

    const collectRows = () => readGlassSymptomsFromForm(prefix);

    const bindRowEvents = () => {
        container.querySelectorAll('.glass-symptom-remove').forEach(btn => {
            btn.addEventListener('click', () => {
                const rowEl = btn.closest('.glass-symptom-row');
                rowEl?.remove();
                const list = container.querySelector('.glass-symptoms-list');
                if (list && !list.querySelector('.glass-symptom-row')) {
                    list.innerHTML = '<li class="glass-symptoms-empty">No symptoms yet.</li>';
                }
            });
        });
        container.querySelectorAll('.glass-symptom-label').forEach(inp => {
            inp.addEventListener('blur', () => {
                const rowEl = inp.closest('.glass-symptom-row');
                const keyInp = rowEl?.querySelector('.glass-symptom-key');
                if (!keyInp || keyInp.value.trim()) return;
                const suggested = suggestSymptomKey(inp.value);
                if (suggested) keyInp.value = suggested;
            });
        });
    };

    container.querySelector('[data-action=add-glass-symptom]')?.addEventListener('click', () => {
        const rows = collectRows();
        rows.push({ key: '', labelDe: '', scoreLevels: defaultGlassScoreLevels() });
        renderList(rows);
        const lastKey = container.querySelector('.glass-symptom-row:last-child .glass-symptom-label');
        lastKey?.focus();
    });

    bindRowEvents();
}

function stringSpinnerOtherEnabled(config) {
    return Boolean(config.otherNextQuestionId || config.otherOptionKey);
}

/** Question types that use config.nextQuestionId (not per-option branching). */
const QUESTION_LEVEL_NEXT_LAYOUTS = new Set([
    'value_spinner',
    'slider_question',
    'free_text',
    'date_spinner',
    'multi_check_box_question',
    'glass_scale_question',
    'client_coach_code_question',
    'last_page',
]);

function questionNextSectionHTML(config, prefix, { label, hint } = {}) {
    const next = config.nextQuestionId || '';
    return `
        <div class="form-group question-next-field" style="margin-top:12px">
            <label>${label || 'Next question'}</label>
            <select id="${prefix}_nextQuestionId">
                <option value="">(next in list order)</option>
                ${branchTargetOptionsHTML(next)}
            </select>
            <span class="field-hint">${hint || 'Where to go after this question. Leave empty to continue in list order.'}</span>
        </div>`;
}

function stringSpinnerNextSectionHTML(config, prefix) {
    return questionNextSectionHTML(config, prefix, {
        label: 'Next question (list selection)',
        hint: 'Where to go after a normal spinner choice. <strong>Required</strong> when &ldquo;Other&rdquo; targets a question that sits next in order (so list picks can skip it).',
    });
}

function readQuestionNextFromForm(prefix, config) {
    const next = document.getElementById(`${prefix}_nextQuestionId`)?.value?.trim() || '';
    if (next) config.nextQuestionId = next;
}

function stringSpinnerOtherSectionHTML(config, prefix) {
    const enabled = stringSpinnerOtherEnabled(config);
    const otherKey = config.otherOptionKey || 'other_option';
    const otherNext = config.otherNextQuestionId || '';
    return `
        <div class="form-group" style="margin-top:12px;padding-top:12px;border-top:1px solid var(--border)">
            <label class="checkbox-label" style="display:inline-flex;margin-bottom:8px">
                <input type="checkbox" id="${prefix}_otherEnabled" ${enabled ? 'checked' : ''}>
                Show &ldquo;Other&rdquo; option (branch to free-text question)
            </label>
            <div id="${prefix}_otherFields" style="${enabled ? '' : 'display:none'}">
                <div class="form-row">
                    <div class="form-group" style="flex:1">
                        <label>Label translation key</label>
                        <input type="text" id="${prefix}_otherOptionKey" value="${esc(otherKey)}"
                            placeholder="e.g. other_country" pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off">
                        <span class="field-hint">Appends this label to the spinner. Translate under <strong>Questionnaire UI strings</strong> (or App strings if listed in catalog).</span>
                    </div>
                    <div class="form-group" style="flex:1">
                        <label>Free-text question when &ldquo;Other&rdquo; is chosen</label>
                        <select id="${prefix}_otherNextQuestionId">
                            <option value="">— select question —</option>
                            ${branchTargetOptionsHTML(otherNext)}
                        </select>
                        <span class="field-hint">Use layout <strong>Free text</strong> for the follow-up (e.g. country name).</span>
                    </div>
                </div>
            </div>
        </div>`;
}

function wireStringSpinnerOtherToggle(prefix) {
    const cb = document.getElementById(`${prefix}_otherEnabled`);
    const panel = document.getElementById(`${prefix}_otherFields`);
    if (!cb || !panel) return;
    const sync = () => { panel.style.display = cb.checked ? '' : 'none'; };
    cb.addEventListener('change', sync);
    sync();
}

function validateStringSpinnerConfig(prefix) {
    const defaultNext = document.getElementById(`${prefix}_nextQuestionId`)?.value.trim() || '';
    const enabled = document.getElementById(`${prefix}_otherEnabled`)?.checked;
    if (enabled) {
        const key = document.getElementById(`${prefix}_otherOptionKey`)?.value.trim() || '';
        const otherNext = document.getElementById(`${prefix}_otherNextQuestionId`)?.value.trim() || '';
        if (!key) {
            showToast('Other label translation key is required', 'error');
            return false;
        }
        if (!validateStableKey(key, 'Other label key')) return false;
        if (!otherNext) {
            showToast('Select the free-text question for “Other”', 'error');
            return false;
        }
        const target = questions.find(q => (q.localId || q.questionID.split('__').pop()) === otherNext);
        if (target && target.type !== 'free_text') {
            showToast('“Other” should branch to a Free text question', 'error');
            return false;
        }
        if (!defaultNext) {
            showToast('Set “Next question (list selection)” so normal choices do not fall through to the free-text step', 'error');
            return false;
        }
        if (defaultNext === otherNext) {
            showToast('“Next question” and “Other” branch cannot target the same question', 'error');
            return false;
        }
    }
    return true;
}

// ── Config form HTML for each layout type ────────────────────────────────

function configFormHTML(layout, config, prefix, opts = {}) {
    let html = '';
    switch (layout) {
        case 'radio_question':
            html = `
                <div class="form-group">
                    <label>Extra text key (optional)</label>
                    <input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="e.g. resilience_reflection_prompt">
                </div>
                <p class="field-hint" style="margin-top:8px">Branching: set <strong>Next question</strong> on each answer option below.</p>`;
            break;
        case 'multi_check_box_question':
            html = `
                <div class="form-group">
                    <label>Min selections</label>
                    <input type="number" id="${prefix}_minSelection" value="${config.minSelection || 0}" min="0">
                </div>
                ${stringSpinnerOtherSectionHTML(config, prefix)}`;
            break;
        case 'glass_scale_question': {
            const scaleType = config.scaleType || 'glass';
            const rows = opts.glassSymptoms || (config.symptoms || []).map(k => ({ key: k, labelDe: '' }));
            html = `
                <div class="form-group">
                    <label>Scale display</label>
                    <select id="${prefix}_scaleType">
                        <option value="glass" ${scaleType === 'glass' ? 'selected' : ''}>Glass (pain scale)</option>
                        <option value="thermometer" ${scaleType === 'thermometer' ? 'selected' : ''}>Thermometer</option>
                    </select>
                </div>
                ${glassSymptomsSectionHTML(rows, prefix)}`;
            break;
        }
        case 'value_spinner':
        case 'slider_question': {
            const scoreRules = opts.scoreRules || [];
            html = `
                <div class="form-row">
                    <div class="form-group">
                        <label>Min</label>
                        <input type="number" id="${prefix}_rangeMin" value="${config.range?.min ?? 0}">
                    </div>
                    <div class="form-group">
                        <label>Max</label>
                        <input type="number" id="${prefix}_rangeMax" value="${config.range?.max ?? 100}">
                    </div>
                    ${layout === 'slider_question' ? `
                    <div class="form-group">
                        <label>Step</label>
                        <input type="number" id="${prefix}_step" value="${config.step ?? 1}" min="1">
                    </div>
                    <div class="form-group">
                        <label>Unit label (optional)</label>
                        <input type="text" id="${prefix}_unitLabel" value="${esc(config.unitLabel || '')}" placeholder="e.g. years">
                    </div>` : ''}
                </div>
                ${valueScoreRulesSectionHTML(prefix, config, scoreRules)}`;
            break;
        }
        case 'date_spinner': {
            const notBefore = config.constraints?.notBefore || '';
            const notAfter = config.constraints?.notAfter || '';
            const precision = config.precision || 'full';
            const dateRange = config.dateRange === 'future' ? 'future' : 'past';
            html = `
                <div class="form-group">
                    <label>Date precision</label>
                    <select id="${prefix}_precision">
                        <option value="full" ${precision === 'full' ? 'selected' : ''}>Year, month, and day</option>
                        <option value="year_month" ${precision === 'year_month' ? 'selected' : ''}>Year and month</option>
                        <option value="year" ${precision === 'year' ? 'selected' : ''}>Year only</option>
                    </select>
                </div>
                <div class="form-group">
                    <label>Allowed dates</label>
                    <select id="${prefix}_dateRange">
                        <option value="past" ${dateRange === 'past' ? 'selected' : ''}>Past (today and earlier)</option>
                        <option value="future" ${dateRange === 'future' ? 'selected' : ''}>Future (today and later)</option>
                    </select>
                    <span class="field-hint">Controls which years the app shows. Default is past.</span>
                </div>
                <div class="form-row">
                    <div class="form-group">
                        <label>Not before (question key)</label>
                        <input type="text" id="${prefix}_notBefore" value="${esc(notBefore)}" placeholder="e.g. departure_country">
                    </div>
                    <div class="form-group">
                        <label>Not after (question key)</label>
                        <input type="text" id="${prefix}_notAfter" value="${esc(notAfter)}" placeholder="e.g. since_in_germany">
                    </div>
                </div>`;
            break;
        }
        case 'string_spinner':
            html = `
                <div class="form-group">
                    <label>Static options (one per line)</label>
                    <textarea id="${prefix}_stringOptions" rows="8" style="font-family:monospace;font-size:.85rem">${(config.options || []).join('\n')}</textarea>
                    <span class="field-hint">Do not include the &ldquo;Other&rdquo; row here; enable it below.</span>
                </div>
                ${stringSpinnerNextSectionHTML(config, prefix)}
                ${stringSpinnerOtherSectionHTML(config, prefix)}`;
            break;
        case 'free_text':
            html = `
                <div class="form-row">
                    <div class="form-group">
                        <label>Hint key (optional)</label>
                        <input type="text" id="${prefix}_hint" value="${esc(config.hint || '')}" placeholder="e.g. enter_text_to_continue">
                    </div>
                    <div class="form-group">
                        <label>Max length</label>
                        <input type="number" id="${prefix}_maxLength" value="${config.maxLength ?? 500}" min="1" max="10000">
                    </div>
                </div>
                <div class="form-group">
                    <label>Extra text key (optional)</label>
                    <input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="e.g. additional_notes_label">
                </div>`;
            break;
        case 'client_coach_code_question':
            html = `
                <div class="form-row">
                    <div class="form-group">
                        <label>Hint 1 key</label>
                        <input type="text" id="${prefix}_hint1" value="${esc(config.hint1 || '')}" placeholder="client_code">
                    </div>
                    <div class="form-group">
                        <label>Hint 2 key</label>
                        <input type="text" id="${prefix}_hint2" value="${esc(config.hint2 || '')}" placeholder="berater_code">
                    </div>
                </div>`;
            break;
        case 'last_page':
            html = `
                <div class="form-group">
                    <label>Text key</label>
                    <input type="text" id="${prefix}_textKey" value="${esc(config.textKey || '')}" placeholder="finish_data_entry">
                </div>`;
            break;
    }
    if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout)) {
        html += questionNextSectionHTML(config, prefix);
    }
    return html ? `<div class="config-section">${html}</div>` : '';
}

function readConfigFromForm(layout, prefix) {
    const config = {};
    const val = id => document.getElementById(`${prefix}_${id}`)?.value?.trim() || '';

    switch (layout) {
        case 'radio_question':
            if (val('textKey')) config.textKey = val('textKey');
            break;
        case 'multi_check_box_question': {
            const ms = parseInt(val('minSelection') || '0', 10);
            if (ms > 0) config.minSelection = ms;
            const otherEnabled = document.getElementById(`${prefix}_otherEnabled`)?.checked;
            if (otherEnabled) {
                const ok = val('otherOptionKey') || 'other_option';
                const on = val('otherNextQuestionId');
                if (ok) config.otherOptionKey = ok;
                if (on) config.otherNextQuestionId = on;
            }
            break;
        }
        case 'glass_scale_question': {
            const st = val('scaleType') || 'glass';
            if (st && st !== 'glass') config.scaleType = st;
            break;
        }
        case 'value_spinner':
        case 'slider_question':
            config.range = {
                min: parseInt(val('rangeMin') || '0', 10),
                max: parseInt(val('rangeMax') || '100', 10),
            };
            if (layout === 'slider_question') {
                const step = parseInt(val('step') || '1', 10);
                if (step > 0) config.step = step;
                if (val('unitLabel')) config.unitLabel = val('unitLabel');
            }
            break;
        case 'date_spinner': {
            const prec = val('precision') || 'full';
            if (prec && prec !== 'full') config.precision = prec;
            const dateRange = val('dateRange') || 'past';
            if (dateRange === 'future') config.dateRange = 'future';
            const nb = val('notBefore');
            const na = val('notAfter');
            if (nb || na) {
                config.constraints = {};
                if (nb) config.constraints.notBefore = nb;
                if (na) config.constraints.notAfter = na;
            }
            break;
        }
        case 'string_spinner': {
            const raw = val('stringOptions');
            const opts = raw.split('\n').map(s => s.trim()).filter(Boolean);
            if (opts.length) config.options = opts;
            const otherEnabled = document.getElementById(`${prefix}_otherEnabled`)?.checked;
            if (otherEnabled) {
                const ok = val('otherOptionKey') || 'other_option';
                const on = val('otherNextQuestionId');
                if (ok) config.otherOptionKey = ok;
                if (on) config.otherNextQuestionId = on;
            }
            break;
        }
        case 'free_text': {
            if (val('textKey')) config.textKey = val('textKey');
            if (val('hint')) config.hint = val('hint');
            const ml = parseInt(val('maxLength') || '500', 10);
            if (!Number.isNaN(ml) && ml > 0) config.maxLength = Math.min(ml, 10000);
            break;
        }
        case 'client_coach_code_question':
            if (val('hint1')) config.hint1 = val('hint1');
            if (val('hint2')) config.hint2 = val('hint2');
            break;
        case 'last_page':
            if (val('textKey')) config.textKey = val('textKey');
            break;
    }
    if (QUESTION_LEVEL_NEXT_LAYOUTS.has(layout) || layout === 'string_spinner') {
        readQuestionNextFromForm(prefix, config);
    }
    return JSON.stringify(config);
}

// ── Main editor render ───────────────────────────────────────────────────

function renderEditor() {
    const container = document.getElementById('editorContent');
    const editable = canEdit();
    const condJson = questionnaire.conditionJson || '{}';

    container.innerHTML = `
        <div class="card" style="margin-bottom:20px">
            <h3 style="margin-bottom:12px">Questionnaire Details</h3>
            <div class="meta-form">
                <div class="form-group">
                    <label>Name</label>
                    <input type="text" id="metaName" value="${esc(questionnaire.name)}" ${editable ? '' : 'disabled'}>
                </div>
                <div class="form-group">
                    <label>Version</label>
                    <input type="text" id="metaVersion" value="${esc(questionnaire.version)}" ${editable ? '' : 'disabled'}>
                </div>
                <div class="form-group">
                    <label>Structure revision</label>
                    <input type="text" value="${esc(String(questionnaire.structureRevision ?? 1))}" disabled
                        title="Auto-incremented when questions or options change structurally">
                </div>
                <div class="form-group">
                    <label>State</label>
                    <select id="metaState" ${editable ? '' : 'disabled'}>
                        ${['draft','active','archived'].map(s =>
                            `<option value="${s}" ${questionnaire.state === s ? 'selected' : ''}>${s}</option>`
                        ).join('')}
                    </select>
                </div>
            </div>
            <div class="meta-form" style="margin-top:0">
                <div class="form-group">
                    <label>Order index</label>
                    <input type="number" id="metaOrder" value="${questionnaire.orderIndex ?? 0}" min="0" ${editable ? '' : 'disabled'}>
                </div>
                <div class="form-group">
                    <label>Category key (app grouping)</label>
                    <input type="text" id="metaCategoryKey" value="${esc(questionnaire.categoryKey || '')}" placeholder="e.g. health_interview" ${editable ? '' : 'disabled'}>
                </div>
            </div>
            <div id="conditionEditorMount" style="margin:16px 0"></div>
            ${editable ? `<button class="btn btn-primary" id="saveMetaBtn">${isNew ? 'Create Questionnaire' : 'Save Changes'}</button>` : ''}
        </div>

        ${!isNew ? `
        <div class="tab-bar" id="editorTabs">
            <button class="active" data-tab="questions">Questions (${questions.length})</button>
            <button data-tab="preview">Preview</button>
            <button data-tab="translations">Translations</button>
        </div>
        <div id="tabContent-questions">
            <div class="card">
                <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
                    <h3>Questions</h3>
                    ${editable ? `<button class="btn btn-primary btn-sm" id="addQuestionBtn">+ Add Question</button>` : ''}
                </div>
                <div id="addQuestionFormWrapper" style="display:none"></div>
                <ul class="question-list" id="questionList"></ul>
                <div id="retiredQuestionsSection" style="display:none;margin-top:20px">
                    <details class="retired-questions-panel">
                        <summary>Retired questions (<span id="retiredCount">0</span>)</summary>
                        <p class="field-hint">Retired questions stay in upload history; Counselors on older forms can still submit.</p>
                        <ul class="question-list retired-question-list" id="retiredQuestionList"></ul>
                    </details>
                </div>
            </div>
        </div>
        <div id="tabContent-translations" style="display:none">
            <div class="card" id="translationsCard">
                <div id="translationsContent"><div class="spinner"></div></div>
            </div>
        </div>
        <div id="tabContent-preview" style="display:none">
            <div id="previewContent"></div>
        </div>
        ` : '<p style="color:var(--text-secondary);margin-top:12px">Save the questionnaire first, then add questions.</p>'}
    `;

    if (editable) {
        document.getElementById('saveMetaBtn').addEventListener('click', saveMeta);
    }
    initConditionEditor(condJson);

    if (!isNew) {
        initTabs();
        renderQuestions();
        renderRetiredQuestions();
        if (editable) {
            document.getElementById('addQuestionBtn').addEventListener('click', showAddQuestionForm);
            initDragReorder();
        }
    }
}

function applyStructureRevision(rev) {
    if (rev == null) return;
    questionnaire.structureRevision = rev;
    const input = document.querySelector('.meta-form input[disabled][title*="Auto-incremented"]');
    if (input) input.value = String(rev);
}

function notifyStructureBump(data) {
    const rev = data?.structureRevision;
    if (rev != null) {
        applyStructureRevision(rev);
        showToast(`Structure revision bumped to ${rev}.`, 'info');
    }
}

function initTabs() {
    const tabBar = document.getElementById('editorTabs');
    if (!tabBar) return;
    tabBar.querySelectorAll('button').forEach(btn => {
        btn.addEventListener('click', () => {
            const tab = btn.dataset.tab;
            if (tab === activeTab) return;
            activeTab = tab;
            tabBar.querySelectorAll('button').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
            document.getElementById('tabContent-questions').style.display = tab === 'questions' ? '' : 'none';
            document.getElementById('tabContent-translations').style.display = tab === 'translations' ? '' : 'none';
            document.getElementById('tabContent-preview').style.display = tab === 'preview' ? '' : 'none';
            if (tab === 'translations') loadTranslationsTab();
            if (tab === 'preview') loadPreviewTab();
        });
    });
}

function loadPreviewTab() {
    const mount = document.getElementById('previewContent');
    if (!mount) return;
    mountQuestionnairePreview(mount, {
        questionnaireName: questionnaire?.name || '',
        questions,
    });
}

async function fetchAppStringGerman(messageKey) {
    if (!messageKey) return '';
    try {
        const data = await apiGet(
            `translations.php?type=app_string&id=${encodeURIComponent(messageKey)}`
        );
        const row = (data.translations || []).find(
            tr => tr.languageCode === SOURCE_LANG
        );
        return row?.text?.trim() || '';
    } catch {
        return '';
    }
}

async function loadQuestionsByQuestionnaire() {
    const ids = allQuestionnaires.map(q => q.questionnaireID).filter(Boolean);
    if (!ids.length) {
        questionsByQuestionnaire = {};
        return;
    }
    const pairs = await Promise.all(ids.map(async id => {
        try {
            const data = await apiGet(
                `questions.php?questionnaireID=${encodeURIComponent(id)}`
            );
            return [id, data.questions || []];
        } catch {
            return [id, []];
        }
    }));
    questionsByQuestionnaire = Object.fromEntries(pairs);
}

async function initConditionEditor(condJson) {
    const mount = document.getElementById('conditionEditorMount');
    if (!mount) return;
    mount.innerHTML = '<div class="spinner"></div>';
    await loadQuestionsByQuestionnaire();
    let scoringProfiles = [];
    try {
        const data = await apiGet('scoring_profiles.php');
        scoringProfiles = (data.profiles || []).filter(p => p.isActive !== 0 && p.isActive !== false);
    } catch {
        scoringProfiles = [];
    }
    const qid = questionnaire?.questionnaireID || 'new';
    const parsed = parseConditionForm(condJson, qid);
    const germanLabel = parsed.messageKey
        ? await fetchAppStringGerman(parsed.messageKey)
        : '';
    conditionEditorApi = mountConditionEditor(mount, {
        questionnaireId: qid,
        allQuestionnaires,
        questionsByQuestionnaire,
        scoringProfiles,
        conditionJson: condJson,
        germanLabel,
        editable: canEdit(),
    });
}

// ── Save questionnaire meta ──────────────────────────────────────────────

async function saveMeta() {
    const name       = document.getElementById('metaName').value.trim();
    const version    = document.getElementById('metaVersion').value.trim();
    const state      = document.getElementById('metaState').value;
    const orderIndex = parseInt(document.getElementById('metaOrder').value || '0', 10);
    const showPoints = 0;
    const categoryKey = (document.getElementById('metaCategoryKey')?.value || '').trim();
    let conditionJson = '{}';
    let conditionForm = null;
    if (conditionEditorApi) {
        try {
            conditionJson = conditionEditorApi.buildJson();
            conditionForm = conditionEditorApi.readForm();
        } catch (e) {
            showToast(e.message || 'Invalid availability settings', 'error');
            return;
        }
    }
    if (!name) { showToast('Name is required', 'error'); return; }

    try {
        if (isNew) {
            const data = await apiPost('questionnaires.php', { name, version, state, orderIndex, showPoints, conditionJson, categoryKey });
            if (data.success) {
                questionnaire = data.questionnaire;
                isNew = false;
                await saveConditionMessageLabel(conditionForm);
                showToast('Questionnaire created', 'success');
                navigate(`#/questionnaire/${questionnaire.questionnaireID}`);
            }
        } else {
            const data = await apiPut('questionnaires.php', {
                questionnaireID: questionnaire.questionnaireID,
                name, version, state, orderIndex, showPoints, conditionJson, categoryKey
            });
            if (data.success) {
                questionnaire = { ...questionnaire, name, version, state, orderIndex, showPoints, conditionJson, categoryKey };
                document.getElementById('editorTitle').textContent = name;
                await saveConditionMessageLabel(conditionForm);
                showToast('Saved', 'success');
            }
        }
    } catch (e) {
        showToast(e.message, 'error');
    }
}

async function saveConditionMessageLabel(form) {
    if (!form || form.mode === 'always') return;
    const messageKey = (form.messageKey || '').trim();
    const germanLabel = (form.germanLabel || '').trim();
    if (!messageKey || !germanLabel) return;
    try {
        await apiPost('questionnaires.php', {
            action: 'updateConditionMessageLabel',
            messageKey,
            germanLabel,
        });
    } catch (e) {
        showToast(`Saved questionnaire; message label failed: ${e.message}`, 'error');
    }
}

// ── Add question form ────────────────────────────────────────────────────

function showAddQuestionForm() {
    const wrapper = document.getElementById('addQuestionFormWrapper');
    if (!wrapper) return;

    const pendingOptions = [];

    wrapper.style.display = '';
    wrapper.innerHTML = `
        <div class="inline-form-card">
            <h4>New Question</h4>
            <div class="form-row">
                <div class="form-group" style="flex:1;min-width:180px">
                    <label>Question key <span class="required-mark">*</span></label>
                    <input type="text" id="aq_questionKey" placeholder="e.g. consent_instruction" required pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off">
                </div>
                <div class="form-group" style="flex:2">
                    <label>German text <span class="required-mark">*</span></label>
                    <input type="text" id="aq_text" placeholder="e.g. Haben Sie zugestimmt?" required>
                </div>
                <div class="form-group" style="flex:1;min-width:200px">
                    <label>Layout type</label>
                    ${layoutSelectHTML('radio_question', 'aq_type')}
                </div>
            </div>
            <label class="checkbox-label" style="margin-bottom:12px;display:inline-flex">
                <input type="checkbox" id="aq_required"> Required
            </label>

            <div id="aq_config_section"></div>
            <div id="aq_notes_section">${notesSectionHTML({}, 'aq_cfg', '')}</div>

            <div id="aq_options_section">
                <div class="options-builder">
                    <label style="font-size:.85rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:8px">
                        Answer Options
                    </label>
                    <ul class="pending-options-list" id="aq_pending_options"></ul>
                    <div style="display:flex;gap:8px;margin-top:6px;align-items:flex-end">
                        <div class="form-group" style="flex:1;min-width:140px;margin-bottom:0">
                            <input type="text" id="aq_opt_key" placeholder="option_key" pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off">
                        </div>
                        <div class="form-group" style="flex:2;margin-bottom:0">
                            <input type="text" id="aq_opt_text" placeholder="German label…" required>
                        </div>
                        <div class="form-group" style="width:90px;margin-bottom:0">
                            <input type="number" id="aq_opt_pts" value="0" min="0" placeholder="Pts">
                        </div>
                        <div class="form-group" style="width:160px;margin-bottom:0">
                            <label style="font-size:.75rem;color:var(--text-secondary)">Next question</label>
                            <select id="aq_opt_next">
                                <option value="">(next in order)</option>
                                ${branchTargetOptionsHTML()}
                            </select>
                        </div>
                        <button class="btn btn-sm" id="aq_opt_add" style="flex-shrink:0;white-space:nowrap">+ Add</button>
                    </div>
                </div>
            </div>

            <div style="display:flex;gap:8px;margin-top:14px">
                <button class="btn btn-primary btn-sm" id="aq_submit">Add Question</button>
                <button class="btn btn-sm" id="aq_cancel">Cancel</button>
            </div>
        </div>
    `;

    function updateTypeUI() {
        const type = document.getElementById('aq_type').value;
        const qKey = document.getElementById('aq_questionKey')?.value.trim() || '';
        document.getElementById('aq_options_section').style.display = OPTION_TYPES.has(type) ? '' : 'none';
        document.getElementById('aq_config_section').innerHTML = configFormHTML(type, {}, 'aq_cfg');
        wireStringSpinnerOtherToggle('aq_cfg');
        if (type === 'glass_scale_question') wireGlassSymptomsSection('aq_cfg');
        if (type === 'slider_question' || type === 'value_spinner') wireValueScoreRules('aq_cfg');
        const notesEl = document.getElementById('aq_notes_section');
        if (notesEl) notesEl.innerHTML = notesSectionHTML({}, 'aq_cfg', qKey);
    }

    function renderPendingOptions() {
        const list = document.getElementById('aq_pending_options');
        if (!list) return;
        if (!pendingOptions.length) {
            list.innerHTML = `<li class="pending-options-empty">No options yet.</li>`;
            return;
        }
        list.innerHTML = pendingOptions.map((opt, i) => `
            <li class="pending-option-item">
                <code class="opt-key">${esc(opt.optionKey)}</code>
                <span class="opt-text">${esc(opt.text)}</span>
                <span class="opt-points">${opt.points} pts</span>
                ${opt.nextQuestionId ? `<span class="opt-branch">&rarr; ${esc(opt.nextQuestionId)}</span>` : ''}
                <button class="btn-icon btn-icon-danger remove-pending-opt" data-idx="${i}" title="Remove">&#x2716;</button>
            </li>
        `).join('');
        list.querySelectorAll('.remove-pending-opt').forEach(btn => {
            btn.addEventListener('click', () => {
                pendingOptions.splice(parseInt(btn.dataset.idx, 10), 1);
                renderPendingOptions();
            });
        });
    }

    function addPendingOption() {
        const optionKey = document.getElementById('aq_opt_key').value.trim();
        const text = document.getElementById('aq_opt_text').value.trim();
        const pts  = parseInt(document.getElementById('aq_opt_pts').value || '0', 10);
        const next = document.getElementById('aq_opt_next').value;
        if (!validateStableKey(optionKey, 'Option key')) return;
        if (!text) { showToast('German label is required', 'error'); return; }
        pendingOptions.push({ optionKey, text, points: pts, nextQuestionId: next });
        renderPendingOptions();
        document.getElementById('aq_opt_key').value = '';
        document.getElementById('aq_opt_text').value = '';
        document.getElementById('aq_opt_pts').value = '0';
        document.getElementById('aq_opt_next').value = '';
        document.getElementById('aq_opt_text').focus();
    }

    updateTypeUI();
    renderPendingOptions();
    document.getElementById('aq_text').focus();

    document.getElementById('aq_type').addEventListener('change', updateTypeUI);
    document.getElementById('aq_questionKey').addEventListener('input', () => {
        const notesEl = document.getElementById('aq_notes_section');
        if (notesEl) {
            const nb = document.getElementById('aq_cfg_noteBefore')?.value ?? '';
            const na = document.getElementById('aq_cfg_noteAfter')?.value ?? '';
            notesEl.innerHTML = notesSectionHTML(
                { noteBefore: nb, noteAfter: na },
                'aq_cfg',
                document.getElementById('aq_questionKey').value.trim()
            );
        }
    });
    document.getElementById('aq_opt_add').addEventListener('click', addPendingOption);
    document.getElementById('aq_opt_text').addEventListener('keydown', (e) => {
        if (e.key === 'Enter') { e.preventDefault(); addPendingOption(); }
    });

    document.getElementById('aq_submit').addEventListener('click', async () => {
        const questionKey = document.getElementById('aq_questionKey').value.trim();
        const text = document.getElementById('aq_text').value.trim();
        const type = document.getElementById('aq_type').value;
        const isRequired = document.getElementById('aq_required').checked ? 1 : 0;
        const btn = document.getElementById('aq_submit');
        const notes = readNotesFromForm('aq_cfg');
        const configJson = readConfigFromForm(type, 'aq_cfg');
        const postBody = {
            questionnaireID: questionnaire.questionnaireID,
            questionKey,
            defaultText: text,
            type,
            isRequired,
            configJson,
            noteBefore: notes.noteBefore,
            noteAfter: notes.noteAfter,
        };
        if (type === 'glass_scale_question') {
            const glassSymptoms = readGlassSymptomsFromForm('aq_cfg');
            if (glassSymptoms.length && !validateGlassSymptoms(glassSymptoms)) return;
            postBody.glassSymptoms = glassSymptoms;
        }
        if (type === 'slider_question' || type === 'value_spinner') {
            postBody.scoreRules = readValueScoreRulesFromForm('aq_cfg');
        }
        if (!validateStableKey(questionKey, 'Question key')) return;
        if (!text) { showToast('German text is required', 'error'); return; }
        if (type === 'string_spinner' && !validateStringSpinnerConfig('aq_cfg')) return;
        if (OPTION_TYPES.has(type) && pendingOptions.length === 0) {
            showToast('Add at least one answer option', 'error');
            return;
        }

        btn.disabled = true;
        btn.textContent = 'Adding...';
        try {
            const qData = await apiPost('questions.php', postBody);
            if (!qData.success) throw new Error(qData.error || 'Failed');

            const newQ = qData.question;
            newQ.answerOptions = [];

            for (const opt of pendingOptions) {
                const oData = await apiPost('answer_options.php', {
                    questionID: newQ.questionID,
                    optionKey: opt.optionKey,
                    defaultText: opt.text,
                    points: opt.points,
                    nextQuestionId: opt.nextQuestionId || '',
                });
                if (oData.success) newQ.answerOptions.push(oData.answerOption);
            }

            questions.push(newQ);
            renderQuestions();
            notifyStructureBump(qData);
            showToast('Question added', 'success');

            document.getElementById('aq_text').value = '';
            pendingOptions.length = 0;
            renderPendingOptions();
            updateTypeUI();
            document.getElementById('aq_text').focus();
        } catch (e) {
            showToast(e.message, 'error');
        } finally {
            if (document.getElementById('aq_submit')) {
                btn.disabled = false;
                btn.textContent = 'Add Question';
            }
        }
    });

    document.getElementById('aq_text').addEventListener('keydown', (e) => {
        if (e.key === 'Escape') hideAddQuestionForm();
    });
    document.getElementById('aq_cancel').addEventListener('click', hideAddQuestionForm);
}

function hideAddQuestionForm() {
    const wrapper = document.getElementById('addQuestionFormWrapper');
    if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; }
}

// ── Questions list ───────────────────────────────────────────────────────

function renderRetiredQuestions() {
    const section = document.getElementById('retiredQuestionsSection');
    const list = document.getElementById('retiredQuestionList');
    const countEl = document.getElementById('retiredCount');
    if (!section || !list) return;
    if (!retiredQuestions.length) {
        section.style.display = 'none';
        list.innerHTML = '';
        if (countEl) countEl.textContent = '0';
        return;
    }
    section.style.display = '';
    if (countEl) countEl.textContent = String(retiredQuestions.length);
    list.innerHTML = retiredQuestions.map((q, idx) => `
        <li class="question-item retired-question-item">
            <div class="question-header">
                <span class="q-text">${idx + 1}. <code>${esc(questionKeyOf(q) || '?')}</code> · ${esc((q.defaultText || '').slice(0, 48))}</span>
                <span class="badge badge-archived">Retired</span>
                ${q.retiredInRevision ? `<span class="badge">rev ${q.retiredInRevision}</span>` : ''}
            </div>
        </li>
    `).join('');
}

function renderQuestions() {
    const list = document.getElementById('questionList');
    if (!list) return;
    const editable = canEdit();

    // update tab label
    const tabBtn = document.querySelector('#editorTabs button[data-tab="questions"]');
    if (tabBtn) tabBtn.textContent = `Questions (${questions.length})`;
    renderRetiredQuestions();

    if (!questions.length) {
        list.innerHTML = `<li class="empty-state" style="padding:30px"><h3>No questions yet</h3><p>Add your first question above.</p></li>`;
        return;
    }

    list.innerHTML = questions.map((q, idx) => `
        <li class="question-item" data-id="${q.questionID}" draggable="${editable}">
            <div class="question-header">
                ${editable ? '<span class="drag-handle" title="Drag to reorder">&#x2630;</span>' : ''}
                <span class="chevron">&#x25B6;</span>
                <span class="q-text">${idx + 1}. <code>${esc(questionKeyOf(q) || '?')}</code> · ${esc((q.defaultText || '').slice(0, 48))}${(q.defaultText || '').length > 48 ? '…' : ''}</span>
                <span class="q-type">${esc(layoutLabel(q.type))}</span>
                ${!questionKeyOf(q) ? '<span class="badge badge-warn">No key</span>' : ''}
                ${q.isRequired ? '<span class="badge badge-active">Required</span>' : ''}
                ${(() => {
                    const c = parseConfig(q);
                    const parts = [];
                    if (c.nextQuestionId) parts.push(`→ ${esc(c.nextQuestionId)}`);
                    if (c.otherNextQuestionId) parts.push(`Other→${esc(c.otherNextQuestionId)}`);
                    return parts.length
                        ? `<span class="badge" title="Branches">${esc(parts.join(' · '))}</span>` : '';
                })()}
            </div>
            <div class="question-body" id="qbody-${q.questionID}"></div>
        </li>
    `).join('');

    list.querySelectorAll('.question-header').forEach((hdr, idx) => {
        hdr.addEventListener('click', (e) => {
            if (e.target.closest('.drag-handle')) return;
            const item = hdr.closest('.question-item');
            const wasOpen = item.classList.contains('open');
            item.classList.toggle('open');
            if (!wasOpen) renderQuestionBody(questions[idx]);
        });
    });
}

// ── Question body (expanded) ─────────────────────────────────────────────

function renderQuestionBody(q) {
    const body = document.getElementById(`qbody-${q.questionID}`);
    if (!body) return;
    const editable = canEdit();
    const options = q.answerOptions || [];
    const config = parseConfig(q);
    const layout = q.type || 'radio_question';
    const qKey = questionKeyOf(q);
    const localId = q.localId || q.questionID.split('__').pop();

    body.innerHTML = `
        ${editable ? `
        <div class="q-edit-section">
            <div class="inline-form-card" style="margin-bottom:12px">
                <p class="field-hint" style="margin-bottom:10px"><strong>Local ID:</strong> <code>${esc(localId)}</code></p>
                <div class="form-row">
                    <div class="form-group" style="flex:1;min-width:180px">
                        <label>Question key <span class="required-mark">*</span></label>
                        <input type="text" id="eq_key_${q.questionID}" value="${esc(qKey)}" required pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off">
                    </div>
                    <div class="form-group" style="flex:2">
                        <label>German text <span class="required-mark">*</span></label>
                        <input type="text" id="eq_text_${q.questionID}" value="${esc(q.defaultText)}" required>
                    </div>
                    <div class="form-group" style="flex:1;min-width:200px">
                        <label>Layout type</label>
                        ${layoutSelectHTML(layout, `eq_type_${q.questionID}`)}
                    </div>
                </div>
                <label class="checkbox-label" style="margin-bottom:12px;display:inline-flex">
                    <input type="checkbox" id="eq_req_${q.questionID}" ${q.isRequired ? 'checked' : ''}> Required
                </label>
                ${notesSectionHTML(config, `eq_cfg_${q.questionID}`, qKey)}
                <div id="eq_config_${q.questionID}">
                    ${configFormHTML(layout, config, `eq_cfg_${q.questionID}`, {
                        glassSymptoms: glassSymptomsRowsFromQuestion(q),
                        scoreRules: q.scoreRules || [],
                    })}
                </div>
                <div style="display:flex;gap:8px;margin-top:8px">
                    <button class="btn btn-primary btn-sm save-q-btn">Save Question</button>
                    <button class="btn btn-sm btn-danger del-q-btn">Delete Question</button>
                </div>
            </div>
        </div>
        ` : `
        <div class="inline-form-card" style="margin-bottom:12px">
            <p><strong>Key:</strong> <code>${esc(qKey || '—')}</code> · <strong>ID:</strong> <code>${esc(localId)}</code></p>
            <p><strong>German:</strong> ${esc(q.defaultText)}</p>
            <p><strong>Layout:</strong> ${esc(layoutLabel(layout))}</p>
            ${config.nextQuestionId || config.otherNextQuestionId
                ? `<p><strong>Branches:</strong>
                    ${config.nextQuestionId ? `→ <code>${esc(config.nextQuestionId)}</code>` : ''}
                    ${config.otherNextQuestionId ? `${config.nextQuestionId ? ' · ' : ''}Other (<code>${esc(config.otherOptionKey || 'other_option')}</code>) → <code>${esc(config.otherNextQuestionId)}</code>` : ''}
                   </p>`
                : ''}
            ${Object.keys(config).length ? `<p><strong>Config:</strong> <code>${esc(JSON.stringify(config))}</code></p>` : ''}
            ${layout === 'glass_scale_question' && glassSymptomsRowsFromQuestion(q).length ? `
                <p><strong>Symptoms:</strong></p>
                <ul class="option-list">
                    ${glassSymptomsRowsFromQuestion(q).map(r =>
                        `<li><code>${esc(r.key)}</code> · ${esc(r.labelDe || '—')}</li>`
                    ).join('')}
                </ul>` : ''}
        </div>
        `}

        ${OPTION_TYPES.has(layout) ? `
        <h4 style="font-size:.85rem;color:var(--text-secondary);margin-bottom:6px">Answer Options (${options.length})</h4>
        <ul class="option-list" id="opts-${q.questionID}">
            ${options.map(o => optionItemHTML(q, o, editable)).join('')
              || '<li style="color:var(--text-secondary);padding:8px;font-size:.85rem">No answer options</li>'}
        </ul>
        ${editable ? `
        <div id="addOptForm-${q.questionID}" style="display:none"></div>
        <button class="btn btn-sm" id="addOpt-${q.questionID}" style="margin-top:6px">+ Add Option</button>
        ` : ''}
        ` : ''}
    `;

    if (!editable) return;

    const cfgPrefix = `eq_cfg_${q.questionID}`;

    body.querySelector('.save-q-btn').addEventListener('click', () => {
        const questionKey = document.getElementById(`eq_key_${q.questionID}`).value.trim();
        const text  = document.getElementById(`eq_text_${q.questionID}`).value.trim();
        const type  = document.getElementById(`eq_type_${q.questionID}`).value;
        const isReq = document.getElementById(`eq_req_${q.questionID}`).checked ? 1 : 0;
        const notes = readNotesFromForm(cfgPrefix);
        const cj    = readConfigFromForm(type, cfgPrefix);
        const payload = {
            questionKey,
            defaultText: text,
            type,
            isRequired: isReq,
            configJson: cj,
            noteBefore: notes.noteBefore,
            noteAfter: notes.noteAfter,
        };
        if (type === 'glass_scale_question') {
            const glassSymptoms = readGlassSymptomsFromForm(cfgPrefix);
            if (glassSymptoms.length && !validateGlassSymptoms(glassSymptoms)) return;
            payload.glassSymptoms = glassSymptoms;
        }
        if (type === 'slider_question' || type === 'value_spinner') {
            payload.scoreRules = readValueScoreRulesFromForm(cfgPrefix);
        }
        if (!validateStableKey(questionKey, 'Question key')) return;
        if (!text) { showToast('German text is required', 'error'); return; }
        if (type === 'string_spinner' && !validateStringSpinnerConfig(cfgPrefix)) return;
        updateQuestion(q.questionID, payload);
    });

    document.getElementById(`eq_key_${q.questionID}`).addEventListener('input', () => {
        const nb = document.getElementById(`${cfgPrefix}_noteBefore`)?.value ?? '';
        const na = document.getElementById(`${cfgPrefix}_noteAfter`)?.value ?? '';
        const key = document.getElementById(`eq_key_${q.questionID}`).value.trim();
        const notesBlock = body.querySelector('.notes-section');
        if (notesBlock) {
            notesBlock.outerHTML = notesSectionHTML({ noteBefore: nb, noteAfter: na }, cfgPrefix, key);
        }
    });

    document.getElementById(`eq_type_${q.questionID}`).addEventListener('change', () => {
        const newLayout = document.getElementById(`eq_type_${q.questionID}`).value;
        const opts = newLayout === 'glass_scale_question'
            ? { glassSymptoms: glassSymptomsRowsFromQuestion(q) }
            : (newLayout === 'slider_question' || newLayout === 'value_spinner')
                ? { scoreRules: q.scoreRules || [] }
                : {};
        document.getElementById(`eq_config_${q.questionID}`).innerHTML =
            configFormHTML(newLayout, {}, cfgPrefix, opts);
        wireStringSpinnerOtherToggle(cfgPrefix);
        if (newLayout === 'glass_scale_question') wireGlassSymptomsSection(cfgPrefix);
        if (newLayout === 'slider_question' || newLayout === 'value_spinner') wireValueScoreRules(cfgPrefix);
    });

    wireStringSpinnerOtherToggle(cfgPrefix);
    if (layout === 'glass_scale_question') wireGlassSymptomsSection(cfgPrefix);
    if (layout === 'slider_question' || layout === 'value_spinner') wireValueScoreRules(cfgPrefix);

    body.querySelector('.del-q-btn').addEventListener('click', () => deleteQuestion(q));

    body.querySelectorAll('.edit-opt-btn').forEach(btn => {
        btn.addEventListener('click', () => {
            const opt = options.find(o => o.answerOptionID === btn.dataset.id);
            if (opt) showEditOptionForm(q, opt);
        });
    });
    body.querySelectorAll('.del-opt-btn').forEach(btn => {
        btn.addEventListener('click', () => deleteOption(q, btn.dataset.id));
    });

    const addOptBtn = document.getElementById(`addOpt-${q.questionID}`);
    if (addOptBtn) addOptBtn.addEventListener('click', () => showAddOptionForm(q));

    if (OPTION_TYPES.has(layout)) initOptionDrag(q);
}

function optionItemHTML(q, o, editable) {
    const ok = optionKeyOf(o);
    const label = optionGermanLabel(o) || o.defaultText;
    return `
        <li class="option-item" data-id="${o.answerOptionID}" draggable="${editable}">
            ${editable ? '<span class="drag-handle" style="font-size:.9rem">&#x2630;</span>' : ''}
            <code class="opt-key">${esc(ok || '?')}</code>
            <span class="opt-text">${esc(label)}</span>
            <span class="opt-points">${o.points} pts</span>
            ${o.nextQuestionId ? `<span class="opt-branch">&rarr; ${esc(o.nextQuestionId)}</span>` : ''}
            ${editable ? `
                <button class="btn-icon edit-opt-btn" data-id="${o.answerOptionID}" title="Edit">&#x270E;</button>
                <button class="btn-icon btn-icon-danger del-opt-btn" data-id="${o.answerOptionID}" title="Delete">&#x2716;</button>
            ` : ''}
        </li>
    `;
}

// ── Add option form ──────────────────────────────────────────────────────

function showAddOptionForm(q) {
    const wrapper = document.getElementById(`addOptForm-${q.questionID}`);
    const toggleBtn = document.getElementById(`addOpt-${q.questionID}`);
    if (!wrapper) return;
    toggleBtn.style.display = 'none';
    wrapper.style.display = '';

    const qIds = questionLocalIds();

    wrapper.innerHTML = `
        <div class="inline-form-card" style="margin-top:8px">
            <div class="form-row">
                <div class="form-group" style="flex:1;min-width:140px">
                    <label>Option key <span class="required-mark">*</span></label>
                    <input type="text" id="ao_key_${q.questionID}" placeholder="e.g. consent_signed" pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off" required>
                </div>
                <div class="form-group" style="flex:2">
                    <label>German label <span class="required-mark">*</span></label>
                    <input type="text" id="ao_text_${q.questionID}" placeholder="e.g. Ja, ich stimme zu" required>
                </div>
                <div class="form-group" style="flex:1;min-width:100px">
                    <label>Points</label>
                    <input type="number" id="ao_pts_${q.questionID}" value="0" min="0">
                </div>
                <div class="form-group" style="flex:1;min-width:160px">
                    <label>Next question</label>
                    <select id="ao_next_${q.questionID}">
                        <option value="">(next in order)</option>
                        ${branchTargetOptionsHTML()}
                    </select>
                </div>
            </div>
            <div style="display:flex;gap:8px">
                <button class="btn btn-primary btn-sm" id="ao_submit_${q.questionID}">Add Option</button>
                <button class="btn btn-sm" id="ao_cancel_${q.questionID}">Cancel</button>
            </div>
        </div>
    `;

    document.getElementById(`ao_text_${q.questionID}`).focus();

    document.getElementById(`ao_submit_${q.questionID}`).addEventListener('click', async () => {
        const optionKey = document.getElementById(`ao_key_${q.questionID}`).value.trim();
        const text   = document.getElementById(`ao_text_${q.questionID}`).value.trim();
        const points = parseInt(document.getElementById(`ao_pts_${q.questionID}`).value || '0', 10);
        const nextQ  = document.getElementById(`ao_next_${q.questionID}`).value;
        if (!validateStableKey(optionKey, 'Option key')) return;
        if (!text) { showToast('German label is required', 'error'); return; }

        const btn = document.getElementById(`ao_submit_${q.questionID}`);
        btn.disabled = true;
        btn.textContent = 'Adding...';
        try {
            const data = await apiPost('answer_options.php', {
                questionID: q.questionID,
                optionKey,
                defaultText: text,
                points,
                nextQuestionId: nextQ,
            });
            if (data.success) {
                if (!q.answerOptions) q.answerOptions = [];
                q.answerOptions.push(data.answerOption);
                renderQuestionBody(q);
                showToast('Option added', 'success');
            }
        } catch (e) {
            showToast(e.message, 'error');
            btn.disabled = false;
            btn.textContent = 'Add Option';
        }
    });

    document.getElementById(`ao_text_${q.questionID}`).addEventListener('keydown', (e) => {
        if (e.key === 'Enter') document.getElementById(`ao_submit_${q.questionID}`).click();
        if (e.key === 'Escape') hideAddOptionForm(q);
    });
    document.getElementById(`ao_cancel_${q.questionID}`).addEventListener('click', () => hideAddOptionForm(q));
}

function hideAddOptionForm(q) {
    const wrapper = document.getElementById(`addOptForm-${q.questionID}`);
    const toggleBtn = document.getElementById(`addOpt-${q.questionID}`);
    if (wrapper) { wrapper.style.display = 'none'; wrapper.innerHTML = ''; }
    if (toggleBtn) toggleBtn.style.display = '';
}

// ── Edit option form ─────────────────────────────────────────────────────

function showEditOptionForm(q, opt) {
    const li = document.querySelector(`#opts-${q.questionID} .option-item[data-id="${opt.answerOptionID}"]`);
    if (!li) return;
    const qIds = questionLocalIds();

    li.innerHTML = `
        <div class="inline-form-card" style="width:100%;padding:8px">
            <div class="form-row">
                <div class="form-group" style="flex:1;min-width:120px;margin-bottom:8px">
                    <label style="font-size:.8rem">Option key <span class="required-mark">*</span></label>
                    <input type="text" id="eo_key_${opt.answerOptionID}" value="${esc(optionKeyOf(opt))}" pattern="[a-zA-Z][a-zA-Z0-9_]*" autocomplete="off" required>
                </div>
                <div class="form-group" style="flex:2;margin-bottom:8px">
                    <label style="font-size:.8rem">German label <span class="required-mark">*</span></label>
                    <input type="text" id="eo_text_${opt.answerOptionID}" value="${esc(optionGermanLabel(opt))}" required>
                </div>
                <div class="form-group" style="flex:1;min-width:80px;margin-bottom:8px">
                    <input type="number" id="eo_pts_${opt.answerOptionID}" value="${opt.points}" min="0">
                </div>
                <div class="form-group" style="flex:1;min-width:160px;margin-bottom:8px">
                    <label style="font-size:.8rem">Next question</label>
                    <select id="eo_next_${opt.answerOptionID}">
                        <option value="">(next in order)</option>
                        ${qIds.map(qi => `<option value="${esc(qi.localId)}" ${opt.nextQuestionId === qi.localId ? 'selected' : ''}>${esc(branchTargetLabel(qi))}</option>`).join('')}
                    </select>
                </div>
            </div>
            <div style="display:flex;gap:6px">
                <button class="btn btn-primary btn-sm" id="eo_save_${opt.answerOptionID}">Save</button>
                <button class="btn btn-sm" id="eo_cancel_${opt.answerOptionID}">Cancel</button>
            </div>
        </div>
    `;
    li.draggable = false;

    document.getElementById(`eo_text_${opt.answerOptionID}`).focus();

    document.getElementById(`eo_save_${opt.answerOptionID}`).addEventListener('click', async () => {
        const optionKey = document.getElementById(`eo_key_${opt.answerOptionID}`).value.trim();
        const text   = document.getElementById(`eo_text_${opt.answerOptionID}`).value.trim();
        const points = parseInt(document.getElementById(`eo_pts_${opt.answerOptionID}`).value || '0', 10);
        const nextQ  = document.getElementById(`eo_next_${opt.answerOptionID}`).value;
        if (!validateStableKey(optionKey, 'Option key')) return;
        if (!text) { showToast('German label is required', 'error'); return; }
        await updateOption(q, opt.answerOptionID, { optionKey, defaultText: text, points, nextQuestionId: nextQ });
    });

    document.getElementById(`eo_text_${opt.answerOptionID}`).addEventListener('keydown', (e) => {
        if (e.key === 'Enter') document.getElementById(`eo_save_${opt.answerOptionID}`).click();
        if (e.key === 'Escape') renderQuestionBody(q);
    });
    document.getElementById(`eo_cancel_${opt.answerOptionID}`).addEventListener('click', () => renderQuestionBody(q));
}

// ── Question CRUD ────────────────────────────────────────────────────────

async function updateQuestion(questionID, fields) {
    try {
        const data = await apiPut('questions.php', { questionID, ...fields });
        if (data.success) {
            const idx = questions.findIndex(q => q.questionID === questionID);
            if (idx >= 0) {
                questions[idx] = { ...questions[idx], ...data.question,
                    answerOptions: questions[idx].answerOptions };
            }
            renderQuestions();
            showToast('Question updated', 'success');
        }
    } catch (e) {
        showToast(e.message, 'error');
    }
}

async function deleteQuestion(q) {
    if (q.retired) {
        showToast('This question is already retired', 'error');
        return;
    }
    const msg = 'Remove this question? If client data exists it will be retired (not deleted) so upload history stays intact.';
    if (!(await confirmAction({
        title: 'Remove question',
        message: msg,
        confirmLabel: 'Remove',
        variant: 'danger',
    }))) return;
    try {
        const data = await apiDelete('questions.php', { questionID: q.questionID });
        questions = questions.filter(x => x.questionID !== q.questionID);
        if (data.retired) {
            retiredQuestions.push({ ...q, retired: true, retiredAt: Date.now() / 1000 | 0 });
            showToast('Question retired', 'success');
        } else {
            showToast('Question deleted', 'success');
        }
        notifyStructureBump(data);
        renderQuestions();
    } catch (e) {
        showToast(e.message, 'error');
    }
}

// ── Answer option CRUD ───────────────────────────────────────────────────

async function updateOption(q, answerOptionID, fields) {
    try {
        const data = await apiPut('answer_options.php', { answerOptionID, ...fields });
        if (data.success) {
            const idx = q.answerOptions.findIndex(o => o.answerOptionID === answerOptionID);
            if (idx >= 0) q.answerOptions[idx] = { ...q.answerOptions[idx], ...data.answerOption };
            renderQuestionBody(q);
            showToast('Option updated', 'success');
        }
    } catch (e) {
        showToast(e.message, 'error');
    }
}

async function deleteOption(q, answerOptionID) {
    if (!(await confirmAction({
        title: 'Remove answer option',
        message: 'Remove this answer option?',
        confirmLabel: 'Remove',
        variant: 'danger',
    }))) return;
    try {
        const data = await apiDelete('answer_options.php', { answerOptionID });
        if (!data.retired) {
            q.answerOptions = (q.answerOptions || []).filter(o => o.answerOptionID !== answerOptionID);
        }
        renderQuestionBody(q);
        notifyStructureBump(data);
        showToast(data.retired ? 'Option retired' : 'Option deleted', 'success');
    } catch (e) {
        showToast(e.message, 'error');
    }
}

// ── Translations tab ─────────────────────────────────────────────────────

let transData = null;
let editorTransLang = '';
let editorTransSaveTimers = {};

async function loadTranslationsTab() {
    const container = document.getElementById('translationsContent');
    if (!container) return;
    container.innerHTML = '<div class="spinner"></div>';

    try {
        try {
            await apiPut('translations.php', { type: 'language', languageCode: SOURCE_LANG, name: 'German' });
        } catch (_) { /* already exists */ }
        const data = await apiGet(`translations.php?questionnaireID=${encodeURIComponent(questionnaire.questionnaireID)}`);
        transData = data;
        transData.entries = (transData.entries || []).map(normalizeEntry);
        renderTranslationsTab();
    } catch (e) {
        container.innerHTML = `<p class="error-text">${esc(e.message)}</p>`;
    }
}

function renderTranslationsTab() {
    const container = document.getElementById('translationsContent');
    if (!container || !transData) return;
    const editable = canEdit();
    const languages = transData.languages || [];
    const qnEntries = transData.entries || [];
    const targets = targetLanguages(languages);

    if (!qnEntries.length) {
        container.innerHTML = `
            ${editable ? languageManagerHTML(languages) : ''}
            <p style="color:var(--text-secondary);margin-top:12px">No translatable content yet. Add questions with German text first.</p>
        `;
        if (editable) bindLanguageManager(languages);
        return;
    }

    if (!editorTransLang || !targets.some(l => l.languageCode === editorTransLang)) {
        editorTransLang = targets[0]?.languageCode || '';
    }

    const langSelectHtml = targets.length
        ? `<label class="trans-control" style="max-width:280px;margin-bottom:14px">
                <span>Translate into</span>
                <select id="editorTransLangSelect" class="trans-select">
                    ${targets.map(l => {
                        const label = l.name ? `${l.name} (${l.languageCode})` : l.languageCode;
                        return `<option value="${escHtml(l.languageCode)}" ${l.languageCode === editorTransLang ? 'selected' : ''}>${escHtml(label)}</option>`;
                    }).join('')}
                </select>
           </label>`
        : `<p class="text-muted" style="margin-bottom:14px">Add a language below (e.g. English) to translate into.</p>`;

    container.innerHTML = `
        ${editable ? languageManagerHTML(languages) : ''}
        ${langSelectHtml}
        ${targets.length && editorTransLang ? renderEditorTransList(qnEntries, editable) : ''}
    `;

    if (editable) bindLanguageManager(languages);

    document.getElementById('editorTransLangSelect')?.addEventListener('change', (e) => {
        editorTransLang = e.target.value;
        renderTranslationsTab();
    });

    if (targets.length && editorTransLang) {
        bindEditorTransEditing(qnEntries, editable);
        bindEditorTransFiltering(qnEntries);
    }
}

function renderEditorTransList(qnEntries, editable) {
    const targets = targetLanguages(transData.languages || []);
    const langLabel = targets.find(l => l.languageCode === editorTransLang)?.name || editorTransLang.toUpperCase();
    const sourceLabel = sourceLanguageLabel(transData.languages || []);
    const missingCount = qnEntries.filter(e => !translationFor(e, editorTransLang).trim()).length;
    const pct = qnEntries.length ? Math.round(((qnEntries.length - missingCount) / qnEntries.length) * 100) : 0;

    const qnGroups = buildTranslationGroups(qnEntries, editorTransLang, 0);
    const bodyHtml = renderCollapsibleGroupedTranslationsHTML(qnGroups, editorTransLang, sourceLabel, editable, {
        openGroupsWithMissing: true,
    });

    return `
        <div class="trans-toolbar">
            <input type="search" id="transFilter" class="trans-search-input" placeholder="Search…">
            <select id="transTypeFilter" class="trans-select trans-type-select">
                <option value="">All types</option>
                <option value="string">UI strings</option>
                <option value="question">Questions</option>
                <option value="answer_option">Answer options</option>
            </select>
        </div>
        <div class="trans-sheet">
            ${translationListHeaderHTML(langLabel, sourceLabel)}
            <div id="transGroupedRoot" class="trans-sheet-body">
                <div class="trans-scope-block">${bodyHtml}</div>
            </div>
        </div>
        <div class="trans-stats">
            <span id="transStats">${missingCount ? `${missingCount} missing · ` : ''}${qnEntries.length} keys</span>
            <span class="trans-stats-progress">
                <span class="trans-progress-bar"><span class="trans-progress-fill" style="width:${pct}%"></span></span>
                <span class="trans-progress-label">${pct}% in ${escHtml(editorTransLang.toUpperCase())}</span>
            </span>
        </div>
    `;
}

function bindEditorTransEditing(entries, editable) {
    if (!editable) return;
    editorTransSaveTimers = {};
    document.querySelectorAll('#transGroupedRoot .trans-cell-input').forEach(inp => {
        inp.addEventListener('input', () => {
            const idx = parseInt(inp.dataset.idx, 10);
            const entry = entries[idx];
            if (!entry) return;
            const isGerman = inp.dataset.field === 'german';
            if (!entry.translations || typeof entry.translations !== 'object') entry.translations = {};
            if (isGerman) {
                entry.germanText = inp.value;
                entry.translations[SOURCE_LANG] = inp.value;
            } else {
                entry.translations[editorTransLang] = inp.value;
                const missing = !inp.value.trim();
                inp.classList.toggle('trans-input-missing', missing);
                const row = inp.closest('.trans-list-item');
                if (row) {
                    row.classList.toggle('trans-list-item-missing', missing);
                    row.dataset.missing = missing ? '1' : '0';
                }
            }
            const timerKey = isGerman ? `${idx}_de` : `${idx}_${editorTransLang}`;
            clearTimeout(editorTransSaveTimers[timerKey]);
            editorTransSaveTimers[timerKey] = setTimeout(() => saveEditorTranslation(entry, inp, isGerman), 500);
        });
    });
}

async function saveEditorTranslation(entry, inp, isGerman) {
    try {
        await apiPut('translations.php', {
            type: entry.type,
            id: entry.entityId,
            languageCode: isGerman ? SOURCE_LANG : editorTransLang,
            text: inp.value,
        });
        inp.classList.add('save-flash');
        setTimeout(() => inp.classList.remove('save-flash'), 400);
    } catch (e) {
        showToast(e.message, 'error');
    }
}

function bindEditorTransFiltering(entries) {
    const filterInput = document.getElementById('transFilter');
    const typeFilter = document.getElementById('transTypeFilter');
    const root = document.getElementById('transGroupedRoot');
    if (!filterInput || !root) return;

    function applyFilter() {
        const text = filterInput.value.toLowerCase();
        const type = typeFilter?.value || '';
        let visible = 0;
        let visibleMissing = 0;
        root.querySelectorAll('.trans-list-item').forEach(row => {
            const key = row.dataset.key?.toLowerCase() || '';
            const label = row.dataset.label?.toLowerCase() || '';
            const rowType = row.dataset.type || '';
            const german = row.querySelector('.trans-source-text')?.textContent?.toLowerCase()
                || row.querySelector('[data-field="german"]')?.value?.toLowerCase() || '';
            const val = row.querySelector('[data-field="translation"]')?.value?.toLowerCase() || '';
            const show = (!type || rowType === type) &&
                (!text || key.includes(text) || label.includes(text) || german.includes(text) || val.includes(text));
            row.classList.toggle('trans-row-hidden', !show);
            if (show) {
                visible++;
                if (row.dataset.missing === '1') visibleMissing++;
            }
        });
        root.querySelectorAll('.trans-group-details').forEach(group => {
            const anyVisible = [...group.querySelectorAll('.trans-list-item')].some(r => !r.classList.contains('trans-row-hidden'));
            group.classList.toggle('trans-group-hidden', !anyVisible);
        });
        root.querySelectorAll('.trans-scope-block').forEach(block => {
            const anyVisible = [...block.querySelectorAll('.trans-list-item')].some(r => !r.classList.contains('trans-row-hidden'));
            block.classList.toggle('trans-qn-hidden', !anyVisible);
        });
        const stats = document.getElementById('transStats');
        if (stats) {
            stats.textContent = text || type
                ? `Showing ${visible} (${visibleMissing} missing)`
                : `${[...root.querySelectorAll('.trans-list-item')].filter(r => r.dataset.missing === '1').length} missing · ${entries.length} keys`;
        }
    }

    filterInput.addEventListener('input', applyFilter);
    typeFilter?.addEventListener('change', applyFilter);
}

function languageManagerHTML(languages) {
    const others = targetLanguages(languages);
    return `
        <div class="lang-manager">
            <p style="font-size:.85rem;color:var(--text-secondary);margin:0 0 10px">
                German source text is edited under Questions. Add other languages here.
            </p>
            <div class="lang-chips">
                <span class="lang-chip lang-chip-fixed">
                    <strong>DE</strong>
                    <span class="lang-chip-name">German (source)</span>
                </span>
                ${others.map(l => `
                    <span class="lang-chip">
                        <strong>${esc(l.languageCode.toUpperCase())}</strong>
                        ${l.name ? `<span class="lang-chip-name">${esc(l.name)}</span>` : ''}
                        <button class="lang-chip-remove" data-lc="${esc(l.languageCode)}" title="Remove language">&times;</button>
                    </span>
                `).join('')}
            </div>
            <div class="lang-add-row">
                <input type="text" id="newLangCode" placeholder="Code (en)" class="lang-add-code">
                <input type="text" id="newLangName" placeholder="Name (English)" class="lang-add-name">
                <button class="btn btn-sm btn-primary" id="addLangBtn">+ Add Language</button>
            </div>
        </div>
    `;
}

function bindLanguageManager(languages) {
    document.getElementById('addLangBtn')?.addEventListener('click', async () => {
        const code = document.getElementById('newLangCode').value.trim().toLowerCase();
        const name = document.getElementById('newLangName').value.trim();
        if (!code) { showToast('Language code is required', 'error'); return; }
        if (code === SOURCE_LANG) {
            showToast('German (de) is always available as the source language', 'error');
            return;
        }
        if (languages.some(l => l.languageCode === code)) {
            showToast('Language already exists', 'error');
            return;
        }
        try {
            await apiPut('translations.php', { type: 'language', languageCode: code, name });
            showToast(`Language "${code}" added`, 'success');
            editorTransLang = code;
            loadTranslationsTab();
        } catch (e) {
            showToast(e.message, 'error');
        }
    });

    document.querySelectorAll('.lang-chip-remove').forEach(btn => {
        btn.addEventListener('click', async () => {
            const lc = btn.dataset.lc;
            if (!(await confirmAction({
                title: 'Remove language',
                message: `Remove language "${lc}" and ALL its translations?`,
                confirmLabel: 'Remove',
                variant: 'danger',
            }))) return;
            try {
                await apiDelete('translations.php', { type: 'language', languageCode: lc });
                showToast(`Language "${lc}" removed`, 'success');
                loadTranslationsTab();
            } catch (e) {
                showToast(e.message, 'error');
            }
        });
    });
}

// ── Drag & drop (questions) ──────────────────────────────────────────────

function initDragReorder() {
    const list = document.getElementById('questionList');
    if (!list) return;
    let dragItem = null;

    list.addEventListener('dragstart', (e) => {
        const item = e.target.closest('.question-item');
        if (!item || !e.target.closest('.drag-handle')) { e.preventDefault(); return; }
        dragItem = item;
        item.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
    });

    list.addEventListener('dragover', (e) => {
        e.preventDefault();
        const afterEl = getDragAfterElement(list, e.clientY, '.question-item');
        if (afterEl) list.insertBefore(dragItem, afterEl);
        else list.appendChild(dragItem);
    });

    list.addEventListener('dragend', async () => {
        if (!dragItem) return;
        dragItem.classList.remove('dragging');
        const newOrder = [...list.querySelectorAll('.question-item')].map(el => el.dataset.id);
        dragItem = null;

        const map = {};
        questions.forEach(q => map[q.questionID] = q);
        questions = newOrder.map(id => map[id]).filter(Boolean);

        try {
            await apiPatch('questions.php', {
                questionnaireID: questionnaire.questionnaireID,
                order: newOrder
            });
            showToast('Order saved', 'success');
        } catch (e) {
            showToast(e.message, 'error');
        }
    });
}

function initOptionDrag(q) {
    const list = document.getElementById(`opts-${q.questionID}`);
    if (!list) return;
    let dragItem = null;

    list.addEventListener('dragstart', (e) => {
        const item = e.target.closest('.option-item');
        if (!item || !e.target.closest('.drag-handle')) { e.preventDefault(); return; }
        dragItem = item;
        item.classList.add('dragging');
        e.dataTransfer.effectAllowed = 'move';
    });

    list.addEventListener('dragover', (e) => {
        e.preventDefault();
        const afterEl = getDragAfterElement(list, e.clientY, '.option-item');
        if (afterEl) list.insertBefore(dragItem, afterEl);
        else list.appendChild(dragItem);
    });

    list.addEventListener('dragend', async () => {
        if (!dragItem) return;
        dragItem.classList.remove('dragging');
        const newOrder = [...list.querySelectorAll('.option-item')].map(el => el.dataset.id);
        dragItem = null;

        const map = {};
        (q.answerOptions || []).forEach(o => map[o.answerOptionID] = o);
        q.answerOptions = newOrder.map(id => map[id]).filter(Boolean);

        try {
            await apiPatch('answer_options.php', {
                questionID: q.questionID,
                order: newOrder
            });
            showToast('Option order saved', 'success');
        } catch (e) {
            showToast(e.message, 'error');
        }
    });
}

function getDragAfterElement(container, y, selector) {
    const elements = [...container.querySelectorAll(`${selector}:not(.dragging)`)];
    return elements.reduce((closest, child) => {
        const box = child.getBoundingClientRect();
        const offset = y - box.top - box.height / 2;
        if (offset < 0 && offset > closest.offset) return { offset, element: child };
        return closest;
    }, { offset: Number.NEGATIVE_INFINITY }).element;
}

function esc(s) {
    const d = document.createElement('div');
    d.textContent = s ?? '';
    return d.innerHTML;
}
