/**
 * High-level `FilesNamespace` — the entry point that the SDK exposes via
 * `Shade.files`. Memoizes the underlying `ShadeFileRpcChannel` and bridges
 * so a single Shade can simultaneously serve files AND consume them from
 * peers without paying the setup cost twice.
 */
import type { Hono } from 'hono';
import type { ShadeBridge } from './shade-bridge.js';
import {
  attachClientRouting,
  attachFileHandler,
  createClientStreamsBridge,
  createFileClient,
  createFileHandler,
  createFilesHttpClient,
  createFilesRpcRoute,
  createServerStreamsBridge,
  PendingRpcRegistry,
  ShadeFileRpcChannel,
  type ClientStreamsBridge,
  type CreateFileClientOptions,
  type FileClient,
  type FileHandler,
  type FileHandlerConfig,
  type FilesHttpClientOptions,
  type FilesRpcRouteOptions,
  type ServerStreamsBridge,
} from '../index.js';
import { IdempotencyCache } from '../server/idempotency-cache.js';

export interface ServeOptions {
  /**
   * Skip the streams bridge setup. Required for deployments that only
   * use the HTTP RPC route ({@link FilesNamespace.rpcRoute}) — those
   * deployments don't need to configure `@shade/transfer` because the
   * RPC route only services inline payloads (≤ 256 KiB). Without this
   * flag, `serve()` calls `createServerStreamsBridge(shade)` which
   * eagerly instantiates the transfer engine and fails when
   * `configureTransfers({ resolveBaseUrl })` has not been called.
   *
   * Default: `false` (build the streams bridge — full server-to-server
   * stack with streamed reads/writes).
   */
  inlineOnly?: boolean;
}

export interface FilesNamespace {
  /**
   * Register a file handler. Throws if a handler is already attached on
   * this Shade — only one server per Shade. The returned function detaches
   * the handler and tears down its idempotency / retention timers.
   *
   * Pass `{ inlineOnly: true }` for HTTP-RPC-only deployments to skip
   * the streams-bridge setup (and the implied `configureTransfers`
   * pre-condition).
   */
  serve(
    handler: FileHandlerConfig,
    options?: ServeOptions,
  ): Promise<() => Promise<void>>;
  /**
   * Build a typed file client for `peer`. Multiple concurrent clients to
   * different peers share the same channel + streams bridge.
   *
   * Use this for **server-to-server** deployments where both peers can
   * receive inbound HTTP. For browser clients (no inbound listener),
   * use {@link httpClient} instead.
   */
  client(peer: string, opts?: Omit<CreateFileClientOptions, 'streamsBridge'>): Promise<FileClient>;
  /**
   * Build a request-response `FileClient` for browser-style consumers.
   * Each RPC is one HTTP POST to the supplied `rpcUrl`; the encrypted
   * response rides back in the same response body. No inbound channel
   * required on the client side.
   *
   * Inline payloads only (≤ 256 KiB). Streamed reads/writes throw a
   * clear error directing callers to {@link client} instead.
   *
   * Pre-condition: the session for `peer` must already be established
   * (typically via `shade.initSessionFromBundle(peer, bundle)`).
   */
  httpClient(peer: string, opts: FilesHttpClientOptions): FileClient;
  /**
   * Mount the server-side request-response RPC route. Returns a Hono
   * app exposing `POST /rpc` that accepts encrypted file-RPC envelopes
   * and returns encrypted responses in the same HTTP roundtrip.
   *
   * Mount under any base path:
   * ```ts
   * app.route('/api/v1/shade-files', shade.files.rpcRoute());
   * ```
   *
   * Requires `shade.files.serve(...)` to have been called first —
   * the route dispatches incoming requests through the attached
   * handler.
   */
  rpcRoute(opts?: FilesRpcRouteOptions): Hono;
  /** Tear down channel + bridges. After destroy(), serve()/client() throw. */
  destroy(): Promise<void>;
}

interface NamespaceState {
  channel: ShadeFileRpcChannel;
  pending: PendingRpcRegistry;
  serverBridge: ServerStreamsBridge | null;
  clientBridge: ClientStreamsBridge | null;
  serverHandler: FileHandler | null;
  serverDetach: (() => void) | null;
  clientDetach: (() => void) | null;
  destroyed: boolean;
}

/**
 * Construct a `FilesNamespace` bound to a Shade instance. The SDK's
 * `Shade.files` getter calls this lazily and memoizes the result.
 */
