import { WebSocket } from 'ws';
import { Agent, EventBus, SessionWriter, ToolRegistry, ModelsRegistry, ConfigStore, SecretVault, ProviderConfig, ProviderApiKey } from '@wrongstack/core';
import * as http from 'node:http';

interface WSServerMessage {
    type: string;
    payload: unknown;
}
interface WSClientMessage {
    type: string;
    payload?: unknown | undefined;
}
interface WebUIOptions {
    port?: number | undefined;
    webuiPort?: number | undefined;
}
interface BackendServices {
    agent: Agent;
    events: EventBus;
    session: SessionWriter;
    toolRegistry: ToolRegistry;
    modelsRegistry: ModelsRegistry;
    configStore: ConfigStore;
    vault: SecretVault;
    globalConfigPath: string;
    projectRoot: string;
}
interface ConnectedClient {
    ws: WebSocket;
    sessionId: string | null;
    connectedAt: number;
}

interface CreateHttpServerOptions {
    /** Port to listen on. Defaults to 3456 (or the `PORT` env var). */
    port?: number | undefined;
    /** Host/interface to bind. Typically the loopback for the WebUI. */
    host: string;
    /** Resolved path to the directory containing the built React assets. */
    distDir: string;
    /**
     * WS port — appears in the CSP `connect-src` directive so the browser
     * is allowed to open a WebSocket back to the local server.
     */
    wsPort: number;
}
/**
 * Inject the live WS port into the served HTML so the frontend connects to
 * THIS instance's backend instead of a hardcoded default. Enables running
 * several WebUI instances simultaneously on different PORT/WS_PORT pairs
 * (e.g. one per project) — each instance serves HTML stamped with its own
 * WS port.
 *
 * A `<meta>` tag is used deliberately rather than an inline `<script>`: the
 * CSP sets `script-src 'self'`, which would block an inline script, but meta
 * tags are not subject to script-src. The frontend reads
 * `meta[name="wrongstack-ws-port"]` (see ws-client.ts `defaultWsUrl`).
 */
declare function injectWsPort(html: string, wsPort: number): string;
/** Build the Content-Security-Policy value for the given WS port. */
declare function buildCspHeader(wsPort: number): string;
/**
 * Create the static-file HTTP server. Returns the `http.Server` (not
 * listening yet) so the caller can attach to a `shutdown()` hook and
 * coordinate the listen() with the WebSocket bootstrap.
 */
declare function createHttpServer(opts: CreateHttpServerOptions): http.Server;

/**
 * Free-port discovery for the standalone WebUI server.
 *
 * When a user runs several instances, the default ports (HTTP 3456 / WS 3457)
 * are taken by the first one. Rather than make the user hand-pick `PORT` /
 * `WS_PORT` for every extra instance, the server probes upward from the
 * requested port and binds the first free one — then stamps that real port into
 * the served HTML and the instance registry so everything stays consistent.
 *
 * The probe binds a throwaway `net.Server`, then closes it, so there is a tiny
 * TOCTOU window between "found free" and "the real server binds it". For local
 * single-user multi-instance use that race is negligible; if it ever loses, the
 * real bind fails loudly with EADDRINUSE exactly as before.
 */
/** Resolve true when `port` can be bound on `host`, false on EADDRINUSE/EACCES. */
declare function isPortFree(host: string, port: number): Promise<boolean>;
interface FindFreePortOptions {
    /** Ports to skip even if free (e.g. one already chosen for the sibling server). */
    exclude?: Set<number> | undefined;
    /** How many consecutive ports to try before giving up. Default 200. */
    maxTries?: number | undefined;
}
/**
 * Find the first free port at or above `startPort` on `host`, skipping any in
 * `exclude`. Throws if nothing is free within `maxTries` steps.
 */
declare function findFreePort(host: string, startPort: number, opts?: FindFreePortOptions): Promise<number>;

/**
 * Best-effort "open this URL in the default browser" for `--webui --open`.
 *
 * Cross-platform via the OS opener (`start` / `open` / `xdg-open`). Fully
 * fire-and-forget: a missing opener, a headless box, or a spawn failure must
 * NEVER take the server down — the URL is always also printed to the console.
 */
/** Resolve the platform's URL-opener command + args. */
declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
    command: string;
    args: string[];
};
/** Spawn the OS browser-opener for `url`. Never throws. */
declare function openBrowser(url: string, platform?: NodeJS.Platform): void;

