import amqp from 'amqplib';

/**
 * Declarative description of an AMQP exchange. Passed to
 * {@link RabbitMQBase.registerExchange} (called automatically from
 * `Producer.initialize()` / `Consumer.initialize()` for every entry in
 * {@link RabbitMQConfig.exchanges}).
 */
interface ExchangeParams {
    /** Exchange name. Must match the routing target used in `Producer.publish()`. */
    name: string;
    /** Exchange type — see the AMQP 0-9-1 specification for routing semantics. */
    type: 'direct' | 'fanout' | 'topic' | 'headers';
    /** Optional amqplib assertion options (`durable`, `autoDelete`, `internal`, etc.). */
    options?: amqp.Options.AssertExchange;
}
/**
 * Declarative description of an AMQP queue plus its bindings and an
 * optional dead-letter destination. Library-injected `x-max-priority`
 * and `x-dead-letter-*` arguments are merged with any caller-supplied
 * `options.arguments` per-key — caller values win on conflict, sibling
 * keys survive.
 */
interface QueueParams {
    /** Queue name. Omit or pass `''` to let the broker auto-generate one. */
    name?: string;
    /** Optional amqplib assertion options (`durable`, `exclusive`, `arguments`, etc.). */
    options?: amqp.Options.AssertQueue;
    /**
     * Maximum supported priority for AMQP priority queues. Translated to
     * `x-max-priority` in the queue arguments. AMQP accepts 1..255; values
     * `<= 0` are treated as "opt out of priority" and the key is omitted
     * before the assert (the broker rejects `x-max-priority: 0`). Values
     * above 255 are not clamped by this library — the broker will reject
     * the assert. RabbitMQ's own guidance is to stay in the 1..10 range.
     * @default 10
     */
    maxPriority?: number;
    /** Exchange-to-queue bindings created together with the queue. */
    bindings: {
        /** Source exchange name. */
        exchange: string;
        /** Routing key for `direct` / `topic` exchanges. Empty string when binding to a `fanout`. */
        routingKey?: string;
        /** Header match arguments for `headers` exchanges. */
        headers?: Record<string, unknown>;
    }[];
    /**
     * Dead-letter destination — translated to `x-dead-letter-exchange` and
     * `x-dead-letter-routing-key` queue arguments. The library merges these
     * with `maxPriority` and any caller-supplied `options.arguments` into
     * one arguments object.
     */
    deadLetter?: {
        exchange: string;
        routingKey?: string;
    };
}
/**
 * The full configuration object accepted by `RabbitMQProducer` and
 * `RabbitMQConsumer`. One config describes the topology end-to-end;
 * `initialize()` asserts exchanges + queues + bindings against the
 * broker.
 */
interface RabbitMQConfig {
    /** AMQP connection settings. Prefer `amqps://` outside localhost. */
    connection: {
        /**
         * Connection URL — `amqp://` or `amqps://` (TLS). Keep credentials
         * in environment variables, not in source-controlled config.
         */
        url: string;
        /**
         * Milliseconds to wait between consumer reconnect attempts.
         * `0` is technically valid (retry as fast as the event loop allows)
         * but not recommended — use the default unless you have a reason.
         * @default 5000
         */
        reconnectInterval?: number;
        /**
         * Maximum number of consumer reconnect attempts after a connection
         * drop. Set to `0` to disable reconnect entirely (the consumer will
         * give up on the first `'close'` event).
         * @default 5
         */
        maxRetries?: number;
    };
    /** Exchanges to assert at `initialize()` time. */
    exchanges: ExchangeParams[];
    /** Queues to assert at `initialize()` time (consumer side). */
    queues: QueueParams[];
    /** Channel-level options applied at `connect()` time. */
    channel?: {
        /** Consumer prefetch: max unacked deliveries per channel. @default 1 */
        prefetchCount?: number;
    };
    /**
     * Optional logger receiver. Defaults to a thin `console.*` wrapper
     * (see `src/logger.ts: defaultLogger`). Pass `pino`, `consola`,
     * the `@bitrix24/b24jssdk` logger, or any object satisfying the
     * {@link Logger} interface to route diagnostics through your own
     * stack.
     *
     * Credentials in connection URLs are scrubbed by THIS LIBRARY
     * before they reach the logger — but only for messages emitted by
     * this library. If your logger implementation also receives errors
     * from your own code paths (e.g. forwards to an error-reporter),
     * sanitize URL credentials there too.
     */
    logger?: Logger;
}
/**
 * Minimal logger interface the library calls into. Compatible by shape
 * with `console`, `pino`, `consola`, and the `@bitrix24/b24jssdk`
 * logger. All four levels are required so call sites stay simple;
 * implementations that don't care about a level can supply a noop.
 */
