import type { PoolClient } from 'pg';
import { DBProvider } from '../../app-providers/db.provider.js';
import { type EmailListenerConfig } from '../../financial-entities/helpers/business-suggestion-data-schema.helper.js';
import { IngestReasonCode } from '../contracts.js';
import { EmailKind, type EmailClassification, type TenantMailContext } from '../helpers/email-ingestion-classify.helper.js';
export type AliasResolutionResult = {
    found: false;
    reason: typeof IngestReasonCode.UNKNOWN_ALIAS;
} | {
    found: true;
    tenantId: string;
};
export type IssuedGrant = {
    jti: string;
    tenantId: string;
    messageId: string;
    rawMessageHash: string;
    action: string;
    expiresAt: Date;
    decisionId: string;
    auditId: string;
};
export type IssueGrantInput = {
    tenantId: string;
    messageId: string;
    rawMessageHash: string;
    expiresAt: Date;
    correlationId?: string;
    /** Recognized issuing business, bound into the grant for the ingest step. */
    businessId?: string | null;
    /** How the email was classified, bound into the grant for the ingest step. */
    classification?: EmailKind | null;
};
export type BusinessRecognitionResult = {
    /** The recognized issuing business, or null when no business matched. */
    businessId: string | null;
    /** The business's email-processing config (empty when unrecognized). */
    config: EmailListenerConfig;
};
export type ValidateGrantInput = {
    jti: string;
    tenantId: string;
    messageId: string;
    rawMessageHash: string;
};
export type ValidatedGrant = {
    jti: string;
    tenantId: string;
    action: string;
    expiresAt: Date;
    /** Recognized issuing business bound at control time; null when unrecognized. */
    businessId: string | null;
    /** Classification bound at control time; null on grants issued before it existed. */
    classification: EmailKind | null;
};
export type GrantValidationResult = {
    valid: true;
    grant: ValidatedGrant;
} | {
    valid: false;
    reason: typeof IngestReasonCode.GRANT_INVALID | typeof IngestReasonCode.TENANT_MISMATCH;
};
export declare class EmailIngestionControlProvider {
    private dbProvider;
    /** Per-tenant mail context, see {@link MAIL_CONTEXT_TTL_MS}. */
    private mailContextCache;
    constructor(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.
     */
    resolveAlias(alias: string): Promise<AliasResolutionResult>;
    /**
     * 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.
     */
    issueGrant(input: IssueGrantInput): Promise<IssuedGrant>;
    /**
     * 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.
     */
    recognizeBusiness(tenantId: string, issuerEmail: string | null): Promise<BusinessRecognitionResult>;
    /**
     * Recognize the issuing business from a {@link classifyEmail} result, trying each
     * candidate address in priority order until one matches. This is the path the
     * resolver uses: the classifier has already removed the tenant's own addresses,
     * its mailing-list addresses and the forwarder, so **manually forwarded** mail
     * resolves to the real issuer rather than to the tenant itself. Runs every lookup
     * inside a single tenant-pinned transaction.
     */
    recognizeBusinessFromClassification(tenantId: string, classification: EmailClassification): Promise<BusinessRecognitionResult>;
    /**
     * Assemble the tenant-scoped facts {@link classifyEmail} needs. Cached briefly —
     * see {@link MAIL_CONTEXT_TTL_MS}.
     *
     * "Own addresses" are deliberately narrow: the tenant's active ingest aliases plus
     * the emails registered on its **own** business row. Every other business in the
     * workspace shares `owner_id` with the tenant but is a *counterparty* — treating
     * their addresses as the tenant's would exclude every supplier from recognition.
     * Colleagues who are not registered anywhere are covered by `ownDomains` config.
     */
    loadTenantMailContext(tenantId: string): Promise<TenantMailContext>;
    /** Return the first business whose suggestion_data.emails matches a candidate. */
    private lookupBusinessByEmails;
    /**
     * Validate a presented grant against the stored record **without** consuming it.
     * Runs all the binding checks (existence, expiry, consumed state, action scope,
     * tenant binding, message/hash binding) so callers can resolve the bound
     * business and reject an obviously-invalid grant up front — before doing the
     * fallible, non-transactional document preparation (Cloudinary upload / OCR).
     * The grant is consumed later, atomically with the durable outcome write, via
     * {@link validateAndConsumeGrant} passing the write transaction's client. This
     * separation is what lets the ingest flow decide the grant's fate based on the
     * outcome: an expected preparation failure (e.g. a Cloudinary upload error) is
     * turned into an UPLOAD_FAILED quarantine that consumes the grant atomically
     * with its own recorded write, while an unexpected error throws with the grant
     * still unconsumed, so a gateway retry can succeed instead of hitting an
     * already-consumed grant with nothing recorded.
     */
    validateGrant(input: ValidateGrantInput): Promise<GrantValidationResult>;
    /**
     * 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.
     *
     * Pass `client` to run inside an existing tenant-pinned transaction so the
     * consume commits atomically with the outcome write (the ingest flow does
     * this); omit it to run in a standalone transaction.
     */
    validateAndConsumeGrant(input: ValidateGrantInput, client?: PoolClient): Promise<GrantValidationResult>;
    /**
     * Shared grant-binding checks, optionally followed by the atomic consume.
     * The caller supplies a tenant-pinned `client` (RLS is enforced by the caller's
     * transaction context).
     */
    private checkGrant;
}
