import { Inject, Injectable } from '@nestjs/common';
import type { ClientReadableStream } from '@grpc/grpc-js';
import type { ChatEvent, ChatRequest, ChatServiceClient } from '../gen/apf-ai/chat';
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
import { AI_CHAT_GRPC_CLIENT } from './tokens';

/**
 * Wrapper around the generated `ChatServiceClient` for the
 * server-streaming `Chat` RPC, per ADR-0024 §"Sub-decision 1".
 *
 * The wrapper does three things on top of the raw gRPC stub:
 *
 *   1. Injects the `x-client-id` + `x-correlation-id` metadata via
 *      `GrpcMetadataBuilder` so every call carries the contract
 *      headers from `apf-ai-service/docs/contract.md`.
 *   2. Wires an optional `AbortSignal` to `call.cancel()` so the SSE
 *      bridge (next PR in the chantier) can propagate browser
 *      disconnects up to the AI service in one line.
 *   3. Returns the raw `ClientReadableStream<ChatEvent>` — Node
 *      `Readable` streams are async-iterable by default, so the SSE
 *      bridge consumes the stream with `for await ... of` and no
 *      intermediate adapter.
 *
 * Cancellation flows in both directions: a `.cancel()` from inside
 * the BFF (timeout, abort) reaches the AI service as a
 * `grpc.status.CANCELLED`, which translates upstream to
 * `ServerCallContext.CancellationToken` stopping LLM generation
 * (per `apf-ai-service/docs/streaming.md`).
 */
@Injectable()
export class ChatClient {
  constructor(
    @Inject(AI_CHAT_GRPC_CLIENT) private readonly grpc: ChatServiceClient,
    private readonly metadata: GrpcMetadataBuilder,
  ) {}

  /**
   * Start a streaming Chat call.
   *
   * The returned `ClientReadableStream<ChatEvent>` emits one event
   * per AI service `ChatEvent` and ends after the terminal
   * `ChatEvent.done` (or on cancellation / error).
   *
   * @param request The full proto request, already populated with
   *        the conversation history and a `Principal` (build the
   *        principal via `PrincipalMapper.fromInputs`).
   * @param options.signal Optional AbortSignal. When it aborts, the
   *        underlying gRPC call is cancelled and the stream closes.
   * @param options.correlationId Override the metadata
   *        correlation-id (defaults to the active OTel trace-id).
   */
  chat(
    request: ChatRequest,
    options: { signal?: AbortSignal; correlationId?: string } = {},
  ): ClientReadableStream<ChatEvent> {
    const metadata = this.metadata.build({ correlationId: options.correlationId });
    const call = this.grpc.chat(request, metadata);

    if (options.signal) {
      if (options.signal.aborted) {
        call.cancel();
      } else {
        options.signal.addEventListener('abort', () => call.cancel(), { once: true });
      }
    }

    return call;
  }
}