interface Logger {
    /** Verbose-level diagnostic; emitted on hot paths if the implementation honours debug. */
    debug(message: string, ...args: unknown[]): void;
    /** Informational diagnostic — lifecycle transitions, successful connects, etc. */
    info(message: string, ...args: unknown[]): void;
    /** Recoverable anomaly — e.g. a reconnect attempt that failed but will be retried. */
    warn(message: string, ...args: unknown[]): void;
    /** Unrecoverable or noteworthy failure — connect errors, reconnect exhaustion, parse errors. */
    error(message: string, ...args: unknown[]): void;
}
/**
 * Loose convenience shape for application messages. The library itself
 * does NOT enforce this — `Consumer.registerHandler<T>` lets callers
 * supply their own narrower type. Provided for users who want a
 * starting point; recommend defining your own typed payload instead of
 * relying on the open index signature.
 */
interface Message {
    routingKey: string;
    date: string;
    entityTypeId?: number;
    entityId?: number;
    retryCount?: number;
    additionalData?: Record<string, unknown>;
    /** Open index signature: extra fields are typed `unknown` and must be narrowed at access. */
    [key: string]: unknown;
}
/**
 * Publish options. Extends `amqplib`'s `Options.Publish` (correlationId,
 * replyTo, messageId, persistent, expiration, etc. — see amqplib types)
 * and pins a sensible default priority.
 */
interface MessageOptions extends amqp.Options.Publish {
    /**
     * Message priority for AMQP priority queues. Valid range 0..255 per
     * the AMQP spec; values above `QueueParams.maxPriority` are capped by
     * the broker. On a non-priority queue (no `x-max-priority` argument)
     * the broker accepts but ignores this field.
     * @default 5
     */
    priority?: number;
    /** Custom AMQP headers (routing for `headers` exchanges; arbitrary metadata otherwise). */
    headers?: Record<string, unknown>;
}
/**
 * Async handler for a single delivery. Receives the parsed JSON payload
 * plus terminal `ack` / `nack` callbacks. AMQP message properties
 * (correlationId, headers, replyTo, …) are NOT currently surfaced —
 * embed them in the body if you need them.
 *
 * `nack` always sends `requeue=false` so the message routes through any
 * configured dead-letter exchange. To replay, publish a fresh copy
 * (see `examples/02-retry-dlq`).
 *
 * First terminal call wins (per-delivery): once `ack()` or `nack()` has
 * fired — or the library's safety-net `nack` has fired because the
 * handler threw without calling either — any further `ack()` / `nack()`
 * call from the same handler invocation is suppressed (logged at
 * `warn`). This guards against the protocol-error path where a handler
 * that called `ack()` and then threw would otherwise have both `ack`
 * and `nack` go on the wire for the same delivery.
 *
 * The library does NOT validate payloads at runtime; the `T = unknown`
 * default forces TypeScript callers to narrow before access, but
 * provides no JS-level guarantee. For untrusted payloads, validate the
 * shape (zod / valibot / hand-rolled type guard) inside the handler
 * before acting on fields.
 *
 * @typeParam T the shape of the parsed message body.
 */
type MessageHandler<T = unknown> = (msg: T, ack: () => void, nack: () => void) => Promise<void>;

