import { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs';

/**
 * Parsed domain information derived from a request hostname.
 *
 * Used both on the Fastify request object (`request.domainInfo`) and in the
 * Unirend React context (`useDomainInfo()`).
 */
interface DomainInfo {
    /** Bare hostname with port stripped (IPv6-safe, e.g. `'app.example.com'` or `'::1'`). */
    hostname: string;
    /**
     * Apex domain without a leading dot (e.g. `'example.com'`).
     * Empty string for localhost and raw IP addresses where no root domain can be resolved.
     *
     * When empty, omit the `domain` attribute entirely — `domain=.localhost` is invalid
     * per RFC 6265 and browsers reject it. A cookie without a `domain` attribute becomes
     * a host-only cookie scoped to the exact hostname, which is correct for localhost and IPs.
     *
     * Prepend `.` when using as a cookie `domain` attribute to span subdomains:
     * ```ts
     * document.cookie = [
     *   'theme=dark',
     *   'path=/',
     *   'max-age=31536000',
     *   domainInfo.rootDomain ? `domain=.${domainInfo.rootDomain}` : null,
     * ].filter(Boolean).join('; ');
     * ```
     */
    rootDomain: string;
}

/**
 * Configuration for a folder in the static router folderMap
 */
interface FolderConfig {
    /** Path to the directory */
    path: string;
    /** Whether to detect and use immutable caching for fingerprinted assets */
    detectImmutableAssets?: boolean;
}
/**
 * Options for the static router middleware
 * Used to serve static files in production SSR mode
 */
interface StaticContentRouterOptions {
    /** Exact URL → absolute file path (optional) */
    singleAssetMap?: Record<string, string>;
    /** URL prefix → absolute directory path (as string) or folder config object */
    folderMap?: Record<string, string | FolderConfig>;
    /** Response compression for buffered static responses; default true */
    compression?: boolean | ResponseCompressionOptions;
    /** Maximum size (in bytes) for hashing & in-memory caching; default 5 MB */
    smallFileMaxSize?: number;
    /** Maximum number of entries in ETag/content caches; default 100 */
    cacheEntries?: number;
    /** Maximum total memory size (in bytes) for content cache; default 50 MB */
    contentCacheMaxSize?: number;
    /** Maximum number of entries in the stat cache; default 250 */
    statCacheEntries?: number;
    /** TTL in milliseconds for negative stat cache entries; default 30 seconds */
    negativeCacheTtl?: number;
    /** TTL in milliseconds for positive stat cache entries; default 1 hour */
    positiveCacheTtl?: number;
    /** Custom Cache-Control header; default 'public, max-age=0, must-revalidate' */
    cacheControl?: string;
    /** Cache-Control header for immutable fingerprinted assets; default 'public, max-age=31536000, immutable' */
    immutableCacheControl?: string;
}
interface ResponseCompressionOptions {
    /** Whether compression is enabled when using the object form */
    enabled?: boolean;
    /** Minimum response size in bytes before compression is attempted */
    threshold?: number;
    /** Prefer Brotli over gzip when the client gives both equal q-values */
    preferBrotli?: boolean;
    /** Brotli quality level passed to Node.js zlib */
    brotliQuality?: number;
    /** gzip compression level passed to Node.js zlib */
    gzipLevel?: number;
}
declare module 'fastify' {
    interface FastifyRequest {
        /**
         * Active SSR app key for multi-app routing.
         *
         * Read-only request value. Defaults to `'__default__'`.
         * Use `request.setActiveSSRApp(appKey)` in SSR middleware to select a
         * registered app and refresh app-derived request values.
         */
        readonly activeSSRApp: string;
        /**
         * Select the active SSR app for this request.
         *
         * Validates that the app exists, updates `request.activeSSRApp`, refreshes
         * `request.publicAppConfig`, and updates the app-level CDN default unless
         * middleware already overrode `request.CDNBaseURL`.
         */
        setActiveSSRApp: (appKey: string) => void;
        /**
         * Resolved client IP address.
         *
         * Set once per request by the framework using `getClientIP` (if provided)
         * or falling back to `request.ip` (which reflects Fastify proxy handling
         * when `fastifyOptions.trustProxy` is configured).
         *
         * Available throughout the entire request lifecycle, including plugins,
         * hooks, page data loader handlers, API route handlers, and access log
         * templates/hooks.
         */
        clientIP: string;
        /**
         * Server label for this instance (e.g. `'SSR'`, `'API'`).
         *
         * Set once per request by the framework from the `serverLabel` server option.
         * Available in access log templates as `{{serverLabel}}` and throughout
         * the request lifecycle via `request.serverLabel`.
         */
        serverLabel: string;
        /**
         * Safe-to-share app configuration cloned and frozen for this request.
         *
         * Available to plugins, handlers, and helpers on SSR and API servers.
         * SSR also exposes it to React and injects it into HTML. API servers only
         * send it to clients if your response includes selected values.
         */
        publicAppConfig?: Record<string, unknown>;
        /**
         * Effective CDN base URL for this SSR request.
         *
         * SSR middleware can set this as a per-request override. If it remains
         * unset, the SSR server populates it from the active app's `CDNBaseURL`
         * before preHandler hooks, route handlers, SSR render, and custom 500
         * pages run.
         *
         * API servers do not set this field.
         */
        CDNBaseURL?: string;
        /**
         * Optional request-scoped helper installed by the built-in CORS plugin.
         *
         * Raw/hijacked response paths can call this before `writeHead(...)` to
         * apply the same actual-response CORS/security headers that normal
         * Fastify-managed responses receive.
         */
        applyCORSHeaders?: (reply: FastifyReply) => void | Promise<void>;
        /**
         * Internal request-start timestamp captured by the framework.
         *
         * Used by framework features that need a stable "request received" time,
         * including API/page envelope timestamps and fallback response-time
         * calculation when Fastify's built-in elapsedTime is unavailable.
         */
        receivedAt?: number;
        /**
         * Per-request mutable key-value store initialized by both SSRServer and APIServer
         * before any plugins or hooks run.
         *
         * Use this to pass data between middleware, hooks, and handlers — for example,
         * seeding user session info, theme preferences, or CSRF tokens from an onRequest
         * hook so that page data loader handlers and API handlers can read them.
         *
         * During SSR, the server injects the final context into the rendered HTML so
         * client-side React can hydrate with the same values via `useRequestContext()`.
         *
         * In separated SSR/API deployments the SSR layer forwards this context to trusted
         * API page data requests and merges returned context back automatically.
         */
        requestContext: Record<string, unknown>;
        /**
         * Parsed domain information for this request, computed once per request from
         * `request.hostname` by both SSRServer and APIServer.
         *
         * - `hostname`: bare hostname with port stripped (IPv6-safe)
         * - `rootDomain`: apex domain without a leading dot (e.g. `'example.com'`);
         *   empty string for localhost and raw IP addresses
         *
         * Use `rootDomain` to set subdomain-spanning cookies by prepending a dot:
         * `domain=.${request.domainInfo.rootDomain}` — the same value that the
         * client-side `cycleTheme()` helper uses.
         */
        domainInfo: DomainInfo;
        /**
         * Set to `true` when the request is handled by the built-in static content
         * handler (fingerprinted assets, public files, etc.), regardless of which
         * server type is serving the file.
         *
         * Initialized to `false` for every request. The static content handler sets
         * it to `true` before calling `reply.hijack()`, so it is observable in
         * `onResponse` hooks and access log templates even though `onSend` is bypassed.
         *
         * Use this to skip work that is inappropriate for static asset responses —
         * for example, cookie renewal should only happen on document/API responses,
         * not on every `.js` or `.css` file request.
         */
        isStaticAsset: boolean;
    }
}

/**
 * Minimal stat info interface with only the properties we actually use
 */
interface MinimalStatInfo {
    isFile: boolean;
    size: number;
    mtime: Date;
    mtimeMs: number;
}
/**
 * Options for getFile() method
 */
interface GetFileOptions {
    /** Whether to detect immutable assets for cache control decisions */
    shouldDetectImmutable?: boolean;
    /** Optional ETag from client's If-None-Match header (for 304 optimization) */
    clientETag?: string;
    /** Accepted content encodings from the request for representation selection */
    acceptEncoding?: string | string[];
}
/**
 * Options for creating a read stream (for range requests)
 */
interface CreateStreamOptions {
    /** Start byte position (inclusive) */
    start?: number;
    /** End byte position (inclusive) */
    end?: number;
}
/**
 * Result from serveFile() indicating what action was taken
 */
type ServeFileResult = {
    served: false;
    reason: 'not-found';
} | {
    served: false;
    reason: 'error';
    error: Error;
} | {
    served: true;
    statusCode: 200 | 206 | 304 | 400 | 416;
};
/**
 * File content discriminated union - either buffered in memory or needs streaming
 */
type FileContent = {
    /** Content is buffered in memory (small files) */
    shouldStream: false;
    /** The file content buffer */
    data: Buffer;
} | {
    /** Content needs to be streamed from disk (large files) */
    shouldStream: true;
    /** Factory function to create a read stream with optional range support */
    createStream: (options?: CreateStreamOptions) => fs.ReadStream;
};
/**
 * Internal logger object used by static content helpers.
 */
type StaticContentWarnLoggerObject = {
    warn: (obj: object, msg: string) => void;
};
/**
 * Result when file is not found (404)
 */
interface FileNotFoundResult {
    status: 'not-found';
}
/**
 * Result when an unexpected error occurs (500)
 */
interface FileErrorResult {
    status: 'error';
    error: Error;
}
/**
 * Result when client's ETag matches (304 Not Modified)
 */
interface FileNotModifiedResult {
    status: 'not-modified';
    /** Generated ETag for the selected response representation */
    etag: string;
    /** Last-Modified date as HTTP header string */
    lastModified: string;
    /** Selected content encoding, if a compressed representation was chosen */
    contentEncoding?: 'br' | 'gzip';
    /** Whether the response should include `Vary: Accept-Encoding` */
    varyByAcceptEncoding: boolean;
}
/**
 * Result when file is found and should be served (200)
 */
interface FileFoundResult {
    status: 'ok';
    /** File stats (size, modification time, etc.) */
    stat: MinimalStatInfo;
    /** Generated ETag for the selected response representation */
    etag: string;
    /** Base file ETag before representation-specific encoding suffixes */
    baseETag: string;
    /** Last-Modified date as HTTP header string */
    lastModified: string;
    /** MIME type based on file extension */
    mimeType: string;
    /** File content - either buffered or needs streaming */
    content: FileContent;
    /** Selected content encoding, if a compressed representation was chosen */
    contentEncoding?: 'br' | 'gzip';
    /** Whether the response should include `Vary: Accept-Encoding` */
    varyByAcceptEncoding: boolean;
    /** Whether this file appears to be fingerprinted/immutable (for aggressive caching) */
    isImmutableAsset: boolean;
}
/**
 * Union type for all possible getFile() results
 */
type FileResult = FileNotFoundResult | FileErrorResult | FileNotModifiedResult | FileFoundResult;
/**
 * Encapsulates caching and serving of static content files.
 *
 * This class manages:
 * - Multiple LRU caches (ETag, file content, and file stats)
 * - Configuration for single asset and folder mappings
 * - Optimized file serving with HTTP caching headers
 * - Content-based ETags for small files, weak ETags for large files
 * - Automatic detection of immutable assets (fingerprinted files)
 *
 * Each instance maintains its own independent caches, allowing
 * multiple instances with different configurations.
 */
declare class StaticContentCache {
    private singleAssetMap;
    private folderMap;
    private readonly smallFileMaxSize;
    private readonly cacheControl;
    private readonly immutableCacheControl;
    private readonly negativeCacheTtl;
    private readonly positiveCacheTtl;
    private readonly compression;
    private readonly etagCache;
    private readonly contentCache;
    private readonly compressedVariantCache;
    private readonly statCache;
    private readonly compressedContentIndex;
    private readonly logger?;
    /**
     * Creates a new StaticContentCache instance
     *
     * @param options Static content configuration (file mappings, cache settings, etc.)
     * @param logger Optional logger (e.g., fastify.log) for error logging
     */
    constructor(options: StaticContentRouterOptions, logger?: StaticContentWarnLoggerObject);
    /**
     * Gets file metadata and content with optimized caching
     *
     * This method handles all the core file operations and caching:
     * - File stats caching to avoid repeated filesystem operations
     * - ETag generation and caching (content-based for small files, weak for large files)
     * - Small file content caching in memory for performance
     * - Proper MIME type detection
     * - Immutable asset detection for cache control decisions
     * - Optional short-circuit if client ETag matches (for 304 responses)
     *
     * Useful for both HTTP serving (via serveFile) and programmatic access
     *
     * @param resolvedPath The absolute path to the file
     * @param options Optional configuration for file retrieval
     * @returns Result with status: 'not-found', 'error', 'not-modified', or 'ok'
     */
    getFile(resolvedPath: string, options?: GetFileOptions): Promise<FileResult>;
    /**
     * Serves a static file via HTTP with conditional responses
     *
     * This is a thin HTTP wrapper around getFile() that handles:
     * - HTTP 304 Not Modified responses when client cache is valid (If-None-Match)
     * - HTTP 206 Partial Content responses for range requests
     * - Proper HTTP headers (Cache-Control, ETag, Content-Type, Last-Modified, etc.)
     * - Streaming large files vs sending cached buffers for small files
     *
     * The heavy lifting (file I/O, caching, ETag generation) is done by getFile()
     *
     * @param req The Fastify request object
     * @param reply The Fastify reply object
     * @param resolvedPath The absolute path to the file to be served
     * @param options Optional configuration for file serving
     * @returns Information about whether the file was served and what status code
     */
    serveFile(req: FastifyRequest, reply: FastifyReply, resolvedPath: string, options?: GetFileOptions): Promise<ServeFileResult>;
    /**
     * Replaces routing maps and clears all file caches in one shot.
     *
     * Use this after a full build has completed. Unlike `updateConfig`, this method
     * makes no attempt at smart per-path invalidation — it simply replaces
     * whichever maps you provide and wipes the content, stat, and ETag caches
     * unconditionally, guaranteeing fresh reads for the next request.
     *
     * You may pass `singleAssetMap`, `folderMap`, or both. Omitted sections retain
     * their current routing configuration. Pass an empty object (`{}`) for a
     * section to clear all mappings in that section. All file caches are always
     * cleared, regardless of which sections are provided — even when only
     * `singleAssetMap` is passed, the rebuilt HTML pages likely reference JS/CSS
     * bundles served from `folderMap` directories that were also regenerated in
     * the same build step, so preserving folder caches would risk serving stale
     * assets alongside fresh pages.
     *
     * For targeted cache invalidation (when URL-to-path mappings changed but
     * file contents at those paths are unchanged), use `updateConfig` instead.
     * Note: `updateConfig` does not detect in-place file content changes — it
     * only tracks which filesystem paths entered or left the map.
     *
     * @param newConfig Sections to replace (at least one should be provided)
     *
     * @example
     * ```typescript
     * // After an SSG build completes (page map only):
     * cache.replaceConfig({ singleAssetMap: await loadPageMap() });
     *
     * // After a build that changes both pages and asset folders:
     * cache.replaceConfig({
     *   singleAssetMap: await loadPageMap(),
     *   folderMap: { '/assets/': { path: './dist/assets', detectImmutableAssets: true } },
     * });
     * ```
     */
    replaceConfig(newConfig: {
        singleAssetMap?: Record<string, string>;
        folderMap?: Record<string, string | FolderConfig>;
    }): void;
    /**
     * Evicts a single file's cached content, stat, and ETag without touching
     * any URL-to-path mappings.
     *
     * Use this when you know a specific file changed on disk and want to force
     * a fresh read on the next request — without flushing the entire cache.
     * Works for files served via `singleAssetMap` or `folderMap`.
     *
     * The parameter is the **filesystem path** (as it appears in the cache key),
     * not a URL.
     *
     * For `singleAssetMap` entries these are the absolute paths you
     * provided.
     *
     * For folder-served files the cache key is the absolute path
     * resolved at request time.
     *
     * @param fsPath Absolute filesystem path of the file to evict
     *
     * @example
     * ```typescript
     * // A file watcher detected /dist/about.html was rewritten:
     * cache.invalidateFile('/dist/about.html');
     * ```
     */
    invalidateFile(fsPath: string): void;
    /**
     * Clears all caches (useful for testing or cache invalidation)
     */
    clearCaches(): void;
    /**
     * Gets statistics about cache usage
     */
    getCacheStats(): {
        etag: {
            items: number;
            byteSize: number;
        };
        content: {
            items: number;
            byteSize: number;
        };
        compressedVariants: {
            items: number;
            byteSize: number;
        };
        stat: {
            items: number;
            byteSize: number;
        };
    };
    /**
     * Updates the static content configuration at runtime with targeted cache
     * invalidation — only evicting entries whose URL-to-path mapping changed.
     *
     * Use this when URL routing is changing but file contents at existing paths
     * are unchanged (e.g., adding or removing pages without rebuilding assets).
     * For post-build reloads where file contents may have changed, use
     * `replaceConfig` instead.
     *
     * **Important:** When providing a section, you must provide the COMPLETE mapping for that section.
     * - If you provide `singleAssetMap`, it replaces the entire single asset map
     * - If you provide `folderMap`, it replaces the entire folder map
     * - You can update one section, the other, or both
     * - Omitted sections remain unchanged
     *
     * **Cache invalidation strategy:**
     * - `singleAssetMap` changes: Only invalidates filesystem paths whose URL-to-path
     *   *mapping* changed (added, removed, or pointed to a different file). Paths whose
     *   mapping is unchanged are not evicted — this method has no visibility into whether
     *   the file content on disk changed. If files were rebuilt in-place, use
     *   `replaceConfig` instead.
     * - `folderMap` changes: Clears all caches (folder changes are structural)
     *
     * @param newConfig Complete mapping(s) for the section(s) you want to update
     *
     * @example Update only single asset mappings
     * ```typescript
     * cache.updateConfig({
     *   singleAssetMap: {
     *     '/': './dist/index.html',
     *     '/blog/new-post': './dist/blog/new-post.html'
     *   }
     * });
     * ```
     *
     * @example Update only folder mappings
     * ```typescript
     * cache.updateConfig({
     *   folderMap: {
     *     '/assets': { path: './dist/assets', detectImmutableAssets: true }
     *   }
     * });
     * ```
     *
     * @example Update both sections
     * ```typescript
     * cache.updateConfig({
     *   singleAssetMap: { '/': './dist/index.html' },
     *   folderMap: { '/assets': './dist/assets' }
     * });
     * ```
     */
    updateConfig(newConfig: {
        singleAssetMap?: Record<string, string>;
        folderMap?: Record<string, string | FolderConfig>;
    }): void;
    /**
     * Handles an HTTP request by resolving the URL to a file path and serving it
     *
     * This is a convenience method that combines URL resolution with file serving.
     * If no file matches the URL, it returns without sending a response (lets the hook fall through).
     *
     * @param rawURL The raw request URL (may include query string or hash)
     * @param req The Fastify request object
     * @param reply The Fastify reply object
     * @returns Information about whether a file was served
     */
    handleRequest(rawURL: string, req: FastifyRequest, reply: FastifyReply): Promise<ServeFileResult>;
    /**
     * Normalizes single asset map keys to ensure leading slash
     * Also validates against null bytes to prevent path injection
     */
    private normalizeSingleAssetMap;
    /**
     * Normalizes folder map with proper prefix formatting
     * Also validates against null bytes to prevent path injection
     *
     * Handles two config formats:
     * 1. String shorthand: { "/assets/": "/path/to/assets" }
     * 2. Full config object: { "/assets/": { path: "/path/to/assets", detectImmutableAssets: true } }
     */
    private normalizeFolderMap;
    /**
     * Normalizes URL prefix: ensures leading and trailing slash, collapses multiple slashes
     */
    private normalizePrefix;
    /**
     * Gets the MIME type for a file based on its extension
     */
    private getMimeType;
    /**
     * Compares two FolderConfig objects for equality
     * Dynamically checks all properties so we don't need to update this if FolderConfig changes
     */
    private isSameFolderConfig;
    /**
     * Checks if a file appears to be fingerprinted/immutable based on filename
     *
     * Detects common build tool fingerprinting patterns:
     * - .{hash}.{ext} format (e.g., main.a1b2c3d4.js, styles.CTpDmzGw.css)
     * - -{hash}.{ext} format (e.g., chunk-a1b2c3d4.js, vendor-5f8e9a2b.js)
     *
     * Hash must be at least 6 alphanumeric characters
     *
     * @param filePath The file path to check
     * @returns True if the file appears to be fingerprinted
     */
    private isImmutableAsset;
    private getCompressedCacheKey;
    private invalidateCompressedVariants;
    private handleCompressedVariantCacheChange;
    private untrackCompressedVariantByKey;
    private untrackCompressedVariant;
}

/**
 * Escapes HTML special characters to prevent XSS attacks
 *
 * Converts the following characters to HTML entities:
 * - & → &amp;
 * - < → &lt;
 * - > → &gt;
 * - " → &quot;
 * - ' → &#39;
 *
 * @param str - The string to escape
 * @returns The escaped string safe for insertion into HTML
 *
 * @example
 * ```ts
 * escapeHTML('<script>alert("xss")</script>');
 * // Returns: '&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;'
 * ```
 */
declare function escapeHTML(str: string): string;
/**
 * Escapes a string for safe insertion into double-quoted HTML attributes.
 *
 * Converts the following characters to HTML entities:
 * - & → &amp;
 * - " → &quot;
 * - < → &lt;
 * - > → &gt;
 *
 * @param str - The string to escape
 * @returns The escaped string safe for insertion into HTML attributes
 */
declare function escapeHTMLAttr(str: string): string;

declare const MINIMUM_SUPPORTED_NODE_MAJOR = 25;
type RuntimeName = 'bun' | 'node' | 'unknown';
interface RuntimeSupportInfo {
    runtime: RuntimeName;
    isSupported: boolean;
    minimumNodeMajor: number;
    nodeVersion?: string;
    bunVersion?: string;
}
interface RuntimeEnvironmentLike {
    Bun?: unknown;
    process?: {
        versions?: Partial<Record<'node' | 'bun', string>>;
    };
}
/**
 * Detect the current JavaScript runtime and whether it satisfies Unirend's
 * runtime requirement. Bun is treated as supported even if it reports an older
 * Node compatibility version via `process.versions.node`.
 */
declare function getRuntimeSupportInfo(minimumNodeMajor?: number, environment?: RuntimeEnvironmentLike): RuntimeSupportInfo;
/**
 * Convenience boolean check for Unirend's runtime requirement.
 */
declare function isSupportedRuntime(minimumNodeMajor?: number, environment?: RuntimeEnvironmentLike): boolean;
/**
 * Throw a descriptive error when the current runtime does not satisfy
 * Unirend's runtime requirement.
 */
declare function assertSupportedRuntime(minimumNodeMajor?: number, environment?: RuntimeEnvironmentLike): void;

export { type CreateStreamOptions, type FileContent, type FileErrorResult, type FileFoundResult, type FileNotFoundResult, type FileNotModifiedResult, type FileResult, type FolderConfig, type GetFileOptions, MINIMUM_SUPPORTED_NODE_MAJOR, type RuntimeName, type RuntimeSupportInfo, type ServeFileResult, StaticContentCache, assertSupportedRuntime, escapeHTML, escapeHTMLAttr, getRuntimeSupportInfo, isSupportedRuntime };
