<?php

declare(strict_types=1);

// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OCA\Forum\Migration;

/**
 * Helper class for seeding initial forum data
 * Can be used by multiple migrations to ensure data exists
 */
class SeedHelper {
	/**
	 * Seed all initial data
	 * Each function checks its own state and returns early if already seeded
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 * @param bool $throwOnError If true, throws exceptions on failure. If false (default), logs errors and continues.
	 *                           Set to false when called from migrations to avoid PostgreSQL transaction abort issues.
	 */
	public static function seedAll($output = null, bool $throwOnError = false, ?array $adminUserIds = null): void {
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$logger->info('Forum seeding: Starting data seed/repair');

		if ($output) {
			$output->info('Forum: Starting data seed/repair...');
		}

		$errors = [];

		// Ensure forum_users table exists (handle rename from forum_user_stats if needed)
		// This is critical and should fail early if it cannot be done
		try {
			self::ensureForumUsersTable($output);
		} catch (\Exception $e) {
			$errors[] = 'ensureForumUsersTable: ' . $e->getMessage();
			$logger->error('Forum seeding: Failed to ensure forum_users table', ['exception' => $e->getMessage()]);
			if ($output) {
				$output->warning('  Failed to ensure forum_users table: ' . $e->getMessage());
			}
			// Try to recover connection state for PostgreSQL
			self::recoverConnectionState($db, $logger);
		}

		// Each function checks its own state and returns early if already seeded
		// They run independently so one failure does not block others
		// This is especially important for PostgreSQL where a failed query aborts the transaction
		$seedOperations = [
			'seedDefaultRoles' => fn () => self::seedDefaultRoles($output),
			'seedCategoryHeaders' => fn () => self::seedCategoryHeaders($output),
			'seedDefaultCategories' => fn () => self::seedDefaultCategories($output),
			'seedCategoryPermissions' => fn () => self::seedCategoryPermissions($output),
			'seedGuestRolePermissions' => fn () => self::seedGuestRolePermissions($output),
			'seedDefaultBBCodes' => fn () => self::seedDefaultBBCodes($output),
			'assignUserRoles' => fn () => self::assignUserRoles($output, $adminUserIds),
			'seedWelcomeThread' => fn () => self::seedWelcomeThread($output),
		];

		foreach ($seedOperations as $name => $operation) {
			try {
				// Before each operation, ensure connection is in a clean state
				self::recoverConnectionState($db, $logger);
				$operation();
			} catch (\Exception $e) {
				$errors[] = "$name: " . $e->getMessage();
				$logger->error("Forum seeding: $name failed", ['exception' => $e->getMessage()]);
				// Try to recover connection state for next operation (especially important for PostgreSQL)
				self::recoverConnectionState($db, $logger);
				// Continue with other operations - don't let one failure block others
			}
		}

		if (!empty($errors)) {
			$errorSummary = 'Some seeding operations failed: ' . implode('; ', $errors);
			$logger->warning('Forum seeding: Completed with errors', ['errors' => $errors]);

			if ($output) {
				$output->warning('Forum: Data seed/repair completed with errors. Run "occ forum:repair-seeds" to retry failed operations.');
			}

			if ($throwOnError) {
				throw new \RuntimeException($errorSummary);
			}
		} else {
			$logger->info('Forum seeding: Completed data seed/repair successfully');

			if ($output) {
				$output->info('Forum: Data seed/repair completed');
			}

			// Mark as initialized on success
			try {
				$appConfig = \OC::$server->get(\OCP\IAppConfig::class);
				$appConfig->setValueBool('forum', 'is_initialized', true, lazy: true);
			} catch (\Exception $e) {
				$logger->warning('Forum seeding: Failed to set is_initialized flag', ['exception' => $e->getMessage()]);
			}
		}
	}

	/**
	 * Recover database connection state after an error
	 * On PostgreSQL, a failed query aborts the entire transaction, and subsequent queries fail.
	 * This method attempts to rollback any open transactions to restore a usable connection state.
	 *
	 * @param \OCP\IDBConnection $db Database connection
	 * @param \Psr\Log\LoggerInterface $logger Logger instance
	 */
	private static function recoverConnectionState(\OCP\IDBConnection $db, \Psr\Log\LoggerInterface $logger): void {
		try {
			// If we're in a transaction, try to roll back to recover the connection
			while ($db->inTransaction()) {
				try {
					$db->rollBack();
					$logger->debug('Forum seeding: Rolled back transaction to recover connection state');
				} catch (\Exception $e) {
					// If rollback fails, the connection might be in an unrecoverable state
					$logger->warning('Forum seeding: Failed to rollback transaction during recovery', ['exception' => $e->getMessage()]);
					break;
				}
			}
		} catch (\Exception $e) {
			// Ignore errors when checking transaction state
			$logger->debug('Forum seeding: Error checking transaction state', ['exception' => $e->getMessage()]);
		}
	}