declare abstract class RabbitMQBase {
    protected connection: amqp.ChannelModel;
    protected channel: amqp.Channel;
    protected config: RabbitMQConfig;
    protected logger: Logger;
    constructor(config: RabbitMQConfig);
    /**
     * Open the AMQP connection and channel. Not a TypeScript `abstract`
     * method — the base implementation throws unless a subclass overrides
     * it; `RabbitMQProducer` and `RabbitMQConsumer` do exactly that
     * (publish channel vs consumer channel + reconnect listener).
     */
    connect(): Promise<void>;
    /**
     * Iterate the config's `exchanges` array and assert each one against
     * the broker. Called by `initialize()`; idempotent on the broker side.
     * @protected
     */
    protected setupExchanges(): Promise<void>;
    /**
     * Declare a single exchange on the active channel. `Producer` overrides
     * this to also cache the exchange in a local map.
     *
     * @param exchange the exchange to assert — see {@link ExchangeParams}.
     */
    registerExchange(exchange: ExchangeParams): Promise<void>;
    /**
     * Iterate the config's `queues` array, assert each queue and create
     * its bindings. Called by `Consumer.initialize()`.
     * @protected
     */
    protected setupQueues(): Promise<void>;
    /**
     * Declare a single queue (and its bindings) on the active channel.
     *
     * The library merges three sources into the queue's `arguments`:
     * (1) library-injected `x-max-priority` (from `queue.maxPriority`,
     * default 10; omitted when set to 0),
     * (2) library-injected `x-dead-letter-exchange` / `x-dead-letter-routing-key`
     * (from `queue.deadLetter`),
     * (3) caller-supplied `queue.options.arguments`.
     *
     * On per-key conflict, the caller wins; sibling keys survive.
     *
     * @param queue the queue to assert — see {@link QueueParams}.
     * @returns the amqplib assertQueue reply (with the broker-assigned
     *   queue name if `queue.name` was empty).
     */
    registerQueue(queue: QueueParams): Promise<amqp.Replies.AssertQueue>;
    /**
     * Close the channel and the connection. Safe to call repeatedly or
     * before `initialize()` (the optional-chaining handles a missing
     * channel/connection). `RabbitMQConsumer` overrides this to also
     * clear its reconnect-tracking state.
     */
    disconnect(): Promise<void>;
}

declare class RabbitMQProducer extends RabbitMQBase {
    private exchanges;
    /**
     * Open the connection + publish channel and assert every exchange
     * from the config. Throws if the broker is unreachable or the
     * topology assertion fails. Call once before {@link publish}.
     */
    initialize(): Promise<void>;
    /**
     * Open the AMQP connection and create a publish channel.
     *
     * Intentionally does NOT call `channel.prefetch()` — `prefetch` is a
     * consumer-side flow-control setting (limits unacked deliveries on the
     * channel) and has no effect on publishing.
     */
    connect(): Promise<void>;
    /**
     * Assert an exchange on the broker (delegates to `RabbitMQBase`) and
     * remember it in the Producer's local map. The cached entries are
     * informational — `publish()` does not re-assert before sending.
     */
    registerExchange(exchange: ExchangeParams): Promise<void>;
    /**
     * Publish a message to an exchange, serialized as JSON.
     *
     * @param exchangeName Target exchange (must be declared in the config).
     * @param routingKey   Routing key for the broker to match against bindings.
     * @param message      Payload — JSON-serialized into a Buffer.
     * @param options      AMQP publish options. Defaults to `priority: 5`.
     *
     * @returns
     * The boolean returned by `amqplib`'s `channel.publish()`. This reflects
     * **only the client-side write buffer state**, not broker acknowledgment:
     * - `true`  — the message was written to the channel's outgoing buffer.
     * - `false` — the buffer is full; the caller should wait for the channel's
     *   `'drain'` event before publishing more (back-pressure signal).
     *
     * It does **not** mean the broker has received or persisted the message.
     * For at-least-once delivery you need publisher confirms (Track 4 capability,
     * post-v0.1) — switch to `createConfirmChannel()` and await
     * `channel.waitForConfirms()`.
     */
    publish<T>(exchangeName: string, routingKey: string, message: T, options?: MessageOptions): Promise<boolean>;
}

