const pool = require("../db/pool"); function getExecutor(client) { return client || pool; } async function withTransaction(handler) { const client = await pool.connect(); try { await client.query("BEGIN"); const result = await handler(client); await client.query("COMMIT"); return result; } catch (error) { await client.query("ROLLBACK"); throw error; } finally { client.release(); } } async function getManageableGroupsForUser(userId, client) { const result = await getExecutor(client).query( `SELECT household_id AS group_id FROM household_members WHERE user_id = $1 AND role IN ('owner', 'admin')`, [userId] ); return result.rows; } async function getUserGroupRole(groupId, userId, client) { const result = await getExecutor(client).query( `SELECT role FROM household_members WHERE household_id = $1 AND user_id = $2`, [groupId, userId] ); return result.rows[0]?.role || null; } async function getGroupById(groupId, client) { const result = await getExecutor(client).query( `SELECT id, name FROM households WHERE id = $1`, [groupId] ); return result.rows[0] || null; } async function listInviteLinks(groupId, client) { const result = await getExecutor(client).query( `SELECT id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at FROM group_invite_links WHERE group_id = $1 ORDER BY created_at DESC`, [groupId] ); return result.rows; } async function createInviteLink( { groupId, createdBy, token, policy, singleUse, expiresAt }, client ) { const result = await getExecutor(client).query( `INSERT INTO group_invite_links ( group_id, created_by, token, policy, single_use, expires_at ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at`, [groupId, createdBy, token, policy, singleUse, expiresAt] ); return result.rows[0]; } async function getInviteLinkById(groupId, linkId, client) { const result = await getExecutor(client).query( `SELECT id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at FROM group_invite_links WHERE group_id = $1 AND id = $2`, [groupId, linkId] ); return result.rows[0] || null; } async function revokeInviteLink(groupId, linkId, client) { const result = await getExecutor(client).query( `UPDATE group_invite_links SET revoked_at = NOW() WHERE group_id = $1 AND id = $2 RETURNING id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at`, [groupId, linkId] ); return result.rows[0] || null; } async function reviveInviteLink(groupId, linkId, expiresAt, client) { const result = await getExecutor(client).query( `UPDATE group_invite_links SET used_at = NULL, revoked_at = NULL, expires_at = $3 WHERE group_id = $1 AND id = $2 RETURNING id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at`, [groupId, linkId, expiresAt] ); return result.rows[0] || null; } async function deleteInviteLink(groupId, linkId, client) { const result = await getExecutor(client).query( `DELETE FROM group_invite_links WHERE group_id = $1 AND id = $2 RETURNING id, group_id, created_by, token, policy, single_use, expires_at, used_at, revoked_at, created_at`, [groupId, linkId] ); return result.rows[0] || null; } async function getInviteLinkSummaryByToken(token, client, forUpdate = false) { const result = await getExecutor(client).query( `SELECT gil.id, gil.group_id, gil.created_by, gil.token, gil.policy, gil.single_use, gil.expires_at, gil.used_at, gil.revoked_at, gil.created_at, h.name AS group_name, gs.join_policy AS current_join_policy FROM group_invite_links gil JOIN households h ON h.id = gil.group_id LEFT JOIN group_settings gs ON gs.group_id = gil.group_id WHERE gil.token = $1 ${forUpdate ? "FOR UPDATE OF gil" : ""}`, [token] ); return result.rows[0] || null; } async function isGroupMember(groupId, userId, client) { const result = await getExecutor(client).query( `SELECT 1 FROM household_members WHERE household_id = $1 AND user_id = $2`, [groupId, userId] ); return result.rows.length > 0; } async function getPendingJoinRequest(groupId, userId, client) { const result = await getExecutor(client).query( `SELECT id, group_id, user_id, status, created_at, updated_at FROM group_join_requests WHERE group_id = $1 AND user_id = $2 AND status = 'PENDING'`, [groupId, userId] ); return result.rows[0] || null; } async function createOrTouchPendingJoinRequest(groupId, userId, client) { const executor = getExecutor(client); const existing = await executor.query( `UPDATE group_join_requests SET updated_at = NOW() WHERE group_id = $1 AND user_id = $2 AND status = 'PENDING' RETURNING id, group_id, user_id, status, created_at, updated_at`, [groupId, userId] ); if (existing.rows[0]) { return existing.rows[0]; } try { const inserted = await executor.query( `INSERT INTO group_join_requests (group_id, user_id, status) VALUES ($1, $2, 'PENDING') RETURNING id, group_id, user_id, status, created_at, updated_at`, [groupId, userId] ); return inserted.rows[0]; } catch (error) { if (error.code !== "23505") { throw error; } const fallback = await executor.query( `SELECT id, group_id, user_id, status, created_at, updated_at FROM group_join_requests WHERE group_id = $1 AND user_id = $2 AND status = 'PENDING' LIMIT 1`, [groupId, userId] ); return fallback.rows[0] || null; } } async function addGroupMember(groupId, userId, role = "member", client) { const result = await getExecutor(client).query( `INSERT INTO household_members (household_id, user_id, role) VALUES ($1, $2, $3) ON CONFLICT (household_id, user_id) DO NOTHING RETURNING id`, [groupId, userId, role] ); return result.rows.length > 0; } async function consumeSingleUseInvite(linkId, client) { const result = await getExecutor(client).query( `UPDATE group_invite_links SET used_at = NOW(), revoked_at = NOW() WHERE id = $1 RETURNING id`, [linkId] ); return result.rows.length > 0; } async function getGroupSettings(groupId, client) { const result = await getExecutor(client).query( `SELECT group_id, join_policy FROM group_settings WHERE group_id = $1`, [groupId] ); return result.rows[0] || null; } async function upsertGroupSettings(groupId, joinPolicy, client) { const result = await getExecutor(client).query( `INSERT INTO group_settings (group_id, join_policy) VALUES ($1, $2) ON CONFLICT (group_id) DO UPDATE SET join_policy = EXCLUDED.join_policy, updated_at = NOW() RETURNING group_id, join_policy`, [groupId, joinPolicy] ); return result.rows[0]; } async function createGroupAuditLog( { groupId, actorUserId, actorRole, eventType, requestId, ip, userAgent, success = true, errorCode = null, metadata = {}, }, client ) { const result = await getExecutor(client).query( `INSERT INTO group_audit_log ( group_id, actor_user_id, actor_role, event_type, request_id, ip, user_agent, success, error_code, metadata ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb) RETURNING id`, [ groupId, actorUserId, actorRole, eventType, requestId, ip, userAgent, success, errorCode, JSON.stringify(metadata || {}), ] ); return result.rows[0]; } module.exports = { addGroupMember, createGroupAuditLog, createInviteLink, createOrTouchPendingJoinRequest, consumeSingleUseInvite, deleteInviteLink, getGroupById, getGroupSettings, getInviteLinkById, getInviteLinkSummaryByToken, getManageableGroupsForUser, getPendingJoinRequest, getUserGroupRole, isGroupMember, listInviteLinks, revokeInviteLink, reviveInviteLink, upsertGroupSettings, withTransaction, };