import { z } from "zod";
import { announce } from "../notifications.js";
import { getPool, query, withTransaction } from "../db.js";
import { ApiError } from "../errors.js";
import { newJoinCode, normalizeJoinCode } from "../joincode.js";
const createSchema = z.object({
    kind: z.string().min(1).max(40),
    name: z.string().min(1).max(120),
    settings: z.record(z.string(), z.unknown()).default({}),
    max_members: z.number().int().min(2).max(500).optional(),
    display_name: z.string().min(1).max(60).optional(),
});
const patchSchema = z
    .object({
    name: z.string().min(1).max(120).optional(),
    settings: z.record(z.string(), z.unknown()).optional(),
    max_members: z.number().int().min(2).max(500).optional(),
})
    .refine((v) => v.name !== undefined || v.settings !== undefined || v.max_members !== undefined, {
    message: "Nothing to update",
});
const joinSchema = z.object({
    code: z.string().min(1).max(20),
    display_name: z.string().min(1).max(60).optional(),
});
const roleSchema = z.object({
    role: z.enum(["owner", "member"]),
});
function roomJson(room, myRole) {
    return {
        id: room.id,
        kind: room.kind,
        name: room.name,
        settings: room.settings,
        join_code: room.join_code,
        max_members: room.max_members,
        created_by: room.created_by,
        created_at: room.created_at,
        archived_at: room.archived_at,
        ...(myRole ? { my_role: myRole } : {}),
    };
}
function memberJson(m) {
    return {
        user_id: m.user_id,
        role: m.role,
        display_name: m.display_name,
        joined_at: m.joined_at,
    };
}
function parse(schema, body) {
    const parsed = schema.safeParse(body);
    if (!parsed.success) {
        throw new ApiError(400, "invalid_body", parsed.error.issues[0]?.message ?? "Invalid body");
    }
    return parsed.data;
}
export async function createRoom(claims, body, knownApps) {
    const input = parse(createSchema, body);
    if (!(await knownApps(claims.appId))) {
        throw new ApiError(403, "unknown_app", "app_id is not registered");
    }
    return withTransaction(async (tx) => {
        let room;
        for (let attempt = 0;; attempt++) {
            try {
                const res = await tx.query(`INSERT INTO rooms (app_id, kind, name, settings, join_code, max_members, created_by)
           VALUES ($1, $2, $3, $4, $5, COALESCE($6::int, 50), $7)
           RETURNING *`, [
                    claims.appId,
                    input.kind,
                    input.name,
                    JSON.stringify(input.settings),
                    newJoinCode(),
                    input.max_members ?? null,
                    claims.userId,
                ]);
                room = res.rows[0];
                break;
            }
            catch (err) {
                const unique = err.code === "23505";
                if (!unique || attempt >= 1)
                    throw err;
            }
        }
        await tx.query(`INSERT INTO room_members (room_id, user_id, role, display_name)
       VALUES ($1, $2, 'owner', $3)`, [room.id, claims.userId, input.display_name ?? null]);
        return roomJson(room, "owner");
    });
}
export async function listRooms(claims) {
    const res = await query(`SELECT r.*, m.role AS my_role
       FROM rooms r
       JOIN room_members m ON m.room_id = r.id
      WHERE m.user_id = $1 AND m.removed_at IS NULL
        AND r.app_id = $2 AND r.archived_at IS NULL
      ORDER BY m.joined_at DESC`, [claims.userId, claims.appId]);
    return { rooms: res.rows.map((r) => roomJson(r, r.my_role)) };
}
export async function getRoom(claims, roomId) {
    const { room, membership } = await loadRoomForMember(claims, roomId);
    const members = await query(`SELECT * FROM room_members
      WHERE room_id = $1 AND removed_at IS NULL
      ORDER BY joined_at`, [room.id]);
    return {
        ...roomJson(room, membership.role),
        members: members.rows.map(memberJson),
    };
}
export async function patchRoom(claims, roomId, body) {
    const input = parse(patchSchema, body);
    await loadRoomForOwner(claims, roomId);
    const res = await query(`UPDATE rooms
        SET name        = COALESCE($2, name),
            settings    = COALESCE($3::jsonb, settings),
            max_members = COALESCE($4::int, max_members)
      WHERE id = $1
      RETURNING *`, [
        roomId,
        input.name ?? null,
        input.settings === undefined ? null : JSON.stringify(input.settings),
        input.max_members ?? null,
    ]);
    return roomJson(res.rows[0], "owner");
}
export async function joinRoom(claims, body, notify) {
    const input = parse(joinSchema, body);
    const code = normalizeJoinCode(input.code);
    if (!code)
        throw new ApiError(404, "not_found", "Unknown join code");
    const result = await withTransaction(async (tx) => {
        const found = await tx.query(`SELECT * FROM rooms
        WHERE (join_code = $1 OR previous_join_code = $1) AND app_id = $2
        FOR UPDATE`, [code, claims.appId]);
        const room = found.rows[0];
        if (!room)
            throw new ApiError(404, "not_found", "Unknown join code");
        if (room.join_code !== code) {
            throw new ApiError(410, "code_rotated", "This code was replaced; ask for the current one");
        }
        if (room.archived_at)
            throw new ApiError(410, "room_archived", "This room is closed");
        const existing = await tx.query(`SELECT * FROM room_members WHERE room_id = $1 AND user_id = $2`, [room.id, claims.userId]);
        const prior = existing.rows[0];
        if (prior && prior.removed_at === null) {
            return { body: { ...roomJson(room, prior.role), rejoined: false }, joined: null };
        }
        if (prior?.removed_reason === "kicked") {
            throw new ApiError(403, "kicked", "You were removed from this room");
        }
        const count = await tx.query(`SELECT count(*) AS n FROM room_members WHERE room_id = $1 AND removed_at IS NULL`, [room.id]);
        if (Number(count.rows[0].n) >= room.max_members) {
            throw new ApiError(409, "room_full", "This room is full");
        }
        if (prior) {
            await tx.query(`UPDATE room_members
            SET removed_at = NULL, removed_reason = NULL, role = 'member',
                display_name = COALESCE($3, display_name)
          WHERE room_id = $1 AND user_id = $2`, [room.id, claims.userId, input.display_name ?? null]);
        }
        else {
            await tx.query(`INSERT INTO room_members (room_id, user_id, role, display_name)
         VALUES ($1, $2, 'member', $3)`, [room.id, claims.userId, input.display_name ?? null]);
        }
        return {
            body: { ...roomJson(room, "member"), rejoined: prior !== undefined },
            joined: { room, displayName: input.display_name ?? prior?.display_name ?? null },
        };
    });
    if (result.joined) {
        const { room, displayName } = result.joined;
        const members = await query(`SELECT user_id FROM room_members WHERE room_id = $1 AND removed_at IS NULL`, [room.id]);
        await announce(notify, {
            app_id: claims.appId,
            category: "room.member_joined",
            recipients: members.rows.map((m) => m.user_id),
            actor_user_id: claims.userId,
            title: room.name,
            body: `${displayName ?? "Someone"} joined`,
            coalesced_body: "{n} people joined",
            collapse_key: `room:${room.id}:joins`,
            data: { room_id: room.id },
        });
    }
    return result.body;
}
export async function leaveRoom(claims, roomId, notify) {
    const left = await withTransaction(async (tx) => {
        const { room, membership } = await loadRoomForMember(claims, roomId, tx);
        if (membership.role === "owner") {
            await assertNotSoleOwner(tx, roomId, claims.userId);
        }
        await tx.query(`UPDATE room_members SET removed_at = now(), removed_reason = 'left'
        WHERE room_id = $1 AND user_id = $2`, [roomId, claims.userId]);
        return { room, displayName: membership.display_name };
    });
    const owners = await query(`SELECT user_id FROM room_members
      WHERE room_id = $1 AND role = 'owner' AND removed_at IS NULL`, [roomId]);
    await announce(notify, {
        app_id: claims.appId,
        category: "room.member_left",
        recipients: owners.rows.map((o) => o.user_id),
        actor_user_id: claims.userId,
        title: left.room.name,
        body: `${left.displayName ?? "A member"} left`,
        data: { room_id: left.room.id },
    });
    return { ok: true };
}
export async function setMemberRole(claims, roomId, targetUserId, body) {
    const input = parse(roleSchema, body);
    return withTransaction(async (tx) => {
        await loadRoomForOwner(claims, roomId, tx);
        const target = await activeMember(tx, roomId, targetUserId);
        if (!target)
            throw new ApiError(404, "not_found", "No such member");
        if (input.role === "member" && target.role === "owner") {
            await assertNotSoleOwner(tx, roomId, targetUserId);
        }
        await tx.query(`UPDATE room_members SET role = $3 WHERE room_id = $1 AND user_id = $2`, [roomId, targetUserId, input.role]);
        return { user_id: targetUserId, role: input.role };
    });
}
export async function kickMember(claims, roomId, targetUserId) {
    return withTransaction(async (tx) => {
        await loadRoomForOwner(claims, roomId, tx);
        if (targetUserId === claims.userId) {
            throw new ApiError(400, "use_leave", "Leaving is not a kick");
        }
        const target = await activeMember(tx, roomId, targetUserId);
        if (!target)
            throw new ApiError(404, "not_found", "No such member");
        if (target.role === "owner") {
            throw new ApiError(403, "cannot_kick_owner", "Demote the owner first");
        }
        await tx.query(`UPDATE room_members SET removed_at = now(), removed_reason = 'kicked'
        WHERE room_id = $1 AND user_id = $2`, [roomId, targetUserId]);
        return { ok: true };
    });
}
export async function rotateJoinCode(claims, roomId) {
    return withTransaction(async (tx) => {
        const { room } = await loadRoomForOwner(claims, roomId, tx);
        for (let attempt = 0;; attempt++) {
            try {
                const res = await tx.query(`UPDATE rooms SET join_code = $2, previous_join_code = $3
            WHERE id = $1 RETURNING *`, [roomId, newJoinCode(), room.join_code]);
                await tx.query(`UPDATE room_members SET removed_reason = 'left'
            WHERE room_id = $1 AND removed_reason = 'kicked'`, [roomId]);
                return roomJson(res.rows[0], "owner");
            }
            catch (err) {
                const unique = err.code === "23505";
                if (!unique || attempt >= 1)
                    throw err;
            }
        }
    });
}
export async function archiveRoom(claims, roomId) {
    return withTransaction(async (tx) => {
        const { room } = await loadRoomForOwner(claims, roomId, tx);
        if (room.archived_at)
            return roomJson(room, "owner");
        const res = await tx.query(`UPDATE rooms SET archived_at = now() WHERE id = $1 RETURNING *`, [roomId]);
        return roomJson(res.rows[0], "owner");
    });
}
async function loadRoom(claims, roomId, q) {
    if (!isUuid(roomId))
        throw new ApiError(404, "not_found", "Unknown room");
    const res = await q.query(`SELECT * FROM rooms WHERE id = $1 AND app_id = $2`, [roomId, claims.appId]);
    const room = res.rows[0];
    if (!room)
        throw new ApiError(404, "not_found", "Unknown room");
    return room;
}
async function loadRoomForMember(claims, roomId, q = getPool()) {
    const room = await loadRoom(claims, roomId, q);
    const membership = await activeMember(q, roomId, claims.userId);
    if (!membership)
        throw new ApiError(404, "not_found", "Unknown room");
    return { room, membership };
}
async function loadRoomForOwner(claims, roomId, q = getPool()) {
    const loaded = await loadRoomForMember(claims, roomId, q);
    if (loaded.membership.role !== "owner") {
        throw new ApiError(403, "owner_only", "Only an owner can do this");
    }
    return loaded;
}
async function activeMember(q, roomId, userId) {
    const res = await q.query(`SELECT * FROM room_members
      WHERE room_id = $1 AND user_id = $2 AND removed_at IS NULL`, [roomId, userId]);
    return res.rows[0] ?? null;
}
async function assertNotSoleOwner(q, roomId, userId) {
    const others = await q.query(`SELECT 1 FROM room_members
      WHERE room_id = $1 AND user_id <> $2 AND role = 'owner' AND removed_at IS NULL
      LIMIT 1`, [roomId, userId]);
    if (others.rows.length === 0) {
        throw new ApiError(409, "sole_owner", "Promote another owner or archive the room first");
    }
}
function isUuid(value) {
    return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
}
