import { Request, RequestHandler } from 'express';
import { IncomingMessage } from 'http';
import { Duplex } from 'stream';

/**
 * DebuggerManager — multi-session debugger manager.
 *
 * Owns a session map keyed by `sessionId`, deduplicated on `yamlPath`. Each
 * session maps to one inner `playwright test` process spawned by the existing
 * `playwrightDebug.ts` helper. The manager exposes raw HTTP + WebSocket
 * proxies to those inner processes; consumers (CLI, testbox, future VSCode
 * extension) wire those proxies into their own server.
 *
 * Used by:
 *   - `shiplight debug foo.yaml` CLI — one session per invocation (N=1).
 *   - apps/testbox OmniTerm server — N sessions per container.
 *   - (future) VSCode extension — N sessions per editor instance.
 *
 * Design notes: see specs/004-testbox-debugger/research.md §1.
 */

type SessionStatus = "idle" | "starting" | "running" | "ended";
interface DebuggerSession {
    /** URL-safe stable id for the lifetime of this session. */
    sessionId: string;
    /** Absolute path of the YAML test this session is pinned to. */
    yamlPath: string;
    /** Localhost port the inner playwright process is listening on. */
    innerPort: number;
    /**
     * Loopback host the inner Express actually bound to. Discovered by the
     * spawner during its readiness probe. Needed because older shiplightai
     * packages bind "localhost" (IPv6 on macOS) while current source binds
     * 127.0.0.1 — the outer must dial whichever family is live.
     */
    innerHost: string;
    /** Inner process pid — used for kill + ps-based liveness. */
    pid: number;
    /** ISO-8601 timestamp for when openSession started. */
    startedAt: string;
    /** Current lifecycle status — see state-machine in data-model.md. */
    status: SessionStatus;
    /**
     * Non-zero exit code or signal if the inner exited abnormally.
     * `null` while running; number on clean exit; string (e.g. "SIGTERM") on signal.
     * Populated when `status === "ended"`.
     */
    exitInfo: number | string | null;
}
/**
 * Shape the manager's spawner must return. Matches
 * `spawnPlaywrightProcess` in `./playwrightDebug.js`; extracted as a
 * dependency-injection seam so unit tests can supply a fake without
 * actually starting Playwright.
 */
