-- Phase 2: groups as a pure org / RBAC / UI container — see
-- docs/design/groups-and-project-tool.md §5, §9.
--
-- Groups form a GitLab-like, single-parent tree ABOVE apps. Phase 2 adds
-- the tree, hierarchy-aware membership, and structural-mutation safety —
-- but NO group-owned resources yet (scripts/vars/secrets stay app-owned;
-- that is Phase 3). The only data-plane touch is apps gaining a parent
-- pointer.
--
-- Adoption (§9): every existing app must have a parent from day one so
-- resolution always terminates. This migration seeds a single instance
-- root group and reparents every app under it, then promotes
-- apps.group_id to NOT NULL.

CREATE TABLE groups (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    -- Single parent keeps inheritance acyclic and resolution
    -- deterministic. NULL parent = a root node. RESTRICT (never CASCADE):
    -- deleting a non-empty group is refused so descendant apps and their
    -- isolated data can't be destroyed implicitly (§5.6). The ancestor-walk
    -- cycle guard that keeps this acyclic lives in manager-core (a SQL
    -- CHECK can't express it).
    parent_id         UUID REFERENCES groups(id) ON DELETE RESTRICT,
    -- Instance-global identifier, frozen at creation. A rename/reparent
    -- updates the display name/path but NEVER rewrites the slug, so the
    -- deployment key stays stable and external references don't break.
    -- Format validation lives in Rust handlers (same rule as app slugs).
    slug              TEXT NOT NULL UNIQUE,
    name              TEXT NOT NULL,
    description       TEXT,
    -- Per-subtree structure version (§6): bumped on every structural
    -- mutation of this node (reparent/rename/delete) so a future CLI/
    -- orchestrator can detect structural drift. NOT an authz input —
    -- authorization is resolved live every request.
    structure_version BIGINT NOT NULL DEFAULT 1,
    -- §7 ownership seam — the project-root that manages this node. Inert
    -- in Phase 2 (no projects table yet); nullable, no FK.
    owner_project     UUID,
    created_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX groups_parent_id_idx ON groups (parent_id);

-- Per-(user, group) explicit grant, mirroring app_members. Inherited
-- membership (GitLab-style) is resolved in code by walking ancestors: a
-- group_admin on any ancestor is implicitly app_admin on every app and
-- subgroup beneath it. Roles reuse the SAME three literals as app_members
-- so AppRole round-trips with zero mapping and the authz rank table
-- covers both tables.
CREATE TABLE group_members (
    group_id    UUID NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
    user_id     UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
    role        TEXT NOT NULL CHECK (role IN ('app_admin', 'editor', 'viewer')),
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    PRIMARY KEY (group_id, user_id)
);

-- Hot path is the authz ancestor walk "what groups does this user have a
-- role on?" plus the per-group member list.
CREATE INDEX group_members_user_id_idx ON group_members (user_id);

-- Add the parent pointer to apps (nullable for the backfill window).
ALTER TABLE apps ADD COLUMN group_id UUID;

-- Seed a single instance root group and reparent every existing app under
-- it. App slugs are already instance-global, so no slug rewrite is needed
-- — the parent pointer is new metadata layered on top.
WITH root_group AS (
    INSERT INTO groups (slug, name, description)
    VALUES ('root', 'Root', 'The instance root group — parent of all apps created before groups landed.')
    RETURNING id
)
UPDATE apps SET group_id = (SELECT id FROM root_group);

-- Every app now has a parent; promote to NOT NULL + FK. RESTRICT so a
-- group with apps can't be deleted out from under them (§5.6).
ALTER TABLE apps ALTER COLUMN group_id SET NOT NULL;
ALTER TABLE apps
    ADD CONSTRAINT apps_group_id_fk FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE RESTRICT;

CREATE INDEX apps_group_id_idx ON apps (group_id);
