import { Type } from 'class-transformer';
import {
  ArrayMaxSize,
  ArrayMinSize,
  IsArray,
  IsIn,
  IsOptional,
  IsString,
  MaxLength,
  MinLength,
  ValidateNested,
} from 'class-validator';

/**
 * Roles the SPA may attach to a message in the chat history.
 *
 * `tool` is intentionally absent — tool-result messages are
 * constructed by the BFF itself (caller-side tool dispatch, per
 * [ADR-0024](../../../../../../docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md)
 * §"Tool-dispatch contract — caller-side execution") and never
 * provided by the SPA. v1 ships with an empty tool registry, so
 * no `tool` message ever reaches this controller.
 */
const ALLOWED_ROLES = ['user', 'assistant', 'system'] as const;
type AllowedRole = (typeof ALLOWED_ROLES)[number];

export class ChatMessageDto {
  @IsIn(ALLOWED_ROLES)
  role!: AllowedRole;

  /**
   * Free-form message body. The cap is generous (16 KB) to support
   * code blocks and pasted excerpts; the BFF leaves further
   * pre-processing (truncation, token counting) to the AI service.
   */
  @IsString()
  @MinLength(0)
  @MaxLength(16_384)
  content!: string;
}

/**
 * POST /api/ai/chat body.
 *
 * Stateless from the BFF's perspective — the SPA owns the
 * conversation history. `conversationId` is an opaque correlation
 * key used by the AI service's audit log (not a database key on
 * either side); the SPA may omit it for one-off calls.
 */
export class ChatRequestDto {
  @IsArray()
  @ArrayMinSize(1)
  @ArrayMaxSize(64)
  @ValidateNested({ each: true })
  @Type(() => ChatMessageDto)
  messages!: ChatMessageDto[];

  @IsOptional()
  @IsString()
  @MaxLength(128)
  conversationId?: string;

  /**
   * Optional model + provider hints. Both default to "" on the
   * wire, which the AI service interprets as "use the configured
   * default". The SPA does not need to pass these in v1.
   */
  @IsOptional()
  @IsString()
  @MaxLength(64)
  model?: string;

  @IsOptional()
  @IsString()
  @MaxLength(64)
  provider?: string;
}
