<?php

declare(strict_types=1);

function scripts_project_root(): string
{
    return dirname(dirname(__DIR__));
}

function scripts_saas_app_path(): string
{
    return dirname(__DIR__);
}

function scripts_env_example_path(): string
{
    return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env.example';
}

function scripts_env_path(): string
{
    return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.env';
}

function scripts_bundle_output_path(): string
{
    return scripts_saas_app_path()
        . DIRECTORY_SEPARATOR . 'database'
        . DIRECTORY_SEPARATOR . 'migrations'
        . DIRECTORY_SEPARATOR . 'generated'
        . DIRECTORY_SEPARATOR . 'all-migrations.sql';
}

function scripts_installer_lock_path(): string
{
    return scripts_saas_app_path() . DIRECTORY_SEPARATOR . '.installer.lock';
}

function scripts_supported_connections(): array
{
    return [
        'mysql' => 'MySQL',
        'mariadb' => 'MariaDB',
        'sqlsrv' => 'SQL Server',
    ];
}

function scripts_normalize_db_connection(?string $connection): string
{
    $normalized = strtolower(trim((string) $connection));

    if ($normalized === 'maria' || $normalized === 'maria-db') {
        $normalized = 'mariadb';
    }

    if ($normalized === 'sqlserver' || $normalized === 'mssql') {
        $normalized = 'sqlsrv';
    }

    return array_key_exists($normalized, scripts_supported_connections()) ? $normalized : 'mysql';
}

function scripts_connection_label(string $connection): string
{
    $connection = scripts_normalize_db_connection($connection);
    $supported = scripts_supported_connections();

    return $supported[$connection];
}

function scripts_default_db_port(string $connection): string
{
    $connection = scripts_normalize_db_connection($connection);

    if ($connection === 'sqlsrv') {
        return '1433';
    }

    return '3306';
}

function scripts_required_pdo_extension(string $connection): string
{
    $connection = scripts_normalize_db_connection($connection);

    return $connection === 'sqlsrv' ? 'pdo_sqlsrv' : 'pdo_mysql';
}

function scripts_parent_directory(string $path): string
{
    return dirname($path);
}

function scripts_string_contains(string $haystack, string $needle): bool
{
    if (function_exists('str_contains')) {
        return str_contains($haystack, $needle);
    }

    return $needle === '' || strpos($haystack, $needle) !== false;
}

function scripts_string_starts_with(string $haystack, string $needle): bool
{
    if (function_exists('str_starts_with')) {
        return str_starts_with($haystack, $needle);
    }

    if ($needle === '') {
        return true;
    }

    return strpos($haystack, $needle) === 0;
}

function scripts_check_command(string $command): bool
{
    $where = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? 'where' : 'command -v';
    $output = [];
    $exitCode = 0;
    $redirect = stripos(PHP_OS_FAMILY, 'Windows') === 0 ? ' 2>NUL' : ' 2>/dev/null';
    @exec($where . ' ' . escapeshellarg($command) . $redirect, $output, $exitCode);

    return $exitCode === 0;
}

function scripts_parse_options(array $argv): array
{
    $options = [];

    foreach (array_slice($argv, 1) as $arg) {
        if (!scripts_string_starts_with($arg, '--')) {
            continue;
        }

        $arg = substr($arg, 2);

        if (scripts_string_contains($arg, '=')) {
            list($key, $value) = explode('=', $arg, 2);
            $options[$key] = $value;
            continue;
        }

        $options[$arg] = true;
    }

    return $options;
}

function scripts_read_env_file(string $path): array
{
    if (!is_file($path)) {
        return [];
    }

    $values = [];

    foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [] as $line) {
        $trimmed = trim($line);

        if ($trimmed === '' || scripts_string_starts_with($trimmed, '#') || !scripts_string_contains($trimmed, '=')) {
            continue;
        }

        list($key, $value) = explode('=', $trimmed, 2);
        $values[trim($key)] = trim($value, " \t\n\r\0\x0B\"");
    }

    return $values;
}

function scripts_write_env_file(array $values, ?string $targetPath = null): string
{
    if ($targetPath === null) {
        $targetPath = scripts_env_path();
    }

    $templateLines = file(scripts_env_example_path(), FILE_IGNORE_NEW_LINES);

    if ($templateLines === false) {
        throw new RuntimeException('Die .env.example konnte nicht gelesen werden.');
    }

    $output = [];

    foreach ($templateLines as $line) {
        if (!scripts_string_contains($line, '=')) {
            $output[] = $line;
            continue;
        }

        list($key) = explode('=', $line, 2);
        $trimmedKey = trim($key);

        if (!array_key_exists($trimmedKey, $values)) {
            $output[] = $line;
            continue;
        }

        $output[] = $trimmedKey . '=' . scripts_format_env_value((string) $values[$trimmedKey]);
        unset($values[$trimmedKey]);
    }

    foreach ($values as $key => $value) {
        $output[] = $key . '=' . scripts_format_env_value((string) $value);
    }

    if (file_put_contents($targetPath, implode(PHP_EOL, $output) . PHP_EOL) === false) {
        throw new RuntimeException('Die .env konnte nicht geschrieben werden: ' . $targetPath);
    }

    return $targetPath;
}

function scripts_path_writable_state(string $path): array
{
    $exists = file_exists($path);
    $parent = scripts_parent_directory($path);
    $targetWritable = $exists ? is_writable($path) : false;
    $parentWritable = is_dir($parent) ? is_writable($parent) : false;

    return [
        'path' => $path,
        'exists' => $exists,
        'readable' => $exists ? is_readable($path) : false,
        'writable' => $exists ? $targetWritable : $parentWritable,
        'target_writable' => $targetWritable,
        'parent' => $parent,
        'parent_exists' => is_dir($parent),
        'parent_writable' => $parentWritable,
    ];
}

function scripts_path_state_label(array $state): string
{
    if (!$state['parent_exists']) {
        return 'Elternordner fehlt';
    }

    if ($state['exists'] && $state['target_writable']) {
        return 'beschreibbar';
    }

    if (!$state['exists'] && $state['parent_writable']) {
        return 'anlegbar';
    }

    if ($state['exists']) {
        return 'nicht beschreibbar';
    }

    return 'nicht anlegbar';
}

function scripts_format_env_value(string $value): string
{
    if ($value === '') {
        return '';
    }

    if (preg_match('/\s|#|"|=/', $value) === 1) {
        return '"' . addcslashes($value, "\\\"") . '"';
    }

    return $value;
}

function scripts_transform_migration_sql(string $sql, string $connection): string
{
    $connection = scripts_normalize_db_connection($connection);
    $sql = trim(str_replace(["\r\n", "\r"], "\n", $sql));

    if ($connection === 'sqlsrv') {
        return $sql;
    }

    $sql = preg_replace('/\bBIT\s+NOT NULL\s+DEFAULT\s+([01])\b/i', 'TINYINT(1) NOT NULL DEFAULT $1', $sql) ?? $sql;
    $sql = preg_replace('/\bBIT\b/i', 'TINYINT(1)', $sql) ?? $sql;

    if (preg_match('/^CREATE TABLE\s+/i', $sql) === 1) {
        $sql = preg_replace(
            '/\)\s*;\s*$/',
            ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;',
            $sql
        ) ?? $sql;
    }

    return $sql;
}

