import { __decorate, __metadata } from "tslib";
import { randomUUID } from 'node:crypto';
import { Injectable, Scope } from 'graphql-modules';
import { sql } from '@pgtyped/runtime';
import { DBProvider } from '../../app-providers/db.provider.js';
import { suggestionDataSchema, } from '../../financial-entities/helpers/business-suggestion-data-schema.helper.js';
import { IngestReasonCode } from '../contracts.js';
import { withTenantContext } from '../helpers/email-ingestion-tenant-context.helper.js';
// ---------------------------------------------------------------------------
// Queries
// ---------------------------------------------------------------------------
const getAliasByAlias = sql `
  SELECT owner_id
    FROM accounter_schema.email_ingestion_alias_routing
   WHERE lower(alias) = $alias
     AND is_active = TRUE
   LIMIT 1
`;
const insertIngestGrant = sql `
  INSERT INTO accounter_schema.email_ingestion_grants
    (jti, owner_id, message_id, raw_message_hash, action, expires_at, business_id)
  VALUES ($jti, $ownerId, $messageId, $rawMessageHash, $action, $expiresAt, $businessId)
  RETURNING id, jti, owner_id, action, expires_at, business_id
`;
// Resolve the issuing business by a sender email listed in its
// suggestion_data.emails. Mirrors BusinessesProvider.getBusinessByEmail, but
// runs on a tenant-pinned client (the control plane has no auth session), so
// businesses RLS scopes the match to the resolved tenant.
const getBusinessByEmailForIngest = sql `
  SELECT id, suggestion_data
    FROM accounter_schema.businesses
   WHERE suggestion_data->'emails' ? $email::text
   LIMIT 1
`;
const getGrantByJtiForValidation = sql `
  SELECT id, jti, owner_id, message_id, raw_message_hash, action, expires_at, consumed_at, business_id
    FROM accounter_schema.email_ingestion_grants
   WHERE jti = $jti
   LIMIT 1
`;
const consumeGrantByJti = sql `
  UPDATE accounter_schema.email_ingestion_grants
     SET consumed_at = NOW()
   WHERE jti = $jti
     AND consumed_at IS NULL
  RETURNING id
`;
// ---------------------------------------------------------------------------
// Provider
// ---------------------------------------------------------------------------
let EmailIngestionControlProvider = class EmailIngestionControlProvider {
    dbProvider;
    constructor(dbProvider) {
        this.dbProvider = dbProvider;
    }
    /**
     * Resolve a recipient alias to the owning tenant.
     * Bypasses RLS via raw pool: alias lookup is a bootstrap step that runs
     * before any tenant context is known, so TenantAwareDBClient would throw
     * UNAUTHENTICATED. The alias_routing table has FOR SELECT USING (TRUE) to
     * explicitly allow cross-tenant reads at the DB policy level.
     */
    async resolveAlias(alias) {
        const rows = await getAliasByAlias.run({ alias: alias.toLowerCase() }, this.dbProvider.pool);
        if (rows.length === 0) {
            return { found: false, reason: IngestReasonCode.UNKNOWN_ALIAS };
        }
        return { found: true, tenantId: rows[0].owner_id };
    }
    /**
     * Issue a short-lived, single-use ingest grant for the given tenant and message.
     * Returns the persisted grant together with decision/audit metadata.
     * The INSERT runs under the tenant's RLS context (see {@link withTenantContext}):
     * the grants table uses FORCE ROW LEVEL SECURITY with a tenant_isolation
     * WITH CHECK policy, so the raw pool cannot bypass it — the owner_id parameter
     * and the pinned business context must agree.
     */
    async issueGrant(input) {
        const jti = randomUUID();
        const decisionId = randomUUID();
        const auditId = randomUUID();
        const { tenantId, messageId, rawMessageHash, expiresAt, businessId } = input;
        const rows = await withTenantContext(this.dbProvider.pool, tenantId, client => insertIngestGrant.run({
            jti,
            ownerId: tenantId,
            messageId,
            rawMessageHash,
            action: 'ingest',
            expiresAt,
            businessId: businessId ?? null,
        }, client));
        const row = rows[0];
        return {
            jti: row.jti,
            tenantId: row.owner_id,
            messageId,
            rawMessageHash,
            action: row.action,
            expiresAt: row.expires_at,
            decisionId,
            auditId,
        };
    }
    /**
     * Recognize the issuing business behind an incoming email and load its
     * email-processing config. Runs on a client pinned to the resolved tenant so
     * the businesses RLS policy scopes the lookup to that tenant; returns a null
     * businessId (and empty config) when no email evidence is available or no
     * business matches, in which case the gateway applies default treatment.
     */
    async recognizeBusiness(tenantId, issuerEmail) {
        if (!issuerEmail) {
            return { businessId: null, config: {} };
        }
        return withTenantContext(this.dbProvider.pool, tenantId, async (client) => {
            const rows = await getBusinessByEmailForIngest.run({ email: issuerEmail }, client);
            if (rows.length === 0) {
                return { businessId: null, config: {} };
            }
            const business = rows[0];
            let config = {};
            if (business.suggestion_data) {
                const parsed = suggestionDataSchema.safeParse(business.suggestion_data);
                if (parsed.success) {
                    if (parsed.data.emailListener) {
                        config = parsed.data.emailListener;
                    }
                }
                else {
                    console.error(`Invalid suggestion_data schema for business [${business.id}]: ${JSON.stringify(parsed.error.issues)}`);
                }
            }
            return { businessId: business.id, config };
        });
    }
    /**
     * Validate a presented grant against the stored record and atomically consume it.
     * Checks: existence, expiry, consumed state, action scope, tenant binding, and message binding.
     * The consume UPDATE (SET consumed_at = NOW() WHERE consumed_at IS NULL) is atomic —
     * if a concurrent request consumed the grant first the UPDATE returns 0 rows and
     * the method returns GRANT_INVALID, preventing double-use.
     * Runs under the claimed tenant's RLS context: the grants table uses FORCE ROW
     * LEVEL SECURITY, so the raw pool cannot read/update it without a pinned
     * business context. Pinning to input.tenantId means a grant owned by another
     * tenant is filtered out by the USING policy and surfaces as GRANT_INVALID;
     * the explicit owner_id check below remains as defense-in-depth.
     */
    async validateAndConsumeGrant(input) {
        return withTenantContext(this.dbProvider.pool, input.tenantId, async (client) => {
            const rows = await getGrantByJtiForValidation.run({ jti: input.jti }, client);
            if (rows.length === 0) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            const grant = rows[0];
            if (grant.consumed_at !== null) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            if (grant.expires_at <= new Date()) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            if (grant.action !== 'ingest') {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            if (grant.owner_id !== input.tenantId) {
                return { valid: false, reason: IngestReasonCode.TENANT_MISMATCH };
            }
            if (grant.message_id !== input.messageId) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            if (grant.raw_message_hash !== input.rawMessageHash) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            // Atomic consume-once: if this returns 0 rows a concurrent request beat us to it.
            const consumed = await consumeGrantByJti.run({ jti: input.jti }, client);
            if (consumed.length === 0) {
                return { valid: false, reason: IngestReasonCode.GRANT_INVALID };
            }
            return {
                valid: true,
                grant: {
                    jti: grant.jti,
                    tenantId: grant.owner_id,
                    action: grant.action,
                    expiresAt: grant.expires_at,
                    businessId: grant.business_id ?? null,
                },
            };
        });
    }
};
EmailIngestionControlProvider = __decorate([
    Injectable({
        scope: Scope.Singleton,
        global: true,
    }),
    __metadata("design:paramtypes", [DBProvider])
], EmailIngestionControlProvider);
export { EmailIngestionControlProvider };
//# sourceMappingURL=email-ingestion-control.provider.js.map