<?php
/**
 * Android app API endpoint.
 * GET (no params)           -> ordered questionnaire list with conditions
 * GET ?id=<questionnaireID> -> single questionnaire in app JSON format
 * GET ?translations=1       -> global app UI strings only (not questionnaire content); requires Bearer token
 * GET ?loginTranslations=1  -> login + auth-help UI strings only; **no auth** (pre-login screen)
 * GET ?clients=1            -> list of clients assigned to the authenticated coach
 * GET ?clientCode=X&answers=1 -> all completed questionnaire answers for one client (encrypted)
 * GET ?answersBulk=1          -> all assigned clients' answers in one payload (coach, encrypted)
 * GET ?scoringProfiles=1      -> active scoring profile definitions (encrypted)
 * GET ?scoringReview=1        -> coach review state (coachBand only; computed on device)
 * POST action=coachScoringReview -> coach agrees with or overrides calculated band (encrypted)
 * POST action=setClientNote      -> upsert counselor note for an assigned client (max 200 chars, encrypted)
 * POST                      -> submit interview answers for a client (coach / supervisor / admin).
 *                             Re-posting the same clientCode + questionnaireID updates answers in place (re-edit).
 *                             Sensitive POST/GET ?clients=1 bodies use encrypted payloads (HKDF from Bearer token).
 */

// Public login-screen catalog (auth + help_auth only; no PII).
if ($method === 'GET' && !empty($_GET['loginTranslations'])) {
    $opened = qdb_open_read_or_fail();
    [$pdo, $tmpDb, $lockFp] = $opened;
    $payload = [
        'translations'       => qdb_build_public_login_translations_map($pdo),
        'availableLanguages' => array_values(array_intersect(
            qdb_available_language_codes($pdo),
            ['de', 'en'],
        )),
    ];
    if ($payload['availableLanguages'] === []) {
        $payload['availableLanguages'] = ['de', 'en'];
    }
    qdb_discard($tmpDb, $lockFp);
    json_success($payload);
}

$tokenRec = require_valid_token();

