/*!
 * I'm Queue Software Project
 * Copyright (C) 2025  imqueue.com <support@imqueue.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * If you want to use this code in a closed source (commercial) project, you can
 * purchase a proprietary commercial license. Please contact us at
 * <support@imqueue.com> to get commercial licensing options.
 */
import { type JsonObject, RedisCache } from '@imqueue/rpc';
import { TagCache } from '@imqueue/tag-cache';
import { PgPubSub } from '@imqueue/pg-pubsub';
import { type ClassDecorator } from './env.js';
/**
 * Options for the {@link PgCache} class decorator: where PostgreSQL and redis
 * live, and how the change-notify triggers behave.
 *
 * Exactly one of `redis` or `redisCache` must be supplied — `redis` to let the
 * decorator build its own connection, `redisCache` to reuse one the service
 * already owns.
 */
export interface PgCacheOptions {
    /**
     * Redis cache key prefix to use. If not specified, decorated service
     * class name will be used as prefix by default.
     *
     */
    prefix?: string;
    /**
     * PostgreSQL database connection string
     *
     */
    postgres: string;
    /**
     * Redis connection options
     *
     */
    redis?: {
        host: string;
        port: number;
        username?: string;
        password?: string;
    };
    /**
     * Initialized redis cache instance. One of redis option or this redisCache
     * option is required to be provided
     *
     */
    redisCache?: RedisCache;
    /**
     * Pass false, if database channel event should not be published by service
     * to connected clients. By default is enabled = true.
     *
     */
    publish?: boolean;
    /**
     * SQL definition of the trigger function, in case default which is used
     * by this lib is not satisfying for some reason. Expected string
     * starting with
     * 'create function post_change_notify_trigger() returns trigger'
     * or will fall back to a default trigger definition. Spaces and case is
     * ignored, 'or replace statement is allowed', if needed.
     *
     */
    triggerDefinition?: string;
    /**
     * Pass true to refuse to cache at all when invalidation could not be
     * established. Off by default.
     *
     * @remarks
     * Regardless of this option, `start()` does not resolve until the triggers
     * and channel subscriptions are confirmed, so a row changed right after
     * start-up is always noticed. This option covers what happens when that
     * setup *fails* — or when no channels were registered, so nothing could ever
     * invalidate the entries.
     *
     * By default such a service still caches, and its entries then expire by ttl
     * alone; the ttl defaults to 24 hours ({@link DEFAULT_CACHE_TTL}), so that is
     * how stale a value can get. Pass true where that is the wrong trade and the
     * service should run uncached instead, paying latency to avoid serving
     * something nothing will ever invalidate.
     */
    requireInvalidation?: boolean;
    /**
     * How long `start()` waits for the change-notify triggers and the channel
     * subscriptions to be confirmed, in milliseconds. Defaults to
     * {@link DEFAULT_INVALIDATION_TIMEOUT}; a non-positive value falls back to
     * the same.
     *
     * @remarks
     * On expiry `start()` resolves with a warning rather than hanging: a database
     * that accepts a connection but never confirms the subscription is a broken
     * deployment, and a service that cannot invalidate is still a service that
     * works. Whether it then caches is
     * {@link PgCacheOptions.requireInvalidation}.
     */
    invalidationTimeout?: number;
}
/**
 * What the {@link PgCache} decorator adds to the class it is applied to. A
 * decorated service gains these three members, so code inside the service can
 * reach the cache and the subscription directly.
 */
export interface PgCacheable {
    /**
     * Tagged redis cache holding the memoised method results. Each entry is
     * tagged with the tables it depends on, which is how a change notification
     * invalidates exactly the right entries.
     *
     * @remarks
     * Absent until invalidation is live: `start()` publishes it once the triggers
     * are installed and the channels subscribed, and does not resolve before
     * then. If that setup fails it is published anyway, so behaviour is
     * unchanged for a service that cannot subscribe — unless
     * {@link PgCacheOptions.requireInvalidation} says otherwise, in which case it
     * stays absent and the method decorators simply run the method, uncached.
     */
    taggedCache: TagCache;
    /**
     * PostgreSQL LISTEN/NOTIFY subscription the triggers publish to. One channel
     * per watched table.
     */
    pubSub: PgPubSub;
    /**
     * Table-to-method registry built by the {@link cacheWith} and
     * {@link cacheBy} method decorators at class-definition time, and read when
     * a notification arrives to decide what to invalidate.
     */
    pgCacheChannels: PgCacheChannels;
}
/**
 * One registered dependency of a cached method: the method to invalidate, and an
 * optional filter narrowing which changes should trigger it.
 *
 * Position 0 is the decorated method name; position 1 is the filter, or
 * `undefined` to invalidate on every change to the table.
 */