function scripts_load_migration_sql(string $file, string $connection): string
{
    $sql = require $file;

    if (!is_string($sql) || trim($sql) === '') {
        throw new RuntimeException('Ungueltige Migration: ' . $file);
    }

    return scripts_transform_migration_sql($sql, $connection);
}

function scripts_bundle_prologue(string $connection): array
{
    $connection = scripts_normalize_db_connection($connection);

    if ($connection === 'sqlsrv') {
        return [
            '-- Generated migration bundle for Kaffeeliste SaaS',
            '-- Generated at ' . date('c'),
            '-- Target: ' . scripts_connection_label($connection),
            'SET XACT_ABORT ON;',
            'BEGIN TRANSACTION;',
        ];
    }

    return [
        '-- Generated migration bundle for Kaffeeliste SaaS',
        '-- Generated at ' . date('c'),
        '-- Target: ' . scripts_connection_label($connection),
        'SET NAMES utf8mb4;',
        'SET FOREIGN_KEY_CHECKS=0;',
    ];
}

function scripts_bundle_epilogue(string $connection): array
{
    $connection = scripts_normalize_db_connection($connection);

    if ($connection === 'sqlsrv') {
        return [
            '',
            'COMMIT TRANSACTION;',
            '',
        ];
    }

    return [
        '',
        'SET FOREIGN_KEY_CHECKS=1;',
        '',
    ];
}

function scripts_build_migration_bundle(?string $outputPath = null, string $connection = 'mysql'): string
{
    $connection = scripts_normalize_db_connection($connection);
    $migrationDir = scripts_saas_app_path()
        . DIRECTORY_SEPARATOR . 'database'
        . DIRECTORY_SEPARATOR . 'migrations';

    if ($outputPath === null) {
        $outputPath = scripts_bundle_output_path();
    }

    if (!is_dir($migrationDir)) {
        throw new RuntimeException('Migrationsverzeichnis nicht gefunden: ' . $migrationDir);
    }

    $files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
    sort($files, SORT_STRING);

    if ($files === []) {
        throw new RuntimeException('Keine Migrationsdateien gefunden.');
    }

    $outputDir = dirname($outputPath);
    if (!is_dir($outputDir) && !mkdir($outputDir, 0777, true) && !is_dir($outputDir)) {
        throw new RuntimeException('Ausgabeverzeichnis konnte nicht erstellt werden: ' . $outputDir);
    }

    $bundle = scripts_bundle_prologue($connection);

    foreach ($files as $file) {
        $sql = scripts_load_migration_sql($file, $connection);

        $bundle[] = '';
        $bundle[] = '-- Migration: ' . basename($file);
        $bundle[] = $sql;
    }

    foreach (scripts_bundle_epilogue($connection) as $line) {
        $bundle[] = $line;
    }

    if (file_put_contents($outputPath, implode(PHP_EOL, $bundle)) === false) {
        throw new RuntimeException('Das SQL-Bundle konnte nicht geschrieben werden: ' . $outputPath);
    }

    return $outputPath;
}

function scripts_run_sql_migrations(array $config): array
{
    $connection = scripts_normalize_db_connection(isset($config['connection']) ? (string) $config['connection'] : 'mysql');
    $server = isset($config['server']) ? $config['server'] : null;
    $database = isset($config['database']) ? $config['database'] : null;
    $port = isset($config['port']) ? (string) $config['port'] : scripts_default_db_port($connection);
    $username = isset($config['username']) ? $config['username'] : null;
    $password = isset($config['password']) ? $config['password'] : null;

    if ($server === null || $database === null) {
        throw new RuntimeException('Bitte Server und Datenbank angeben.');
    }

    $requiredExtension = scripts_required_pdo_extension($connection);
    if (!extension_loaded($requiredExtension)) {
        throw new RuntimeException('Die PHP-Erweiterung ' . $requiredExtension . ' ist nicht geladen.');
    }

    $migrationDir = scripts_saas_app_path()
        . DIRECTORY_SEPARATOR . 'database'
        . DIRECTORY_SEPARATOR . 'migrations';

    $files = glob($migrationDir . DIRECTORY_SEPARATOR . '*.php');
    sort($files, SORT_STRING);

    if ($files === []) {
        throw new RuntimeException('Keine Migrationsdateien gefunden.');
    }

    if ($connection === 'sqlsrv') {
        $dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);
        $pdo = new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        ]);
    } else {
        $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $server, $port, $database);
        $pdo = new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    }

    $executed = [];

    try {
        if ($connection === 'sqlsrv') {
            $pdo->beginTransaction();
        } else {
            $pdo->exec('SET NAMES utf8mb4');
            $pdo->exec('SET FOREIGN_KEY_CHECKS=0');
        }

        foreach ($files as $file) {
            $sql = scripts_load_migration_sql($file, $connection);

            $pdo->exec($sql);
            $executed[] = basename($file);
        }

        if ($connection === 'sqlsrv') {
            $pdo->commit();
        } else {
            $pdo->exec('SET FOREIGN_KEY_CHECKS=1');
        }
    } catch (Throwable $exception) {
        if ($pdo->inTransaction()) {
            $pdo->rollBack();
        }

        if ($connection !== 'sqlsrv') {
            try {
                $pdo->exec('SET FOREIGN_KEY_CHECKS=1');
            } catch (Throwable $ignored) {
            }
        }

        throw $exception;
    }

    return $executed;
}

function scripts_installer_is_locked(): bool
{
    return is_file(scripts_installer_lock_path());
}

