import {
  Body,
  Controller,
  Get,
  Param,
  Patch,
  Post,
  Query,
  Req,
  Res,
  UnauthorizedException,
  UploadedFiles,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { Response } from 'express';
import { ApiTags, ApiOperation, ApiBearerAuth, ApiBody, ApiConsumes } from '@nestjs/swagger';
import { SUPPORT_ATTACHMENT_MAX_PER_MESSAGE } from '@edr/types';
import { IsPublic } from '@tria-plc/api-common/modules/auth/decorators/public.decorator';
import { SupportService } from './support.service';
import { JwtGuard } from '../../common/jwt.guard';
import {
  SUPPORT_ATTACHMENT_FIELD,
  supportAttachmentMulterOptions,
} from './attachment-upload.options';
import {
  CreateConversationDto,
  CreateGuestConversationDto,
  DeviceIdBodyDto,
  DeviceSendMessageDto,
  DeviceThreadQueryDto,
  GuestIdBodyDto,
  GuestSendMessageDto,
  ListConversationsQueryDto,
  ListMessagesQueryDto,
  SendMessageDto,
  UpdateStatusDto,
} from './support.dto';

function userId(req: any): string {
  const id = req?.user?.id ?? req?.user?.sub;
  if (!id) throw new UnauthorizedException();
  return id;
}

/**
 * Multipart send routes accept `text` + `attachments` file parts; a plain-JSON
 * body still works (Multer passes non-multipart requests through untouched), so
 * text-only clients are unaffected.
 */
const attachmentsInterceptor = () =>
  UseInterceptors(
    FilesInterceptor(
      SUPPORT_ATTACHMENT_FIELD,
      SUPPORT_ATTACHMENT_MAX_PER_MESSAGE,
      supportAttachmentMulterOptions,
    ),
  );

/** Swagger body schema for a send route: optional text + optional files. */
const sendBodySchema = (extra: Record<string, unknown> = {}) => ({
  schema: {
    type: 'object',
    properties: {
      ...extra,
      text: { type: 'string' },
      attachments: {
        type: 'array',
        items: { type: 'string', format: 'binary' },
      },
    },
  },
});

@ApiTags('Support')
@Controller('support')
export class SupportController {
  constructor(private service: SupportService) {}

  // ---- FAQ (public) ------------------------------------------------------

  @Get('faq')
  @IsPublic()
  @ApiOperation({ summary: 'Get FAQ categories' })
  getFaqCategories() {
    return this.service.getFaqCategories();
  }

  @Get('faq/:categoryId/articles')
  @IsPublic()
  @ApiOperation({ summary: 'Get FAQ articles for a category' })
  getFaqArticles(@Param('categoryId') id: string) {
    return this.service.getFaqArticles(id);
  }

  // ---- customer: authenticated passenger --------------------------------

  @Post('conversations')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Open a new support conversation' })
  createConversation(@Req() req: any, @Body() body: CreateConversationDto) {
    return this.service.createConversation(userId(req), body);
  }

  @Get('conversations')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'List my support conversations' })
  listMine(@Req() req: any, @Query() query: ListConversationsQueryDto) {
    return this.service.listForCustomer({ iamUserId: userId(req) }, query);
  }

  @Get('conversations/:id/messages')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({
    summary: 'List messages in one of my conversations (newest page first)',
    description:
      'Keyset-paginated backwards from newest. Omit `before` for the newest ' +
      'page, then pass the previous `nextCursor`. Null means start of thread.',
  })
  messages(@Req() req: any, @Param('id') id: string, @Query() query: ListMessagesQueryDto) {
    return this.service.getMessages(id, query, { iamUserId: userId(req) });
  }

  @Post('conversations/:id/messages')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @attachmentsInterceptor()
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody(sendBodySchema())
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Send a message as the customer' })
  send(
    @Req() req: any,
    @Param('id') id: string,
    @Body() body: SendMessageDto,
    @UploadedFiles() attachments?: Express.Multer.File[],
  ) {
    return this.service.sendMessage(
      id,
      'USER',
      body.text,
      { iamUserId: userId(req) },
      attachments ?? [],
    );
  }

  @Post('conversations/:id/read')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Mark a conversation read (customer side)' })
  read(@Req() req: any, @Param('id') id: string) {
    return this.service.markRead(id, 'USER', { iamUserId: userId(req) });
  }

  @Get('unread-count')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Count my unread support conversations' })
  unread(@Req() req: any) {
    return this.service.unreadCount('USER', { iamUserId: userId(req) });
  }

  // ---- customer: device-scoped single thread (portal) -------------------
  // No auth, no forms. One conversation per device id (localStorage). Anyone
  // with the device id can see that thread — accepted MVP trade-off.

  @Get('device/thread')
  @IsPublic()
  @ApiOperation({
    summary: "Get the device's support thread + its newest page of messages",
    description:
      '`messages` is the newest page only, not the whole thread — page back ' +
      'with `nextCursor` via this same route.',
  })
  deviceThread(@Query() query: DeviceThreadQueryDto) {
    return this.service.getDeviceThread(query.deviceId, query);
  }

  @Post('device/messages')
  @IsPublic()
  @attachmentsInterceptor()
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody(sendBodySchema({ deviceId: { type: 'string' } }))
  @ApiOperation({
    summary: 'Send a message (creates the thread on first send)',
  })
  deviceSend(
    @Body() body: DeviceSendMessageDto,
    @UploadedFiles() attachments?: Express.Multer.File[],
  ) {
    return this.service.sendDeviceMessage(body.deviceId, body.text, attachments ?? []);
  }

  @Post('device/read')
  @IsPublic()
  @ApiOperation({ summary: 'Mark the device thread read' })
  deviceRead(@Body() body: DeviceIdBodyDto) {
    return this.service.markDeviceRead(body.deviceId);
  }

  @Get('device/unread-count')
  @IsPublic()
  @ApiOperation({ summary: "Count the device thread's unread messages" })
  deviceUnread(@Query('deviceId') deviceId: string) {
    return this.service.unreadCount('USER', { guestId: deviceId });
  }

  // ---- chat attachments --------------------------------------------------

  /**
   * Serves both audiences (portal device threads and the backoffice inbox) from
   * one path, because a new message is pushed to both over the socket in a
   * single payload — an identity-bearing URL would be wrong for one of them.
   *
   * Public for the same reason the device thread is: access to a passenger
   * support thread is already whoever-holds-the-id. See `streamAttachment` for
   * the full trade-off and the TODO to tighten it with the agent-route gating.
   */
  @Get('attachments/:fileId')
  @IsPublic()
  @ApiOperation({ summary: 'Stream a support chat attachment' })
  async attachment(
    @Param('fileId') fileId: string,
    @Query('download') download: string | undefined,
    @Res() res: Response,
  ) {
    const { stream, mimeType, name } = await this.service.streamAttachment(fileId);
    const forceDownload = download === '1' || download === 'true';

    res.setHeader('Content-Type', mimeType);
    res.setHeader(
      'Content-Disposition',
      `${forceDownload ? 'attachment' : 'inline'}; filename="${name}"`,
    );
    res.setHeader('Cache-Control', 'private, max-age=300');
    stream.pipe(res);
  }

  // ---- customer: guest (unauthenticated, multi-ticket) ------------------
  // No JwtGuard. Access is scoped by a client-generated `guestId` (the bearer
  // of access — anyone with it sees that thread; accepted MVP trade-off).

  @Post('guest/conversations')
  @IsPublic()
  @ApiOperation({ summary: 'Open a support conversation as a guest' })
  guestCreate(@Body() body: CreateGuestConversationDto) {
    return this.service.createGuestConversation(body);
  }

  @Get('guest/conversations')
  @IsPublic()
  @ApiOperation({ summary: "List a guest's conversations" })
  guestList(@Query('guestId') guestId: string, @Query() query: ListConversationsQueryDto) {
    return this.service.listForCustomer({ guestId }, query);
  }

  @Get('guest/conversations/:id/messages')
  @IsPublic()
  @ApiOperation({
    summary: 'List messages in a guest conversation (newest page first)',
  })
  guestMessages(
    @Param('id') id: string,
    @Query('guestId') guestId: string,
    @Query() query: ListMessagesQueryDto,
  ) {
    return this.service.getMessages(id, query, { guestId });
  }

  @Post('guest/conversations/:id/messages')
  @IsPublic()
  @attachmentsInterceptor()
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody(sendBodySchema({ guestId: { type: 'string' } }))
  @ApiOperation({ summary: 'Send a message as a guest' })
  guestSend(
    @Param('id') id: string,
    @Body() body: GuestSendMessageDto,
    @UploadedFiles() attachments?: Express.Multer.File[],
  ) {
    return this.service.sendMessage(
      id,
      'USER',
      body.text,
      { guestId: body.guestId },
      attachments ?? [],
    );
  }

  @Post('guest/conversations/:id/read')
  @IsPublic()
  @ApiOperation({ summary: 'Mark a guest conversation read' })
  guestRead(@Param('id') id: string, @Body() body: GuestIdBodyDto) {
    return this.service.markRead(id, 'USER', { guestId: body.guestId });
  }

  @Get('guest/unread-count')
  @IsPublic()
  @ApiOperation({ summary: "Count a guest's unread conversations" })
  guestUnread(@Query('guestId') guestId: string) {
    return this.service.unreadCount('USER', { guestId });
  }

  // ---- agent (backoffice) ------------------------------------------------
  // TODO: gate agent routes with a staff permission once passenger RBAC is wired.

  @Get('agent/conversations')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'List all support conversations (shared inbox)' })
  agentList(@Query() query: ListConversationsQueryDto) {
    return this.service.listForAgents(query);
  }

  @Get('agent/conversations/:id/messages')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({
    summary: 'List messages in a conversation (newest page first)',
    description:
      'Keyset-paginated backwards from newest. Omit `before` for the newest ' +
      'page, then pass the previous `nextCursor`. Null means start of thread.',
  })
  agentMessages(@Param('id') id: string, @Query() query: ListMessagesQueryDto) {
    return this.service.getMessages(id, query);
  }

  @Post('agent/conversations/:id/messages')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @attachmentsInterceptor()
  @ApiConsumes('multipart/form-data', 'application/json')
  @ApiBody(sendBodySchema())
  @ApiOperation({ summary: 'Reply as an agent, optionally with attachments' })
  agentSend(
    @Param('id') id: string,
    @Body() body: SendMessageDto,
    @UploadedFiles() attachments?: Express.Multer.File[],
  ) {
    return this.service.sendMessage(id, 'AGENT', body.text, undefined, attachments ?? []);
  }

  @Patch('agent/conversations/:id/status')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: "Change a conversation's status" })
  agentStatus(@Param('id') id: string, @Body() body: UpdateStatusDto) {
    return this.service.setStatus(id, body.status);
  }

  @Post('agent/conversations/:id/read')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Mark a conversation read (agent side)' })
  agentRead(@Param('id') id: string) {
    return this.service.markRead(id, 'AGENT');
  }

  @Get('agent/unread-count')
  @UseGuards(JwtGuard)
  @ApiBearerAuth('JWT-auth')
  @ApiOperation({ summary: 'Count unread conversations (agent side)' })
  agentUnread() {
    return this.service.unreadCount('AGENT');
  }
}