declare class RabbitMQConsumer extends RabbitMQBase {
    private retries;
    private handlers;
    /** Queue names we have an active `consume()` subscription on, so we can re-subscribe after reconnect. */
    private subscribedQueues;
    /** queueName → active amqplib consumerTag, so `unRegisterHandler` can issue `basic.cancel`. */
    private consumerTags;
    /** Guard so multiple 'close' events don't kick off concurrent reconnect loops. */
    private reconnectInProgress;
    /**
     * Open the connection + consumer channel, assert every exchange and
     * queue (with bindings) from the config, then arm the reconnect
     * listener. Throws if the broker is unreachable or any assertion
     * fails. Call once before {@link registerHandler} + {@link consume}.
     */
    initialize(): Promise<void>;
    /**
     * Open a connection, create a channel, apply prefetch, and wire the
     * close-listener that drives reconnect. Throws on failure — the caller
     * (initialize or the reconnect loop) decides what to do with errors.
     */
    connect(): Promise<void>;
    /**
     * Bounded async reconnect loop. Triggered by the 'close' event on the
     * connection. Sleeps `reconnectInterval` between attempts, re-asserts the
     * topology and re-subscribes any queues that had active consume() calls.
     *
     * Never throws — a synchronous throw out of an event listener would be
     * uncatchable for the caller and cause an unhandled-exception crash.
     * On exhaustion we log and give up; the process stays alive and the
     * caller can decide to recreate the consumer.
     *
     * `maxRetries: 0` disables reconnect entirely (the loop never enters);
     * the log message in that case is "reconnect disabled" rather than
     * "max retries exceeded".
     */
    private handleReconnect;
    /**
     * Register an async handler for a queue. Storage only — no broker
     * traffic happens here; the handler activates once {@link consume} is
     * called for the same `queueName`. Registering twice for the same
     * queue replaces the previous handler.
     *
     * The handler receives the JSON-parsed message body plus terminal
     * `ack` / `nack` callbacks. `nack` always sends `requeue=false` so
     * rejected messages route through any configured dead-letter
     * exchange — to replay, publish a fresh copy.
     *
     * Parse errors (invalid JSON) and uncaught handler rejections are
     * logged and `nack`'d by the library — the handler itself never sees
     * a parse failure.
     *
     * The library does NOT validate the parsed body — spreading it into
     * a trusted object (`{ ...trusted, ...msg }`) or assigning to
     * `Object.prototype`-adjacent keys is the caller's responsibility.
     * Validate the shape before merging.
     *
     * @typeParam T the parsed message body shape. The library does not
     *   validate at runtime; supply a narrow type and validate at the
     *   call site if needed.
     * @param queueName must match an asserted queue (see `RabbitMQConfig.queues`).
     * @param handler async callback invoked for every delivery.
     */
    registerHandler<T>(queueName: string, handler: MessageHandler<T>): void;
    /**
     * Remove a previously registered handler, issue an AMQP `basic.cancel`
     * for the queue's active consumer (if any), and drop the queue from the
     * reconnect-resubscribe set.
     *
     * Issuing `basic.cancel` is what actually stops the broker delivering on
     * the live channel: without it the consumer tag stays open, deliveries
     * keep arriving at a now-handlerless callback, and — with
     * `prefetchCount=1` — the unacked delivery blocks the queue. Dropping the
     * queue from `subscribedQueues` covers the same hazard across a reconnect.
     *
     * Returns a promise because the cancel is a broker round-trip. The handler
     * map and tracking sets are cleared synchronously **before** the await, so
     * even an un-awaited call immediately stops dispatching to the handler.
     *
     * @param queueName the queue whose subscription to tear down. Unknown
     *   queues are a no-op.
     */
    unRegisterHandler(queueName: string): Promise<void>;
    /**
     * Subscribe to a queue. Tracks the subscription so the reconnect
     * loop can re-attach after a broker drop. Each delivery is
     * JSON-parsed and forwarded to the handler registered via
     * {@link registerHandler}; if no handler exists for the queue, the
     * delivery is silently ignored (it stays un-acked until the channel
     * closes — register your handler **before** calling `consume`).
     *
     * @param queueName name of an already-asserted queue. Server-named
     *   queues (where the broker assigns the name in `initialize()`)
     *   should be looked up via the `assertQueue` reply if you need to
     *   pass the generated name back in.
     * @returns the amqplib `Replies.Consume` (contains `consumerTag`).
     */
    consume(queueName: string): Promise<amqp.Replies.Consume>;
    /**
     * Close the channel and connection, and reset the consumer to a clean
     * state so it can be safely re-`initialize()`d. Without this override
     * the reconnect tracking (`subscribedQueues`, `reconnectInProgress`,
     * `retries`) would leak across consumer lifecycles.
     */
    disconnect(): Promise<void>;
    /**
     * Build the delivery callback for a queue. Extracted so the reconnect path
     * can use the same logic without duplicating the closure inline.
     *
     * Each invocation owns a per-delivery `terminated` flag that guards
     * `channel.ack` / `channel.nack` so only the FIRST terminal call wins:
     *
     * - `ack()` then `throw` → only `ack` fires; the catch's safety-net
     *   `nack` is suppressed. amqplib treats a second terminal call on the
     *   same delivery as a protocol error (channel close in production).
     * - `nack()` then `throw` → only `nack` fires.
     * - `ack(); ack()` → only the first ack fires.
     * - Handler throws without calling `ack`/`nack` → the catch nacks once.
     *
     * Idempotency is per-delivery: a separate `terminated` lives in each
     * outer-`return async (msg) => …` invocation, so two messages each get
     * their own first-call-wins guard.
     */
    private buildDeliveryCallback;
}

export { RabbitMQBase, RabbitMQConsumer, RabbitMQProducer };
export type { ExchangeParams, Logger, Message, MessageHandler, MessageOptions, QueueParams, RabbitMQConfig };
