import { __decorate, __metadata } from "tslib";
import DataLoader from 'dataloader';
import { GraphQLError } from 'graphql';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
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 { Auth0ManagementProvider } from './auth0-management.provider.js';
// Cap on parallel Auth0 Management API lookups, to stay well within Auth0's
// rate limits even when a business has many users.
const AUTH0_LOOKUP_CONCURRENCY = 5;
/**
 * Map over `items` running at most `limit` mappers concurrently.
 * A small pool-based limiter so we don't fan out unbounded external calls
 * (and without pulling in an extra dependency).
 */
async function mapWithConcurrency(items, limit, mapper) {
    const results = new Array(items.length);
    let nextIndex = 0;
    const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
        while (nextIndex < items.length) {
            const current = nextIndex++;
            results[current] = await mapper(items[current]);
        }
    });
    await Promise.all(workers);
    return results;
}
const getBusinessUsersByAuth0Ids = sql `
  SELECT user_id, auth0_user_id, business_id, role_id
  FROM accounter_schema.business_users
  WHERE auth0_user_id IN $$auth0UserIds
  ORDER BY updated_at DESC;
`;
const insertBusinessUser = sql `
  INSERT INTO accounter_schema.business_users (user_id, auth0_user_id, business_id, role_id)
    VALUES ($userId, $auth0UserId, $ownerId, $roleId)
    ON CONFLICT (user_id, business_id) DO NOTHING
    RETURNING *;
`;
// Strictly tenant-scoped: only rows for the current business are returned.
// `business_users` does not store email/name (identity lives in Auth0); the
// linked invitation email is selected as a fallback for users that have not
// yet completed Auth0 login.
const listBusinessUsers = sql `
  SELECT
    bu.user_id,
    bu.auth0_user_id,
    bu.role_id,
    bu.created_at,
    (
      SELECT i.email
      FROM accounter_schema.invitations i
      WHERE i.user_id = bu.user_id
        AND i.business_id = bu.business_id
      ORDER BY i.created_at DESC
      LIMIT 1
    ) AS fallback_email
  FROM accounter_schema.business_users bu
  WHERE bu.business_id = $ownerId
  ORDER BY bu.created_at DESC;
`;
// Strictly tenant-scoped: the business id guard prevents removing a membership
// that belongs to another tenant even if a valid user id is supplied.
const deleteBusinessUser = sql `
  DELETE FROM accounter_schema.business_users
  WHERE user_id = $userId
    AND business_id = $ownerId
  RETURNING user_id;
`;
let BusinessUsersProvider = class BusinessUsersProvider {
    db;
    authContextProvider;
    auth0ManagementProvider;
    auditLogsProvider;
    constructor(db, authContextProvider, auth0ManagementProvider, auditLogsProvider) {
        this.db = db;
        this.authContextProvider = authContextProvider;
        this.auth0ManagementProvider = auth0ManagementProvider;
        this.auditLogsProvider = auditLogsProvider;
    }
    async batchBusinessUsersByAuth0Ids(auth0UserIds) {
        const businessUsers = await getBusinessUsersByAuth0Ids.run({
            auth0UserIds: auth0UserIds ? [...auth0UserIds] : [],
        }, this.db);
        return auth0UserIds.map(auth0UserId => businessUsers.filter(t => t.auth0_user_id === auth0UserId));
    }
    getBusinessUsersByAuth0IdsLoader = new DataLoader((auth0UserIds) => this.batchBusinessUsersByAuth0Ids(auth0UserIds));
    /**
     * Resolve every business membership for an authenticated Auth0 user.
     * Reuses the batching loader; returns an empty array when the user has no
     * memberships.
     */
    async getMembershipsByAuth0UserId(auth0UserId) {
        const rows = await this.getBusinessUsersByAuth0IdsLoader.load(auth0UserId);
        return rows.map(row => ({
            businessId: row.business_id,
            roleId: row.role_id,
        }));
    }
    async insertBusinessUser(params) {
        return insertBusinessUser.run(params, this.db);
    }
    /**
     * List all users linked to the current business.
     * Restricted to business owners and strictly scoped to the caller's tenant.
     * Email/name are enriched from Auth0 (the identity system of record), with the
     * linked invitation email used as a fallback when an Auth0 lookup is unavailable.
     */
    async listBusinessUsers() {
        const authContext = await this.requireBusinessOwner();
        const rows = await listBusinessUsers.run({ ownerId: authContext.tenant.businessId }, this.db);
        return mapWithConcurrency(rows, AUTH0_LOOKUP_CONCURRENCY, async (row) => {
            let email = row.fallback_email ?? null;
            let name = null;
            if (row.auth0_user_id) {
                const profile = await this.auth0ManagementProvider.getUserProfileById(row.auth0_user_id);
                if (profile) {
                    email = profile.email ?? email;
                    name = profile.name ?? null;
                }
            }
            return {
                id: row.user_id,
                email: email ?? '',
                name,
                roleId: row.role_id,
                createdAt: row.created_at,
            };
        });
    }
    /**
     * Remove a user's membership from the current business.
     * Restricted to business owners and strictly scoped to the caller's tenant, so
     * a membership belonging to another business can never be removed. Deleting the
     * relation does not touch the user's global Auth0 identity.
     * Owners cannot remove themselves, which would cause an immediate self-lockout
     * (and orphan the tenant if they were the sole owner).
     * Returns false when no matching membership exists in the current business.
     */
    async removeBusinessUser(userId) {
        const authContext = await this.requireBusinessOwner();
        if (userId === authContext.user.userId) {
            throw new GraphQLError('Cannot remove yourself from the business', {
                extensions: { code: 'CANNOT_REMOVE_SELF' },
            });
        }
        return this.db.transaction(async (client) => {
            const rows = await deleteBusinessUser.run({ userId, ownerId: authContext.tenant.businessId }, client);
            const removed = rows.length > 0;
            if (!removed) {
                return false;
            }
            await this.auditLogsProvider.log({
                ownerId: authContext.tenant.businessId,
                userId: authContext.user.userId,
                auth0UserId: authContext.user.auth0UserId ?? undefined,
                action: 'BUSINESS_USER_REMOVED',
                entity: 'BusinessUser',
                entityId: userId,
                details: {
                    removed: 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 };
    }
};
BusinessUsersProvider = __decorate([
    Injectable({
        scope: Scope.Operation,
        global: true,
    }),
    __metadata("design:paramtypes", [TenantAwareDBClient,
        AuthContextProvider,
        Auth0ManagementProvider,
        AuditLogsProvider])
], BusinessUsersProvider);
export { BusinessUsersProvider };
//# sourceMappingURL=business-users.provider.js.map