if ($method === 'POST') {
    require_role(['admin', 'supervisor', 'coach'], $tokenRec);

    $body = read_encrypted_json_body($tokenRec['_token']);

    if (($body['action'] ?? '') === 'coachScoringReview') {
        require_fields($body, ['clientCode', 'profileID', 'coachBand']);
        $clientCode = trim((string)$body['clientCode']);
        $profileID = trim((string)$body['profileID']);
        $coachBand = trim((string)($body['coachBand'] ?? ''));
        $calculatedBand = trim((string)($body['calculatedBand'] ?? ''));
        $weightedTotal = isset($body['weightedTotal']) ? (float)$body['weightedTotal'] : null;

        try {
            $opened = qdb_open_write_or_fail();
            [$pdo, $tmpDb, $lockFp] = $opened;

            [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
            $clStmt = $pdo->prepare(
                "SELECT cl.clientCode FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
            );
            $clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
            if (!$clStmt->fetch()) {
                qdb_discard($tmpDb, $lockFp);
                json_error('NOT_FOUND', 'Client not found or not authorized', 404);
            }

            require_once __DIR__ . '/../lib/scoring.php';
            if (!qdb_is_valid_scoring_band($coachBand)) {
                qdb_discard($tmpDb, $lockFp);
                json_error('INVALID_FIELD', 'coachBand must be green, yellow, or red', 400);
            }

            $profile = qdb_save_coach_scoring_review(
                $pdo,
                $clientCode,
                $profileID,
                $coachBand,
                (string)($tokenRec['userID'] ?? ''),
                $weightedTotal,
                $calculatedBand !== '' ? $calculatedBand : null,
            );

            qdb_save($tmpDb, $lockFp);
            json_success(['profile' => $profile]);
        } catch (InvalidArgumentException $e) {
            qdb_discard($tmpDb ?? null, $lockFp ?? null);
            json_error('INVALID_FIELD', $e->getMessage(), 400);
        } catch (RuntimeException $e) {
            qdb_discard($tmpDb ?? null, $lockFp ?? null);
            json_error('NOT_FOUND', $e->getMessage(), 404);
        } catch (Throwable $e) {
            qdb_handler_fail($e, 'Coach scoring review', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
        }
    }

    if (($body['action'] ?? '') === 'setClientNote') {
        require_role(['coach'], $tokenRec);
        require_fields($body, ['clientCode']);
        $clientCode = trim((string)$body['clientCode']);
        require_once __DIR__ . '/../lib/analytics.php';
        $note = qdb_normalize_client_followup_note($body['note'] ?? '');

        try {
            $opened = qdb_open_write_or_fail();
            [$pdo, $tmpDb, $lockFp] = $opened;
            qdb_analytics_set_followup_note($pdo, $tokenRec, $clientCode, $note);
            qdb_save($tmpDb, $lockFp);
            json_success_sensitive(
                ['clientCode' => $clientCode, 'note' => $note],
                $tokenRec['_token']
            );
        } catch (Throwable $e) {
            qdb_handler_fail($e, 'Save client note', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
        }
    }

    require_fields($body, ['questionnaireID', 'clientCode', 'answers']);

    $qnID      = trim($body['questionnaireID']);
    $clientCode = trim($body['clientCode']);
    $answers   = $body['answers'];

    if (!is_array($answers)) {
        json_error('INVALID_FIELD', 'answers must be an array', 400);
    }

    $startedAt   = isset($body['startedAt'])   ? (int)$body['startedAt']   : null;
    $completedAt = isset($body['completedAt']) ? (int)$body['completedAt'] : null;

    try {
        $opened = qdb_open_write_or_fail();
        [$pdo, $tmpDb, $lockFp] = $opened;

        require_once __DIR__ . '/../lib/questionnaire_structure.php';

        $qnStmt = $pdo->prepare(
            'SELECT questionnaireID, COALESCE(structureRevision, 1) AS structureRevision
             FROM questionnaire WHERE questionnaireID = :id'
        );
        $qnStmt->execute([':id' => $qnID]);
        $qnRow = $qnStmt->fetch(PDO::FETCH_ASSOC);
        if (!$qnRow) {
            qdb_discard($tmpDb, $lockFp);
            json_error('NOT_FOUND', 'Questionnaire not found', 404);
        }
        $currentRev = max(1, (int)$qnRow['structureRevision']);
        $submitRev  = max(1, (int)($body['structureRevision'] ?? 1));
        if ($submitRev > $currentRev) {
            qdb_discard($tmpDb, $lockFp);
            json_error('STRUCTURE_REVISION_NEWER', 'App structure revision is newer than server', 400);
        }

        [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
        $clStmt = $pdo->prepare(
            "SELECT cl.clientCode, cl.coachID FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
        );
        $clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
        $clientRow = $clStmt->fetch(PDO::FETCH_ASSOC);
        if (!$clientRow) {
            qdb_discard($tmpDb, $lockFp);
            json_error('NOT_FOUND', 'Client not found or not authorized', 404);
        }

        $assignedByCoach = ($tokenRec['role'] === 'coach')
            ? ($tokenRec['entityID'] ?? '')
            : ($clientRow['coachID'] ?? '');

        $isLegacySubmit = $submitRev < $currentRev;
        if ($isLegacySubmit) {
            $submitManifest = qdb_load_structure_manifest($pdo, $qnID, $submitRev);
            if ($submitManifest === null) {
                qdb_discard($tmpDb, $lockFp);
                json_error('STRUCTURE_REVISION_UNKNOWN', 'Unknown structure revision', 400);
            }
        } else {
            $submitManifest = qdb_build_structure_manifest($pdo, $qnID, false);
            $submitManifest['structureRevision'] = $currentRev;
        }

        $maps = qdb_submit_maps_from_manifest($pdo, $qnID, $submitManifest);
        $shortIdMap = $maps['shortIdMap'];
        $shortIdToType = $maps['shortIdToType'];
        $optionMap = $maps['optionMap'];
        $symptomParentMap = $maps['symptomParentMap'];
        $freeTextMaxLen = $maps['freeTextMaxLen'];

        require_once __DIR__ . '/../lib/app_submit_validate.php';
        $validationErrors = qdb_validate_app_submit_payload(
            $pdo,
            $qnID,
            $answers,
            $shortIdMap,
            $shortIdToType,
            $symptomParentMap,
            $optionMap,
            $submitManifest
        );
        if ($validationErrors !== []) {
            qdb_discard($tmpDb, $lockFp);
            json_error(
                'VALIDATION_FAILED',
                'Some answers are invalid or missing',
                400,
                ['errors' => $validationErrors]
            );
        }

        $pdo->beginTransaction();

        $glassByParent = [];
        $submittedQuestionIds = [];
        $answerInsert = $pdo->prepare('
            INSERT INTO client_answer (clientCode, questionID, answerOptionID, freeTextValue, numericValue, answeredAt)
            VALUES (:cc, :qid, :aoid, :ftv, :nv, :at) '
            . qdb_upsert_update(
                ['clientCode', 'questionID'],
                [
                    'answerOptionID' => 'excluded.answerOptionID',
                    'freeTextValue' => 'excluded.freeTextValue',
                    'numericValue' => 'excluded.numericValue',
                    'answeredAt' => 'excluded.answeredAt',
                ]
            )
        );
        $answerDelete = $pdo->prepare(
            'DELETE FROM client_answer WHERE clientCode = :cc AND questionID = :qid'
        );

        require_once __DIR__ . '/../lib/app_answers.php';
        $answers = qdb_group_app_submit_answers($answers, $shortIdToType, $symptomParentMap);

        foreach ($answers as $a) {
            $shortId = trim($a['questionID'] ?? '');
            if ($shortId === '') {
                continue;
            }

            $answeredAt = isset($a['answeredAt']) ? (int)$a['answeredAt'] : null;

            // Glass-scale symptoms use string keys (not question localIds).
            if (isset($symptomParentMap[$shortId])) {
                $label = trim((string)($a['answerOptionKey'] ?? $a['freeTextValue'] ?? ''));
                if ($label !== '') {
                    $parentId = $symptomParentMap[$shortId];
                    $glassByParent[$parentId][$shortId] = $label;
                }
                continue;
            }

            // Resolve short ID to full questionID
            $fullQID = $shortIdMap[$shortId] ?? null;
            if ($fullQID === null) {
                continue;
            }

            $answerOptionID = null;
            $freeTextValue  = isset($a['freeTextValue'])  ? (string)$a['freeTextValue']  : null;
            $numericValue   = isset($a['numericValue'])   ? (float)$a['numericValue']    : null;

            if ($freeTextValue !== null && $freeTextValue !== '') {
                $maxLen = $freeTextMaxLen[$fullQID] ?? 2000;
                $freeTextValue = sanitize_free_text($freeTextValue, $maxLen);
                if ($freeTextValue === '') {
                    $answerDelete->execute([':cc' => $clientCode, ':qid' => $fullQID]);
                    $submittedQuestionIds[$fullQID] = true;
                    continue;
                }
            }

            // Resolve option key to answerOptionID (points computed by scoring engine after save)
            $optionKey = $a['answerOptionKey'] ?? null;
            if ($optionKey !== null && isset($optionMap[$fullQID][(string)$optionKey])) {
                $opt = $optionMap[$fullQID][(string)$optionKey];
                $answerOptionID = $opt['answerOptionID'];
            }

            $hasValue = $answerOptionID !== null
                || ($freeTextValue !== null && $freeTextValue !== '')
                || $numericValue !== null;
            if (!$hasValue) {
                $answerDelete->execute([':cc' => $clientCode, ':qid' => $fullQID]);
                $submittedQuestionIds[$fullQID] = true;
                continue;
            }

            $answerInsert->execute([
                ':cc'   => $clientCode,
                ':qid'  => $fullQID,
                ':aoid' => $answerOptionID,
                ':ftv'  => $freeTextValue,
                ':nv'   => $numericValue,
                ':at'   => $answeredAt,
            ]);
            $submittedQuestionIds[$fullQID] = true;
        }

        foreach ($glassByParent as $parentQID => $symptomLabels) {
            $json = qdb_build_glass_symptom_json($symptomLabels);
            if ($json === null) {
                $answerDelete->execute([':cc' => $clientCode, ':qid' => $parentQID]);
            } else {
                $answerInsert->execute([
                    ':cc'   => $clientCode,
                    ':qid'  => $parentQID,
                    ':aoid' => null,
                    ':ftv'  => $json,
                    ':nv'   => null,
                    ':at'   => $completedAt ?? $startedAt,
                ]);
            }
            $submittedQuestionIds[$parentQID] = true;
        }

        if (!$isLegacySubmit) {
            $activeMaps = qdb_submit_maps_from_active_questions($pdo, $qnID);
            $staleQuestionIds = array_diff(
                array_values($activeMaps['shortIdMap']),
                array_keys($submittedQuestionIds)
            );
            if ($staleQuestionIds !== []) {
                $placeholders = implode(',', array_fill(0, count($staleQuestionIds), '?'));
                $staleDelete = $pdo->prepare(
                    "DELETE FROM client_answer WHERE clientCode = ? AND questionID IN ($placeholders)"
                );
                $staleDelete->execute(array_merge([$clientCode], array_values($staleQuestionIds)));
            }
        }

        require_once __DIR__ . '/../lib/scoring.php';
        $sumPoints = $isLegacySubmit
            ? qdb_compute_questionnaire_score_from_manifest($pdo, $clientCode, $submitManifest)
            : qdb_compute_questionnaire_score($pdo, $clientCode, $qnID);

        $pdo->prepare('
            INSERT INTO completed_questionnaire (clientCode, questionnaireID, assignedByCoach, status, startedAt, completedAt, sumPoints)
            VALUES (:cc, :qn, :abc, \'completed\', :sa, :ca, :sp) '
            . qdb_upsert_update(
                ['clientCode', 'questionnaireID'],
                [
                    'assignedByCoach' => 'excluded.assignedByCoach',
                    'status' => "'completed'",
                    'startedAt' => 'COALESCE(excluded.startedAt, startedAt)',
                    'completedAt' => 'excluded.completedAt',
                    'sumPoints' => 'excluded.sumPoints',
                ]
            )
        )->execute([
            ':cc'  => $clientCode,
            ':qn'  => $qnID,
            ':abc' => $assignedByCoach !== '' ? $assignedByCoach : null,
            ':sa'  => $startedAt,
            ':ca'  => $completedAt,
            ':sp'  => $sumPoints,
        ]);

        require_once __DIR__ . '/../lib/submissions.php';
        qdb_record_submission_after_submit(
            $pdo,
            $clientCode,
            $qnID,
            $tokenRec,
            $startedAt,
            $completedAt,
            $sumPoints,
            $assignedByCoach,
            $submitRev,
            $submitManifest,
            array_keys($submittedQuestionIds)
        );

        qdb_recompute_profile_scores_for_client($pdo, $clientCode, $qnID);

        $pdo->commit();
        qdb_save($tmpDb, $lockFp);

        json_success([
            'submitted'         => true,
            'sumPoints'         => $sumPoints,
            'structureRevision' => $submitRev,
            'legacySubmit'      => $isLegacySubmit,
        ]);
    } catch (Throwable $e) {
        qdb_handler_fail($e, 'Submit questionnaire', $pdo ?? null, $tmpDb ?? null, $lockFp ?? null);
    }
}

if ($method !== 'GET') {
    json_error('METHOD_NOT_ALLOWED', 'Method not allowed', 405);
}

$opened = qdb_open_read_or_fail();
[$pdo, $tmpDb, $lockFp] = $opened;

$qnID         = $_GET['id'] ?? '';
$translations = $_GET['translations'] ?? '';
$fetchClients = $_GET['clients'] ?? '';
$fetchAnswers = $_GET['answers'] ?? '';
$fetchAnswersBulk = $_GET['answersBulk'] ?? '';
$fetchScoringProfiles = $_GET['scoringProfiles'] ?? '';
$fetchScoringReview = $_GET['scoringReview'] ?? '';
$clientCode   = trim($_GET['clientCode'] ?? '');

if ($translations) {
    $payload = [
        'translations'        => qdb_build_app_translations_map($pdo),
        'availableLanguages'  => qdb_available_language_codes($pdo),
    ];
    qdb_discard($tmpDb, $lockFp);
    json_success($payload);
}

if ($fetchScoringProfiles) {
    require_role(['coach'], $tokenRec);
    require_once __DIR__ . '/../lib/scoring.php';
    $profiles = array_values(array_filter(
        qdb_list_scoring_profiles($pdo),
        static fn(array $p): bool => (int)($p['isActive'] ?? 0) === 1
    ));
    qdb_discard($tmpDb, $lockFp);
    json_success_sensitive(['profiles' => $profiles], $tokenRec['_token']);
}

if ($fetchScoringReview) {
    require_role(['coach'], $tokenRec);
    require_once __DIR__ . '/../lib/scoring.php';

    $coachID = $tokenRec['entityID'] ?? '';
    if ($clientCode !== '') {
        [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
        $clStmt = $pdo->prepare(
            "SELECT cl.clientCode FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
        );
        $clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
        if (!$clStmt->fetch()) {
            qdb_discard($tmpDb, $lockFp);
            json_error('NOT_FOUND', 'Client not found or not authorized', 404);
        }
        $clientCodes = [$clientCode];
    } else {
        $stmt = $pdo->prepare(
            'SELECT clientCode FROM client
             WHERE coachID = :cid AND COALESCE(archived, 0) = 0
             ORDER BY clientCode'
        );
        $stmt->execute([':cid' => $coachID]);
        $clientCodes = $stmt->fetchAll(PDO::FETCH_COLUMN);
    }

    $clients = qdb_app_scoring_review_for_clients($pdo, $clientCodes);
    qdb_discard($tmpDb, $lockFp);
    json_success_sensitive(['clients' => $clients], $tokenRec['_token']);
}

if ($fetchAnswersBulk) {
    require_role(['coach'], $tokenRec);
    require_once __DIR__ . '/../lib/app_answers.php';
    $clients = qdb_export_app_bulk_answers_for_coach($pdo, $tokenRec);
    qdb_discard($tmpDb, $lockFp);
    json_success_sensitive(['clients' => $clients], $tokenRec['_token']);
}

if ($fetchAnswers) {
    require_role(['admin', 'supervisor', 'coach'], $tokenRec);
    if ($clientCode === '') {
        qdb_discard($tmpDb, $lockFp);
        json_error('MISSING_PARAM', 'clientCode query param required', 400);
    }

    [$rbacClause, $rbacParams] = rbac_client_filter($tokenRec, 'cl');
    $clStmt = $pdo->prepare(
        "SELECT cl.clientCode FROM client cl WHERE cl.clientCode = :cc AND ($rbacClause)"
    );
    $clStmt->execute(array_merge([':cc' => $clientCode], $rbacParams));
    if (!$clStmt->fetch()) {
        qdb_discard($tmpDb, $lockFp);
        json_error('NOT_FOUND', 'Client not found or not authorized', 404);
    }

    require_once __DIR__ . '/../lib/app_answers.php';
    $questionnaires = qdb_export_app_client_answers_bundle($pdo, $clientCode);
    qdb_discard($tmpDb, $lockFp);
    json_success_sensitive([
        'clientCode'     => $clientCode,
        'questionnaires' => $questionnaires,
    ], $tokenRec['_token']);
}

if ($fetchClients) {
    require_role(['coach'], $tokenRec);

    $coachID = $tokenRec['entityID'] ?? '';

    $stmt = $pdo->prepare("
        SELECT clientCode
        FROM client
        WHERE coachID = :cid AND COALESCE(archived, 0) = 0
        ORDER BY clientCode
    ");
    $stmt->execute([':cid' => $coachID]);
    $clientCodes = $stmt->fetchAll(PDO::FETCH_COLUMN);

    $completions = [];
    $notesByClient = [];
    if (!empty($clientCodes)) {
        $placeholders = implode(',', array_fill(0, count($clientCodes), '?'));
        $stmt2 = $pdo->prepare("
            SELECT clientCode, questionnaireID, sumPoints, completedAt
            FROM completed_questionnaire
            WHERE clientCode IN ($placeholders)
        ");
        $stmt2->execute($clientCodes);
        foreach ($stmt2->fetchAll(PDO::FETCH_ASSOC) as $row) {
            $completions[$row['clientCode']][] = [
                'questionnaireID' => $row['questionnaireID'],
                'sumPoints'       => (int) $row['sumPoints'],
                'completedAt'     => $row['completedAt'] !== null ? (int) $row['completedAt'] : null,
            ];
        }

        if (qdb_table_exists($pdo, 'client_followup_note')) {
            $stmtNotes = $pdo->prepare(
                "SELECT clientCode, note FROM client_followup_note WHERE clientCode IN ($placeholders)"
            );
            $stmtNotes->execute($clientCodes);
            foreach ($stmtNotes->fetchAll(PDO::FETCH_ASSOC) as $row) {
                $notesByClient[$row['clientCode']] = (string)($row['note'] ?? '');
            }
        }
    }

    $clients = array_map(function ($code) use ($completions, $notesByClient) {
        return [
            'clientCode'              => $code,
            'completedQuestionnaires' => $completions[$code] ?? [],
            'note'                    => $notesByClient[$code] ?? '',
        ];
    }, $clientCodes);

    qdb_discard($tmpDb, $lockFp);
    json_success_sensitive(['coachID' => $coachID, 'clients' => $clients], $tokenRec['_token']);
}

if ($qnID) {
    require_once __DIR__ . '/../lib/questionnaire_structure.php';
    $qn = $pdo->prepare("SELECT * FROM questionnaire WHERE questionnaireID = :id");
    $qn->execute([':id' => $qnID]);
    $qnRow = $qn->fetch(PDO::FETCH_ASSOC);
    if (!$qnRow) {
        qdb_discard($tmpDb, $lockFp);
        json_error('NOT_FOUND', 'Questionnaire not found', 404);
    }

    $structureRevision = qdb_questionnaire_structure_revision($pdo, $qnID);
    $stmt = $pdo->prepare(
        'SELECT questionID, defaultText, type, orderIndex, configJson
         FROM question WHERE questionnaireID = :id AND ' . qdb_active_questions_clause('question') . '
         ORDER BY orderIndex'
    );
    $stmt->execute([':id' => $qnID]);
    $dbQuestions = $stmt->fetchAll(PDO::FETCH_ASSOC);

    $questions = [];
    foreach ($dbQuestions as $dbQ) {
        $localId = $dbQ['questionID'];
        $parts = explode('__', $localId);
        $shortId = end($parts);

        $layout = $dbQ['type'];
        $config = qdb_parse_config_json($dbQ['configJson']);
        $qKey = qdb_question_key($config, $dbQ['defaultText']);

        $q = [
            'id'       => $shortId,
            'layout'   => $layout,
            'question' => $qKey !== '' ? $qKey : $dbQ['defaultText'],
        ];

        if ($qKey !== '' && !empty($config['noteBefore'])) {
            $q['noteBeforeKey'] = qdb_note_before_key($qKey);
        }
        if ($qKey !== '' && !empty($config['noteAfter'])) {
            $q['noteAfterKey'] = qdb_note_after_key($qKey);
        }
        if (isset($config['textKey']))      $q['textKey']      = $config['textKey'];
        if (isset($config['textKey1']))     $q['textKey1']     = $config['textKey1'];
        if (isset($config['textKey2']))     $q['textKey2']     = $config['textKey2'];
        if (isset($config['hint']))         $q['hint']         = $config['hint'];
        if (isset($config['hint1']))        $q['hint1']        = $config['hint1'];
        if (isset($config['hint2']))        $q['hint2']        = $config['hint2'];
        if (isset($config['symptoms']))     $q['symptoms']     = $config['symptoms'];
        if ($layout === 'glass_scale_question' && !empty($config['symptoms'])) {
            $q['glassSymptoms'] = qdb_glass_symptoms_with_labels($pdo, $config);
        }
        if (isset($config['scaleType']))    $q['scaleType']    = $config['scaleType'];
        if (isset($config['range']))        $q['range']        = $config['range'];
        if (isset($config['step']))         $q['step']         = (int)$config['step'];
        if (isset($config['unitLabel']))    $q['unitLabel']    = $config['unitLabel'];
        if (isset($config['constraints']))  $q['constraints']  = $config['constraints'];
        if (isset($config['precision']))    $q['precision']    = $config['precision'];
        if (isset($config['dateRange']))    $q['dateRange']    = $config['dateRange'];
        if (isset($config['minSelection'])) $q['minSelection'] = $config['minSelection'];
        if (isset($config['maxLength']))    $q['maxLength']    = (int)$config['maxLength'];
        if (isset($config['nextQuestionId']))        $q['nextQuestionId']        = $config['nextQuestionId'];
        if (isset($config['otherNextQuestionId'])) $q['otherNextQuestionId'] = $config['otherNextQuestionId'];
        if (isset($config['otherOptionKey']))      $q['otherOptionKey']      = $config['otherOptionKey'];

        if ($layout === 'string_spinner' && isset($config['options'])) {
            $q['options'] = $config['options'];
        }
        if (($layout === 'value_spinner' || $layout === 'slider_question') && isset($config['valueOptions'])) {
            $q['options'] = $config['valueOptions'];
        }

        if (in_array($layout, ['glass_scale_question', 'slider_question', 'value_spinner'], true)) {
            require_once __DIR__ . '/../lib/scoring.php';
            $ruleMap = qdb_score_rule_map_for_question($pdo, $dbQ['questionID']);
            if ($ruleMap !== []) {
                $q['scoreRuleMap'] = $ruleMap;
            }
        }

        $ao = $pdo->prepare(
            'SELECT defaultText, points, nextQuestionId
             FROM answer_option WHERE questionID = :qid AND ' . qdb_active_options_clause('answer_option') . '
             ORDER BY orderIndex'
        );
        $ao->execute([':qid' => $dbQ['questionID']]);
        $opts = $ao->fetchAll(PDO::FETCH_ASSOC);

        if ($opts) {
            $optionsArr = [];
            $pointsMap = [];
            foreach ($opts as $opt) {
                $o = ['key' => $opt['defaultText']];
                if ($opt['nextQuestionId'] !== '') {
                    $o['nextQuestionId'] = $opt['nextQuestionId'];
                }
                $optionsArr[] = $o;
                $pointsMap[$opt['defaultText']] = (int)$opt['points'];
            }
            // DB answer_option rows are for choice layouts only — do not overwrite string/value spinners.
            if (in_array($layout, ['radio_question', 'multi_check_box_question', 'glass_scale_question'], true)) {
                $q['options'] = $optionsArr;
                $q['pointsMap'] = $pointsMap;
            }
        }

        $questions[] = $q;
    }

    qdb_discard($tmpDb, $lockFp);
    json_success([
        'meta'                => ['id' => $qnID],
        'structureRevision'   => $structureRevision,
        'questions'           => $questions,
        'translations'        => qdb_build_questionnaire_translations_map($pdo, $qnID),
        'availableLanguages'  => qdb_available_language_codes($pdo, $qnID),
    ]);
}

// Default: ordered questionnaire list
require_once __DIR__ . '/../lib/questionnaire_structure.php';

$rows = $pdo->query("
    SELECT questionnaireID, name, showPoints, conditionJson, categoryKey,
           COALESCE(structureRevision, 1) AS structureRevision
    FROM questionnaire
    WHERE state = 'active'
    ORDER BY orderIndex
")->fetchAll(PDO::FETCH_ASSOC);

$list = [];
foreach ($rows as $r) {
    $item = [
        'id'                => $r['questionnaireID'],
        'name'              => $r['name'],
        'showPoints'        => (bool)(int)$r['showPoints'],
        'structureRevision' => max(1, (int)$r['structureRevision']),
        'condition'         => json_decode($r['conditionJson'], true) ?: new stdClass(),
    ];
    $cat = trim($r['categoryKey'] ?? '');
    if ($cat !== '') {
        $item['categoryKey'] = $cat;
    }
    $list[] = $item;
}

qdb_discard($tmpDb, $lockFp);
json_success($list);