interface SpawnerResult {
    port: number;
    /** Loopback host the inner Express is reachable on (e.g. "127.0.0.1" or "[::1]"). */
    host: string;
    pid: number;
    cleanup: () => Promise<void>;
}
interface SpawnerArgs {
    yamlFilePath: string;
    configPath: string;
    tempSuffix?: string;
    /**
     * Whether to launch Chromium with `--headed`. False = headless; the SPA's
     * CDP live-view is the only browser surface. True = a separate visible
     * window (use when the embedding host can't comfortably show the live-view).
     * Required field; callers default it via `ManagerOptions.headed`, which
     * itself defaults to false.
     */
    headed: boolean;
}
type Spawner = (args: SpawnerArgs) => Promise<SpawnerResult>;
interface ManagerOptions {
    /**
     * Invoked whenever a session transitions between statuses. Useful for
     * consumers that want to react to lifecycle events without polling.
     */
    onSessionStateChange?: (session: DebuggerSession) => void;
    /**
     * Log-line hook. CLI mode pipes to stdout/stderr; testbox mode typically
     * routes to its own telemetry/console logger. When unset, the manager is
     * silent.
     */
    onLog?: (line: string) => void;
    /**
     * Inner-process spawner. Defaults to `spawnPlaywrightProcess` from
     * `./playwrightDebug.js`; tests and future consumers may substitute a
     * fake (e.g. one that doesn't actually shell out to `npx playwright test`).
     */
    spawner?: Spawner;
    /**
     * Default headed mode for sessions opened by this manager. See `SpawnerArgs.headed`.
     * Unset → defaults to false: the debugger SPA's CDP live-view already shows
     * the running browser in the right pane, so injecting `--headed` would
     * surface a second redundant window AND override the user's
     * `use.headless` config in playwright.config.ts. Consumers that genuinely
     * want a separate headed window can opt in.
     */
    headed?: boolean;
}
interface ProxyOptions {
    /**
     * Optional builder for the `/api/int-runner/liveview-url` response. The
     * liveview URL is the WS endpoint the UI should connect to for the live
     * DevTools view. The manager can't know the outer URL scheme (CLI vs.
     * testbox prefix it differently), so consumers supply the URL here.
     *
     * When unset, liveview-url returns an empty string (disables the live view).
     */
    liveviewUrlBuilder?: (req: Request) => string;
}
declare class DebuggerManager {
    private readonly sessions;
    private readonly byYamlPath;
    private readonly options;
    private readonly spawner;
    private readonly headed;
    constructor(options?: ManagerOptions);
    private log;
    /**
     * Reset a session's runtime state back to idle. Shared by stopSandbox,
     * spawn-failure catch blocks, and the liveness probe.
     */
    private resetToIdle;
    private notifyStateChange;
    /**
     * Open a new debugger session for the given yaml path. If a session for
     * the same absolute path already exists in a live status, returns the
     * existing session (idempotent — satisfies FR-003).
     *
     * The session starts in `"idle"` status — no Playwright process is
     * spawned. Call `startSandbox(sessionId)` to launch the inner process
     * on demand (e.g. when the user clicks "Start" in the debugger bar).
     */
    openSession(yamlPath: string): DebuggerSession;
    /**
     * Spawn the inner Playwright process for an existing idle session.
     * Idempotent — returns immediately if the sandbox is already running or
     * starting. Throws on spawn failure; the session is left marked "ended".
     */
    startSandbox(sessionId: string): Promise<void>;
    /**
     * Kill the sandbox (inner Playwright process) but keep the session in
     * the map so the user can re-start. Status transitions to `"idle"`.
     */
    stopSandbox(sessionId: string): Promise<void>;
    /**
     * Poll for inner-process liveness via its pid. When the pid is gone, flip
     * the session to "ended" so subsequent requests return 410 and the UI can
     * show the error in place (FR-019).
     *
     * We use `process.kill(pid, 0)` — signal 0 checks deliverability without
     * actually signalling — rather than a TCP probe against the inner's port.
     * Earlier iterations probed 127.0.0.1:<innerPort>, but Playwright's
     * generated spec spawns the inner with `listen(port, "localhost")`, which
     * on macOS resolves to IPv6 (::1) first. An IPv4 probe then fails even
     * though the inner is healthy, which caused sessions to flip to "ended"
     * immediately after spawn.
     *
     * Pid liveness is the right signal: if the playwright process is alive,
     * whatever address family its Express happens to bind to is irrelevant —
     * the in-process proxy uses the actual port, not our probe.
     */
    private startLivenessProbe;
    /**
     * Terminate a session: SIGTERM via the cleanup hook, wait up to 5s, then
     * remove from map. Idempotent — returns silently if already gone. See
     * contracts/session-api.md for the full lifecycle diagram.
     */
    closeSession(sessionId: string): Promise<void>;
    /**
     * Kill the current inner Playwright process and spawn a fresh one for the
     * same session. The sessionId and the session-map entry are preserved, so
     * the consumer's `/debugger/<sid>/` URL and webview base href keep working.
     *
     * Used by the SPA's "Reset" button (via POST /api/int-runner/terminate-session)
     * to get a clean browser without forcing the consumer to re-create the
     * session and reload its UI.
     *
     * Throws on respawn failure; the session is left marked "ended" so the
     * consumer can react.
     */
    restartInner(sessionId: string): Promise<void>;
    /** Snapshot of all current sessions across all states. */
    listSessions(): DebuggerSession[];
    getSession(sessionId: string): DebuggerSession | undefined;
    /**
     * Express middleware that proxies HTTP requests to the inner process for
     * the given `sessionId`. Intercepts path-matched debugger endpoints that
     * the outer layer used to own (create-session, terminate-session,
     * liveview-url) so consumers don't duplicate that logic.
     */
    httpProxyFor(sessionId: string, options?: ProxyOptions): RequestHandler;
    /**
     * HTTP upgrade handler for WebSocket traffic to the inner process.
     *
     * The upgrade handshake forwards to the inner's `/api/browser-cdp` to
     * discover the Chrome CDP URL (inner holds the live Chromium), then opens
     * a raw TCP pipe between the client socket and the Chrome CDP socket.
     *
     * Call from your HTTP server's upgrade listener:
     *
     *   server.on("upgrade", (req, socket, head) => {
     *     const sessionId = resolveSessionFromUrl(req.url);
     *     manager.wsUpgradeFor(sessionId)(req, socket, head, innerSubpath);
     *   });
     */
    wsUpgradeFor(sessionId: string): (req: IncomingMessage, socket: Duplex, head: Buffer, innerSubpath?: string) => void;
    /**
     * Graceful teardown — closes every session concurrently, resolves when
     * all have exited. Safe to call on SIGINT/SIGTERM from the CLI or from
     * the testbox container shutdown hook.
     */
    shutdown(): Promise<void>;
}

export { DebuggerManager, type DebuggerSession, type ManagerOptions, type SessionStatus };
