-- +goose Up

-- entity-keys (docs/identifiers.md §2 to §4, §7). Every entity has
-- an ID (UUIDv7, always database-generated; Decision 30 stands) and a name.
-- Tenants and catalog entities also carry a nullable `key`: a unique string
-- that lets something outside the database (a seed, a configuration file, a
-- test, a script, an API client) address the row without knowing its
-- generated ID. Vocabulary tables keep their primary keys as their keys.
--
-- Column contract, one rule for every table below:
--   * `key TEXT NULL`;
--   * grammar `^[a-z][a-z0-9_]*$`, at most 64 characters (the vocabulary
--     grammar already ratified for org types, resource keys, and roles);
--   * unique within the smallest namespace the entity inhabits: the table
--     for a root entity, `(parent_id, key)` for a child. NULLs are distinct,
--     so rows without a key never conflict;
--   * never derived from a person's data. The system writes a key only as a
--     fixed constant for a row it creates by design: the System tenant
--     (`system`) and each organization's default pool (`default`).
--
-- The three `slug` columns become `key` (the address role they were meant
-- for, without the defect: they were NOT NULL and filled from a login name or
-- a display name, which is why they read as asking for the name twice). The
-- plan ladder's `ladder_key` becomes `key` under the same contract.
-- Entitlement sets, products, and prices gain the column. The System tenant
-- singleton and the two name guards from the earlier cut of this migration
-- are unchanged.

-- ---------------------------------------------------------------------------
-- (a) Organizations: `slug` → `key`, nullable, root scope.
-- ---------------------------------------------------------------------------
-- The singleton first, so a database holding two system organizations fails
-- on the index that names the rule rather than on the key assignment below.
CREATE UNIQUE INDEX uq_organizations_one_system
    ON core.organizations (org_type) WHERE org_type = 'system';

ALTER TABLE core.organizations
    DROP CONSTRAINT uq_organizations_slug;
DROP INDEX core.idx_organizations_slug;
ALTER TABLE core.organizations
    RENAME COLUMN slug TO key;
ALTER TABLE core.organizations
    ALTER COLUMN key TYPE TEXT,
    ALTER COLUMN key DROP NOT NULL;

-- Every existing value was derived from the owner's login name or the
-- organization's name at creation; none was chosen, none is referenced. The
-- System tenant keeps the constant it is created with; everything else is
-- cleared (a personal organization gets no key by rule).
UPDATE core.organizations
SET key = CASE WHEN org_type = 'system' THEN 'system' END;

ALTER TABLE core.organizations
    ADD CONSTRAINT uq_organizations_key UNIQUE (key),
    ADD CONSTRAINT chk_organizations_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

-- ---------------------------------------------------------------------------
-- (b) Workspaces: `slug` → `key`, nullable, scoped to the organization.
-- ---------------------------------------------------------------------------
ALTER TABLE core.workspaces
    DROP CONSTRAINT uq_workspaces_org_id_slug;
ALTER TABLE core.workspaces
    RENAME COLUMN slug TO key;
ALTER TABLE core.workspaces
    ALTER COLUMN key TYPE TEXT,
    ALTER COLUMN key DROP NOT NULL;

-- Every value was derived from the workspace's name; a singleton child (the
-- System tenant's workspace, an organization's first workspace) is addressed
-- through its parent and carries no key.
UPDATE core.workspaces SET key = NULL;

ALTER TABLE core.workspaces
    ADD CONSTRAINT uq_workspaces_org_id_key UNIQUE (org_id, key),
    ADD CONSTRAINT chk_workspaces_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

-- Workspace names disambiguate within their organization among live rows
-- (a soft-deleted workspace frees its name; upstream data model v16.1
-- documents the same predicate). Dedupe first: within each organization,
-- the second and later live workspaces sharing a name case-insensitively get
-- ' (2)', ' (3)', ... appended, oldest kept intact. created_at is the
-- authoritative order; workspace_id (uuidv7, time-ordered) breaks ties.
WITH ranked AS (
    SELECT workspace_id,
           ROW_NUMBER() OVER (
               PARTITION BY org_id, lower(name)
               ORDER BY created_at ASC, workspace_id ASC
           ) AS rn
    FROM core.workspaces
    WHERE status <> 'deleted'
)
UPDATE core.workspaces w
SET name = w.name || ' (' || ranked.rn || ')'
FROM ranked
WHERE w.workspace_id = ranked.workspace_id AND ranked.rn > 1;

CREATE UNIQUE INDEX uq_workspaces_org_id_name_ci
    ON core.workspaces (org_id, lower(name)) WHERE status <> 'deleted';

-- ---------------------------------------------------------------------------
-- (c) Resource pools: `slug` → `key`, nullable, scoped to the organization.
-- ---------------------------------------------------------------------------
ALTER TABLE core.resource_pools
    DROP CONSTRAINT uq_resource_pools_org_id_slug;
ALTER TABLE core.resource_pools
    RENAME COLUMN slug TO key;
ALTER TABLE core.resource_pools
    ALTER COLUMN key TYPE TEXT,
    ALTER COLUMN key DROP NOT NULL;

-- The default pool is the one row the system creates by design for every
-- organization; uq_resource_pools_one_default_per_org (00010) guarantees at
-- most one per organization, so the constant cannot collide.
UPDATE core.resource_pools
SET key = CASE WHEN pool_type = 'default' THEN 'default' END;

ALTER TABLE core.resource_pools
    ADD CONSTRAINT uq_resource_pools_org_id_key UNIQUE (org_id, key),
    ADD CONSTRAINT chk_resource_pools_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