	/**
	 * Public wrapper for ensureForumUsersTable for use in migrations
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function ensureForumUsersTablePublic($output = null): void {
		self::ensureForumUsersTable($output);
	}

	/**
	 * Ensure forum_users table exists, renaming from forum_user_stats if needed,
	 * or creating it from scratch for fresh installations.
	 * This handles cases where migrations partially failed or fresh installs.
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	private static function ensureForumUsersTable($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$config = \OC::$server->get(\OCP\IConfig::class);
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);

		$prefix = $config->getSystemValueString('dbtableprefix', 'oc_');
		$oldTable = $prefix . 'forum_user_stats';
		$newTable = $prefix . 'forum_users';

		$oldTableExists = self::tableExists($db, $oldTable);
		$newTableExists = self::tableExists($db, $newTable);

		if ($oldTableExists && !$newTableExists) {
			// Case 1: Old table exists, rename it
			$logger->info('Forum seeding: Renaming forum_user_stats to forum_users...');
			if ($output) {
				$output->info('  → Renaming forum_user_stats to forum_users...');
			}

			try {
				$platform = $db->getDatabasePlatform();
				$platformName = $platform->getName();

				if ($platformName === 'mysql' || $platformName === 'mariadb') {
					$db->executeStatement("RENAME TABLE `{$oldTable}` TO `{$newTable}`");
				} else {
					// PostgreSQL, SQLite and others
					$db->executeStatement("ALTER TABLE \"{$oldTable}\" RENAME TO \"{$newTable}\"");
				}

				$logger->info('Forum seeding: Table renamed successfully');
				if ($output) {
					$output->info('  ✓ Table renamed successfully');
				}
			} catch (\Exception $e) {
				$logger->error('Forum seeding: Failed to rename table', ['exception' => $e->getMessage()]);
				throw $e;
			}
		} elseif (!$oldTableExists && !$newTableExists) {
			// Case 2: Neither table exists (fresh install), create forum_users
			$logger->info('Forum seeding: Creating forum_users table...');
			if ($output) {
				$output->info('  → Creating forum_users table...');
			}

			try {
				self::createForumUsersTable($db);

				$logger->info('Forum seeding: Table created successfully');
				if ($output) {
					$output->info('  ✓ Table created successfully');
				}
			} catch (\Exception $e) {
				$logger->error('Forum seeding: Failed to create forum_users table', ['exception' => $e->getMessage()]);
				throw $e;
			}
		}
		// Case 3: $newTableExists is true - nothing to do
	}

	/**
	 * Create the forum_users table from scratch
	 * This mirrors the final schema from Version1 + Version2 + Version8 migrations
	 * (id as primary key, user_id as unique, includes signature column)
	 */
	private static function createForumUsersTable(\OCP\IDBConnection $db): void {
		$platform = $db->getDatabasePlatform();
		$config = \OC::$server->get(\OCP\IConfig::class);
		$prefix = $config->getSystemValueString('dbtableprefix', 'oc_');
		$tableName = $prefix . 'forum_users';

		// Use instanceof checks for reliable platform detection (getName() is deprecated)
		if ($platform instanceof \Doctrine\DBAL\Platforms\MySQLPlatform) {
			// MySQL and MariaDB both extend MySQLPlatform
			$db->executeStatement("
				CREATE TABLE `{$tableName}` (
					`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
					`user_id` VARCHAR(64) NOT NULL,
					`post_count` INT UNSIGNED NOT NULL DEFAULT 0,
					`thread_count` INT UNSIGNED NOT NULL DEFAULT 0,
					`last_post_at` INT UNSIGNED DEFAULT NULL,
					`deleted_at` INT UNSIGNED DEFAULT NULL,
					`signature` TEXT DEFAULT NULL,
					`created_at` INT UNSIGNED NOT NULL,
					`updated_at` INT UNSIGNED NOT NULL,
					PRIMARY KEY (`id`),
					UNIQUE INDEX `forum_users_user_id_uniq` (`user_id`),
					INDEX `forum_users_post_count_idx` (`post_count`),
					INDEX `forum_users_thread_count_idx` (`thread_count`),
					INDEX `forum_users_deleted_at_idx` (`deleted_at`)
				) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
			");
		} elseif ($platform instanceof \Doctrine\DBAL\Platforms\PostgreSQLPlatform) {
			$db->executeStatement("
				CREATE TABLE \"{$tableName}\" (
					\"id\" BIGSERIAL PRIMARY KEY,
					\"user_id\" VARCHAR(64) NOT NULL,
					\"post_count\" INTEGER NOT NULL DEFAULT 0,
					\"thread_count\" INTEGER NOT NULL DEFAULT 0,
					\"last_post_at\" INTEGER DEFAULT NULL,
					\"deleted_at\" INTEGER DEFAULT NULL,
					\"signature\" TEXT DEFAULT NULL,
					\"created_at\" INTEGER NOT NULL,
					\"updated_at\" INTEGER NOT NULL
				)
			");
			$db->executeStatement("CREATE UNIQUE INDEX \"forum_users_user_id_uniq\" ON \"{$tableName}\" (\"user_id\")");
			$db->executeStatement("CREATE INDEX \"forum_users_post_count_idx\" ON \"{$tableName}\" (\"post_count\")");
			$db->executeStatement("CREATE INDEX \"forum_users_thread_count_idx\" ON \"{$tableName}\" (\"thread_count\")");
			$db->executeStatement("CREATE INDEX \"forum_users_deleted_at_idx\" ON \"{$tableName}\" (\"deleted_at\")");
		} else {
			// SQLite (and any other platform as fallback)
			$db->executeStatement("
				CREATE TABLE \"{$tableName}\" (
					\"id\" INTEGER PRIMARY KEY AUTOINCREMENT,
					\"user_id\" VARCHAR(64) NOT NULL,
					\"post_count\" INTEGER NOT NULL DEFAULT 0,
					\"thread_count\" INTEGER NOT NULL DEFAULT 0,
					\"last_post_at\" INTEGER DEFAULT NULL,
					\"deleted_at\" INTEGER DEFAULT NULL,
					\"signature\" TEXT DEFAULT NULL,
					\"created_at\" INTEGER NOT NULL,
					\"updated_at\" INTEGER NOT NULL
				)
			");
			$db->executeStatement("CREATE UNIQUE INDEX \"forum_users_user_id_uniq\" ON \"{$tableName}\" (\"user_id\")");
			$db->executeStatement("CREATE INDEX \"forum_users_post_count_idx\" ON \"{$tableName}\" (\"post_count\")");
			$db->executeStatement("CREATE INDEX \"forum_users_thread_count_idx\" ON \"{$tableName}\" (\"thread_count\")");
			$db->executeStatement("CREATE INDEX \"forum_users_deleted_at_idx\" ON \"{$tableName}\" (\"deleted_at\")");
		}
	}

	/**
	 * Check if a table exists in the database
	 */
	private static function tableExists(\OCP\IDBConnection $db, string $tableName): bool {
		$platform = $db->getDatabasePlatform();
		$platformName = $platform->getName();

		try {
			if ($platformName === 'mysql' || $platformName === 'mariadb') {
				$result = $db->executeQuery(
					'SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?',
					[$tableName]
				);
			} elseif ($platformName === 'postgresql') {
				$result = $db->executeQuery(
					'SELECT COUNT(*) FROM information_schema.tables WHERE table_name = ?',
					[$tableName]
				);
			} else {
				// SQLite
				$result = $db->executeQuery(
					"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name = ?",
					[$tableName]
				);
			}

			$count = (int)$result->fetchOne();
			$result->closeCursor();
			return $count > 0;
		} catch (\Exception $e) {
			return false;
		}
	}

	/**
	 * Seed default roles (Admin, Moderator, User, Guest)
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedDefaultRoles($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$l = \OC::$server->getL10N('forum');
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		try {
			// Get existing roles by role_type (not hardcoded IDs) to check what needs to be created
			$qb = $db->getQueryBuilder();
			$qb->select('role_type')
				->from('forum_roles')
				->where($qb->expr()->in('role_type', $qb->createNamedParameter([
					\OCA\Forum\Db\Role::ROLE_TYPE_ADMIN,
					\OCA\Forum\Db\Role::ROLE_TYPE_MODERATOR,
					\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT,
					\OCA\Forum\Db\Role::ROLE_TYPE_GUEST,
				], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR_ARRAY)));
			$result = $qb->executeQuery();
			$existingRoles = $result->fetchAll();
			$result->closeCursor();

			// Use array_unique to handle duplicates (shouldn't happen after cleanup migration, but be defensive)
			$existingTypes = array_unique(array_map(fn ($role) => $role['role_type'], $existingRoles));

			if (count($existingTypes) === 4) {
				$logger->info('Forum seeding: Default roles already exist, skipping');
				if ($output) {
					$output->info('  ✓ Default roles already exist');
				}
				return;
			}

			if ($output) {
				$output->info('  → Creating default roles...');
			}

			// Note: We don't use explicit transactions here to avoid PostgreSQL transaction abort cascade.
			// Each INSERT is independent and idempotent, so partial success is acceptable.
			$rolesCreated = 0;

			// Define roles by role_type (not hardcoded IDs)
			$rolesToCreate = [
				\OCA\Forum\Db\Role::ROLE_TYPE_ADMIN => [
					'name' => $l->t('Admin'),
					'description' => $l->t('Administrator role with full permissions'),
					'color_light' => '#dc2626',
					'color_dark' => '#f87171',
					'can_access_admin_tools' => true,
					'can_edit_roles' => true,
					'can_edit_categories' => true,
					'is_system_role' => true,
					'role_type' => \OCA\Forum\Db\Role::ROLE_TYPE_ADMIN,
				],
				\OCA\Forum\Db\Role::ROLE_TYPE_MODERATOR => [
					'name' => $l->t('Moderator'),
					'description' => $l->t('Moderator role with elevated permissions'),
					'color_light' => '#2563eb',
					'color_dark' => '#60a5fa',
					'can_access_admin_tools' => true,
					'can_edit_roles' => false,
					'can_edit_categories' => false,
					'is_system_role' => true,
					'role_type' => \OCA\Forum\Db\Role::ROLE_TYPE_MODERATOR,
				],
				\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT => [
					'name' => $l->t('User'),
					'description' => $l->t('Default user role with basic permissions'),
					'color_light' => '#059669',
					'color_dark' => '#34d399',
					'can_access_admin_tools' => false,
					'can_edit_roles' => false,
					'can_edit_categories' => false,
					'is_system_role' => true,
					'role_type' => \OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT,
				],
				\OCA\Forum\Db\Role::ROLE_TYPE_GUEST => [
					'name' => $l->t('Guest'),
					'description' => $l->t('Guest role for unauthenticated users with read-only access'),
					'color_light' => '#868e96',
					'color_dark' => '#adb5bd',
					'can_access_admin_tools' => false,
					'can_edit_roles' => false,
					'can_edit_categories' => false,
					'is_system_role' => true,
					'role_type' => \OCA\Forum\Db\Role::ROLE_TYPE_GUEST,
				],
			];

			foreach ($rolesToCreate as $roleType => $roleData) {
				if (!in_array($roleType, $existingTypes)) {
					try {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_roles')
							->values([
								'name' => $qb->createNamedParameter($roleData['name']),
								'description' => $qb->createNamedParameter($roleData['description']),
								'color_light' => $qb->createNamedParameter($roleData['color_light'] ?? null),
								'color_dark' => $qb->createNamedParameter($roleData['color_dark'] ?? null),
								'can_access_admin_tools' => $qb->createNamedParameter($roleData['can_access_admin_tools'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_edit_roles' => $qb->createNamedParameter($roleData['can_edit_roles'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_edit_categories' => $qb->createNamedParameter($roleData['can_edit_categories'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'is_system_role' => $qb->createNamedParameter($roleData['is_system_role'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'role_type' => $qb->createNamedParameter($roleData['role_type'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							])
							->executeStatement();
						$rolesCreated++;
						$logger->info("Forum seeding: Created role with type '$roleType'");
					} catch (\Exception $e) {
						// Log but continue - other roles might succeed
						$logger->warning("Forum seeding: Failed to create role '$roleType': " . $e->getMessage());
					}
				}
			}

			// Validate that critical roles can be found by role_type after creation
			// Note: We query directly instead of using RoleMapper to avoid MultipleObjectsReturnedException
			// if duplicates somehow exist (the cleanup migration should have removed them, but be defensive)
			$criticalRoles = [
				\OCA\Forum\Db\Role::ROLE_TYPE_GUEST => 'Guest',
				\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT => 'Default User',
				\OCA\Forum\Db\Role::ROLE_TYPE_ADMIN => 'Admin',
			];

			foreach ($criticalRoles as $roleType => $roleName) {
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_roles')
					->where($qb->expr()->eq('role_type', $qb->createNamedParameter($roleType, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
					->setMaxResults(1);
				$result = $qb->executeQuery();
				$role = $result->fetch();
				$result->closeCursor();

				if ($role) {
					$logger->info("Forum seeding: Validated $roleName role (ID {$role['id']}, type: $roleType)");
				} else {
					$logger->error("Forum seeding: CRITICAL - $roleName role not found after creation. This will break functionality.");
					if ($output) {
						$output->warning("  ✗ CRITICAL: $roleName role not found - forum may not function correctly");
					}
				}
			}

			$logger->info("Forum seeding: Created $rolesCreated default roles");
			if ($output) {
				$output->info("  ✓ Created $rolesCreated default roles (Admin, Moderator, User, Guest)");
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to create default roles', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create default roles: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create default roles: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed guest role permissions (copy view permissions from User role)
	 * Note: Guest role must be created first in seedDefaultRoles()
	 * Note: Category permissions must exist first from seedCategoryPermissions()
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedGuestRolePermissions($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);

		try {
			// Find the Guest role
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_GUEST, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$guestRole = $result->fetch();
			$result->closeCursor();

			if (!$guestRole) {
				$logger->warning('Forum seeding: Guest role not found, cannot seed permissions');
				if ($output) {
					$output->warning('  ⚠ Guest role not found, skipping permission seeding');
				}
				return;
			}

			$guestRoleId = (int)$guestRole['id'];

			// Find the User (default) role ID
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$userRole = $result->fetch();
			$result->closeCursor();

			if (!$userRole) {
				$logger->warning('Forum seeding: User (default) role not found, cannot determine which categories to grant guest access to');
				if ($output) {
					$output->warning('  ⚠ User role not found, cannot seed guest permissions');
				}
				return;
			}

			$userRoleId = (int)$userRole['id'];

			// Check if guest role already has permissions (idempotency check)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_category_perms')
				->where($qb->expr()->eq('target_type', $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
				->andWhere($qb->expr()->eq('target_id', $qb->createNamedParameter((string)$guestRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
				->setMaxResults(1);
			$result = $qb->executeQuery();
			$hasPermissions = $result->fetch();
			$result->closeCursor();

			if ($hasPermissions) {
				$logger->info('Forum seeding: Guest role permissions already exist, skipping');
				if ($output) {
					$output->info('  ✓ Guest role permissions already exist');
				}
				return;
			}

			if ($output) {
				$output->info('  → Setting guest role permissions...');
			}

			// Get only categories where the User role has view permission
			$qb = $db->getQueryBuilder();
			$qb->select('category_id')
				->from('forum_category_perms')
				->where($qb->expr()->eq('target_type', $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
				->andWhere($qb->expr()->eq('target_id', $qb->createNamedParameter((string)$userRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
				->andWhere($qb->expr()->eq('can_view', $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL)));
			$result = $qb->executeQuery();
			$userAccessibleCategories = $result->fetchAll();
			$result->closeCursor();

			// Note: No explicit transaction - each INSERT auto-commits to avoid PostgreSQL transaction abort cascade
			$categoriesGranted = 0;
			foreach ($userAccessibleCategories as $categoryRow) {
				$categoryId = (int)$categoryRow['category_id'];

				// Check if permission already exists
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_category_perms')
					->where($qb->expr()->eq('category_id', $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
					->andWhere($qb->expr()->eq('target_type', $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
					->andWhere($qb->expr()->eq('target_id', $qb->createNamedParameter((string)$guestRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
				$result = $qb->executeQuery();
				$permExists = $result->fetch();
				$result->closeCursor();

				if (!$permExists) {
					try {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_category_perms')
							->values([
								'category_id' => $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
								'target_type' => $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'target_id' => $qb->createNamedParameter((string)$guestRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'can_view' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_post' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_reply' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_moderate' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
							])
							->executeStatement();
						$categoriesGranted++;
					} catch (\Exception $e) {
						// Log but continue - other categories might succeed
						$logger->warning("Forum seeding: Failed to set guest permission for category $categoryId: " . $e->getMessage());
					}
				}
			}

			$logger->info('Forum seeding: Set guest role view-only permissions for ' . $categoriesGranted . ' categories (matching User role access)');
			if ($output) {
				$output->info('  ✓ Set guest role view-only permissions for ' . $categoriesGranted . ' categories');
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to set guest role permissions', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to set guest role permissions: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to set guest role permissions: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed category headers
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedCategoryHeaders($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$l = \OC::$server->getL10N('forum');
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		try {
			// Check if headers already exist
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_cat_headers')
				->setMaxResults(1);
			$result = $qb->executeQuery();
			$exists = $result->fetch();
			$result->closeCursor();

			if ($exists) {
				$logger->info('Forum seeding: Category headers already exist, skipping');
				if ($output) {
					$output->info('  ✓ Category headers already exist');
				}
				return;
			}

			if ($output) {
				$output->info('  → Creating category headers...');
			}

			// Note: No explicit transaction - single INSERT auto-commits
			// Create "General" category header
			$qb = $db->getQueryBuilder();
			$qb->insert('forum_cat_headers')
				->values([
					'name' => $qb->createNamedParameter($l->t('General')),
					'description' => $qb->createNamedParameter($l->t('General discussion categories')),
					'sort_order' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
				])
				->executeStatement();

			$logger->info('Forum seeding: Created category headers');
			if ($output) {
				$output->info('  ✓ Created category headers');
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to create category headers', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create category headers: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create category headers: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed default categories
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedDefaultCategories($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$l = \OC::$server->getL10N('forum');
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		try {
			// Get the header ID (should be 1 if created by seedCategoryHeaders)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_cat_headers')
				->orderBy('id', 'ASC')
				->setMaxResults(1);
			$result = $qb->executeQuery();
			$header = $result->fetch();
			$result->closeCursor();

			if (!$header) {
				$logger->error('Forum seeding: No category headers found, cannot create categories');
				if ($output) {
					$output->warning('  ✗ No category headers found, cannot create categories');
				}
				throw new \RuntimeException('Cannot create categories: category headers must be created first');
			}

			if ($output) {
				$output->info('  → Creating default categories...');
			}

			$headerId = (int)$header['id'];
			// Note: No explicit transaction - each INSERT auto-commits to avoid PostgreSQL transaction abort cascade
			$categoriesCreated = 0;

			// Check if "General Discussions" category exists
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_categories')
				->where($qb->expr()->eq('slug', $qb->createNamedParameter('general-discussions')));
			$result = $qb->executeQuery();
			$exists = $result->fetch();
			$result->closeCursor();

			if (!$exists) {
				try {
					// Create "General Discussions" category
					$qb = $db->getQueryBuilder();
					$qb->insert('forum_categories')
						->values([
							'header_id' => $qb->createNamedParameter($headerId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'name' => $qb->createNamedParameter($l->t('General discussions')),
							'description' => $qb->createNamedParameter($l->t('A place for general conversations and discussions')),
							'slug' => $qb->createNamedParameter('general-discussions'),
							'sort_order' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'thread_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'post_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'updated_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						])
						->executeStatement();
					$categoriesCreated++;
				} catch (\Exception $e) {
					$logger->warning('Forum seeding: Failed to create General Discussions category: ' . $e->getMessage());
				}
			}

			// Check if "Support" category exists
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_categories')
				->where($qb->expr()->eq('slug', $qb->createNamedParameter('support')));
			$result = $qb->executeQuery();
			$exists = $result->fetch();
			$result->closeCursor();

			if (!$exists) {
				try {
					// Create "Support" category
					$qb = $db->getQueryBuilder();
					$qb->insert('forum_categories')
						->values([
							'header_id' => $qb->createNamedParameter($headerId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'name' => $qb->createNamedParameter($l->t('Support')),
							'description' => $qb->createNamedParameter($l->t('Ask questions about the forum, provide feedback or report issues.')),
							'slug' => $qb->createNamedParameter('support'),
							'sort_order' => $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'thread_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'post_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'updated_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						])
						->executeStatement();
					$categoriesCreated++;
				} catch (\Exception $e) {
					$logger->warning('Forum seeding: Failed to create Support category: ' . $e->getMessage());
				}
			}

			$logger->info("Forum seeding: Created $categoriesCreated default categories");
			if ($output) {
				$output->info("  ✓ Created $categoriesCreated default categories (General Discussions, Support)");
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to create default categories', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create default categories: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create default categories: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed category permissions for all roles
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedCategoryPermissions($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);

		try {
			// Get all category IDs
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_categories');
			$result = $qb->executeQuery();
			$categories = $result->fetchAll();
			$result->closeCursor();

			if (empty($categories)) {
				$logger->error('Forum seeding: No categories found, cannot create permissions');
				if ($output) {
					$output->warning('  ✗ No categories found, cannot create permissions');
				}
				throw new \RuntimeException('Cannot create category permissions: categories must be created first');
			}

			// Find Moderator role by role_type (not hardcoded ID)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_MODERATOR, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$moderatorRole = $result->fetch();
			$result->closeCursor();

			// Find User (default) role by role_type (not hardcoded ID)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$userRole = $result->fetch();
			$result->closeCursor();

			if (!$moderatorRole || !$userRole) {
				$logger->error('Forum seeding: Not all required roles exist, cannot create permissions');
				if ($output) {
					$output->warning('  ✗ Required roles do not exist, cannot create permissions');
				}
				throw new \RuntimeException('Cannot create category permissions: roles must be created first');
			}

			$moderatorRoleId = (int)$moderatorRole['id'];
			$userRoleId = (int)$userRole['id'];

			if ($output) {
				$output->info('  → Creating category permissions...');
			}

			// Note: No explicit transaction - each INSERT auto-commits to avoid PostgreSQL transaction abort cascade
			$permissionsCreated = 0;

			// Create permissions for Moderator and User roles (Admin has implicit permissions)
			foreach ($categories as $category) {
				$categoryId = (int)$category['id'];

				// Check and create Moderator role permissions
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_category_perms')
					->where($qb->expr()->eq('category_id', $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
					->andWhere($qb->expr()->eq('target_type', $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
					->andWhere($qb->expr()->eq('target_id', $qb->createNamedParameter((string)$moderatorRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
				$result = $qb->executeQuery();
				$exists = $result->fetch();
				$result->closeCursor();

				if (!$exists) {
					try {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_category_perms')
							->values([
								'category_id' => $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
								'target_type' => $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'target_id' => $qb->createNamedParameter((string)$moderatorRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'can_view' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_post' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_reply' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_moderate' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
							])
							->executeStatement();
						$permissionsCreated++;
					} catch (\Exception $e) {
						$logger->warning("Forum seeding: Failed to create moderator permission for category $categoryId: " . $e->getMessage());
					}
				}

				// Check and create User role permissions
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_category_perms')
					->where($qb->expr()->eq('category_id', $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
					->andWhere($qb->expr()->eq('target_type', $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)))
					->andWhere($qb->expr()->eq('target_id', $qb->createNamedParameter((string)$userRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
				$result = $qb->executeQuery();
				$exists = $result->fetch();
				$result->closeCursor();

				if (!$exists) {
					try {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_category_perms')
							->values([
								'category_id' => $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
								'target_type' => $qb->createNamedParameter('role', \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'target_id' => $qb->createNamedParameter((string)$userRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR),
								'can_view' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_post' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_reply' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'can_moderate' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
							])
							->executeStatement();
						$permissionsCreated++;
					} catch (\Exception $e) {
						$logger->warning("Forum seeding: Failed to create user permission for category $categoryId: " . $e->getMessage());
					}
				}
			}

			$logger->info("Forum seeding: Created $permissionsCreated category permissions");
			if ($output) {
				$output->info("  ✓ Created $permissionsCreated category permissions for " . count($categories) . ' categories');
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to create category permissions', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create category permissions: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create category permissions: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed default BBCodes
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedDefaultBBCodes($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$l = \OC::$server->getL10N('forum');
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		try {
			if ($output) {
				$output->info('  → Creating default BBCodes...');
			}

			// Note: No explicit transaction - each INSERT auto-commits to avoid PostgreSQL transaction abort cascade
			$bbcodes = [
				[
					'tag' => 'icode',
					'replacement' => '<code>{content}</code>',
					'example' => '[icode]' . $l->t('Inline code') . '[/icode]',
					'description' => $l->t('Inline code'),
					'parse_inner' => false,
					'is_builtin' => true,
					'special_handler' => null,
				],
				[
					'tag' => 'spoiler',
					'replacement' => '<details><summary>{title}</summary>{content}</details>',
					'example' => '[spoiler="' . $l->t('Spoiler title') . '"]' . $l->t('Hidden content') . '[/spoiler]',
					'description' => $l->t('Spoilers'),
					'parse_inner' => false,
					'is_builtin' => true,
					'special_handler' => null,
				],
				[
					'tag' => 'attachment',
					'replacement' => '{content}',
					'example' => '[attachment]/file/path.txt[/attachment]',
					'description' => $l->t('Attachment'),
					'parse_inner' => false,
					'is_builtin' => true,
					'special_handler' => 'attachment',
				],
			];

			$bbcodesCreated = 0;
			foreach ($bbcodes as $bbcode) {
				// Check if this specific BBCode already exists
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_bbcodes')
					->where($qb->expr()->eq('tag', $qb->createNamedParameter($bbcode['tag'])));
				$result = $qb->executeQuery();
				$exists = $result->fetch();
				$result->closeCursor();

				if (!$exists) {
					try {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_bbcodes')
							->values([
								'tag' => $qb->createNamedParameter($bbcode['tag']),
								'replacement' => $qb->createNamedParameter($bbcode['replacement']),
								'example' => $qb->createNamedParameter($bbcode['example']),
								'description' => $qb->createNamedParameter($bbcode['description']),
								'enabled' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'parse_inner' => $qb->createNamedParameter($bbcode['parse_inner'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'is_builtin' => $qb->createNamedParameter($bbcode['is_builtin'], \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
								'special_handler' => $qb->createNamedParameter($bbcode['special_handler']),
								'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							])
							->executeStatement();
						$bbcodesCreated++;
					} catch (\Exception $e) {
						$logger->warning("Forum seeding: Failed to create BBCode '{$bbcode['tag']}': " . $e->getMessage());
					}
				}
			}

			$logger->info("Forum seeding: Created $bbcodesCreated default BBCodes");
			if ($output) {
				$output->info("  ✓ Created $bbcodesCreated default BBCodes (icode, spoiler, attachment)");
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to create default BBCodes', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create default BBCodes: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create default BBCodes: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Assign roles to all Nextcloud users
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function assignUserRoles($output = null, ?array $adminUserIds = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$userManager = \OC::$server->get(\OCP\IUserManager::class);
		$groupManager = \OC::$server->get(\OCP\IGroupManager::class);
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		try {
			// Find Admin role by role_type (not hardcoded ID)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_ADMIN, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$adminRole = $result->fetch();
			$result->closeCursor();

			// Find User (default) role by role_type (not hardcoded ID)
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_roles')
				->where($qb->expr()->eq('role_type', $qb->createNamedParameter(\OCA\Forum\Db\Role::ROLE_TYPE_DEFAULT, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_STR)));
			$result = $qb->executeQuery();
			$userRole = $result->fetch();
			$result->closeCursor();

			if (!$adminRole || !$userRole) {
				$logger->error('Forum seeding: Required roles do not exist, cannot assign user roles');
				if ($output) {
					$output->warning('  ✗ Required roles do not exist, cannot assign user roles');
				}
				throw new \RuntimeException('Cannot assign user roles: roles must be created first');
			}

			$adminRoleId = (int)$adminRole['id'];
			$userRoleId = (int)$userRole['id'];

			if ($output) {
				$output->info('  → Assigning roles to users...');
			}

			// Assign roles to all users
			$usersProcessed = 0;
			$usersSkipped = 0;
			$userManager->callForAllUsers(function ($user) use ($db, $timestamp, $groupManager, $logger, $output, &$usersProcessed, &$usersSkipped, $adminRoleId, $userRoleId, $adminUserIds) {
				try {
					$userId = $user->getUID();
					$isAdmin = $adminUserIds !== null
						? in_array($userId, $adminUserIds, true)
						: $groupManager->isAdmin($userId);

					// Check if user already has the User role
					$qb = $db->getQueryBuilder();
					$qb->select('id')
						->from('forum_user_roles')
						->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
						->andWhere($qb->expr()->eq('role_id', $qb->createNamedParameter($userRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
					$result = $qb->executeQuery();
					$hasUserRole = $result->fetch();
					$result->closeCursor();

					// Assign User role to all users if they do not have it
					if (!$hasUserRole) {
						$qb = $db->getQueryBuilder();
						$qb->insert('forum_user_roles')
							->values([
								'user_id' => $qb->createNamedParameter($userId),
								'role_id' => $qb->createNamedParameter($userRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
								'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							])
							->executeStatement();
					}

					// Check if admin user already has the Admin role
					if ($isAdmin) {
						$qb = $db->getQueryBuilder();
						$qb->select('id')
							->from('forum_user_roles')
							->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
							->andWhere($qb->expr()->eq('role_id', $qb->createNamedParameter($adminRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
						$result = $qb->executeQuery();
						$hasAdminRole = $result->fetch();
						$result->closeCursor();

						// Assign Admin role to admin group members if they do not have it
						if (!$hasAdminRole) {
							$qb = $db->getQueryBuilder();
							$qb->insert('forum_user_roles')
								->values([
									'user_id' => $qb->createNamedParameter($userId),
									'role_id' => $qb->createNamedParameter($adminRoleId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
									'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
								])
								->executeStatement();
						}
					}

					$usersProcessed++;
					if ($usersProcessed % 100 === 0) {
						$logger->info("Forum seeding: Processed $usersProcessed users");
						if ($output) {
							$output->info("    → Processed $usersProcessed users...");
						}
					}
				} catch (\Exception $e) {
					// Log error but continue with other users
					$logger->warning('Forum seeding: Failed to assign roles to user ' . $user->getUID(), [
						'exception' => $e->getMessage(),
					]);
					$usersSkipped++;
				}
			});

			$logger->info("Forum seeding: Assigned roles to $usersProcessed users" . ($usersSkipped > 0 ? " ($usersSkipped skipped due to errors)" : ''));
			if ($output) {
				$output->info("  ✓ Assigned roles to $usersProcessed users" . ($usersSkipped > 0 ? " ($usersSkipped skipped)" : ''));
			}
		} catch (\Exception $e) {
			$logger->error('Forum seeding: Failed to assign user roles', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to assign user roles: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to assign user roles: ' . $e->getMessage(), 0, $e);
		}
	}

	/**
	 * Seed welcome thread
	 *
	 * @param \OCP\Migration\IOutput|null $output Optional output for console messages
	 */
	public static function seedWelcomeThread($output = null): void {
		$db = \OC::$server->get(\OCP\IDBConnection::class);
		$userManager = \OC::$server->get(\OCP\IUserManager::class);
		$groupManager = \OC::$server->get(\OCP\IGroupManager::class);
		$l = \OC::$server->getL10N('forum');
		$logger = \OC::$server->get(\Psr\Log\LoggerInterface::class);
		$timestamp = time();

		// Recover connection state before starting (important for PostgreSQL)
		self::recoverConnectionState($db, $logger);

		try {
			// Check if welcome thread already exists
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_threads')
				->where($qb->expr()->eq('slug', $qb->createNamedParameter('welcome-to-nextcloud-forums')));
			$result = $qb->executeQuery();
			$exists = $result->fetch();
			$result->closeCursor();

			if ($exists) {
				$logger->info('Forum seeding: Welcome thread already exists, skipping');
				if ($output) {
					$output->info('  ✓ Welcome thread already exists');
				}
				return;
			}

			// Get first category ID
			$qb = $db->getQueryBuilder();
			$qb->select('id')
				->from('forum_categories')
				->orderBy('id', 'ASC')
				->setMaxResults(1);
			$result = $qb->executeQuery();
			$category = $result->fetch();
			$result->closeCursor();

			if (!$category) {
				$logger->error('Forum seeding: No categories found, cannot create welcome thread');
				if ($output) {
					$output->warning('  ✗ No categories found, cannot create welcome thread');
				}
				throw new \RuntimeException('Cannot create welcome thread: categories must be created first');
			}

			if ($output) {
				$output->info('  → Creating welcome thread...');
			}

			$categoryId = (int)$category['id'];

			// Find first admin user (fallback to 'admin')
			$adminUserId = 'admin';
			$userManager->callForSeenUsers(function ($user) use ($groupManager, &$adminUserId) {
				if ($groupManager->isAdmin($user->getUID())) {
					$adminUserId = $user->getUID();
					return false; // Stop iteration
				}
			});

			// Prepare welcome post content
			$welcomeContent = $l->t('Welcome to the Nextcloud Forums!') . "\n\n"
				. $l->t('This is a community-driven forum built right into your Nextcloud instance. '
				. 'Here you can discuss topics, share ideas and collaborate with other users.') . "\n\n"
				. '[b]' . $l->t('Features:') . "[/b]\n"
				. "[list]\n"
				. '[*]' . $l->t('Create and reply to threads') . "\n"
				. '[*]' . $l->t('Organize discussions by categories') . "\n"
				. '[*]' . $l->t('Use BBCode for rich text formatting') . "\n"
				. '[*]' . $l->t('Attach files from your Nextcloud storage') . "\n"
				. '[*]' . $l->t('React to posts') . "\n"
				. '[*]' . $l->t('Track read/unread threads') . "\n\n"
				. "[/list]\n"
				. '[b]' . $l->t('BBCode examples:') . "[/b]\n"
				. "[list]\n"
				. '[*][b]' . $l->t('Bold text') . '[/b] - ' . $l->t('Use %1$stext%2$s', ['[icode][b]', '[/b][/icode]']) . "\n"
				. '[*][i]' . $l->t('Italic text') . '[/i] - ' . $l->t('Use %1$stext%2$s', ['[icode][i]', '[/i][/icode]']) . "\n"
				. '[*][u]' . $l->t('Underlined text') . '[/u] - ' . $l->t('Use %1$stext%2$s', ['[icode][u]', '[/u][/icode]']) . "\n\n"
				. "[/list]\n"
				. $l->t('Feel free to start a new discussion or reply to existing threads. Happy posting!');

			$db->beginTransaction();

			// Create welcome thread
			$qb = $db->getQueryBuilder();
			$qb->insert('forum_threads')
				->values([
					'category_id' => $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'author_id' => $qb->createNamedParameter($adminUserId),
					'title' => $qb->createNamedParameter($l->t('Welcome to Nextcloud Forums')),
					'slug' => $qb->createNamedParameter('welcome-to-nextcloud-forums'),
					'view_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'post_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'last_post_id' => $qb->createNamedParameter(null, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'is_locked' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
					'is_pinned' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
					'is_hidden' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
					'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					'updated_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
				])
				->executeStatement();
			$threadId = $qb->getLastInsertId();

			// Build post values (slug column was removed in Version8)
			$qb = $db->getQueryBuilder();
			$postValues = [
				'thread_id' => $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
				'author_id' => $qb->createNamedParameter($adminUserId),
				'content' => $qb->createNamedParameter($welcomeContent),
				'is_edited' => $qb->createNamedParameter(false, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
				'is_first_post' => $qb->createNamedParameter(true, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_BOOL),
				'edited_at' => $qb->createNamedParameter(null, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
				'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
				'updated_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
			];

			$qb->insert('forum_posts')
				->values($postValues)
				->executeStatement();
			$postId = $qb->getLastInsertId();

			// Update thread's last_post_id
			$qb = $db->getQueryBuilder();
			$qb->update('forum_threads')
				->set('last_post_id', $qb->createNamedParameter($postId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
				->where($qb->expr()->eq('id', $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
				->executeStatement();

			// Update category counts
			// Note: post_count is 0 because the first post (is_first_post=true) doesn't count
			$qb = $db->getQueryBuilder();
			$qb->update('forum_categories')
				->set('thread_count', $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
				->set('post_count', $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
				->where($qb->expr()->eq('id', $qb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
				->executeStatement();

			// Subscribe the admin user to the welcome thread
			if ($db->tableExists('forum_thread_subs')) {
				// Check if subscription already exists
				$qb = $db->getQueryBuilder();
				$qb->select('id')
					->from('forum_thread_subs')
					->where($qb->expr()->eq('thread_id', $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
					->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($adminUserId)));
				$result = $qb->executeQuery();
				$subExists = $result->fetch();
				$result->closeCursor();

				if (!$subExists) {
					$qb = $db->getQueryBuilder();
					$qb->insert('forum_thread_subs')
						->values([
							'thread_id' => $qb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
							'user_id' => $qb->createNamedParameter($adminUserId),
							'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						])
						->executeStatement();
				}
			}

			// Create forum user for the admin user if it does not exist
			$qb = $db->getQueryBuilder();
			$qb->select('user_id')
				->from('forum_users')
				->where($qb->expr()->eq('user_id', $qb->createNamedParameter($adminUserId)));
			$result = $qb->executeQuery();
			$statsExists = $result->fetch();
			$result->closeCursor();

			if (!$statsExists) {
				$qb = $db->getQueryBuilder();
				$qb->insert('forum_users')
					->values([
						'user_id' => $qb->createNamedParameter($adminUserId),
						'post_count' => $qb->createNamedParameter(0, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						'thread_count' => $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						'last_post_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						'created_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
						'updated_at' => $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT),
					])
					->executeStatement();
			} else {
				// Update existing stats to increment thread count
				$qb = $db->getQueryBuilder();
				$qb->update('forum_users')
					->set('thread_count', $qb->func()->add('thread_count', $qb->createNamedParameter(1, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
					->set('last_post_at', $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
					->set('updated_at', $qb->createNamedParameter($timestamp, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
					->where($qb->expr()->eq('user_id', $qb->createNamedParameter($adminUserId)))
					->executeStatement();
			}

			$db->commit();
			$logger->info('Forum seeding: Created welcome thread');
			if ($output) {
				$output->info('  ✓ Created welcome thread');
			}
		} catch (\Exception $e) {
			// Try to rollback if we're in a transaction - important for PostgreSQL recovery
			try {
				if ($db->inTransaction()) {
					$db->rollBack();
				}
			} catch (\Exception $rollbackEx) {
				$logger->debug('Forum seeding: Failed to rollback after welcome thread error', ['exception' => $rollbackEx->getMessage()]);
			}
			$logger->error('Forum seeding: Failed to create welcome thread', [
				'exception' => $e->getMessage(),
			]);
			if ($output) {
				$output->warning('  ✗ Failed to create welcome thread: ' . $e->getMessage());
			}
			throw new \RuntimeException('Failed to create welcome thread: ' . $e->getMessage(), 0, $e);
		}
	}
}
