import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  HttpCode,
  HttpException,
  HttpStatus,
  Inject,
  NotFoundException,
  Param,
  Patch,
  Post,
  Query,
  UseGuards,
} from '@nestjs/common';
import type { Brain } from '@mosaicstack/brain';
import { BRAIN } from '../brain/brain.tokens.js';
import { AuthGuard } from '../auth/auth.guard.js';
import { CurrentUser } from '../auth/current-user.decorator.js';
import { ChatRuntimeRouter } from '../chat/chat-runtime-router.js';
import {
  CreateConversationDto,
  UpdateConversationDto,
  SendMessageDto,
  SearchMessagesDto,
} from './conversations.dto.js';

/**
 * Under `pi-rpc` the durable/harness conversation path (Task 15) owns message persistence, so the
 * legacy direct-repository write must fail closed with a fixed typed `runtime_unsupported` before
 * the repository is touched — never a duplicate write. The `code` field is exposed at the top level
 * so callers can discriminate the refusal while the 503 status carries the browser-safe surface.
 */
class HarnessRuntimeWriteUnsupportedException extends HttpException {
  readonly code = 'runtime_unsupported' as const;

  constructor() {
    super(
      {
        code: 'runtime_unsupported',
        message:
          'Conversation message writes are handled by the harness runtime on this deployment.',
      },
      HttpStatus.SERVICE_UNAVAILABLE,
    );
  }
}

@Controller('api/conversations')
@UseGuards(AuthGuard)
export class ConversationsController {
  /**
   * `router` supplies the ONE immutable runtime mode resolved at module init (Task 5, item 3).
   * The pre-write fence reads `router.runtimeMode`, never `resolveChatRuntimeMode(process.env)` at
   * request time — a single source of truth, so the controller cannot disagree with the router
   * about the live runtime if the environment is mutated after startup. Narrowed to `runtimeMode`
   * so this class depends on nothing else the router exposes.
   */
  constructor(
    @Inject(BRAIN) private readonly brain: Brain,
    @Inject(ChatRuntimeRouter) private readonly router: Pick<ChatRuntimeRouter, 'runtimeMode'>,
  ) {}

  @Get()
  async list(@CurrentUser() user: { id: string }) {
    return this.brain.conversations.findAll(user.id);
  }

  @Get('search')
  async search(@Query() dto: SearchMessagesDto, @CurrentUser() user: { id: string }) {
    if (!dto.q || dto.q.trim().length === 0) {
      throw new BadRequestException('Query parameter "q" is required and must not be empty');
    }
    const limit = dto.limit ?? 20;
    const offset = dto.offset ?? 0;
    return this.brain.conversations.searchMessages(user.id, dto.q.trim(), limit, offset);
  }

  @Get(':id')
  async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
    const conversation = await this.brain.conversations.findById(id, user.id);
    if (!conversation) throw new NotFoundException('Conversation not found');
    return conversation;
  }

  @Post()
  async create(@CurrentUser() user: { id: string }, @Body() dto: CreateConversationDto) {
    return this.brain.conversations.create({
      userId: user.id,
      title: dto.title,
      projectId: dto.projectId,
    });
  }

  @Patch(':id')
  async update(
    @Param('id') id: string,
    @Body() dto: UpdateConversationDto,
    @CurrentUser() user: { id: string },
  ) {
    const conversation = await this.brain.conversations.update(id, user.id, dto);
    if (!conversation) throw new NotFoundException('Conversation not found');
    return conversation;
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
    const deleted = await this.brain.conversations.remove(id, user.id);
    if (!deleted) throw new NotFoundException('Conversation not found');
  }

  @Get(':id/messages')
  async listMessages(@Param('id') id: string, @CurrentUser() user: { id: string }) {
    // Verify ownership explicitly to return a clear 404 rather than an empty list.
    const conversation = await this.brain.conversations.findById(id, user.id);
    if (!conversation) throw new NotFoundException('Conversation not found');
    return this.brain.conversations.findMessages(id, user.id);
  }

  @Post(':id/messages')
  async addMessage(
    @Param('id') id: string,
    @Body() dto: SendMessageDto,
    @CurrentUser() user: { id: string },
  ) {
    // Fail the legacy repository write closed under pi-rpc BEFORE touching the repository — the
    // harness path owns persistence there, so a direct write would duplicate the message. The mode
    // comes from the router's init-time resolution, not a request-time env read.
    if (this.router.runtimeMode === 'pi-rpc') {
      throw new HarnessRuntimeWriteUnsupportedException();
    }

    const message = await this.brain.conversations.addMessage(
      {
        conversationId: id,
        role: dto.role,
        content: dto.content,
        metadata: dto.metadata,
      },
      user.id,
    );
    if (!message) throw new ForbiddenException('Conversation not found or access denied');
    return message;
  }
}
