-- Migration 003: RLS policies + is_active_member helper
-- All business access rules live here. Every table has RLS enabled in its own
-- migration; this file only adds the policies.

-- ── Helper: is_active_member ──────────────────────────────────────────────────
-- Returns true if the calling user is a non-guest member of the collective.
-- Used by all content-table policies. Declared STABLE so PostgreSQL can cache
-- the result within a single statement.

CREATE OR REPLACE FUNCTION public.is_active_member(p_collective_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT EXISTS (
        SELECT 1
        FROM   public.collective_members
        WHERE  collective_id = p_collective_id
        AND    user_id       = auth.uid()
        AND    role IN ('admin', 'member')
    );
$$;

-- Variant that also accepts guests (read access).
CREATE OR REPLACE FUNCTION public.is_member(p_collective_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT EXISTS (
        SELECT 1
        FROM   public.collective_members
        WHERE  collective_id = p_collective_id
        AND    user_id       = auth.uid()
    );
$$;

CREATE OR REPLACE FUNCTION public.is_admin(p_collective_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = public
AS $$
    SELECT EXISTS (
        SELECT 1
        FROM   public.collective_members
        WHERE  collective_id = p_collective_id
        AND    user_id       = auth.uid()
        AND    role = 'admin'
    );
$$;

-- ── public.users policies ─────────────────────────────────────────────────────

-- Any authenticated user can read their own row.
CREATE POLICY users_select_own
    ON public.users FOR SELECT
    USING (id = auth.uid());

-- Members can read profiles of users in the same collective.
CREATE POLICY users_select_collective_peers
    ON public.users FOR SELECT
    USING (
        EXISTS (
            SELECT 1
            FROM   public.collective_members cm1
            JOIN   public.collective_members cm2
                ON cm2.collective_id = cm1.collective_id
                AND cm2.user_id = auth.uid()
            WHERE  cm1.user_id = public.users.id
        )
    );

-- A user can only update their own row.
CREATE POLICY users_update_own
    ON public.users FOR UPDATE
    USING  (id = auth.uid())
    WITH CHECK (id = auth.uid());

-- Insert is handled by the trigger (SECURITY DEFINER). No direct INSERT allowed.
CREATE POLICY users_insert_deny
    ON public.users FOR INSERT
    WITH CHECK (false);

-- Delete cascades from auth.users deletion; no direct DELETE allowed here.
CREATE POLICY users_delete_deny
    ON public.users FOR DELETE
    USING (false);

-- ── public.collectives policies ───────────────────────────────────────────────

-- Members (any role) can read collectives they belong to.
CREATE POLICY collectives_select
    ON public.collectives FOR SELECT
    USING (public.is_member(id));

-- Any authenticated user can create a collective (they become its first admin).
CREATE POLICY collectives_insert
    ON public.collectives FOR INSERT
    WITH CHECK (created_by = auth.uid());

-- Only admins can update the collective name / emoji.
CREATE POLICY collectives_update
    ON public.collectives FOR UPDATE
    USING  (public.is_admin(id))
    WITH CHECK (public.is_admin(id));

-- Admins can delete a collective.
CREATE POLICY collectives_delete
    ON public.collectives FOR DELETE
    USING (public.is_admin(id));

-- ── public.collective_members policies ───────────────────────────────────────

-- Members of a collective can read its member list.
CREATE POLICY collective_members_select
    ON public.collective_members FOR SELECT
    USING (public.is_member(collective_id));

-- Only admins can add members (accepting invitations handled via server action
-- with SECURITY DEFINER function, not direct INSERT).
CREATE POLICY collective_members_insert
    ON public.collective_members FOR INSERT
    WITH CHECK (public.is_admin(collective_id));

-- Only admins can change roles; users cannot promote themselves.
CREATE POLICY collective_members_update
    ON public.collective_members FOR UPDATE
    USING  (public.is_admin(collective_id))
    WITH CHECK (public.is_admin(collective_id));

-- Admins can remove any member; members can remove themselves.
CREATE POLICY collective_members_delete
    ON public.collective_members FOR DELETE
    USING (
        public.is_admin(collective_id)
        OR user_id = auth.uid()
    );

-- ── public.collective_invitations policies ────────────────────────────────────

-- Members of a collective can read its invitations (to copy the link).
CREATE POLICY collective_invitations_select
    ON public.collective_invitations FOR SELECT
    USING (public.is_member(collective_id));

-- Only admins can create invitations.
CREATE POLICY collective_invitations_insert
    ON public.collective_invitations FOR INSERT
    WITH CHECK (
        public.is_admin(collective_id)
        AND created_by = auth.uid()
    );

-- Only admins can revoke (delete) invitations.
CREATE POLICY collective_invitations_delete
    ON public.collective_invitations FOR DELETE
    USING (public.is_admin(collective_id));

-- Accepting an invitation (setting accepted_at) is done via a SECURITY DEFINER
-- server action — no UPDATE policy needed here.
CREATE POLICY collective_invitations_update_deny
    ON public.collective_invitations FOR UPDATE
    USING (false);

-- ── SECURITY DEFINER: accept invitation ──────────────────────────────────────
-- Called from the SvelteKit server action for /invitation/[token].
-- Validates token, expiry, and that user isn't already a member, then inserts
-- into collective_members and stamps accepted_at.

CREATE OR REPLACE FUNCTION public.accept_invitation(p_token uuid)
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
    v_invitation public.collective_invitations%ROWTYPE;
    v_user_id    uuid := auth.uid();
BEGIN
    IF v_user_id IS NULL THEN
        RETURN jsonb_build_object('error', 'unauthenticated');
    END IF;

    SELECT * INTO v_invitation
    FROM   public.collective_invitations
    WHERE  token = p_token
    FOR    UPDATE;

    IF NOT FOUND THEN
        RETURN jsonb_build_object('error', 'not_found');
    END IF;

    IF v_invitation.accepted_at IS NOT NULL THEN
        RETURN jsonb_build_object('error', 'already_used');
    END IF;

    IF v_invitation.expires_at < now() THEN
        RETURN jsonb_build_object('error', 'expired');
    END IF;

    IF EXISTS (
        SELECT 1 FROM public.collective_members
        WHERE collective_id = v_invitation.collective_id
        AND   user_id       = v_user_id
    ) THEN
        RETURN jsonb_build_object('error', 'already_member');
    END IF;

    INSERT INTO public.collective_members (collective_id, user_id, role)
    VALUES (v_invitation.collective_id, v_user_id, v_invitation.role);

    UPDATE public.collective_invitations
    SET    accepted_at = now(),
           accepted_by = v_user_id
    WHERE  id = v_invitation.id;

    RETURN jsonb_build_object(
        'ok',           true,
        'collective_id', v_invitation.collective_id,
        'role',          v_invitation.role
    );
END;
$$;
