-- WP-0009-T02: per-human sub-credentials for a single Licensor identity.
-- Depends on migrations/0001_registries.sql (licensors, phase_manifests,
-- extensions) and migrations/0002_ledger.sql (ledger_entries).
--
-- Supersedes 0001's implicit one-token-per-licensor assumption (its
-- `licensors.licensor_id UNIQUE` constraint) so a single Licensor (e.g.
-- `binky`) can issue multiple, individually-labeled, individually-
-- revocable credentials to different human users, each still resolving
-- to the same `licensor_id` for phase-ownership checks (registry.py,
-- ledger.py) unchanged.
--
-- Structural note: `phase_manifests.licensor_id` and
-- `extensions.licensor_id` both carry a foreign key to
-- `licensors(licensor_id)`, which only worked because that column used
-- to be UNIQUE. Once one `licensor_id` can have many `licensors` rows
-- (many credentials), that FK target is no longer valid — a FK must
-- reference a unique/PK column. This migration introduces a dedicated
-- `licensor_identities` table (one row per tenant, e.g. `binky`) as the
-- new FK target for all three tables, and repoints the existing
-- constraints at it before relaxing `licensors.licensor_id`'s uniqueness.
--
-- Does NOT change ledger_entry.schema.json or the signed entry payload —
-- that schema is frozen Stage 0 normative surface
-- (specs/TargetLedgerSpecification.md), additionalProperties:false, and
-- is out of this task's scope. Per-entry human attribution is instead a
-- hosting-layer-only column (`ledger_entries.submitted_by_token`)
-- alongside the already-signed entry, not inside its cryptographically
-- signed content — an honest, narrower guarantee than "the signature
-- itself names the human," which would require reopening WP-0002's
-- shipped schema.

BEGIN;

CREATE TABLE IF NOT EXISTS licensor_identities (
    licensor_id text PRIMARY KEY,
    created_at  timestamptz NOT NULL DEFAULT now()
);

-- Backfill: every licensor_id already present becomes a formal identity.
INSERT INTO licensor_identities (licensor_id)
SELECT DISTINCT licensor_id FROM licensors
ON CONFLICT (licensor_id) DO NOTHING;

-- Repoint phase_manifests/extensions at licensor_identities before
-- relaxing licensors.licensor_id's uniqueness below.
ALTER TABLE phase_manifests DROP CONSTRAINT IF EXISTS phase_manifests_licensor_id_fkey;
ALTER TABLE phase_manifests
    ADD CONSTRAINT phase_manifests_licensor_id_fkey
    FOREIGN KEY (licensor_id) REFERENCES licensor_identities(licensor_id);

ALTER TABLE extensions DROP CONSTRAINT IF EXISTS extensions_licensor_id_fkey;
ALTER TABLE extensions
    ADD CONSTRAINT extensions_licensor_id_fkey
    FOREIGN KEY (licensor_id) REFERENCES licensor_identities(licensor_id);

-- breach_records (migrations/0004_breach_records.sql) also references
-- licensors(licensor_id) via published_by — repoint it too. Guarded so
-- this migration still applies cleanly against a database that doesn't
-- have 0004 applied (breach_records is optional/independent).
DO $$
BEGIN
    IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'breach_records') THEN
        ALTER TABLE breach_records DROP CONSTRAINT IF EXISTS breach_records_published_by_fkey;
        ALTER TABLE breach_records
            ADD CONSTRAINT breach_records_published_by_fkey
            FOREIGN KEY (published_by) REFERENCES licensor_identities(licensor_id);
    END IF;
END
$$;

-- Now safe: drop the old one-credential-per-tenant uniqueness and have
-- `licensors` itself reference the identity table instead of being its
-- own FK target.
ALTER TABLE licensors DROP CONSTRAINT IF EXISTS licensors_licensor_id_key;
ALTER TABLE licensors DROP CONSTRAINT IF EXISTS licensors_licensor_id_fkey;
ALTER TABLE licensors
    ADD CONSTRAINT licensors_licensor_id_fkey
    FOREIGN KEY (licensor_id) REFERENCES licensor_identities(licensor_id);

ALTER TABLE licensors
    ADD COLUMN IF NOT EXISTS credential_label text,
    ADD COLUMN IF NOT EXISTS rights text NOT NULL DEFAULT 'operator'
        CHECK (rights IN ('viewer', 'contributor', 'operator', 'admin')),
    ADD COLUMN IF NOT EXISTS issued_by text,
    ADD COLUMN IF NOT EXISTS revoked_at timestamptz;

-- A credential_label is unique per Licensor tenant (not globally) among
-- currently-active credentials — two different Licensors may each have
-- their own "alice", and a revoked "alice" does not block reissuing a
-- new active credential with the same label later.
CREATE UNIQUE INDEX IF NOT EXISTS licensors_licensor_id_label_idx
    ON licensors (licensor_id, credential_label)
    WHERE credential_label IS NOT NULL AND revoked_at IS NULL;

-- Auto-create the identity row on first credential for a licensor_id, so
-- existing and future code that inserts directly into `licensors` (e.g.
-- test fixtures, or `registry.issue_sub_credential`'s own INSERT) doesn't
-- need to remember a separate "declare the tenant first" step for the
-- common case. `registry.create_licensor_identity` remains available for
-- callers that want that declaration as its own explicit, auditable step.
CREATE OR REPLACE FUNCTION ensure_licensor_identity() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
    INSERT INTO licensor_identities (licensor_id) VALUES (NEW.licensor_id)
    ON CONFLICT (licensor_id) DO NOTHING;
    RETURN NEW;
END;
$$;

DROP TRIGGER IF EXISTS licensors_ensure_identity ON licensors;
CREATE TRIGGER licensors_ensure_identity
    BEFORE INSERT ON licensors
    FOR EACH ROW
    EXECUTE FUNCTION ensure_licensor_identity();

-- Per-entry attribution: which credential (hence which human) submitted
-- each Ledger entry, recorded alongside — not inside — the entry's own
-- signed JSON payload.
-- Guarded like breach_records above: this migration must also apply
-- cleanly against a database that only has 0001 (e.g. a deployment or
-- test fixture that hosts registries but not yet the Ledger).
DO $$
BEGIN
    IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ledger_entries') THEN
        ALTER TABLE ledger_entries
            ADD COLUMN IF NOT EXISTS submitted_by_token text REFERENCES licensors(token);
    END IF;
END
$$;

-- Revocation is a governance action, not an ordinary application UPDATE —
-- same pattern as set_extension_status() in migrations/0001_registries.sql.
-- trf_app has no UPDATE grant on `licensors` at all; this SECURITY
-- DEFINER function is the only sanctioned way to revoke a credential.
CREATE OR REPLACE FUNCTION revoke_credential(
    p_token text
) RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
    UPDATE licensors
       SET revoked_at = now()
     WHERE token = p_token AND revoked_at IS NULL;
    -- Idempotent: revoking an already-revoked or unknown token is not an
    -- error — the caller's intent (this token must not work) is already
    -- satisfied either way, so no FOUND check here.
END;
$$;

GRANT SELECT, INSERT ON licensor_identities TO trf_app;
GRANT EXECUTE ON FUNCTION revoke_credential(text) TO trf_app;

COMMIT;
