/**
 * 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 { Shade } from '@shade/sdk';
import {
  attachClientRouting,
  attachFileHandler,
  createClientStreamsBridge,
  createFileClient,
  createFileHandler,
  createServerStreamsBridge,
  PendingRpcRegistry,
  ShadeFileRpcChannel,
  type ClientStreamsBridge,
  type CreateFileClientOptions,
  type FileClient,
  type FileHandler,
  type FileHandlerConfig,
  type ServerStreamsBridge,
} from '../index.js';
import { IdempotencyCache } from '../server/idempotency-cache.js';

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.
   */
  serve(handler: FileHandlerConfig): Promise<() => Promise<void>>;
  /**
   * Build a typed file client for `peer`. Multiple concurrent clients to
   * different peers share the same channel + streams bridge.
   */
  client(peer: string, opts?: Omit<CreateFileClientOptions, 'streamsBridge'>): Promise<FileClient>;
  /** 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: Shade): 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) {
      ensureAlive();
      if (state.serverHandler !== null) {
        throw new Error('FilesNamespace: a handler is already registered (one per Shade)');
      }
      // Lazy server-side streams bridge.
      if (state.serverBridge === null) {
        state.serverBridge = await createServerStreamsBridge(shade);
      }
      const inheritedObservability = shade.getObservability?.();
      const handler = createFileHandler(shade, {
        ...handlerConfig,
        streamsBridge: state.serverBridge,
        ...(handlerConfig.observability === undefined && inheritedObservability !== undefined
          ? { observability: inheritedObservability }
          : {}),
      });
      const 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,
      });
    },

    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'));
    },
  };
}
