/**
 * Role grant offer types and client-safe schemas.
 *
 * An offer is a pending grant awaiting recipient consent. Lifecycle states
 * are mutually exclusive via a CHECK constraint (`role_grant_offer_single_terminal`):
 * at most one of `accepted_at` / `declined_at` / `retracted_at` may be set.
 * On accept, the offer's `resulting_role_grant_id` links to the role_grant row
 * produced by `query_accept_offer`.
 *
 * Table DDL and index-side sentinel constants live in `auth/role_grant_offer_ddl.ts`.
 *
 * @module
 */
import { z } from 'zod';
import { Uuid } from '@fuzdev/fuz_util/id.js';
import { RoleName } from './role_schema.js';
/** Maximum length of the optional message attached to an offer. */
export const ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX = 500;
/** Default TTL for a newly created offer — 30 days. Matches GitHub org-invite expiry. */
export const ROLE_GRANT_OFFER_DEFAULT_TTL_MS = 30 * 24 * 60 * 60 * 1000;
/** Zod schema for client-safe role_grant offer data. */
export const RoleGrantOfferJson = z
    .strictObject({
    id: Uuid.meta({ description: 'Offer id.' }),
    from_actor_id: Uuid.meta({ description: 'Actor that issued the offer.' }),
    to_account_id: Uuid.meta({ description: 'Account the offer is directed to.' }),
    to_actor_id: Uuid.nullable().meta({
        description: 'Optional actor-grain target on the recipient account. When set, only this actor may accept; when null any actor on `to_account_id` may accept.',
    }),
    role: RoleName.meta({ description: 'Role being offered.' }),
    scope_kind: z.string().nullable().meta({
        description: 'Machine-readable kind tag for `scope_id` — paired-null with `scope_id` (both null for global, both non-null for scoped).',
    }),
    scope_id: Uuid.nullable().meta({
        description: 'Scope the offered role_grant applies to (e.g. a classroom id). `null` for global role_grants.',
    }),
    message: z
        .string()
        .max(ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX)
        .nullable()
        .meta({ description: 'Optional free-form note from the grantor.' }),
    created_at: z.string().meta({ description: 'ISO timestamp when the offer was created.' }),
    expires_at: z
        .string()
        .meta({ description: 'ISO timestamp after which the offer is no longer valid.' }),
    accepted_at: z
        .string()
        .nullable()
        .meta({ description: 'ISO timestamp when the offer was accepted.' }),
    declined_at: z
        .string()
        .nullable()
        .meta({ description: 'ISO timestamp when the offer was declined.' }),
    decline_reason: z
        .string()
        .max(ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX)
        .nullable()
        .meta({ description: 'Optional reason given on decline.' }),
    retracted_at: z
        .string()
        .nullable()
        .meta({ description: 'ISO timestamp when the grantor retracted the offer.' }),
    superseded_at: z.string().nullable().meta({
        description: 'ISO timestamp when this offer was obsoleted by a sibling accept or by revoke of the resulting role_grant.',
    }),
    resulting_role_grant_id: Uuid.nullable().meta({
        description: 'Role grant produced by accepting this offer. `null` until/unless accepted.',
    }),
})
    .meta({ description: 'A role_grant offer — a pending grant awaiting recipient consent.' });
/** Convert a `RoleGrantOffer` row to its JSON payload shape. */
export const to_role_grant_offer_json = (offer) => ({
    id: offer.id,
    from_actor_id: offer.from_actor_id,
    to_account_id: offer.to_account_id,
    to_actor_id: offer.to_actor_id,
    role: offer.role,
    scope_kind: offer.scope_kind,
    scope_id: offer.scope_id,
    message: offer.message,
    created_at: offer.created_at,
    expires_at: offer.expires_at,
    accepted_at: offer.accepted_at,
    declined_at: offer.declined_at,
    decline_reason: offer.decline_reason,
    retracted_at: offer.retracted_at,
    superseded_at: offer.superseded_at,
    resulting_role_grant_id: offer.resulting_role_grant_id,
});
