import { Injectable, type OnModuleInit } from '@nestjs/common';
import { HarnessRegistry } from '../harness/harness.registry.js';
import {
  isHarnessConversationServiceAvailable,
  type HarnessConversationServiceBinding,
} from '../harness/harness.tokens.js';
import type {
  ChatRuntime,
  ChatRuntimeMode,
  LegacyBrowserMessagePayload,
  LegacyEmbeddedChatPort,
  LegacyRuntimeResult,
  LegacyRuntimeStream,
  LegacySessionPresentation,
  LegacySocketTurnLease,
  OwnedConversationContext,
  VerifiedDiscordIngressContext,
  VerifiedDiscordTurnLease,
} from './chat-runtime.js';
import { ChatRuntimeUnavailableError, resolveChatRuntimeMode } from './chat-runtime.js';

/** The fixed fail-closed result for a legacy browser operation issued under `pi-rpc`. */
const RUNTIME_UNSUPPORTED = {
  ok: false as const,
  code: 'runtime_unsupported' as const,
  retryable: false as const,
};

/**
 * Resolves the one live {@link ChatRuntime} for this process and enforces the
 * `pi-rpc` readiness preconditions at module init — before the gateway accepts
 * traffic. It never falls back from `pi-rpc` to embedded execution: an unmet
 * `pi-rpc` precondition is a typed startup failure ({@link ChatRuntimeUnavailableError}),
 * and until `onModuleInit` selects a runtime, {@link active} throws rather than
 * exposing any runtime — a failed `pi-rpc` init can never leak the embedded one.
 */
@Injectable()
export class ChatRuntimeRouter implements OnModuleInit, LegacyEmbeddedChatPort {
  private readonly mode: ChatRuntimeMode;

  /** The single resolved runtime. Undefined until a successful `onModuleInit`. */
  private resolved: ChatRuntime | undefined;

  constructor(
    private readonly harnessRegistry: HarnessRegistry,
    private readonly conversationService: HarnessConversationServiceBinding,
    private readonly embedded: ChatRuntime,
    private readonly harness: ChatRuntime,
    mode: ChatRuntimeMode = resolveChatRuntimeMode(),
  ) {
    this.mode = mode;
  }

  onModuleInit(): void {
    if (this.mode === 'legacy') {
      // Legacy ignores the pi-rpc preconditions entirely and always runs embedded.
      this.resolved = this.embedded;
      return;
    }

    // pi-rpc: both preconditions are hard startup failures, checked in a fixed order.
    if (!this.harnessRegistry.has('pi')) {
      this.resolved = undefined;
      throw new ChatRuntimeUnavailableError('adapter_unavailable');
    }
    if (!isHarnessConversationServiceAvailable(this.conversationService)) {
      this.resolved = undefined;
      throw new ChatRuntimeUnavailableError('conversation_service_unavailable');
    }

    this.resolved = this.harness;
  }

  get active(): ChatRuntime {
    if (this.resolved === undefined) {
      // Reached only if init has not run or failed closed; never expose a runtime here.
      throw new Error('The chat runtime is not available: startup did not resolve a runtime.');
    }
    return this.resolved;
  }

  /**
   * The process-wide mode, available before {@link onModuleInit}. Production handlers read
   * this to fail a legacy browser turn closed under `pi-rpc` *before* parsing the payload as
   * either browser-legacy input or a Discord envelope — never to branch into a fallback.
   */
  get runtimeMode(): ChatRuntimeMode {
    return this.mode;
  }

  /**
   * The embedded runtime narrowed to its port. Only reached on the legacy path (and for the
   * verified-Discord op in both modes), where the injected runtime is always a real
   * `EmbeddedChatRuntime`. The router spec constructs the router with a bare `{ kind }` stub
   * but never invokes a port op, so this narrowing is never exercised against the stub.
   */
  private get embeddedPort(): LegacyEmbeddedChatPort {
    return this.embedded as unknown as LegacyEmbeddedChatPort;
  }

  // --- LegacyEmbeddedChatPort: legacy browser operations fail closed under pi-rpc ---

  completeLegacyRestTurn(
    context: OwnedConversationContext,
    input: Readonly<{ content: string }>,
  ): Promise<
    LegacyRuntimeResult<Readonly<{ text: string; presentation: LegacySessionPresentation }>>
  > {
    if (this.mode === 'pi-rpc') {
      return Promise.resolve(RUNTIME_UNSUPPORTED);
    }
    return this.embeddedPort.completeLegacyRestTurn(context, input);
  }

  prepareLegacySocketTurn(
    context: OwnedConversationContext,
    input: LegacyBrowserMessagePayload,
    stream: LegacyRuntimeStream,
  ): Promise<LegacyRuntimeResult<LegacySocketTurnLease>> {
    if (this.mode === 'pi-rpc') {
      return Promise.resolve(RUNTIME_UNSUPPORTED);
    }
    return this.embeddedPort.prepareLegacySocketTurn(context, input, stream);
  }

  setLegacyThinking(
    context: OwnedConversationContext,
    level: string,
  ): LegacyRuntimeResult<LegacySessionPresentation> {
    if (this.mode === 'pi-rpc') {
      return RUNTIME_UNSUPPORTED;
    }
    return this.embeddedPort.setLegacyThinking(context, level);
  }

  abortLegacyTurn(context: OwnedConversationContext): Promise<LegacyRuntimeResult<void>> {
    if (this.mode === 'pi-rpc') {
      return Promise.resolve(RUNTIME_UNSUPPORTED);
    }
    return this.embeddedPort.abortLegacyTurn(context);
  }

  applyLegacyModelOverride(
    context: OwnedConversationContext,
    modelId: string,
  ): LegacyRuntimeResult<LegacySessionPresentation> {
    if (this.mode === 'pi-rpc') {
      return RUNTIME_UNSUPPORTED;
    }
    return this.embeddedPort.applyLegacyModelOverride(context, modelId);
  }

  readLegacySessionPresentation(
    context: OwnedConversationContext,
  ): LegacyRuntimeResult<LegacySessionPresentation> {
    if (this.mode === 'pi-rpc') {
      return RUNTIME_UNSUPPORTED;
    }
    return this.embeddedPort.readLegacySessionPresentation(context);
  }

  /**
   * Verified Discord ingress bypasses browser mode: it is embedded-only in BOTH modes and
   * never reaches the harness or routing-engine selection. It is reached only through a
   * {@link VerifiedDiscordIngressContext}, which exists only after every ingress check.
   */
  dispatchVerifiedDiscordIngress(
    context: VerifiedDiscordIngressContext,
    stream: LegacyRuntimeStream,
  ): Promise<LegacyRuntimeResult<VerifiedDiscordTurnLease>> {
    return this.embeddedPort.dispatchVerifiedDiscordIngress(context, stream);
  }
}
