<?php
// Shared helpers for opening, creating, and saving the questionnaire database.
require_once __DIR__ . '/common.php';
require_once __DIR__ . '/lib/sql_dialect.php';

if (defined('QDB_TEST_UPLOADS')) {
    define('QDB_UPLOADS_DIR', QDB_TEST_UPLOADS);
    define('QDB_PATH', QDB_UPLOADS_DIR . '/questionnaire_database');
    define('QDB_LOCK', QDB_UPLOADS_DIR . '/.qdb_lock');
} else {
    define('QDB_UPLOADS_DIR', __DIR__ . '/uploads');
    define('QDB_PATH', QDB_UPLOADS_DIR . '/questionnaire_database');
    define('QDB_LOCK', QDB_UPLOADS_DIR . '/.qdb_lock');
}
define('QDB_SCHEMA', __DIR__ . '/schema.sql');
define('QDB_VERSION', 14);

/**
 * Apply incremental schema fixes. Returns true if the DB was modified.
 */
function qdb_apply_migrations(PDO $pdo, string $schemaSql): bool {
    $changed = false;

    if (!qdb_column_exists($pdo, 'questionnaire', 'categoryKey')) {
        $pdo->exec("ALTER TABLE questionnaire ADD COLUMN categoryKey TEXT NOT NULL DEFAULT ''");
        $changed = true;
    }

    if (qdb_table_exists($pdo, 'client')) {
        if (!qdb_column_exists($pdo, 'client', 'archived')) {
            $pdo->exec('ALTER TABLE client ADD COLUMN archived INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'client', 'archivedAt')) {
            $pdo->exec('ALTER TABLE client ADD COLUMN archivedAt INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
    }

    if (qdb_table_exists($pdo, 'questionnaire')) {
        if (!qdb_column_exists($pdo, 'questionnaire', 'structureRevision')) {
            $pdo->exec('ALTER TABLE questionnaire ADD COLUMN structureRevision INTEGER NOT NULL DEFAULT 1');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'questionnaire', 'structureChangedAt')) {
            $pdo->exec('ALTER TABLE questionnaire ADD COLUMN structureChangedAt INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
    }

    if (!qdb_table_exists($pdo, 'questionnaire_structure_snapshot')) {
        $pdo->exec("
            CREATE TABLE questionnaire_structure_snapshot (
                questionnaireID   TEXT    NOT NULL,
                structureRevision INTEGER NOT NULL,
                createdAt         INTEGER NOT NULL,
                manifestJson      TEXT    NOT NULL DEFAULT '{}',
                PRIMARY KEY (questionnaireID, structureRevision),
                FOREIGN KEY(questionnaireID) REFERENCES questionnaire(questionnaireID) ON DELETE CASCADE
            )
        ");
        $changed = true;
    }

    if (qdb_table_exists($pdo, 'question')) {
        if (!qdb_column_exists($pdo, 'question', 'retiredAt')) {
            $pdo->exec('ALTER TABLE question ADD COLUMN retiredAt INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'question', 'retiredInRevision')) {
            $pdo->exec('ALTER TABLE question ADD COLUMN retiredInRevision INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
    }

    if (qdb_table_exists($pdo, 'answer_option')) {
        if (!qdb_column_exists($pdo, 'answer_option', 'retiredAt')) {
            $pdo->exec('ALTER TABLE answer_option ADD COLUMN retiredAt INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
    }

    if (qdb_table_exists($pdo, 'questionnaire_submission')) {
        if (!qdb_column_exists($pdo, 'questionnaire_submission', 'structureRevision')) {
            $pdo->exec('ALTER TABLE questionnaire_submission ADD COLUMN structureRevision INTEGER NOT NULL DEFAULT 1');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'questionnaire_submission', 'structureSnapshotJson')) {
            $pdo->exec("ALTER TABLE questionnaire_submission ADD COLUMN structureSnapshotJson TEXT NOT NULL DEFAULT '{}'");
            $changed = true;
        }
    }

    if (qdb_table_exists($pdo, 'questionnaire_structure_snapshot')
        && qdb_table_exists($pdo, 'questionnaire')) {
        require_once __DIR__ . '/lib/questionnaire_structure.php';
        if (qdb_backfill_structure_snapshots($pdo)) {
            $changed = true;
        }
    }

    if (!qdb_table_exists($pdo, 'questionnaire_submission')) {
        $pdo->exec("
            CREATE TABLE questionnaire_submission (
                submissionID      TEXT    NOT NULL PRIMARY KEY,
                clientCode        TEXT    NOT NULL,
                questionnaireID   TEXT    NOT NULL,
                version           INTEGER NOT NULL,
                submittedAt       INTEGER NOT NULL,
                submittedByUserID TEXT    NOT NULL DEFAULT '',
                submittedByRole   TEXT    NOT NULL DEFAULT '',
                assignedByCoach   TEXT,
                status            TEXT    NOT NULL DEFAULT '',
                startedAt         INTEGER,
                completedAt       INTEGER,
                sumPoints         INTEGER,
                FOREIGN KEY(clientCode)        REFERENCES client(clientCode),
                FOREIGN KEY(questionnaireID)   REFERENCES questionnaire(questionnaireID),
                FOREIGN KEY(assignedByCoach)   REFERENCES coach(coachID),
                UNIQUE(clientCode, questionnaireID, version)
            )
        ");
        qdb_create_index_if_not_exists($pdo, 'idx_submission_client_qn', 'questionnaire_submission', 'clientCode, questionnaireID');
        qdb_create_index_if_not_exists($pdo, 'idx_submission_client_time', 'questionnaire_submission', 'clientCode, submittedAt');
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'client_answer_submission')) {
        $pdo->exec("
            CREATE TABLE client_answer_submission (
                submissionID   TEXT NOT NULL,
                questionID     TEXT NOT NULL,
                answerOptionID TEXT,
                freeTextValue  TEXT,
                numericValue   REAL,
                answeredAt     INTEGER,
                PRIMARY KEY (submissionID, questionID),
                FOREIGN KEY(submissionID) REFERENCES questionnaire_submission(submissionID) ON DELETE CASCADE,
                FOREIGN KEY(questionID)   REFERENCES question(questionID),
                FOREIGN KEY(answerOptionID) REFERENCES answer_option(answerOptionID)
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'client_followup_note')) {
        $pdo->exec("
            CREATE TABLE client_followup_note (
                clientCode      TEXT NOT NULL PRIMARY KEY,
                note            TEXT NOT NULL DEFAULT '',
                updatedByUserID TEXT NOT NULL DEFAULT '',
                updatedAt       INTEGER NOT NULL,
                FOREIGN KEY(clientCode) REFERENCES client(clientCode)
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'session')) {
        $pdo->exec("
            CREATE TABLE session (
                token     TEXT    NOT NULL PRIMARY KEY,
                userID    TEXT    NOT NULL,
                role      TEXT    NOT NULL,
                entityID  TEXT    NOT NULL DEFAULT '',
                createdAt INTEGER NOT NULL,
                expiresAt INTEGER NOT NULL,
                temp      INTEGER NOT NULL DEFAULT 0
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'system_setting')) {
        $pdo->exec("
            CREATE TABLE system_setting (
                settingKey   TEXT NOT NULL PRIMARY KEY,
                settingValue TEXT NOT NULL DEFAULT ''
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'question_score_rule')) {
        $pdo->exec("
            CREATE TABLE question_score_rule (
                ruleID     TEXT    NOT NULL PRIMARY KEY,
                questionID TEXT    NOT NULL,
                scopeKey   TEXT    NOT NULL DEFAULT '',
                levelKey   TEXT    NOT NULL,
                points     INTEGER NOT NULL DEFAULT 0,
                FOREIGN KEY(questionID) REFERENCES question(questionID) ON DELETE CASCADE,
                UNIQUE(questionID, scopeKey, levelKey)
            )
        ");
        qdb_create_index_if_not_exists($pdo, 'idx_score_rule_question', 'question_score_rule', 'questionID');
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'scoring_profile')) {
        $pdo->exec("
            CREATE TABLE scoring_profile (
                profileID   TEXT    NOT NULL PRIMARY KEY,
                name        TEXT    NOT NULL DEFAULT '',
                description TEXT    NOT NULL DEFAULT '',
                isActive    INTEGER NOT NULL DEFAULT 1,
                greenMin    INTEGER NOT NULL DEFAULT 0,
                greenMax    INTEGER NOT NULL DEFAULT 12,
                yellowMin   INTEGER NOT NULL DEFAULT 13,
                yellowMax   INTEGER NOT NULL DEFAULT 36,
                redMin      INTEGER NOT NULL DEFAULT 37,
                processCompleteBands TEXT NOT NULL DEFAULT '[]',
                createdAt   INTEGER NOT NULL DEFAULT 0,
                updatedAt   INTEGER NOT NULL DEFAULT 0
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'scoring_profile_questionnaire')) {
        $pdo->exec("
            CREATE TABLE scoring_profile_questionnaire (
                profileID       TEXT    NOT NULL,
                questionnaireID TEXT    NOT NULL,
                weight          REAL    NOT NULL DEFAULT 1.0,
                orderIndex      INTEGER NOT NULL DEFAULT 0,
                PRIMARY KEY (profileID, questionnaireID),
                FOREIGN KEY(profileID)       REFERENCES scoring_profile(profileID) ON DELETE CASCADE,
                FOREIGN KEY(questionnaireID) REFERENCES questionnaire(questionnaireID) ON DELETE CASCADE
            )
        ");
        $changed = true;
    }

    if (!qdb_table_exists($pdo, 'client_scoring_profile_result')) {
        $pdo->exec("
            CREATE TABLE client_scoring_profile_result (
                clientCode            TEXT NOT NULL,
                profileID             TEXT NOT NULL,
                weightedTotal         REAL NOT NULL DEFAULT 0,
                band                  TEXT NOT NULL DEFAULT '',
                computedAt            INTEGER NOT NULL DEFAULT 0,
                questionnaireSnapshot TEXT NOT NULL DEFAULT '{}',
                coachBand             TEXT NOT NULL DEFAULT '',
                coachReviewedAt       INTEGER NOT NULL DEFAULT 0,
                coachReviewedByUserID TEXT NOT NULL DEFAULT '',
                PRIMARY KEY (clientCode, profileID),
                FOREIGN KEY(clientCode) REFERENCES client(clientCode) ON DELETE CASCADE,
                FOREIGN KEY(profileID)  REFERENCES scoring_profile(profileID) ON DELETE CASCADE
            )
        ");
        qdb_create_index_if_not_exists($pdo, 'idx_client_profile_result_profile', 'client_scoring_profile_result', 'profileID');
        $changed = true;
    }

    if (qdb_table_exists($pdo, 'client_scoring_profile_result')) {
        if (!qdb_column_exists($pdo, 'client_scoring_profile_result', 'coachBand')) {
            $pdo->exec("ALTER TABLE client_scoring_profile_result ADD COLUMN coachBand TEXT NOT NULL DEFAULT ''");
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'client_scoring_profile_result', 'coachReviewedAt')) {
            $pdo->exec('ALTER TABLE client_scoring_profile_result ADD COLUMN coachReviewedAt INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'client_scoring_profile_result', 'coachReviewedByUserID')) {
            $pdo->exec("ALTER TABLE client_scoring_profile_result ADD COLUMN coachReviewedByUserID TEXT NOT NULL DEFAULT ''");
            $changed = true;
        }
    }

    if (qdb_table_exists($pdo, 'questionnaire_submission')) {
        require_once __DIR__ . '/lib/submissions.php';
        if (qdb_backfill_submissions_from_live($pdo)) {
            $changed = true;
        }
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 10
        && qdb_table_exists($pdo, 'completed_questionnaire')
        && qdb_table_exists($pdo, 'client_scoring_profile_result')) {
        require_once __DIR__ . '/lib/scoring.php';
        if (qdb_recompute_all_questionnaire_scores($pdo) > 0) {
            $changed = true;
        }
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 7 && qdb_table_exists($pdo, 'question_score_rule')) {
        require_once __DIR__ . '/lib/scoring.php';
        if (qdb_backfill_glass_score_rules($pdo) > 0) {
            $changed = true;
        }
        if (qdb_recompute_all_questionnaire_scores($pdo) > 0) {
            $changed = true;
        }
        if (qdb_seed_default_rhs_scoring_profile($pdo) !== null) {
            $changed = true;
        }
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 8 && qdb_table_exists($pdo, 'scoring_profile')) {
        if (!qdb_column_exists($pdo, 'scoring_profile', 'greenMin')) {
            $pdo->exec('ALTER TABLE scoring_profile ADD COLUMN greenMin INTEGER NOT NULL DEFAULT 0');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'scoring_profile', 'yellowMin')) {
            $pdo->exec('ALTER TABLE scoring_profile ADD COLUMN yellowMin INTEGER NOT NULL DEFAULT 13');
            $changed = true;
        }
        if (!qdb_column_exists($pdo, 'scoring_profile', 'redMin')) {
            $pdo->exec('ALTER TABLE scoring_profile ADD COLUMN redMin INTEGER NOT NULL DEFAULT 37');
            $changed = true;
        }
        $pdo->exec(
            'UPDATE scoring_profile SET
                greenMin = COALESCE(greenMin, 0),
                yellowMin = greenMax + 1,
                redMin = yellowMax + 1'
        );
        $changed = true;
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 13 && qdb_table_exists($pdo, 'scoring_profile')) {
        if (!qdb_column_exists($pdo, 'scoring_profile', 'processCompleteBands')) {
            $pdo->exec("ALTER TABLE scoring_profile ADD COLUMN processCompleteBands TEXT NOT NULL DEFAULT '[]'");
            $changed = true;
        }
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 14) {
        if (qdb_table_exists($pdo, 'language') && !qdb_column_exists($pdo, 'language', 'enabled')) {
            $pdo->exec('ALTER TABLE language ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1');
            $changed = true;
        }
        if (!qdb_table_exists($pdo, 'questionnaire_language_disable')) {
            $pdo->exec(
                'CREATE TABLE questionnaire_language_disable (
                    questionnaireID TEXT NOT NULL,
                    languageCode TEXT NOT NULL,
                    PRIMARY KEY (questionnaireID, languageCode),
                    FOREIGN KEY(questionnaireID) REFERENCES questionnaire(questionnaireID) ON DELETE CASCADE,
                    FOREIGN KEY(languageCode) REFERENCES language(languageCode) ON DELETE CASCADE
                )'
            );
            $changed = true;
        }
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < 6 && qdb_table_exists($pdo, 'questionnaire_submission')) {
        $pdo->exec(
            'UPDATE questionnaire_submission SET submittedAt = completedAt
             WHERE completedAt IS NOT NULL AND submittedAt != completedAt'
        );
        $changed = true;
    }

    $currentVersion = qdb_schema_version($pdo);
    if ($currentVersion < QDB_VERSION) {
        qdb_set_foreign_keys($pdo, false);
        try {
            qdb_exec_schema_sql($pdo, $schemaSql);
            if (!qdb_column_exists($pdo, 'questionnaire', 'categoryKey')) {
                $pdo->exec("ALTER TABLE questionnaire ADD COLUMN categoryKey TEXT NOT NULL DEFAULT ''");
            }
            qdb_set_schema_version($pdo, QDB_VERSION);
        } finally {
            qdb_set_foreign_keys($pdo, true);
        }
        $changed = true;
    }

    return $changed;
}

function qdb_mysql_ensure_ready(PDO $pdo): void {
    static $ready = false;
    if ($ready) {
        return;
    }

    $lockName = 'nat_as_schema_migration';
    $lockStmt = $pdo->prepare('SELECT GET_LOCK(:name, 30)');
    $lockStmt->execute([':name' => $lockName]);
    if ((int)$lockStmt->fetchColumn() !== 1) {
        throw new RuntimeException('Could not acquire MySQL schema migration lock');
    }

    try {
        $schemaPath = qdb_schema_file();
        $schemaSql = file_get_contents($schemaPath);
        if ($schemaSql === false) {
            throw new Exception('Could not read MySQL schema');
        }
        if (!qdb_table_exists($pdo, 'users') || qdb_schema_version($pdo) < QDB_VERSION) {
            qdb_exec_schema_file($pdo, $schemaPath);
        }
        qdb_apply_migrations($pdo, $schemaSql);
        $ready = true;
    } finally {
        $releaseStmt = $pdo->prepare('SELECT RELEASE_LOCK(:name)');
        $releaseStmt->execute([':name' => $lockName]);
    }
}

function qdb_flock_exclusive_with_retry($lockFp, int $maxAttempts = 12, int $sleepMicros = 75000): void {
    for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
        if (flock($lockFp, LOCK_EX | LOCK_NB)) {
            return;
        }
        if ($attempt < $maxAttempts - 1) {
            usleep($sleepMicros);
        }
    }
    throw new Exception('Could not acquire lock');
}

function qdb_open_writable_lock() {
    $lockFp = fopen(QDB_LOCK, 'c');
    if ($lockFp === false) {
        throw new Exception('Could not open lock file');
    }
    qdb_flock_exclusive_with_retry($lockFp);
    return $lockFp;
}

function qdb_acquire_lock() {
    return qdb_open_writable_lock();
}

/**
 * Returns [PDO $pdo, string $tmpPath, resource|null $lockFp].
 */
function qdb_open(bool $writable = false): array {
    if (qdb_is_mysql()) {
        [$pdo] = qdb_mysql_open();
        qdb_mysql_ensure_ready($pdo);
        return [$pdo, '', null];
    }

    if (!$writable) {
        require_once __DIR__ . '/lib/read_db_cache.php';
        return qdb_read_db_open();
    }

    $dbPath = QDB_PATH;

    if (!is_dir(dirname($dbPath))) {
        if (!mkdir(dirname($dbPath), 0755, true) && !is_dir(dirname($dbPath))) {
            throw new Exception('Could not create uploads directory');
        }
    }

    $lockFp = qdb_open_writable_lock();

    $tmpDb = tempnam(sys_get_temp_dir(), 'qdb_');
    $masterKey = get_master_key_bytes();

    $sql = file_get_contents(QDB_SCHEMA);
    if ($sql === false) {
        flock($lockFp, LOCK_UN);
        fclose($lockFp);
        throw new Exception('Could not read schema.sql');
    }

    if (file_exists($dbPath) && is_file($dbPath)) {
        $storedEnc = @file_get_contents($dbPath);
        if ($storedEnc === false) {
            flock($lockFp, LOCK_UN);
            fclose($lockFp);
            throw new Exception('Could not read stored DB');
        }
        try {
            $decrypted = aes256_cbc_decrypt_bytes($storedEnc, $masterKey);
        } catch (Throwable $e) {
            error_log(sprintf(
                'qdb_open decrypt failed: path=%s realpath=%s enc_bytes=%d key_sha=%s sapi=%s env_readable=%s common=%s',
                $dbPath,
                realpath($dbPath) ?: 'none',
                strlen($storedEnc),
                hash('sha256', $masterKey),
                PHP_SAPI,
                is_readable(__DIR__ . '/.env') ? 'yes' : 'no',
                realpath(__DIR__ . '/common.php') ?: __DIR__ . '/common.php'
            ));
            throw $e;
        }
        if (file_put_contents($tmpDb, $decrypted) === false) {
            flock($lockFp, LOCK_UN);
            fclose($lockFp);
            throw new Exception('Could not write temp DB');
        }
    } else {
        $pdo = new PDO("sqlite:$tmpDb");
        $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        $pdo->exec($sql);
        qdb_set_schema_version($pdo, QDB_VERSION);
        $pdo = null;
    }

    $pdo = new PDO("sqlite:$tmpDb");
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    qdb_set_foreign_keys($pdo, true);

    qdb_apply_migrations($pdo, $sql);

    return [$pdo, $tmpDb, $lockFp];
}

function qdb_save(string $tmpDb, $lockFp): void {
    if (qdb_is_mysql()) {
        return;
    }

    $masterKey = get_master_key_bytes();
    try {
        $ck = new PDO('sqlite:' . $tmpDb);
        $ck->exec('PRAGMA wal_checkpoint(FULL);');
    } catch (Throwable $e) {
        // best-effort before reading plaintext bytes
    }
    $plainDb = file_get_contents($tmpDb);
    if ($plainDb === false) {
        throw new Exception('Could not read temp DB for save');
    }
    $enc = aes256_cbc_encrypt_bytes($plainDb, $masterKey);

    $dbPath = QDB_PATH;
    $tmpEncrypted = tempnam(dirname($dbPath), 'enc_qdb_');
    if (file_put_contents($tmpEncrypted, $enc) === false) {
        throw new Exception('Could not write encrypted DB');
    }
    if (!@rename($tmpEncrypted, $dbPath)) {
        if (!@copy($tmpEncrypted, $dbPath) || !@unlink($tmpEncrypted)) {
            throw new Exception('Could not save encrypted DB (rename/copy failed)');
        }
    }
    @chmod($dbPath, 0644);
    @unlink($tmpDb);

    if ($lockFp) {
        flock($lockFp, LOCK_UN);
        fclose($lockFp);
    }

    require_once __DIR__ . '/lib/read_db_cache.php';
    qdb_read_cache_invalidate();
}

function qdb_discard(string $tmpDb, $lockFp): void {
    if (qdb_is_mysql()) {
        return;
    }
    if (defined('QDB_READONLY_CACHE_HANDLE') && $tmpDb === QDB_READONLY_CACHE_HANDLE) {
        return;
    }
    @unlink($tmpDb);
    if ($lockFp) {
        @flock($lockFp, LOCK_UN);
        @fclose($lockFp);
    }
}

/** @return array{0: PDO, 1: string, 2: resource|null} */
function qdb_open_read_or_fail(): array {
    try {
        return qdb_open(false);
    } catch (Throwable $e) {
        require_once __DIR__ . '/lib/errors.php';
        qdb_emit_api_error($e, 'Database read');
    }
}

/** @return array{0: PDO, 1: string, 2: resource|null} */
function qdb_open_write_or_fail(): array {
    try {
        return qdb_open(true);
    } catch (Throwable $e) {
        require_once __DIR__ . '/lib/errors.php';
        qdb_emit_api_error($e, 'Database write');
    }
}
