import { __decorate, __metadata } from "tslib";
import { createHash, randomBytes, randomUUID } from 'node:crypto';
import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { AdminContextProvider } from '../../admin-context/providers/admin-context.provider.js';
import { TenantAwareDBClient } from '../../app-providers/tenant-db-client.js';
import { AuditLogsProvider } from '../../common/providers/audit-logs.provider.js';
import { AuthContextProvider } from './auth-context.provider.js';
import { BusinessUsersProvider } from './business-users.provider.js';
const INVITATION_EXPIRATION_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
const getActiveInvitationsByEmails = sql `
  SELECT id, email
  FROM accounter_schema.invitations
  WHERE business_id = $ownerId
    AND lower(email) = ANY(SELECT lower(u) FROM unnest($emails::varchar[]) AS u)
    AND accepted_at IS NULL
    AND expires_at > NOW();`;
const getInvitationsByTokens = sql `
  SELECT id, business_id, role_id, auth0_user_id, accepted_at, expires_at, token_hash
    FROM accounter_schema.invitations
    WHERE token_hash IN $$tokenHashes;`;
// Strictly tenant-scoped: only pending (not yet accepted) invitations for the
// current business are listed. The one-time token is never selected.
const listInvitations = sql `
  SELECT id, email, role_id, expires_at
  FROM accounter_schema.invitations
  WHERE business_id = $ownerId
    AND accepted_at IS NULL
  ORDER BY created_at DESC;
`;
// Strictly tenant-scoped: the business id guard prevents revoking an invitation
// that belongs to another tenant. Only pending invitations can be revoked.
const deleteInvitation = sql `
  DELETE FROM accounter_schema.invitations
  WHERE id = $invitationId
    AND business_id = $ownerId
    AND accepted_at IS NULL
  RETURNING id, user_id;
`;
// Removes the pending placeholder membership created alongside an invitation.
// Guarded by `auth0_user_id IS NULL` so an accepted member is never deleted.
const deletePendingInvitationPlaceholder = sql `
  DELETE FROM accounter_schema.business_users
  WHERE user_id = $userId
    AND business_id = $ownerId
    AND auth0_user_id IS NULL;
`;
const insertInvitation = sql `
  INSERT INTO accounter_schema.invitations (
    user_id,
    business_id,
    email,
    role_id,
    token_hash,
    auth0_user_created,
    auth0_user_id,
    invited_by_user_id,
    invited_by_business_id,
    expires_at
  )
  VALUES ($userId, $ownerId, $email, $roleId, $tokenHash, $auth0UserCreated, $auth0UserId, $invitedByUserId, $invitedByBusinessId, $expiresAt)
  RETURNING id, email, business_id, role_id, expires_at;
`;
let InvitationsProvider = class InvitationsProvider {
    db;
    adminContextProvider;
    businessUsersProvider;
    auditLogsProvider;
    authContextProvider;
    constructor(db, adminContextProvider, businessUsersProvider, auditLogsProvider, authContextProvider) {
        this.db = db;
        this.adminContextProvider = adminContextProvider;
        this.businessUsersProvider = businessUsersProvider;
        this.auditLogsProvider = auditLogsProvider;
        this.authContextProvider = authContextProvider;
    }
    async batchInvitationsByEmails(emails) {
        const { ownerId } = await this.adminContextProvider.getVerifiedAdminContext();
        const invitations = await getActiveInvitationsByEmails.run({
            emails: [...emails],
            ownerId,
        }, this.db);
        return emails.map(email => invitations.filter(t => t.email.toLowerCase() === email.toLowerCase()));
    }
    getInvitationByEmailLoader = new DataLoader((emails) => this.batchInvitationsByEmails(emails), {
        cacheKeyFn: email => email.toLowerCase(),
    });
    async batchInvitationsByTokens(tokenHashes) {
        const invitations = await getInvitationsByTokens.run({
            tokenHashes: [...tokenHashes],
        }, this.db);
        return tokenHashes.map(tokenHash => invitations.filter(t => t.token_hash === tokenHash));
    }
    getInvitationByTokensLoader = new DataLoader((tokenHashes) => this.batchInvitationsByTokens(tokenHashes));
    async insertInvitation({ email, roleId, auth0UserId, invitedByUserId, ownerId, }) {
        return this.db.transaction(async (client) => {
            const token = randomBytes(32).toString('hex');
            const tokenHash = createHash('sha256').update(token).digest('hex');
            const localUserId = randomUUID();
            const invitationParams = {
                userId: localUserId,
                email,
                roleId,
                tokenHash,
                auth0UserCreated: true,
                auth0UserId,
                invitedByUserId,
                invitedByBusinessId: ownerId,
                ownerId,
                expiresAt: new Date(Date.now() + INVITATION_EXPIRATION_PERIOD_MS),
            };
            await this.businessUsersProvider.insertBusinessUser({
                userId: localUserId,
                auth0UserId: null,
                roleId,
                ownerId,
            });
            const insertedInvitation = await insertInvitation.run(invitationParams, client);
            const [invitation] = insertedInvitation;
            if (!invitation) {
                throw new GraphQLError('Invitation creation failed: no record returned from DB.', {
                    extensions: { code: 'INTERNAL_SERVER_ERROR' },
                });
            }
            await this.auditLogsProvider.insertAuditLog({
                ownerId: invitation.business_id,
                userId: invitedByUserId,
                auth0UserId,
                action: 'INVITATION_CREATED',
                entity: 'Invitation',
                entityId: invitation.id,
                details: { email, roleId, auth0UserId },
            });
            return {
                id: invitation.id,
                email: invitation.email,
                roleId: invitation.role_id,
                expiresAt: invitation.expires_at,
                token,
            };
        });
    }
    /**
     * List the pending invitations for the current business.
     * Restricted to business owners and strictly scoped to the caller's tenant.
     * The one-time invitation token is never exposed.
     */
    async listInvitations() {
        const authContext = await this.requireBusinessOwner();
        const rows = await listInvitations.run({ ownerId: authContext.tenant.businessId }, this.db);
        return rows.map(row => ({
            id: row.id,
            email: row.email,
            roleId: row.role_id,
            expiresAt: row.expires_at,
        }));
    }
    /**
     * Revoke (hard-delete) a pending invitation.
     * Restricted to business owners and strictly scoped to the caller's tenant, so
     * an invitation belonging to another business can never be revoked. Already
     * accepted invitations are not revocable. The pending placeholder membership
     * created alongside the invitation is removed too, so it does not linger as a
     * ghost user. The Auth0 user is intentionally left untouched: the same blocked
     * user may be shared by invitations across businesses (and is reused on
     * re-invite), so deleting it here could break unrelated tenants.
     * Returns false when no matching pending invitation exists in the current business.
     */
    async revokeInvitation(invitationId) {
        const authContext = await this.requireBusinessOwner();
        const ownerId = authContext.tenant.businessId;
        return this.db.transaction(async (client) => {
            const deleted = await deleteInvitation.run({ invitationId, ownerId }, client);
            const [invitation] = deleted;
            if (!invitation) {
                return false;
            }
            await deletePendingInvitationPlaceholder.run({ userId: invitation.user_id, ownerId }, client);
            await this.auditLogsProvider.log({
                ownerId,
                userId: authContext.user.userId,
                auth0UserId: authContext.user.auth0UserId ?? undefined,
                action: 'INVITATION_REVOKED',
                entity: 'Invitation',
                entityId: invitation.id,
                details: { revoked: true },
            }, client);
            return true;
        });
    }
    async requireBusinessOwner() {
        const authContext = await this.authContextProvider.getAuthContext();
        if (!authContext) {
            throw new GraphQLError('Authentication required', {
                extensions: { code: 'UNAUTHENTICATED' },
            });
        }
        const { user, tenant, ...restOfAuthContext } = authContext;
        if (!tenant?.businessId) {
            throw new GraphQLError('Authentication required', {
                extensions: { code: 'UNAUTHENTICATED' },
            });
        }
        if (!user) {
            throw new GraphQLError('Authentication required', {
                extensions: { code: 'UNAUTHENTICATED' },
            });
        }
        if (user.roleId !== 'business_owner') {
            throw new GraphQLError('Requires role: business_owner', {
                extensions: { code: 'FORBIDDEN' },
            });
        }
        return { ...restOfAuthContext, user, tenant };
    }
};
InvitationsProvider = __decorate([
    Injectable({
        scope: Scope.Operation,
        global: true,
    }),
    __metadata("design:paramtypes", [TenantAwareDBClient,
        AdminContextProvider,
        BusinessUsersProvider,
        AuditLogsProvider,
        AuthContextProvider])
], InvitationsProvider);
export { InvitationsProvider };
//# sourceMappingURL=invitations.provider.js.map