export type PgCacheChannel = [
    string,
    // called method name
    ChannelFilter | undefined
];
/**
 * Registry of cached methods keyed by the PostgreSQL notification channel that
 * invalidates them. The key is a table name: the installed trigger uses the
 * table name as its NOTIFY channel, so the two are the same string.
 */
export interface PgCacheChannels {
    [name: string]: PgCacheChannel[];
}
/**
 * The row-level operation that produced a change notification. Matches the
 * PostgreSQL trigger's `TG_OP`.
 */
export declare enum ChannelOperation {
    /** A row was inserted. */
    INSERT = "INSERT",
    /** A row was updated. */
    UPDATE = "UPDATE",
    /** A row was deleted. */
    DELETE = "DELETE"
}
/**
 * Payload delivered on a table's notification channel by the installed trigger,
 * describing a single row change.
 */
export interface ChannelPayload {
    /**
     * When the change occurred. Arrives JSON-encoded and is revived into a
     * `Date` before a {@link ChannelPayloadFilter} sees it.
     */
    timestamp: Date;
    /** Which row-level operation fired the trigger. */
    operation: ChannelOperation;
    /** PostgreSQL schema of the changed table. */
    schema: string;
    /** Name of the changed table — also the notification channel name. */
    table: string;
    /**
     * The changed row. `NEW` for inserts and updates, `OLD` for deletes, so this
     * is always the row the change is about.
     */
    record: JsonObject;
}
/**
 * Predicate deciding whether one change should invalidate the cached method.
 *
 * Returning `true` invalidates. Unlike the array form of {@link ChannelFilter},
 * this reads the way you expect — see that type for the inversion.
 */
export type ChannelPayloadFilter = (payload: ChannelPayload) => boolean;
/**
 * Narrows which changes to a table invalidate a cached method.
 *
 * The two forms behave in OPPOSITE directions, which is easy to get wrong:
 *
 * - A {@link ChannelOperation} array is an **exclusion** list. Operations named
 *   in it do NOT invalidate; everything else does. So `[ChannelOperation.DELETE]`
 *   means "invalidate on inserts and updates, ignore deletes" — not "invalidate
 *   on deletes".
 * - A {@link ChannelPayloadFilter} is an **inclusion** predicate: it invalidates
 *   when it returns `true`.
 *
 * Omitting the filter invalidates on every change to the table.
 */
export type ChannelFilter = ChannelOperation[] | ChannelPayloadFilter;
/**
 * Map of table name to the filter that decides which of its changes matter,
 * for method decorators that watch several tables with different rules.
 */
export interface FilteredChannels {
    [channel: string]: ChannelFilter;
}
/**
 * Class decorator turning an `@imqueue` service into a PostgreSQL-invalidated
 * cache: method results are memoised in redis, and PostgreSQL itself tells the
 * service when to drop them.
 *
 * It installs a change-notify trigger on every table the service's
 * {@link cacheWith} and {@link cacheBy} decorators declare a dependency on, and
 * subscribes to one LISTEN/NOTIFY channel per table. When a row changes, the
 * matching cached results are invalidated by tag — so a cache entry lives exactly
 * as long as the data behind it is unchanged, rather than for a guessed TTL.
 *
 * ```typescript
 * import { PgCache, cacheWith } from '@imqueue/pg-cache';
 *
 * @PgCache({
 *     postgres: process.env.DB_URL!,
 *     redis: { host: 'localhost', port: 6379 },
 * })
 * class UserService extends IMQService {
 *     @cacheWith({ channels: ['users'] })
 *     public async list(): Promise<User[]> { ... }
 * }
 * ```
 *
 * Applied to the class, it wraps `start()`: the subscription and the triggers are
 * established there, after any existing `start()` implementation has run. So the
 * cache is inert until the service is started, and a service that never calls
 * `start()` is never cached.
 *
 * Awaiting `start()` is enough — it does not resolve until the triggers exist and
 * the channels are subscribed, so a row changed immediately afterwards cannot go
 * unnoticed. That costs a few tens of milliseconds at boot. If the setup fails,
 * or is not confirmed within {@link PgCacheOptions.invalidationTimeout}, the
 * service still caches and reports the failure loudly; pass
 * {@link PgCacheOptions.requireInvalidation} to have it run uncached instead.
 *
 * Works both as a standard (TC39) decorator and as a legacy
 * (`experimentalDecorators`) one, matching `@imqueue/rpc`, so it can be applied in
 * either compilation mode.
 *
 * Redis is resolved in order: `options.redisCache`, then `options.redis`, then a
 * `cache` property already on the service. If none is available `start()` throws.
 *
 * @param options - PostgreSQL and redis connection details, plus the cache-key
 *                  prefix, publication and trigger-definition overrides
 * @returns the class decorator to apply, which augments the class with
 *          {@link PgCacheable}
 */
export declare function PgCache(options: PgCacheOptions): ClassDecorator;
