import { Inject, Module, type OnApplicationShutdown, type Provider } from '@nestjs/common';
import { ChannelCredentials, type Client } from '@grpc/grpc-js';
import { HashUserIdService } from '../../audit/hash-user-id.service';
import { assertAiServiceConfig, type AiServiceConfig } from '../../config/check-ai-service-config';
import { ChatServiceClient } from '../gen/apf-ai/chat';
import { IngestionServiceClient } from '../gen/apf-ai/ingestion';
import { ModelsServiceClient } from '../gen/apf-ai/models';
import { RagServiceClient } from '../gen/apf-ai/rag';
import { ChatClient } from './chat.client';
import { GrpcMetadataBuilder } from './grpc-metadata.builder';
import { IngestionClient } from './ingestion.client';
import { ModelsClient } from './models.client';
import { PrincipalMapper } from './principal.mapper';
import { RagClient } from './rag.client';
import {
  AI_CHAT_GRPC_CLIENT,
  AI_CONFIG,
  AI_CREDENTIALS,
  AI_INGESTION_GRPC_CLIENT,
  AI_MODELS_GRPC_CLIENT,
  AI_RAG_GRPC_CLIENT,
} from './tokens';

/**
 * Wires the BFF's integration surface to `apf-ai-service`, per
 * [ADR-0024](../../../../../docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md).
 *
 * The module is intentionally NOT imported in `AppModule` yet — the
 * SSE-bridge controller that consumes these clients ships in the
 * next chantier. Until then the module compiles, type-checks, and
 * unit-tests against an in-process fake gRPC server, but it does not
 * register any HTTP route.
 *
 * Provider layout:
 *
 *   1. `AI_CONFIG` factory reads + validates `AI_SERVICE_*` env vars.
 *   2. `AI_CREDENTIALS` factory builds the gRPC channel credentials
 *      (insecure h2c in dev, system-CA-trusted SSL in prod).
 *   3. One token per generated stub — `AI_CHAT_GRPC_CLIENT` etc. —
 *      backed by a factory that opens a `Client` against the
 *      configured endpoint. gRPC-js multiplexes the underlying
 *      HTTP/2 connection across stubs sharing the same address +
 *      credentials, so the four stubs share one TCP+TLS channel.
 *   4. Hand-written wrapper services (`ChatClient`, `RagClient`,
 *      `IngestionClient`, `ModelsClient`) add metadata injection
 *      and promisification on top of the generated stubs.
 *   5. `PrincipalMapper` + `GrpcMetadataBuilder` collaborators.
 *
 * `PrincipalMapper` depends on `HashUserIdService` — the same hash
 * service the audit writer uses — so the `Principal.subject` field
 * matches `audit.events.actor_id_hash` exactly (per ADR-0024
 * §"Audit cross-referencing"). `HashUserIdService` is declared as a
 * local provider here (rather than relying on `AuditModule`'s
 * `@Global()` export) for two reasons:
 *
 *   1. **Test isolation.** The module compiles standalone with no
 *      other portal module in scope; no need to pull `AuditWriter`
 *      and its Prisma + CLS deps into a unit test that only exercises
 *      the gRPC surface.
 *   2. **Self-containment of the wire boundary.** The Principal-side
 *      contract is a property of `AiClientModule`, not of the audit
 *      module. Co-locating the hash provider keeps the boundary
 *      readable from one file.
 *
 * Functional identity with the audit-side instance is guaranteed by
 * `HashUserIdService` being purely a function of
 * `LOG_USER_ID_SALT` — same env value ⇒ same hash output, regardless
 * of how many instances exist. The cross-service join key invariant
 * from ADR-0013 still holds.
 */

function buildCredentials(config: AiServiceConfig): ChannelCredentials {
  return config.useTls ? ChannelCredentials.createSsl() : ChannelCredentials.createInsecure();
}

const grpcStubProviders: Provider[] = [
  {
    provide: AI_CHAT_GRPC_CLIENT,
    inject: [AI_CONFIG, AI_CREDENTIALS],
    useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
      new ChatServiceClient(config.endpoint, credentials),
  },
  {
    provide: AI_RAG_GRPC_CLIENT,
    inject: [AI_CONFIG, AI_CREDENTIALS],
    useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
      new RagServiceClient(config.endpoint, credentials),
  },
  {
    provide: AI_INGESTION_GRPC_CLIENT,
    inject: [AI_CONFIG, AI_CREDENTIALS],
    useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
      new IngestionServiceClient(config.endpoint, credentials),
  },
  {
    provide: AI_MODELS_GRPC_CLIENT,
    inject: [AI_CONFIG, AI_CREDENTIALS],
    useFactory: (config: AiServiceConfig, credentials: ChannelCredentials) =>
      new ModelsServiceClient(config.endpoint, credentials),
  },
];

@Module({
  providers: [
    {
      provide: AI_CONFIG,
      useFactory: () => assertAiServiceConfig(),
    },
    {
      provide: AI_CREDENTIALS,
      inject: [AI_CONFIG],
      useFactory: buildCredentials,
    },
    ...grpcStubProviders,
    HashUserIdService,
    GrpcMetadataBuilder,
    PrincipalMapper,
    ChatClient,
    RagClient,
    IngestionClient,
    ModelsClient,
  ],
  exports: [ChatClient, RagClient, IngestionClient, ModelsClient, PrincipalMapper],
})
export class AiClientModule implements OnApplicationShutdown {
  constructor(
    @Inject(AI_CHAT_GRPC_CLIENT) private readonly chatStub: Client,
    @Inject(AI_RAG_GRPC_CLIENT) private readonly ragStub: Client,
    @Inject(AI_INGESTION_GRPC_CLIENT) private readonly ingestionStub: Client,
    @Inject(AI_MODELS_GRPC_CLIENT) private readonly modelsStub: Client,
  ) {}

  /**
   * Close every generated gRPC stub when the BFF receives `SIGTERM`
   * / `SIGINT`. Each `Client.close()` flushes pending RPCs (with
   * their own gRPC `CANCELLED` semantics) and tears down the
   * shared HTTP/2 channel so the process can exit promptly without
   * waiting for the channel's keepalive PINGs.
   *
   * The four stubs share the same underlying HTTP/2 channel (same
   * endpoint + same credentials, gRPC-js de-duplicates), so the
   * four `close()` calls are cheap but kept explicit — adding a
   * fifth stub later means adding a fifth `close()` line, which is
   * easier to spot than iterating an array that grew without
   * review.
   */
  onApplicationShutdown(): void {
    this.chatStub.close();
    this.ragStub.close();
    this.ingestionStub.close();
    this.modelsStub.close();
  }
}