function scripts_installer_lock(array $meta = []): string
{
    $payload = json_encode([
        'locked_at' => date('c'),
        'meta' => $meta,
    ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

    $path = scripts_installer_lock_path();
    if (file_put_contents($path, $payload . PHP_EOL) === false) {
        throw new RuntimeException('Die Installer-Sperrdatei konnte nicht geschrieben werden.');
    }

    return $path;
}

function scripts_query_all(PDO $pdo, string $sql, array $params = []): array
{
    $statement = $pdo->prepare($sql);
    $statement->execute($params);

    return $statement->fetchAll(PDO::FETCH_ASSOC) ?: [];
}

function scripts_query_one(PDO $pdo, string $sql, array $params = []): ?array
{
    $statement = $pdo->prepare($sql);
    $statement->execute($params);
    $row = $statement->fetch(PDO::FETCH_ASSOC);

    return is_array($row) ? $row : null;
}

function scripts_execute(PDO $pdo, string $sql, array $params = []): void
{
    $statement = $pdo->prepare($sql);
    $statement->execute($params);
}

function scripts_uuid(): string
{
    $bytes = random_bytes(16);
    $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
    $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);

    return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
}

function scripts_uuid_from_string(string $input): string
{
    $hash = md5($input);

    return sprintf(
        '%08s-%04s-%04s-%04s-%012s',
        substr($hash, 0, 8),
        substr($hash, 8, 4),
        substr($hash, 12, 4),
        substr($hash, 16, 4),
        substr($hash, 20, 12)
    );
}

function scripts_connect_pdo(array $config): PDO
{
    $connection = scripts_normalize_db_connection((string) ($config['connection'] ?? 'mysql'));
    $server = (string) ($config['server'] ?? '');
    $database = (string) ($config['database'] ?? '');
    $port = (string) ($config['port'] ?? scripts_default_db_port($connection));
    $username = (string) ($config['username'] ?? '');
    $password = (string) ($config['password'] ?? '');

    if ($server === '' || $database === '') {
        throw new RuntimeException('Bitte Server und Datenbank angeben.');
    }

    $requiredExtension = scripts_required_pdo_extension($connection);

    if (!extension_loaded($requiredExtension)) {
        throw new RuntimeException('Die PHP-Erweiterung ' . $requiredExtension . ' ist nicht geladen.');
    }

    if ($connection === 'sqlsrv') {
        $dsn = sprintf('sqlsrv:Server=%s,%s;Database=%s;TrustServerCertificate=1', $server, $port, $database);

        return new PDO($dsn, $username, $password, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    }

    $dsn = sprintf('mysql:host=%s;port=%s;dbname=%s;charset=utf8mb4', $server, $port, $database);

    return new PDO($dsn, $username, $password, [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
}

function scripts_table_exists(PDO $pdo, string $tableName): bool
{
    if (preg_match('/^[A-Za-z0-9_.]+$/', $tableName) !== 1) {
        throw new RuntimeException('Ungueltiger Tabellenname: ' . $tableName);
    }

    try {
        $pdo->query('SELECT 1 FROM ' . $tableName . ' WHERE 1 = 0');

        return true;
    } catch (Throwable $exception) {
        return false;
    }
}

function scripts_schema_is_installed(PDO $pdo): bool
{
    return scripts_table_exists($pdo, 'tenants')
        && scripts_table_exists($pdo, 'users')
        && scripts_table_exists($pdo, 'tenant_users')
        && scripts_table_exists($pdo, 'members');
}

function scripts_ensure_schema(array $config): bool
{
    $pdo = scripts_connect_pdo($config);

    if (scripts_schema_is_installed($pdo)) {
        return false;
    }

    scripts_run_sql_migrations($config);

    return true;
}

function scripts_updates_path(): string
{
    return scripts_saas_app_path() . DIRECTORY_SEPARATOR . 'updates';
}

function scripts_ensure_updates_table(PDO $pdo, string $connection): void
{
    if (scripts_table_exists($pdo, 'app_updates')) {
        return;
    }

    if ($connection === 'sqlsrv') {
        $pdo->exec(
            <<<'SQL'
CREATE TABLE app_updates (
    id CHAR(36) NOT NULL PRIMARY KEY,
    update_key VARCHAR(190) NOT NULL,
    title VARCHAR(255) NOT NULL,
    checksum VARCHAR(64) NULL,
    status VARCHAR(50) NOT NULL,
    executed_at DATETIME NULL,
    execution_time_ms INT NULL,
    executed_by VARCHAR(255) NULL,
    error_message NVARCHAR(MAX) NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    CONSTRAINT app_updates_update_key_unique UNIQUE (update_key)
);
SQL
        );

        return;
    }

    $pdo->exec(
        <<<'SQL'
CREATE TABLE IF NOT EXISTS app_updates (
    id CHAR(36) NOT NULL PRIMARY KEY,
    update_key VARCHAR(190) NOT NULL,
    title VARCHAR(255) NOT NULL,
    checksum VARCHAR(64) NULL,
    status VARCHAR(50) NOT NULL,
    executed_at DATETIME NULL,
    execution_time_ms INT NULL,
    executed_by VARCHAR(255) NULL,
    error_message TEXT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY app_updates_update_key_unique (update_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL
    );
}

function scripts_update_files(): array
{
    $dir = scripts_updates_path();

    if (!is_dir($dir)) {
        return [];
    }

    $files = glob($dir . DIRECTORY_SEPARATOR . '*.php') ?: [];
    sort($files, SORT_STRING);

    return $files;
}

function scripts_load_update_definition(string $file): array
{
    $definition = require $file;

    if (!is_array($definition)) {
        throw new RuntimeException('Ungültige Update-Datei: ' . $file);
    }

    $key = trim((string) ($definition['key'] ?? pathinfo($file, PATHINFO_FILENAME)));
    $title = trim((string) ($definition['title'] ?? $key));
    $description = trim((string) ($definition['description'] ?? ''));
    $statements = $definition['statements'] ?? [];

    if ($key === '' || !is_array($statements) || $statements === []) {
        throw new RuntimeException('Update-Datei ohne gültige Metadaten: ' . $file);
    }

    return [
        'key' => $key,
        'title' => $title,
        'description' => $description,
        'statements' => array_values(array_filter(array_map(
            static fn(mixed $statement): string => trim((string) $statement),
            $statements
        ), static fn(string $statement): bool => $statement !== '')),
        'file' => $file,
        'checksum' => sha1_file($file) ?: null,
    ];
}

function scripts_applied_updates(PDO $pdo): array
{
    if (!scripts_table_exists($pdo, 'app_updates')) {
        return [];
    }

    $rows = scripts_query_all($pdo, 'SELECT update_key, status, checksum, executed_at, execution_time_ms, executed_by, error_message, title FROM app_updates ORDER BY update_key ASC');
    $applied = [];

    foreach ($rows as $row) {
        $applied[(string) $row['update_key']] = $row;
    }

    return $applied;
}

function scripts_update_config_from_env(array $env): array
{
    return [
        'connection' => scripts_normalize_db_connection((string) ($env['DB_CONNECTION'] ?? 'mysql')),
        'server' => (string) ($env['DB_HOST'] ?? '127.0.0.1'),
        'database' => (string) ($env['DB_DATABASE'] ?? ''),
        'port' => (string) ($env['DB_PORT'] ?? scripts_default_db_port((string) ($env['DB_CONNECTION'] ?? 'mysql'))),
        'username' => (string) ($env['DB_USERNAME'] ?? ''),
        'password' => (string) ($env['DB_PASSWORD'] ?? ''),
    ];
}

function scripts_list_updates_status(array $config): array
{
    $pdo = scripts_connect_pdo($config);
    $connection = scripts_normalize_db_connection((string) ($config['connection'] ?? 'mysql'));
    scripts_ensure_updates_table($pdo, $connection);

    $applied = scripts_applied_updates($pdo);
    $items = [];

    foreach (scripts_update_files() as $file) {
        $definition = scripts_load_update_definition($file);
        $current = $applied[$definition['key']] ?? null;
        $status = $current['status'] ?? 'pending';

        if ($status === 'success') {
            $status = 'success';
        } elseif ($status === 'running') {
            $status = 'running';
        } elseif ($current !== null) {
            $status = 'failed';
        }

        $items[] = [
            'key' => $definition['key'],
            'title' => $definition['title'],
            'description' => $definition['description'],
            'status' => $status,
            'checksum' => $definition['checksum'],
            'executed_at' => $current['executed_at'] ?? null,
            'execution_time_ms' => $current['execution_time_ms'] ?? null,
            'executed_by' => $current['executed_by'] ?? null,
            'error_message' => $current['error_message'] ?? null,
        ];
    }

    return $items;
}

function scripts_run_pending_updates(array $config, ?string $executedBy = null): array
{
    $pdo = scripts_connect_pdo($config);
    $connection = scripts_normalize_db_connection((string) ($config['connection'] ?? 'mysql'));
    scripts_ensure_updates_table($pdo, $connection);
    $applied = scripts_applied_updates($pdo);
    $results = [];

    foreach (scripts_update_files() as $file) {
        $definition = scripts_load_update_definition($file);
        $current = $applied[$definition['key']] ?? null;

        if (($current['status'] ?? null) === 'success') {
            $results[] = [
                'key' => $definition['key'],
                'title' => $definition['title'],
                'status' => 'skipped',
                'message' => 'Bereits erfolgreich ausgeführt.',
            ];
            continue;
        }

        $start = microtime(true);
        $updateId = (string) ($current['id'] ?? scripts_uuid());
        $now = date('Y-m-d H:i:s');

        scripts_execute(
            $pdo,
            'DELETE FROM app_updates WHERE update_key = :update_key',
            ['update_key' => $definition['key']]
        );

        scripts_execute(
            $pdo,
            'INSERT INTO app_updates (id, update_key, title, checksum, status, executed_at, execution_time_ms, executed_by, error_message, created_at, updated_at) VALUES (:id, :update_key, :title, :checksum, :status, NULL, NULL, :executed_by, NULL, :created_at, :updated_at)',
            [
                'id' => $updateId,
                'update_key' => $definition['key'],
                'title' => $definition['title'],
                'checksum' => $definition['checksum'],
                'status' => 'running',
                'executed_by' => $executedBy,
                'created_at' => $now,
                'updated_at' => $now,
            ]
        );

        try {
            foreach ($definition['statements'] as $statement) {
                $pdo->exec($statement);
            }

            $duration = (int) round((microtime(true) - $start) * 1000);

            scripts_execute(
                $pdo,
                'UPDATE app_updates SET status = :status, executed_at = :executed_at, execution_time_ms = :execution_time_ms, error_message = NULL, updated_at = :updated_at WHERE id = :id',
                [
                    'status' => 'success',
                    'executed_at' => date('Y-m-d H:i:s'),
                    'execution_time_ms' => $duration,
                    'updated_at' => date('Y-m-d H:i:s'),
                    'id' => $updateId,
                ]
            );

            $results[] = [
                'key' => $definition['key'],
                'title' => $definition['title'],
                'status' => 'success',
                'message' => 'Erfolgreich ausgeführt.',
            ];
        } catch (Throwable $exception) {
            $duration = (int) round((microtime(true) - $start) * 1000);

            scripts_execute(
                $pdo,
                'UPDATE app_updates SET status = :status, executed_at = :executed_at, execution_time_ms = :execution_time_ms, error_message = :error_message, updated_at = :updated_at WHERE id = :id',
                [
                    'status' => 'failed',
                    'executed_at' => date('Y-m-d H:i:s'),
                    'execution_time_ms' => $duration,
                    'error_message' => $exception->getMessage(),
                    'updated_at' => date('Y-m-d H:i:s'),
                    'id' => $updateId,
                ]
            );

            throw $exception;
        }
    }

    return $results;
}

function scripts_ensure_migrations_table(PDO $pdo, string $connection): void
{
    if (scripts_table_exists($pdo, 'app_migrations')) {
        return;
    }

    if ($connection === 'sqlsrv') {
        $pdo->exec(
            <<<'SQL'
CREATE TABLE app_migrations (
    id CHAR(36) NOT NULL PRIMARY KEY,
    migration_key VARCHAR(190) NOT NULL,
    title VARCHAR(255) NOT NULL,
    checksum VARCHAR(64) NULL,
    status VARCHAR(50) NOT NULL,
    executed_at DATETIME NULL,
    execution_time_ms INT NULL,
    executed_by VARCHAR(255) NULL,
    notes NVARCHAR(MAX) NULL,
    error_message NVARCHAR(MAX) NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    CONSTRAINT app_migrations_migration_key_unique UNIQUE (migration_key)
);
SQL
        );

        return;
    }

    $pdo->exec(
        <<<'SQL'
CREATE TABLE IF NOT EXISTS app_migrations (
    id CHAR(36) NOT NULL PRIMARY KEY,
    migration_key VARCHAR(190) NOT NULL,
    title VARCHAR(255) NOT NULL,
    checksum VARCHAR(64) NULL,
    status VARCHAR(50) NOT NULL,
    executed_at DATETIME NULL,
    execution_time_ms INT NULL,
    executed_by VARCHAR(255) NULL,
    notes TEXT NULL,
    error_message TEXT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY app_migrations_migration_key_unique (migration_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL
    );
}

function scripts_migration_files(): array
{
    $dir = scripts_saas_app_path()
        . DIRECTORY_SEPARATOR . 'database'
        . DIRECTORY_SEPARATOR . 'migrations';

    if (!is_dir($dir)) {
        return [];
    }

    $files = glob($dir . DIRECTORY_SEPARATOR . '*.php') ?: [];
    sort($files, SORT_STRING);

    return $files;
}

/**
 * @return array<int, string>
 */
function scripts_declared_tables_from_sql(string $sql): array
{
    preg_match_all(
        '/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\[]?([A-Za-z0-9_.]+)[`"\]]?/i',
        $sql,
        $matches
    );

    $tables = array_map(
        static fn(string $table): string => trim($table),
        $matches[1] ?? []
    );

    return array_values(array_unique(array_filter($tables, static fn(string $table): bool => $table !== '')));
}

/**
 * @return array<string, mixed>
 */
function scripts_load_migration_definition(string $file, string $connection): array
{
    $sql = scripts_load_migration_sql($file, $connection);
    $key = pathinfo($file, PATHINFO_FILENAME);

    return [
        'key' => $key,
        'title' => $key,
        'file' => $file,
        'checksum' => sha1_file($file) ?: null,
        'sql' => $sql,
        'tables' => scripts_declared_tables_from_sql($sql),
    ];
}

function scripts_applied_migrations(PDO $pdo): array
{
    if (!scripts_table_exists($pdo, 'app_migrations')) {
        return [];
    }

    $rows = scripts_query_all(
        $pdo,
        'SELECT migration_key, status, checksum, executed_at, execution_time_ms, executed_by, notes, error_message, title, id FROM app_migrations ORDER BY migration_key ASC'
    );
    $applied = [];

    foreach ($rows as $row) {
        $applied[(string) $row['migration_key']] = $row;
    }

    return $applied;
}

function scripts_migration_targets_exist(PDO $pdo, array $definition): bool
{
    $tables = $definition['tables'] ?? [];

    if (!is_array($tables) || $tables === []) {
        return false;
    }

    foreach ($tables as $table) {
        if (!scripts_table_exists($pdo, (string) $table)) {
            return false;
        }
    }

    return true;
}

function scripts_list_migrations_status(array $config): array
{
    $pdo = scripts_connect_pdo($config);
    $connection = scripts_normalize_db_connection((string) ($config['connection'] ?? 'mysql'));
    scripts_ensure_migrations_table($pdo, $connection);

    $applied = scripts_applied_migrations($pdo);
    $items = [];

    foreach (scripts_migration_files() as $file) {
        $definition = scripts_load_migration_definition($file, $connection);
        $current = $applied[$definition['key']] ?? null;
        $status = $current['status'] ?? 'pending';
        $notes = $current['notes'] ?? null;

        if ($current === null && scripts_migration_targets_exist($pdo, $definition)) {
            $status = 'baselined';
            $notes = 'Tabellen sind bereits vorhanden und koennen als bestehender Stand uebernommen werden.';
        } elseif ($status === 'success') {
            $status = 'success';
        } elseif ($status === 'running') {
            $status = 'running';
        } elseif ($current !== null) {
            $status = 'failed';
        }

        $items[] = [
            'key' => $definition['key'],
            'title' => $definition['title'],
            'status' => $status,
            'checksum' => $definition['checksum'],
            'tables' => $definition['tables'],
            'executed_at' => $current['executed_at'] ?? null,
            'execution_time_ms' => $current['execution_time_ms'] ?? null,
            'executed_by' => $current['executed_by'] ?? null,
            'notes' => $notes,
            'error_message' => $current['error_message'] ?? null,
        ];
    }

    return $items;
}

function scripts_run_pending_migrations(array $config, ?string $executedBy = null): array
{
    $pdo = scripts_connect_pdo($config);
    $connection = scripts_normalize_db_connection((string) ($config['connection'] ?? 'mysql'));
    scripts_ensure_migrations_table($pdo, $connection);
    $applied = scripts_applied_migrations($pdo);
    $results = [];

    foreach (scripts_migration_files() as $file) {
        $definition = scripts_load_migration_definition($file, $connection);
        $current = $applied[$definition['key']] ?? null;

        if (($current['status'] ?? null) === 'success') {
            $results[] = [
                'key' => $definition['key'],
                'title' => $definition['title'],
                'status' => 'skipped',
                'message' => 'Bereits erfolgreich ausgefuehrt.',
            ];
            continue;
        }

        if ($current === null && scripts_migration_targets_exist($pdo, $definition)) {
            $migrationId = scripts_uuid();
            $now = date('Y-m-d H:i:s');
            $notes = 'Automatisch als bestehender Stand uebernommen, da die Zieltabellen bereits vorhanden sind.';

            scripts_execute(
                $pdo,
                'INSERT INTO app_migrations (id, migration_key, title, checksum, status, executed_at, execution_time_ms, executed_by, notes, error_message, created_at, updated_at) VALUES (:id, :migration_key, :title, :checksum, :status, :executed_at, 0, :executed_by, :notes, NULL, :created_at, :updated_at)',
                [
                    'id' => $migrationId,
                    'migration_key' => $definition['key'],
                    'title' => $definition['title'],
                    'checksum' => $definition['checksum'],
                    'status' => 'success',
                    'executed_at' => $now,
                    'executed_by' => $executedBy !== null ? $executedBy . ' (baseline)' : 'baseline',
                    'notes' => $notes,
                    'created_at' => $now,
                    'updated_at' => $now,
                ]
            );

            $results[] = [
                'key' => $definition['key'],
                'title' => $definition['title'],
                'status' => 'baselined',
                'message' => $notes,
            ];
            continue;
        }

        $start = microtime(true);
        $migrationId = (string) ($current['id'] ?? scripts_uuid());
        $now = date('Y-m-d H:i:s');

        scripts_execute(
            $pdo,
            'DELETE FROM app_migrations WHERE migration_key = :migration_key',
            ['migration_key' => $definition['key']]
        );

        scripts_execute(
            $pdo,
            'INSERT INTO app_migrations (id, migration_key, title, checksum, status, executed_at, execution_time_ms, executed_by, notes, error_message, created_at, updated_at) VALUES (:id, :migration_key, :title, :checksum, :status, NULL, NULL, :executed_by, NULL, NULL, :created_at, :updated_at)',
            [
                'id' => $migrationId,
                'migration_key' => $definition['key'],
                'title' => $definition['title'],
                'checksum' => $definition['checksum'],
                'status' => 'running',
                'executed_by' => $executedBy,
                'created_at' => $now,
                'updated_at' => $now,
            ]
        );

        try {
            $pdo->exec((string) $definition['sql']);
            $duration = (int) round((microtime(true) - $start) * 1000);

            scripts_execute(
                $pdo,
                'UPDATE app_migrations SET status = :status, executed_at = :executed_at, execution_time_ms = :execution_time_ms, notes = :notes, error_message = NULL, updated_at = :updated_at WHERE id = :id',
                [
                    'status' => 'success',
                    'executed_at' => date('Y-m-d H:i:s'),
                    'execution_time_ms' => $duration,
                    'notes' => 'Migration erfolgreich ausgefuehrt.',
                    'updated_at' => date('Y-m-d H:i:s'),
                    'id' => $migrationId,
                ]
            );

            $results[] = [
                'key' => $definition['key'],
                'title' => $definition['title'],
                'status' => 'success',
                'message' => 'Erfolgreich ausgefuehrt.',
            ];
        } catch (Throwable $exception) {
            $duration = (int) round((microtime(true) - $start) * 1000);

            scripts_execute(
                $pdo,
                'UPDATE app_migrations SET status = :status, executed_at = :executed_at, execution_time_ms = :execution_time_ms, error_message = :error_message, updated_at = :updated_at WHERE id = :id',
                [
                    'status' => 'failed',
                    'executed_at' => date('Y-m-d H:i:s'),
                    'execution_time_ms' => $duration,
                    'error_message' => $exception->getMessage(),
                    'updated_at' => date('Y-m-d H:i:s'),
                    'id' => $migrationId,
                ]
            );

            throw $exception;
        }
    }

    return $results;
}

function scripts_role_id(string $roleKey, string $scope): string
{
    return scripts_uuid_from_string('role:' . $scope . ':' . $roleKey);
}

function scripts_ensure_role(PDO $pdo, string $roleKey, string $name, string $scope): string
{
    $role = scripts_query_one(
        $pdo,
        'SELECT id FROM roles WHERE role_key = :role_key AND scope = :scope LIMIT 1',
        ['role_key' => $roleKey, 'scope' => $scope]
    );

    if ($role !== null) {
        return (string) $role['id'];
    }

    $id = scripts_role_id($roleKey, $scope);
    $now = date('Y-m-d H:i:s');

    scripts_execute(
        $pdo,
        'INSERT INTO roles (id, role_key, name, scope, created_at, updated_at) VALUES (:id, :role_key, :name, :scope, :created_at, :updated_at)',
        [
            'id' => $id,
            'role_key' => $roleKey,
            'name' => $name,
            'scope' => $scope,
            'created_at' => $now,
            'updated_at' => $now,
        ]
    );

    return $id;
}

function scripts_ensure_core_roles(PDO $pdo): array
{
    return [
        'tenant_admin' => scripts_ensure_role($pdo, 'tenant_admin', 'Tenant Admin', 'tenant'),
        'finance_admin' => scripts_ensure_role($pdo, 'finance_admin', 'Finance Admin', 'tenant'),
        'support_contact' => scripts_ensure_role($pdo, 'support_contact', 'Support Contact', 'tenant'),
        'survey_manager' => scripts_ensure_role($pdo, 'survey_manager', 'Survey Manager', 'tenant'),
        'platform_admin' => scripts_ensure_role($pdo, 'platform_admin', 'Platform Admin', 'platform'),
    ];
}

function scripts_create_platform_admin(array $targetConfig, string $email, string $displayName, string $password): array
{
    $email = strtolower(trim($email));
    $displayName = trim($displayName);

    if ($email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
        throw new RuntimeException('Bitte eine gueltige E-Mail-Adresse fuer den Global-Admin angeben.');
    }

    if ($displayName === '') {
        throw new RuntimeException('Bitte einen Anzeigenamen fuer den Global-Admin angeben.');
    }

    if (strlen($password) < 8) {
        throw new RuntimeException('Das Global-Admin-Passwort muss mindestens 8 Zeichen lang sein.');
    }

    $pdo = scripts_connect_pdo($targetConfig);
    scripts_ensure_core_roles($pdo);

    $now = date('Y-m-d H:i:s');
    $hash = password_hash($password, PASSWORD_BCRYPT);
    $existing = scripts_query_one($pdo, 'SELECT id FROM users WHERE LOWER(email) = LOWER(:email) LIMIT 1', ['email' => $email]);

    if ($existing !== null) {
        scripts_execute(
            $pdo,
            'UPDATE users SET display_name = :display_name, password_hash = :password_hash, is_platform_admin = 1, updated_at = :updated_at WHERE id = :id',
            [
                'display_name' => $displayName,
                'password_hash' => $hash,
                'updated_at' => $now,
                'id' => $existing['id'],
            ]
        );

        return [
            'user_id' => $existing['id'],
            'created' => false,
        ];
    }

    $userId = scripts_uuid_from_string('platform-user:' . $email);

    scripts_execute(
        $pdo,
        'INSERT INTO users (id, email, password_hash, display_name, is_platform_admin, created_at, updated_at) VALUES (:id, :email, :password_hash, :display_name, 1, :created_at, :updated_at)',
        [
            'id' => $userId,
            'email' => $email,
            'password_hash' => $hash,
            'display_name' => $displayName,
            'created_at' => $now,
            'updated_at' => $now,
        ]
    );

    return [
        'user_id' => $userId,
        'created' => true,
    ];
}

function scripts_datetime_string(mixed $value): string
{
    if ($value instanceof DateTimeInterface) {
        return $value->format('Y-m-d H:i:s');
    }

    $normalized = trim((string) $value);

    if ($normalized === '') {
        return date('Y-m-d H:i:s');
    }

    $timestamp = strtotime($normalized);

    return $timestamp === false ? date('Y-m-d H:i:s') : date('Y-m-d H:i:s', $timestamp);
}

function scripts_legacy_source_tables(PDO $pdo): array
{
    $tables = [
        'members' => 'kl_Mitarbeiter',
        'coffee' => 'kl_Kaffeeverbrauch',
        'payments' => 'kl_Einzahlungen',
        'announcements' => 'kl_hinweise',
        'config' => 'kl_config',
        'survey_votes' => 'CoffeeSurveyVotedEmails',
        'survey_responses' => 'CoffeeSurveyResponses',
    ];

    $result = [];

    foreach ($tables as $key => $table) {
        $result[$key] = [
            'table' => $table,
            'exists' => scripts_table_exists($pdo, $table),
        ];
    }

    return $result;
}

function scripts_ensure_tenant(PDO $pdo, string $tenantKey, string $tenantName): array
{
    $existing = scripts_query_one(
        $pdo,
        'SELECT id, tenant_key, name, status FROM tenants WHERE tenant_key = :tenant_key LIMIT 1',
        ['tenant_key' => $tenantKey]
    );

    if ($existing !== null) {
        scripts_execute(
            $pdo,
            'UPDATE tenants SET name = :name, updated_at = :updated_at WHERE id = :id',
            [
                'name' => $tenantName,
                'updated_at' => date('Y-m-d H:i:s'),
                'id' => $existing['id'],
            ]
        );
        $existing['created'] = false;

        return $existing;
    }

    $id = scripts_uuid_from_string('tenant:' . $tenantKey);
    $now = date('Y-m-d H:i:s');

    scripts_execute(
        $pdo,
        'INSERT INTO tenants (id, tenant_key, name, status, created_at, updated_at) VALUES (:id, :tenant_key, :name, :status, :created_at, :updated_at)',
        [
            'id' => $id,
            'tenant_key' => $tenantKey,
            'name' => $tenantName,
            'status' => 'active',
            'created_at' => $now,
            'updated_at' => $now,
        ]
    );

    return [
        'id' => $id,
        'tenant_key' => $tenantKey,
        'name' => $tenantName,
        'status' => 'active',
        'created' => true,
    ];
}

function scripts_migrate_legacy_data(array $sourceConfig, array $targetConfig, array $options = []): array
{
    $tenantKey = trim((string) ($options['tenant_key'] ?? 'legacy-import'));
    $tenantName = trim((string) ($options['tenant_name'] ?? 'Legacy Import'));

    if ($tenantKey === '' || $tenantName === '') {
        throw new RuntimeException('Bitte Tenant-Key und Tenant-Name fuer die Migration angeben.');
    }

    $schemaCreated = scripts_ensure_schema($targetConfig);
    $sourcePdo = scripts_connect_pdo($sourceConfig);
    $targetPdo = scripts_connect_pdo($targetConfig);
    $roles = scripts_ensure_core_roles($targetPdo);
    $tenant = scripts_ensure_tenant($targetPdo, $tenantKey, $tenantName);
    $legacyTables = scripts_legacy_source_tables($sourcePdo);
    $counts = [
        'users_created' => 0,
        'users_updated' => 0,
        'tenant_users_created' => 0,
        'members_created' => 0,
        'members_updated' => 0,
        'tenant_roles_created' => 0,
        'coffee_entries_created' => 0,
        'payment_entries_created' => 0,
        'ledger_entries_created' => 0,
        'announcements_created' => 0,
    ];
    $warnings = [];
    $skipped = [];
    $memberMap = [];

    $targetPdo->beginTransaction();

    try {
        if (!$legacyTables['members']['exists']) {
            throw new RuntimeException('Die Legacy-Tabelle kl_Mitarbeiter wurde in der Quelldatenbank nicht gefunden.');
        }

        $legacyMembers = scripts_query_all(
            $sourcePdo,
            'SELECT MitarbeiterID, Name, Email, aktiv, admin FROM kl_Mitarbeiter ORDER BY MitarbeiterID ASC'
        );

        foreach ($legacyMembers as $row) {
            $sourceMemberId = (string) ($row['MitarbeiterID'] ?? '');
            $email = strtolower(trim((string) ($row['Email'] ?? '')));
            $displayName = trim((string) ($row['Name'] ?? ''));
            $isActive = (int) ($row['aktiv'] ?? 0) === 1;
            $isAdmin = (int) ($row['admin'] ?? 0) === 1;

            if ($sourceMemberId === '' || $email === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
                $warnings[] = 'Mitglied ohne gueltige E-Mail wurde uebersprungen: ' . ($displayName !== '' ? $displayName : $sourceMemberId);
                continue;
            }

            $user = scripts_query_one($targetPdo, 'SELECT id FROM users WHERE LOWER(email) = LOWER(:email) LIMIT 1', ['email' => $email]);

            if ($user === null) {
                $userId = scripts_uuid_from_string('legacy-user:' . $email);
                $now = date('Y-m-d H:i:s');
                scripts_execute(
                    $targetPdo,
                    'INSERT INTO users (id, email, password_hash, display_name, is_platform_admin, created_at, updated_at) VALUES (:id, :email, NULL, :display_name, 0, :created_at, :updated_at)',
                    [
                        'id' => $userId,
                        'email' => $email,
                        'display_name' => $displayName !== '' ? $displayName : $email,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['users_created']++;
            } else {
                $userId = (string) $user['id'];
                scripts_execute(
                    $targetPdo,
                    'UPDATE users SET display_name = :display_name, updated_at = :updated_at WHERE id = :id',
                    [
                        'display_name' => $displayName !== '' ? $displayName : $email,
                        'updated_at' => date('Y-m-d H:i:s'),
                        'id' => $userId,
                    ]
                );
                $counts['users_updated']++;
            }

            $tenantUser = scripts_query_one(
                $targetPdo,
                'SELECT id FROM tenant_users WHERE tenant_id = :tenant_id AND user_id = :user_id LIMIT 1',
                ['tenant_id' => $tenant['id'], 'user_id' => $userId]
            );

            if ($tenantUser === null) {
                $tenantUserId = scripts_uuid_from_string('tenant-user:' . $tenant['id'] . ':' . $userId);
                $now = date('Y-m-d H:i:s');
                scripts_execute(
                    $targetPdo,
                    'INSERT INTO tenant_users (id, tenant_id, user_id, status, created_at, updated_at) VALUES (:id, :tenant_id, :user_id, :status, :created_at, :updated_at)',
                    [
                        'id' => $tenantUserId,
                        'tenant_id' => $tenant['id'],
                        'user_id' => $userId,
                        'status' => $isActive ? 'active' : 'inactive',
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['tenant_users_created']++;
            } else {
                $tenantUserId = (string) $tenantUser['id'];
                scripts_execute(
                    $targetPdo,
                    'UPDATE tenant_users SET status = :status, updated_at = :updated_at WHERE id = :id',
                    [
                        'status' => $isActive ? 'active' : 'inactive',
                        'updated_at' => date('Y-m-d H:i:s'),
                        'id' => $tenantUserId,
                    ]
                );
            }

            $member = scripts_query_one(
                $targetPdo,
                'SELECT id FROM members WHERE tenant_id = :tenant_id AND email = :email LIMIT 1',
                ['tenant_id' => $tenant['id'], 'email' => $email]
            );

            if ($member === null) {
                $memberId = scripts_uuid_from_string('member:' . $tenant['id'] . ':' . $email);
                $now = date('Y-m-d H:i:s');
                scripts_execute(
                    $targetPdo,
                    'INSERT INTO members (id, tenant_id, tenant_user_id, display_name, email, status, created_at, updated_at) VALUES (:id, :tenant_id, :tenant_user_id, :display_name, :email, :status, :created_at, :updated_at)',
                    [
                        'id' => $memberId,
                        'tenant_id' => $tenant['id'],
                        'tenant_user_id' => $tenantUserId,
                        'display_name' => $displayName !== '' ? $displayName : $email,
                        'email' => $email,
                        'status' => $isActive ? 'active' : 'inactive',
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['members_created']++;
            } else {
                $memberId = (string) $member['id'];
                scripts_execute(
                    $targetPdo,
                    'UPDATE members SET tenant_user_id = :tenant_user_id, display_name = :display_name, status = :status, updated_at = :updated_at WHERE id = :id',
                    [
                        'tenant_user_id' => $tenantUserId,
                        'display_name' => $displayName !== '' ? $displayName : $email,
                        'status' => $isActive ? 'active' : 'inactive',
                        'updated_at' => date('Y-m-d H:i:s'),
                        'id' => $memberId,
                    ]
                );
                $counts['members_updated']++;
            }

            if ($isAdmin) {
                $existingRole = scripts_query_one(
                    $targetPdo,
                    'SELECT id FROM tenant_user_roles WHERE tenant_user_id = :tenant_user_id AND role_id = :role_id LIMIT 1',
                    ['tenant_user_id' => $tenantUserId, 'role_id' => $roles['tenant_admin']]
                );

                if ($existingRole === null) {
                    scripts_execute(
                        $targetPdo,
                        'INSERT INTO tenant_user_roles (id, tenant_user_id, role_id, created_at) VALUES (:id, :tenant_user_id, :role_id, :created_at)',
                        [
                            'id' => scripts_uuid_from_string('tenant-role:' . $tenantUserId . ':' . $roles['tenant_admin']),
                            'tenant_user_id' => $tenantUserId,
                            'role_id' => $roles['tenant_admin'],
                            'created_at' => date('Y-m-d H:i:s'),
                        ]
                    );
                    $counts['tenant_roles_created']++;
                }
            }

            $memberMap[$sourceMemberId] = [
                'member_id' => $memberId,
                'tenant_user_id' => $tenantUserId,
                'display_name' => $displayName !== '' ? $displayName : $email,
                'email' => $email,
            ];
        }

        if ($legacyTables['coffee']['exists']) {
            $coffeeEntries = scripts_query_all(
                $sourcePdo,
                'SELECT MitarbeiterID, AnzahlStriche, Kosten, KostenproStrich, Datum FROM kl_Kaffeeverbrauch ORDER BY Datum ASC, MitarbeiterID ASC, Kosten ASC, AnzahlStriche ASC'
            );

            foreach ($coffeeEntries as $index => $row) {
                $sourceMemberId = (string) ($row['MitarbeiterID'] ?? '');

                if (!isset($memberMap[$sourceMemberId])) {
                    $warnings[] = 'Kaffeeeintrag ohne migriertes Mitglied wurde uebersprungen: ' . $sourceMemberId;
                    continue;
                }

                $signature = implode('|', [
                    'coffee',
                    $tenant['id'],
                    $sourceMemberId,
                    (string) ($row['AnzahlStriche'] ?? ''),
                    (string) ($row['Kosten'] ?? ''),
                    (string) ($row['KostenproStrich'] ?? ''),
                    scripts_datetime_string($row['Datum'] ?? null),
                    (string) $index,
                ]);
                $entryId = scripts_uuid_from_string($signature);
                $ledgerId = scripts_uuid_from_string('ledger:' . $signature);

                if (scripts_query_one($targetPdo, 'SELECT id FROM coffee_entries WHERE id = :id LIMIT 1', ['id' => $entryId]) !== null) {
                    continue;
                }

                $strokes = max(0, (int) ($row['AnzahlStriche'] ?? 0));
                $totalCost = (float) ($row['Kosten'] ?? 0);
                $unitPrice = (float) ($row['KostenproStrich'] ?? 0);
                $bookedAt = scripts_datetime_string($row['Datum'] ?? null);

                if ($strokes < 1) {
                    continue;
                }

                if ($unitPrice <= 0 && $totalCost > 0) {
                    $unitPrice = $totalCost / $strokes;
                }

                $now = date('Y-m-d H:i:s');

                scripts_execute(
                    $targetPdo,
                    'INSERT INTO coffee_entries (id, tenant_id, member_id, strokes, unit_price, total_cost, booking_source, booked_at, created_at, updated_at) VALUES (:id, :tenant_id, :member_id, :strokes, :unit_price, :total_cost, :booking_source, :booked_at, :created_at, :updated_at)',
                    [
                        'id' => $entryId,
                        'tenant_id' => $tenant['id'],
                        'member_id' => $memberMap[$sourceMemberId]['member_id'],
                        'strokes' => $strokes,
                        'unit_price' => number_format($unitPrice, 2, '.', ''),
                        'total_cost' => number_format($totalCost, 2, '.', ''),
                        'booking_source' => 'legacy-import',
                        'booked_at' => $bookedAt,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['coffee_entries_created']++;

                scripts_execute(
                    $targetPdo,
                    'INSERT INTO ledger_entries (id, tenant_id, member_id, entry_type, amount, reference_type, reference_id, booked_at, created_at, updated_at) VALUES (:id, :tenant_id, :member_id, :entry_type, :amount, :reference_type, :reference_id, :booked_at, :created_at, :updated_at)',
                    [
                        'id' => $ledgerId,
                        'tenant_id' => $tenant['id'],
                        'member_id' => $memberMap[$sourceMemberId]['member_id'],
                        'entry_type' => 'coffee_charge',
                        'amount' => number_format($totalCost * -1, 2, '.', ''),
                        'reference_type' => 'coffee_entry',
                        'reference_id' => $entryId,
                        'booked_at' => $bookedAt,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['ledger_entries_created']++;
            }
        } else {
            $skipped[] = 'Die Legacy-Tabelle kl_Kaffeeverbrauch wurde nicht gefunden.';
        }

        if ($legacyTables['payments']['exists']) {
            $paymentEntries = scripts_query_all(
                $sourcePdo,
                'SELECT MitarbeiterID, Betrag, Datum FROM kl_Einzahlungen ORDER BY Datum ASC, MitarbeiterID ASC, Betrag ASC'
            );

            foreach ($paymentEntries as $index => $row) {
                $sourceMemberId = (string) ($row['MitarbeiterID'] ?? '');

                if (!isset($memberMap[$sourceMemberId])) {
                    $warnings[] = 'Einzahlung ohne migriertes Mitglied wurde uebersprungen: ' . $sourceMemberId;
                    continue;
                }

                $signature = implode('|', [
                    'payment',
                    $tenant['id'],
                    $sourceMemberId,
                    (string) ($row['Betrag'] ?? ''),
                    scripts_datetime_string($row['Datum'] ?? null),
                    (string) $index,
                ]);
                $entryId = scripts_uuid_from_string($signature);
                $ledgerId = scripts_uuid_from_string('ledger:' . $signature);

                if (scripts_query_one($targetPdo, 'SELECT id FROM payment_entries WHERE id = :id LIMIT 1', ['id' => $entryId]) !== null) {
                    continue;
                }

                $amount = (float) ($row['Betrag'] ?? 0);

                if ($amount === 0.0) {
                    continue;
                }

                $bookedAt = scripts_datetime_string($row['Datum'] ?? null);
                $now = date('Y-m-d H:i:s');

                scripts_execute(
                    $targetPdo,
                    'INSERT INTO payment_entries (id, tenant_id, member_id, amount, payment_method, booked_at, created_at, updated_at) VALUES (:id, :tenant_id, :member_id, :amount, :payment_method, :booked_at, :created_at, :updated_at)',
                    [
                        'id' => $entryId,
                        'tenant_id' => $tenant['id'],
                        'member_id' => $memberMap[$sourceMemberId]['member_id'],
                        'amount' => number_format($amount, 2, '.', ''),
                        'payment_method' => 'legacy-import',
                        'booked_at' => $bookedAt,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['payment_entries_created']++;

                scripts_execute(
                    $targetPdo,
                    'INSERT INTO ledger_entries (id, tenant_id, member_id, entry_type, amount, reference_type, reference_id, booked_at, created_at, updated_at) VALUES (:id, :tenant_id, :member_id, :entry_type, :amount, :reference_type, :reference_id, :booked_at, :created_at, :updated_at)',
                    [
                        'id' => $ledgerId,
                        'tenant_id' => $tenant['id'],
                        'member_id' => $memberMap[$sourceMemberId]['member_id'],
                        'entry_type' => 'payment_credit',
                        'amount' => number_format($amount, 2, '.', ''),
                        'reference_type' => 'payment_entry',
                        'reference_id' => $entryId,
                        'booked_at' => $bookedAt,
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['ledger_entries_created']++;
            }
        } else {
            $skipped[] = 'Die Legacy-Tabelle kl_Einzahlungen wurde nicht gefunden.';
        }

        if ($legacyTables['announcements']['exists']) {
            $announcements = scripts_query_all(
                $sourcePdo,
                'SELECT id, nachricht, gueltig_bis FROM kl_hinweise ORDER BY gueltig_bis ASC, id ASC'
            );

            foreach ($announcements as $row) {
                $message = trim((string) ($row['nachricht'] ?? ''));

                if ($message === '') {
                    continue;
                }

                $sourceId = (string) ($row['id'] ?? $message);
                $announcementId = scripts_uuid_from_string('announcement:' . $tenant['id'] . ':' . $sourceId);

                if (scripts_query_one($targetPdo, 'SELECT id FROM announcements WHERE id = :id LIMIT 1', ['id' => $announcementId]) !== null) {
                    continue;
                }

                $title = mb_substr(strip_tags($message), 0, 80);
                $title = $title !== '' ? $title : 'Legacy Hinweis';
                $now = date('Y-m-d H:i:s');

                scripts_execute(
                    $targetPdo,
                    'INSERT INTO announcements (id, tenant_id, title, message, visible_until, is_active, created_at, updated_at) VALUES (:id, :tenant_id, :title, :message, :visible_until, 1, :created_at, :updated_at)',
                    [
                        'id' => $announcementId,
                        'tenant_id' => $tenant['id'],
                        'title' => $title,
                        'message' => $message,
                        'visible_until' => scripts_datetime_string($row['gueltig_bis'] ?? null),
                        'created_at' => $now,
                        'updated_at' => $now,
                    ]
                );
                $counts['announcements_created']++;
            }
        }

        if ($legacyTables['config']['exists']) {
            $skipped[] = 'kl_config wurde erkannt, hat aber aktuell kein direktes Zielmodell im neuen Schema.';
        }

        if ($legacyTables['survey_votes']['exists'] || $legacyTables['survey_responses']['exists']) {
            $skipped[] = 'Die alten Umfrage-Tabellen wurden erkannt, werden aber noch nicht automatisch migriert.';
        }

        $targetPdo->commit();
    } catch (Throwable $exception) {
        if ($targetPdo->inTransaction()) {
            $targetPdo->rollBack();
        }

        throw $exception;
    }

    return [
        'schema_created' => $schemaCreated,
        'tenant' => $tenant,
        'counts' => $counts,
        'warnings' => $warnings,
        'skipped' => $skipped,
        'source_tables' => $legacyTables,
    ];
}

function scripts_stdout(string $message): void
{
    fwrite(STDOUT, $message . PHP_EOL);
}

function scripts_stderr(string $message, int $exitCode = 1): void
{
    fwrite(STDERR, $message . PHP_EOL);
    exit($exitCode);
}