/**
 * Running-instance registry for the standalone WebUI server.
 *
 * Every live `webui` process records itself in a single JSON file under the
 * wstack home dir (`~/.wrongstack/webui-instances.json`) so a user running
 * several instances (one per project, or several per project on different
 * ports) can see at a glance which ports are open for which path.
 *
 * Design notes:
 * - **Self-healing**: every register/unregister/list prunes entries whose PID
 *   is no longer alive (`process.kill(pid, 0)`), so a crashed instance that
 *   never got to unregister doesn't leave a ghost behind.
 * - **Atomic writes**: the file is rewritten via `atomicWrite` (tmp + rename),
 *   so a concurrent reader never sees a half-written file. Two instances
 *   starting at the *exact* same millisecond could still race the
 *   read-modify-write — acceptable for a best-effort tracking file, and the
 *   next register() heals any dropped entry.
 * - **Best-effort**: a failure to read/write the registry must NEVER take the
 *   server down. Callers wrap these in `.catch()`.
 */
/** One running WebUI process. */
interface WebUIInstanceRecord {
    /** OS process id — also the liveness key. */
    pid: number;
    /** HTTP port serving the React frontend. */
    httpPort: number;
    /** WebSocket port for the agent backend. */
    wsPort: number;
    /** Bind host (e.g. 127.0.0.1 or 0.0.0.0). */
    host: string;
    /** Absolute project root the instance booted against. */
    projectRoot: string;
    /** Display name (basename of projectRoot). */
    projectName: string;
    /** ISO timestamp when the instance registered. */
    startedAt: string;
    /** Convenience open-in-browser URL. */
    url: string;
}
/** Default wstack home dir (`~/.wrongstack`). Callers may override the base. */
declare function defaultBaseDir(): string;
/** Resolve the registry file path for a given base dir. */
declare function registryPath(baseDir?: string): string;
/**
 * Register (or refresh) this instance. Prunes dead entries and any stale entry
 * for our own PID before adding the current record. Best-effort — rejects only
 * on a hard fs error, which callers swallow.
 */
declare function registerInstance(record: WebUIInstanceRecord, baseDir?: string): Promise<void>;
/** Remove this instance (called on graceful shutdown). Also prunes dead pids. */
declare function unregisterInstance(pid: number, baseDir?: string): Promise<void>;
/** List live instances, pruning any dead entries encountered. */
declare function listInstances(baseDir?: string): Promise<WebUIInstanceRecord[]>;
/** Human-readable table of running instances for `webui --list`. */
declare function formatInstances(instances: WebUIInstanceRecord[]): string;

/**
 * Send a JSON message to a single WebSocket client.
 * No-op when the socket is not in OPEN state (disconnected / closing).
 */
declare function send(ws: WebSocket, msg: WSServerMessage): void;
/**
 * Broadcast a JSON message to every connected client.
 * Swallows per-socket send errors — a client that disconnected between the
 * readyState check and `ws.send()` is cleaned up by its own `close` handler.
 */
declare function broadcast(clients: Map<WebSocket, ConnectedClient>, msg: WSServerMessage): void;
/**
 * Send a success/failure result message (used by key.* and provider.* handlers).
 * The frontend expects `key.operation_result` with `{ success, message }`.
 */
declare function sendResult(ws: WebSocket, success: boolean, message: string): void;
/**
 * Extract a human-readable message from an unknown thrown value.
 */
declare function errMessage(err: unknown): string;
/**
 * Generate a cryptographically random WebSocket auth token (hex string).
 * Shared between standalone and CLI-embedded WebUI servers.
 */
declare function generateAuthToken(): string;

/** A hostname that refers to the local machine. */
declare function isLoopbackHostname(hostname: string): boolean;
/** True when the server is bound to a loopback interface (vs. LAN/0.0.0.0). */
declare function isLoopbackBind(wsHost: string): boolean;
/**
 * Constant-time comparison of a provided token against the expected one.
 * A length mismatch short-circuits (lengths aren't secret); equal-length
 * inputs are compared with `timingSafeEqual` so the token can't be recovered
 * byte-by-byte via response timing.
 */
declare function tokenMatches(provided: string | undefined, expected: string): boolean;
/** Pull the `token` query param out of a request URL (`/?token=…`). */
declare function extractToken(url: string): string | undefined;
/**
 * DNS-rebinding defense. On a loopback bind, the `Host` header must resolve to
 * a loopback name. When the operator deliberately exposes the socket (wsHost is
 * a LAN/0.0.0.0 address) the Host is legitimately non-loopback, so the guard is
 * skipped and connection auth falls to the token check.
 */
declare function hostHeaderOk(input: {
    hostHeader: string | undefined;
    wsHost: string;
}): boolean;
interface VerifyClientInput {
    /** Browser `Origin` header, or undefined for non-browser clients. */
    origin?: string | undefined;
    /** Request URL (`req.url`) — carries the `?token=…` query param. */
    url: string;
    /** `Host` header (`req.headers.host`). */
    hostHeader?: string | undefined;
    /** Peer address (`req.socket.remoteAddress`). */
    remoteAddress?: string | undefined;
    /** Host/interface the WS server is bound to. */
    wsHost: string;
    /** The server's generated auth token. */
    expectedToken: string;
}
/**
 * Decide whether to accept an incoming WebSocket handshake. Pure mirror of the
 * closure previously inlined in `index.ts`; see the module doc for the layered
 * policy. Returns `true` to accept, `false` to reject.
 */