-- ---------------------------------------------------------------------------
-- (d) Plan ladders: `ladder_key` → `key`, nullable, root scope.
-- ---------------------------------------------------------------------------
ALTER TABLE core.plan_ladders
    DROP CONSTRAINT uq_plan_ladders_ladder_key;
ALTER TABLE core.plan_ladders
    RENAME COLUMN ladder_key TO key;
ALTER TABLE core.plan_ladders
    ALTER COLUMN key TYPE TEXT,
    ALTER COLUMN key DROP NOT NULL;

-- Existing ladder keys were shown to and confirmed by an operator at
-- creation, and the demo seed addresses its ladder by one, so they are kept
-- and brought under the grammar rather than cleared: lower-cased, separator
-- runs folded to '_', leading non-letters removed. A value the grammar still
-- rejects, or that collides after folding (the later row by created_at),
-- becomes NULL; nothing outside the database names those values.
UPDATE core.plan_ladders
SET key = regexp_replace(regexp_replace(lower(key), '[^a-z0-9_]+', '_', 'g'), '^[^a-z]+', '');
UPDATE core.plan_ladders
SET key = NULL
WHERE key !~ '^[a-z][a-z0-9_]*$' OR length(key) > 64;
WITH ranked AS (
    SELECT plan_ladder_id,
           ROW_NUMBER() OVER (PARTITION BY key ORDER BY created_at ASC, plan_ladder_id ASC) AS rn
    FROM core.plan_ladders
    WHERE key IS NOT NULL
)
UPDATE core.plan_ladders l
SET key = NULL
FROM ranked
WHERE l.plan_ladder_id = ranked.plan_ladder_id AND ranked.rn > 1;

ALTER TABLE core.plan_ladders
    ADD CONSTRAINT uq_plan_ladders_key UNIQUE (key),
    ADD CONSTRAINT chk_plan_ladders_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

-- Plan ladder names disambiguate globally: ladder names lead every catalog
-- and composite surface, so two ladders named the same are indistinguishable
-- to an operator. Same dedupe rule as (b).
WITH ranked AS (
    SELECT plan_ladder_id,
           ROW_NUMBER() OVER (
               PARTITION BY lower(name)
               ORDER BY created_at ASC, plan_ladder_id ASC
           ) AS rn
    FROM core.plan_ladders
)
UPDATE core.plan_ladders l
SET name = l.name || ' (' || ranked.rn || ')'
FROM ranked
WHERE l.plan_ladder_id = ranked.plan_ladder_id AND ranked.rn > 1;

CREATE UNIQUE INDEX uq_plan_ladders_name_ci
    ON core.plan_ladders (lower(name));

-- ---------------------------------------------------------------------------
-- (e) Entitlement sets and products gain `key` (root scope); prices gain
--     `key` scoped to their product. All NULL until a seed, a configuration
--     loader, an API client, or an operator sets one.
-- ---------------------------------------------------------------------------
ALTER TABLE core.entitlement_sets
    ADD COLUMN key TEXT,
    ADD CONSTRAINT uq_entitlement_sets_key UNIQUE (key),
    ADD CONSTRAINT chk_entitlement_sets_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

ALTER TABLE core.products
    ADD COLUMN key TEXT,
    ADD CONSTRAINT uq_products_key UNIQUE (key),
    ADD CONSTRAINT chk_products_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

ALTER TABLE core.prices
    ADD COLUMN key TEXT,
    ADD CONSTRAINT uq_prices_product_id_key UNIQUE (product_id, key),
    ADD CONSTRAINT chk_prices_key_grammar
        CHECK (key IS NULL OR (key ~ '^[a-z][a-z0-9_]*$' AND length(key) <= 64));

-- +goose Down

-- The rollback restores the shape, not the values: the renamed columns go
-- back to their old names, nullable, holding whatever keys were set (the
-- cleared and folded values are not recoverable), and the added columns are
-- dropped. The dedupe renames in (b) and (d) are data changes with no record
-- of the prior names; Down leaves them in place. This matches the
-- repository's snapshot-based rollback convention (see
-- 00010_schema_hardening.sql).

ALTER TABLE core.prices
    DROP CONSTRAINT chk_prices_key_grammar,
    DROP CONSTRAINT uq_prices_product_id_key,
    DROP COLUMN key;
ALTER TABLE core.products
    DROP CONSTRAINT chk_products_key_grammar,
    DROP CONSTRAINT uq_products_key,
    DROP COLUMN key;
ALTER TABLE core.entitlement_sets
    DROP CONSTRAINT chk_entitlement_sets_key_grammar,
    DROP CONSTRAINT uq_entitlement_sets_key,
    DROP COLUMN key;

DROP INDEX core.uq_plan_ladders_name_ci;
ALTER TABLE core.plan_ladders
    DROP CONSTRAINT chk_plan_ladders_key_grammar,
    DROP CONSTRAINT uq_plan_ladders_key;
ALTER TABLE core.plan_ladders RENAME COLUMN key TO ladder_key;

ALTER TABLE core.resource_pools
    DROP CONSTRAINT chk_resource_pools_key_grammar,
    DROP CONSTRAINT uq_resource_pools_org_id_key;
ALTER TABLE core.resource_pools RENAME COLUMN key TO slug;

DROP INDEX core.uq_workspaces_org_id_name_ci;
ALTER TABLE core.workspaces
    DROP CONSTRAINT chk_workspaces_key_grammar,
    DROP CONSTRAINT uq_workspaces_org_id_key;
ALTER TABLE core.workspaces RENAME COLUMN key TO slug;

ALTER TABLE core.organizations
    DROP CONSTRAINT chk_organizations_key_grammar,
    DROP CONSTRAINT uq_organizations_key;
ALTER TABLE core.organizations RENAME COLUMN key TO slug;
DROP INDEX core.uq_organizations_one_system;