export function createFilesNamespace(shade: ShadeBridge): FilesNamespace {
  const state: NamespaceState = {
    channel: new ShadeFileRpcChannel(shade),
    pending: new PendingRpcRegistry(),
    serverBridge: null,
    clientBridge: null,
    serverHandler: null,
    serverDetach: null,
    clientDetach: null,
    destroyed: false,
  };

  function ensureAlive(): void {
    if (state.destroyed) throw new Error('FilesNamespace: destroyed');
  }

  return {
    async serve(handlerConfig, options = {}) {
      ensureAlive();
      if (state.serverHandler !== null) {
        throw new Error('FilesNamespace: a handler is already registered (one per Shade)');
      }
      // Lazy server-side streams bridge — skip when the deployment is
      // HTTP-RPC-only and does not need `@shade/transfer` wired up.
      if (!options.inlineOnly && state.serverBridge === null) {
        state.serverBridge = await createServerStreamsBridge(shade);
      }
      const inheritedObservability = shade.getObservability?.();
      const handler = createFileHandler(shade, {
        ...handlerConfig,
        ...(state.serverBridge !== null
          ? { streamsBridge: state.serverBridge }
          : {}),
        ...(handlerConfig.observability === undefined && inheritedObservability !== undefined
          ? { observability: inheritedObservability }
          : {}),
      });
      // In inlineOnly mode, the rpc-route is the sole inbound path —
      // do NOT also subscribe the channel's onMessage handler to this
      // file handler, because that would cause every incoming request
      // to be dispatched twice (once by the rpc-route's direct call,
      // once by the channel's onMessage handler) and the channel-side
      // response would attempt an outbound POST via
      // `deliverControlEnvelope`, which is exactly the path that fails
      // for browser clients.
      const detach: () => void = options.inlineOnly
        ? () => {
            /* no channel subscription to detach */
          }
        : attachFileHandler(state.channel, handler);
      state.serverHandler = handler;
      state.serverDetach = detach;

      // Wire BackgroundHooks.onPruneFiles to the new handler's idempotency
      // cache. Use the symbol-exposed internals (works because FileHandler
      // attaches them via Object.assign).
      const internals = (handler as unknown as { [k: symbol]: { idempotency: IdempotencyCache } })[
        Symbol.for('@shade/files/internal')
      ];
      const background = (shade as unknown as { background?: { setHook?: (n: string, f: () => void) => void } }).background;
      if (background?.setHook !== undefined && internals !== undefined) {
        background.setHook('onPruneFiles', () => {
          internals.idempotency.prune();
        });
      }

      return async () => {
        if (state.serverDetach !== null) state.serverDetach();
        state.serverHandler = null;
        state.serverDetach = null;
        if (background?.setHook !== undefined) {
          background.setHook('onPruneFiles', undefined as unknown as () => void);
        }
      };
    },

    async client(peer, opts = {}) {
      ensureAlive();
      // Lazy client-side streams bridge.
      if (state.clientBridge === null) {
        state.clientBridge = await createClientStreamsBridge(shade);
      }
      // Attach response routing once.
      if (state.clientDetach === null) {
        state.clientDetach = attachClientRouting(state.channel, state.pending);
      }
      return createFileClient(shade, state.channel, state.pending, peer, {
        ...opts,
        streamsBridge: state.clientBridge,
      });
    },

    httpClient(peer, opts) {
      ensureAlive();
      return createFilesHttpClient(shade, peer, opts);
    },

    rpcRoute(opts = {}) {
      ensureAlive();
      if (state.serverHandler === null) {
        throw new Error(
          'FilesNamespace.rpcRoute(): no handler attached. Call shade.files.serve(...) before mounting the RPC route.',
        );
      }
      return createFilesRpcRoute(shade, state.serverHandler, opts);
    },

    async destroy() {
      if (state.destroyed) return;
      state.destroyed = true;
      if (state.serverDetach !== null) state.serverDetach();
      if (state.clientDetach !== null) state.clientDetach();
      if (state.serverHandler !== null) state.serverHandler.destroy();
      if (state.serverBridge !== null) await state.serverBridge.destroy();
      if (state.clientBridge !== null) await state.clientBridge.destroy();
      state.channel.destroy();
      state.pending.rejectAll(new Error('FilesNamespace destroyed'));
    },
  };
}