declare function verifyClient(input: VerifyClientInput): boolean;

/**
 * Pure provider/API-key record transforms for the WebUI server's `key.*` and
 * `provider.*` WebSocket handlers.
 *
 * These operate on an in-memory `providers` record (the decrypted
 * `config.providers` map) and return a `{ ok, message }` result mirroring the
 * status string the handler sends back to the client. All persistence
 * (load/decrypt, encrypt/atomic-write) and WS messaging stays in `index.ts` —
 * keeping this layer pure means the security-sensitive key bookkeeping (which
 * key is active, when a provider is dropped, how legacy single-key configs are
 * normalized) is unit-testable without a vault or a socket.
 *
 * Extracted from `index.ts`; transforms mutate the passed record in place, the
 * same way the original handlers did before calling `saveProviders`.
 */

type ProvidersRecord = Record<string, ProviderConfig>;
interface KeyOpResult {
    ok: boolean;
    message: string;
}
/**
 * Normalize a provider's keys to the array form, upgrading a legacy single
 * `apiKey` string to a one-element `[{ label: 'default', ... }]` list. Returns
 * fresh copies so callers can mutate without aliasing the stored config.
 */
declare function normalizeKeys(cfg: ProviderConfig): ProviderApiKey[];
/**
 * Write a normalized key list back onto a provider config: drop all key fields
 * when empty, otherwise sync `apiKeys`, the legacy `apiKey` mirror (the active
 * key), and re-point `activeKey` if it no longer names a present key.
 */
declare function writeKeysBack(cfg: ProviderConfig, keys: ProviderApiKey[]): void;
/** Mask a secret for display: `••••` for short keys, `abcd…wxyz` otherwise. */
declare function maskedKey(key: string | undefined): string;
/** Add or replace a labeled key for a provider, creating the provider if new. */
declare function upsertKey(providers: ProvidersRecord, providerId: string, label: string, apiKey: string, nowIso: string): KeyOpResult;
/** Remove a labeled key; drops the provider entirely when its last key goes. */
declare function deleteKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
/** Point a provider's active key at the given label. */
declare function setActiveKey(providers: ProvidersRecord, providerId: string, label: string): KeyOpResult;
/** Register a brand-new provider (optionally with an initial `default` key). */
declare function addProvider(providers: ProvidersRecord, payload: {
    id: string;
    family: string;
    baseUrl?: string | undefined;
    apiKey?: string | undefined;
}, nowIso: string): KeyOpResult;
/** Remove an entire provider and all its keys. */
declare function removeProvider(providers: ProvidersRecord, providerId: string): KeyOpResult;

/**
 * Read the `providers` section from the global config, decrypting
 * secret-bearing fields. Returns an empty record when the config file
 * doesn't exist or has no `providers` key.
 */
declare function loadSavedProviders(configPath: string, vault: SecretVault): Promise<Record<string, ProviderConfig>>;
/**
 * Write `providers` back into the global config, encrypting secrets first.
 * Refuses to overwrite a corrupt-but-existing config file (the operator
 * should fix it manually). When the config file is missing (ENOENT), starts
 * from an empty object.
 */
declare function saveProviders(configPath: string, vault: SecretVault, providers: Record<string, ProviderConfig>): Promise<void>;
/**
 * Small helper for the standalone WebUI entry point: create a
 * `{ load, save }` pair from a config path alone (uses the
 * config-directory-relative `.key` file for the vault). The `--webui`
 * CLI mode and the standalone server both need to read/write the
 * `providers` map identically.
 */
declare function createProviderConfigIO(configPath: string): {
    load: () => Promise<Record<string, ProviderConfig>>;
    save: (providers: Record<string, ProviderConfig>) => Promise<void>;
};

declare function startWebUI(opts?: {
    wsPort?: number | undefined;
    wsHost?: string | undefined;
    open?: boolean | undefined;
}): Promise<void>;

export { type BackendServices, type ConnectedClient, type KeyOpResult, type ProvidersRecord, type VerifyClientInput, type WSClientMessage, type WSServerMessage, type WebUIInstanceRecord, type WebUIOptions, addProvider, broadcast, browserOpenCommand, buildCspHeader, createHttpServer, createProviderConfigIO, defaultBaseDir, deleteKey, errMessage, extractToken, findFreePort, formatInstances, generateAuthToken, hostHeaderOk, injectWsPort, isLoopbackBind, isLoopbackHostname, isPortFree, listInstances, loadSavedProviders, maskedKey, normalizeKeys, openBrowser, registerInstance, registryPath, removeProvider, saveProviders, send, sendResult, setActiveKey, startWebUI, tokenMatches, unregisterInstance, upsertKey, verifyClient, writeKeysBack };
