import { randomUUID } from 'node:crypto';
import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  Headers,
  HttpCode,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
} from '@nestjs/common';
import { ThreadsService } from './threads.service';
import { MessageService } from '../messaging/message.service';
import { SessionVerifier } from '../platform/session.verifier';
import { ContextAttestationGuard } from '../platform/context-attestation.guard';
import { SendMessageDto } from './send-message.dto';

@Controller('v1/threads')
@UseGuards(ContextAttestationGuard)
export class ThreadsController {
  constructor(
    private readonly threads: ThreadsService,
    private readonly messages: MessageService,
    private readonly session: SessionVerifier,
  ) {}

  /**
   * Generic "my threads" — every thread the caller participates in (last message + unread).
   * `?metadata[key]=value` narrows to threads whose opaque attribute bag matches (equality on each key).
   */
  @Get()
  async listThreads(@Headers('authorization') auth?: string, @Query('metadata') metadata?: Record<string, string>) {
    const metaFilter = metadata && typeof metadata === 'object' ? metadata : undefined;
    return this.messages.listThreads(this.principal(auth), metaFilter ? { metadata: metaFilter } : undefined);
  }

  /** Generic "my annotated messages" (e.g. ?type=save for a personal bookmarks list). */
  @Get('my-annotations')
  async myAnnotations(@Headers('authorization') auth?: string, @Query('type') type = 'save') {
    return this.messages.listMyAnnotated(this.principal(auth), type);
  }

  /**
   * Discover threads across the caller's scope (not just ones they're in) matching an opaque
   * metadata filter — e.g. `?metadata[membership]=channel&metadata[visibility]=public` to browse
   * public channels. Each result is flagged `joined`. Declared before `:id` routes so the static
   * "discover" segment wins.
   */
  @Get('discover')
  async discover(@Headers('authorization') auth?: string, @Query('metadata') metadata?: Record<string, string>) {
    const metaFilter = metadata && typeof metadata === 'object' ? metadata : undefined;
    return this.messages.discoverThreads(this.principal(auth), metaFilter ? { metadata: metaFilter } : undefined);
  }

  /** Create a thread; `membership`/`creatorRole`/`subject`/`metadata` are opaque, app-supplied attributes the kernel stores but never interprets. */
  @Post()
  @HttpCode(201)
  async createThread(@Body() body: { membership?: string; creatorRole?: string; subject?: string; metadata?: Record<string, unknown> }, @Headers('authorization') auth?: string) {
    return this.messages.openThread(null, this.principal(auth), { membership: body?.membership, creatorRole: body?.creatorRole, subject: body?.subject, metadata: body?.metadata });
  }

  /** Governed membership: add a user (by userId) to a thread — policy enforces DM cap / roles. */
  @Post(':id/participants')
  @HttpCode(201)
  async addParticipant(@Param('id') id: string, @Body() body: { userId: string; role?: string }, @Headers('authorization') auth?: string) {
    return this.messages.addParticipant(id, this.principal(auth), body.userId, body.role);
  }

  /**
   * Governed bulk membership: add many users in one call (e.g. importing another channel's roster).
   * Reports per-user outcome — `skipped` are already members — so a partial result is never silent.
   */
  @Post(':id/participants/bulk')
  @HttpCode(200)
  async addParticipants(@Param('id') id: string, @Body() body: { userIds?: string[]; role?: string }, @Headers('authorization') auth?: string) {
    if (!Array.isArray(body?.userIds)) throw new BadRequestException('userIds must be an array');
    return this.messages.addParticipants(id, this.principal(auth), body.userIds, body.role);
  }

  /** Members of a thread with their role — drives the group settings member list. */
  @Get(':id/participants')
  async listParticipants(@Param('id') id: string, @Headers('authorization') auth?: string) {
    return this.messages.listParticipants(id, this.principal(auth));
  }

  /** Governed removal: remove a user from a thread — policy enforces group-admin. */
  @Delete(':id/participants/:userId')
  @HttpCode(200)
  async removeParticipant(@Param('id') id: string, @Param('userId') userId: string, @Headers('authorization') auth?: string) {
    return this.messages.removeParticipant(id, this.principal(auth), userId);
  }

  /** Governed rename (group settings): change a thread subject — policy enforces group-admin. */
  @Patch(':id')
  @HttpCode(200)
  async updateThread(@Param('id') id: string, @Body() body: { subject?: string }, @Headers('authorization') auth?: string) {
    if (body?.subject == null) throw new BadRequestException('subject is required');
    return this.messages.renameThread(id, this.principal(auth), body.subject);
  }

  /** Self-leave: remove the caller from a thread (e.g. leave a channel). */
  @Delete(':id/me')
  @HttpCode(200)
  async leave(@Param('id') id: string, @Headers('authorization') auth?: string) {
    return this.messages.leaveThread(id, this.principal(auth));
  }

  @Get(':id/messages')
  async listMessages(@Param('id') id: string) {
    return this.threads.getMessages(id);
  }

  /** Mute / unmute notifications from this thread for the caller. */
  @Post(':id/mute')
  @HttpCode(200)
  async mute(@Param('id') id: string, @Headers('authorization') auth?: string) {
    return this.messages.muteThread(id, this.principal(auth), true);
  }

  @Post(':id/unmute')
  @HttpCode(200)
  async unmute(@Param('id') id: string, @Headers('authorization') auth?: string) {
    return this.messages.muteThread(id, this.principal(auth), false);
  }

  /** REST/polling fallback for native send (same write as the socket path). */
  @Post(':id/messages')
  @HttpCode(201)
  async send(
    @Param('id') id: string,
    @Body() body: SendMessageDto,
    @Headers('authorization') authorization?: string,
    @Headers('idempotency-key') idempotencyKey?: string,
  ) {
    return this.messages.send(
      id,
      this.principal(authorization),
      { content: body.content, contentRef: body.contentRef, mimeType: body.mimeType, sizeBytes: body.sizeBytes, checksumSha256: body.checksumSha256 },
      idempotencyKey ?? randomUUID(),
      undefined,
      body.parentInteractionId,
    );
  }

  private principal(authorization?: string) {
    const token = (authorization ?? '').replace(/^Bearer\s+/i, '');
    if (!token) throw new BadRequestException('Authorization bearer token is required');
    return this.session.verify(token);
  }
}
