-- name: CreateGrant :one
INSERT INTO core.grants (product_id, granted_to_org_id, granted_by_person_id, grant_reason, description, quantity, valid_from, valid_until, extends_grant_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING *;

-- name: GetGrantByID :one
SELECT * FROM core.grants
WHERE grant_id = $1;

-- name: ListGrantsByOrgID :many
SELECT * FROM core.grants
WHERE granted_to_org_id = $1
ORDER BY created_at DESC;

-- name: ListGrantsWithDelivery :many
-- The one shared delivery-state derivation (ux-honest-surfaces design
-- decision 1): every operator surface that lists grants renders from this
-- query so the grants index and the org-detail composite can never
-- disagree about what "live" / "superseded" / "inactive" mean.
--
--   live       -> at least one linked pool_provision is status='active'
--   superseded -> not live, AND a later grant actually names this one as
--                 its extends_grant_id ancestor (a replacement was issued
--                 -- see replaced_by_grant_id below)
--   inactive   -> not live and not superseded: revoked/expired with no
--                 recorded successor, or a grant that never delivered
--
-- This is deliberately narrower than "not live" alone: a grant whose
-- provision simply ended (revoked, expired) with no replacement grant on
-- record is "inactive", not "superseded" -- "superseded" is reserved for
-- the case the UI can point at a specific replacing grant (the lineage
-- requirement: a superseded row must be able to name what replaced it).
--
-- replaced_by_grant_id is the most recently created grant (if any) whose
-- extends_grant_id points at this row; NULL when nothing replaced it.
-- extends_grant_id (this grant's own ancestor pointer) is passed through
-- unchanged so the UI can walk lineage in either direction.
--
-- Off-ladder grants (add-ons / usage / one-time) have no ladder attachment,
-- so activated_at / ended_at may be NULL even when delivery_state='live'.
-- Ordered live-first, then created_at DESC so the operationally-relevant
-- row appears at the top with history beneath.
--
-- sqlc.narg(org_id): NULL returns every grant system-wide (the grants
-- index, /operator/grants); set, scopes to one organization (org-detail).
SELECT
    g.grant_id,
    g.granted_to_org_id,
    g.product_id,
    g.grant_reason,
    g.quantity,
    g.status                                                    AS grant_status,
    g.created_at,
    g.extends_grant_id,
    CASE
        WHEN BOOL_OR(p.status = 'active') THEN 'live'
        WHEN EXISTS (
            SELECT 1 FROM core.grants child
            WHERE child.extends_grant_id = g.grant_id
        ) THEN 'superseded'
        ELSE 'inactive'
    END                                                         AS delivery_state,
    MAX((
        SELECT child.grant_id::text FROM core.grants child
        WHERE child.extends_grant_id = g.grant_id
        ORDER BY child.created_at DESC
        LIMIT 1
    ))                                                           AS replaced_by_grant_id,
    MAX(l.activated_at)                                         AS activated_at,
    MAX(CASE WHEN l.status = 'ended' THEN l.ended_at END)       AS ended_at
FROM core.grants g
LEFT JOIN core.pool_provisions        p ON p.grant_id    = g.grant_id
LEFT JOIN core.pool_provision_ladders l ON l.provision_id = p.provision_id
WHERE sqlc.narg(org_id)::uuid IS NULL OR g.granted_to_org_id = sqlc.narg(org_id)::uuid
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.quantity, g.status, g.created_at, g.extends_grant_id
ORDER BY
    CASE WHEN BOOL_OR(p.status = 'active') THEN 0 ELSE 1 END,
    g.created_at DESC;

-- name: ListGrantsWithDeliveryPage :many
-- Paginated, searchable, filterable system-wide variant of
-- ListGrantsWithDelivery for /operator/grants (operator-list-scale,
-- design D4). Same derivation, join shape, and ordering as
-- ListGrantsWithDelivery above -- do not let the two drift, and never
-- scope this one to an org_id; the org-detail composite keeps calling
-- ListGrantsWithDelivery unpaged.
--
-- sqlc.narg(q): NULL means search is inactive and every row matches.
-- Non-NULL matches a grant when the granted-to organization's name
-- ILIKE's the term, OR the grant's product_id is present in
-- sqlc.narg(product_ids) -- the grants module cannot query the billing
-- schema directly, so the caller (Go) pre-resolves product IDs whose name
-- matches the search term and passes them here; a NULL or empty array
-- with a non-NULL q simply means the q criterion rides on org name alone.
--
-- sqlc.narg(delivery_state): NULL returns every delivery state; otherwise
-- only rows whose derived state equals the value. The HAVING clause below
-- repeats the exact CASE expression from the SELECT list (HAVING cannot
-- reference a SELECT-list alias) so the filter can never disagree with
-- what the SELECT list -- and therefore the Go derivation reading it --
-- renders as that row's state (a mixed-fixture test pins this agreement).
--
-- count(*) OVER() is evaluated after GROUP BY/HAVING, so with the GROUP
-- BY below it counts grouped (one-per-grant) rows that passed WHERE and
-- HAVING -- the true total of matching grants, not a join-multiplied
-- count of the underlying pool_provisions/pool_provision_ladders rows.
SELECT
    g.grant_id,
    g.granted_to_org_id,
    g.product_id,
    g.grant_reason,
    g.quantity,
    g.status                                                    AS grant_status,
    g.created_at,
    g.extends_grant_id,
    CASE
        WHEN BOOL_OR(p.status = 'active') THEN 'live'
        WHEN EXISTS (
            SELECT 1 FROM core.grants child
            WHERE child.extends_grant_id = g.grant_id
        ) THEN 'superseded'
        ELSE 'inactive'
    END                                                         AS delivery_state,
    MAX((
        SELECT child.grant_id::text FROM core.grants child
        WHERE child.extends_grant_id = g.grant_id
        ORDER BY child.created_at DESC
        LIMIT 1
    ))                                                           AS replaced_by_grant_id,
    MAX(l.activated_at)                                         AS activated_at,
    MAX(CASE WHEN l.status = 'ended' THEN l.ended_at END)       AS ended_at,
    count(*) OVER()                                              AS total_count
FROM core.grants g
LEFT JOIN core.pool_provisions        p   ON p.grant_id    = g.grant_id
LEFT JOIN core.pool_provision_ladders l   ON l.provision_id = p.provision_id
LEFT JOIN core.organizations          org ON org.org_id     = g.granted_to_org_id
WHERE
    sqlc.narg(q)::text IS NULL
    OR org.name ILIKE ('%' || sqlc.narg(q)::text || '%')
    OR g.product_id = ANY(sqlc.narg(product_ids)::uuid[])
GROUP BY g.grant_id, g.granted_to_org_id, g.product_id, g.grant_reason, g.quantity, g.status, g.created_at, g.extends_grant_id
HAVING
    sqlc.narg(delivery_state)::text IS NULL
    OR (
        CASE
            WHEN BOOL_OR(p.status = 'active') THEN 'live'
            WHEN EXISTS (
                SELECT 1 FROM core.grants child
                WHERE child.extends_grant_id = g.grant_id
            ) THEN 'superseded'
            ELSE 'inactive'
        END
    ) = sqlc.narg(delivery_state)::text
ORDER BY
    CASE WHEN BOOL_OR(p.status = 'active') THEN 0 ELSE 1 END,
    g.created_at DESC
LIMIT sqlc.arg(page_limit)
OFFSET sqlc.arg(page_offset);

-- name: ListDeliveringGrantsByOrgID :many
-- Returns only grants currently delivering entitlements to a pool in the
-- given org — i.e. grants with at least one provision in status='active'.
-- Used by the member-facing Sources panel where audit history would be
-- noise (members only need to see what they actually have right now).
-- DISTINCT collapses multi-provision grants to one row.
SELECT DISTINCT
    g.grant_id,
    g.product_id,
    g.granted_to_billing_account_id,
    g.granted_to_org_id,
    g.granted_to_person_id,
    g.granted_by_person_id,
    g.grant_reason,
    g.description,
    g.quantity,
    g.valid_from,
    g.valid_until,
    g.status,
    g.revoked_at,
    g.revoked_by_person_id,
    g.revocation_reason,
    g.metadata,
    g.created_at,
    g.updated_at,
    g.extends_grant_id
FROM core.grants g
INNER JOIN core.pool_provisions p ON p.grant_id = g.grant_id
WHERE g.granted_to_org_id = $1
  AND p.status = 'active'
ORDER BY g.created_at DESC;

-- name: ListRecentGrants :many
-- Recent grants for the operator landing activity timeline. Returns the
-- raw row data with org_id + granted_by_person_id; Go-side merger resolves
-- those UUIDs to display names via batch lookups (avoids cross-schema
-- joins that don't fit per-module sqlc scope). Only org-targeted grants —
-- person-targeted and billing-account-targeted grants don't show up on
-- the org-centric operator timeline.
SELECT
    grant_id,
    product_id,
    granted_to_org_id,
    granted_by_person_id,
    grant_reason,
    quantity,
    created_at
FROM core.grants
WHERE granted_to_org_id IS NOT NULL
ORDER BY created_at DESC
LIMIT $1;

-- name: CountDeliveringOperatorGrants :one
-- Operator overview headline: grants someone deliberately issued that are
-- delivering right now. Signup defaults are excluded — conferral mints one
-- per org, so they drown the number an operator can act on; their count
-- rides the tile's caption (CountDeliveringDefaultGrants). Non-default is
-- exactly the operator-authored set per chk_grants_default_iff_system_authored.
--
-- grants.status is an issuance LEDGER — it records what was written and
-- whether it was later revoked, not whether service is flowing today. The
-- current-delivery fact lives on core.pool_provisions.status, so this counts
-- distinct grants that own at least one active provision rather than
-- filtering grants.status alone.
SELECT COUNT(DISTINCT g.grant_id) FROM core.grants g
JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
WHERE pp.status = 'active' AND g.grant_reason <> 'default';

-- name: CountDeliveringDefaultGrants :one
-- The delivering-grants caption: how many signup defaults are delivering,
-- excluded from the headline above. Same provision join as
-- CountDeliveringOperatorGrants so the two can never disagree about what
-- "delivering" means.
SELECT COUNT(DISTINCT g.grant_id) FROM core.grants g
JOIN core.pool_provisions pp ON pp.grant_id = g.grant_id
WHERE pp.status = 'active' AND g.grant_reason = 'default';

-- name: ListAllGrants :many
SELECT * FROM core.grants
ORDER BY created_at DESC;

-- name: RevokeGrant :one
UPDATE core.grants
SET status = 'revoked', revoked_at = NOW(), revoked_by_person_id = $2, revocation_reason = $3
WHERE grant_id = $1 AND status = 'active'
RETURNING *;

-- name: ExpireGrant :one
-- Marks a grant expired at its valid_until bound. Enacting the position end is
-- end_conferral's job; this is the decree half (design D5).
UPDATE core.grants
SET status = 'expired'
WHERE grant_id = $1 AND status = 'active'
RETURNING *;

-- name: GetGrantLineage :many
-- Walks the ancestry chain of the focal grant via extends_grant_id, returning
-- the focal grant first followed by each ancestor (parent, grandparent, ...)
-- in chain-walk order. Revoked or expired ancestors are included; lineage is
-- about who-came-before, not current validity.
WITH RECURSIVE lineage AS (
    SELECT g.*, 0 AS depth FROM core.grants g
    WHERE g.grant_id = $1
    UNION ALL
    SELECT g.*, l.depth + 1 FROM core.grants g
    JOIN lineage l ON g.grant_id = l.extends_grant_id
)
SELECT lineage.grant_id, lineage.product_id,
       lineage.granted_to_billing_account_id, lineage.granted_to_org_id,
       lineage.granted_to_person_id, lineage.granted_by_person_id,
       lineage.grant_reason, lineage.description, lineage.quantity,
       lineage.valid_from, lineage.valid_until, lineage.status,
       lineage.revoked_at, lineage.revoked_by_person_id,
       lineage.revocation_reason, lineage.metadata, lineage.created_at,
       lineage.updated_at, lineage.extends_grant_id
FROM lineage
ORDER BY lineage.depth ASC;
