import { BadRequestException, Body, Controller, ForbiddenException, Get, Headers, Param, Post, Query, Res, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { JwtService } from '@nestjs/jwt';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { getAuthorizedUserId } from '../document-access';
import { AvatarUploadDto, ChatMediaUploadDto, ConfirmAvatarDto, DocumentPhotoUploadDto } from '../dto/media.dto';
import { buildContentDisposition } from '../media-content-disposition';

type StreamResponse = {
  setHeader: (key: string, value: string) => void;
  status: (code: number) => { json: (body: unknown) => void };
} & NodeJS.WritableStream;

@ApiTags('Медиа')
@Controller('media')
export class MediaController {
  private s3Client: S3Client | null = null;

  constructor(
    private readonly core: CoreGrpcService,
    private readonly jwt: JwtService
  ) {}

  private getS3Client() {
    if (!this.s3Client) {
      const endpoint = process.env.MINIO_ENDPOINT ?? 'localhost:9000';
      const useSsl = process.env.MINIO_USE_SSL === 'true';
      this.s3Client = new S3Client({
        endpoint: `${useSsl ? 'https' : 'http'}://${endpoint}`,
        region: process.env.MINIO_REGION ?? 'us-east-1',
        credentials: {
          accessKeyId: process.env.MINIO_ACCESS_KEY ?? 'minioadmin',
          secretAccessKey: process.env.MINIO_SECRET_KEY ?? 'minioadmin'
        },
        forcePathStyle: true
      });
    }
    return this.s3Client;
  }

  private async authUserId(authorization?: string) {
    return getAuthorizedUserId(this.jwt, this.core, authorization);
  }

  @Post('upload')
  @ApiConsumes('multipart/form-data')
  @ApiOperation({ summary: 'Загрузить файл через API', description: 'Принимает файл по uploadToken из upload-url ответа. Обходит прямой доступ браузера к MinIO.' })
  @ApiResponse({ status: 201, description: 'Файл загружен' })
  @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 50 * 1024 * 1024 } }))
  async uploadObject(
    @Headers('x-upload-token') uploadToken: string | undefined,
    @UploadedFile() file: { buffer: Buffer; mimetype: string } | undefined
  ) {
    if (!uploadToken) {
      throw new BadRequestException('Не передан uploadToken');
    }
    if (!file) {
      throw new BadRequestException('Файл не передан');
    }

    let payload: { purpose?: string; storageKey?: string; contentType?: string };
    try {
      payload = await this.jwt.verifyAsync(uploadToken, {
        secret: process.env.JWT_ACCESS_SECRET ?? 'docker-access-secret',
        issuer: 'id.lendry.ru'
      });
    } catch {
      throw new ForbiddenException('Ссылка загрузки недействительна или истекла');
    }

    if (payload.purpose !== 'media-upload' || !payload.storageKey) {
      throw new ForbiddenException('Ссылка загрузки недействительна');
    }

    const bucket = process.env.MINIO_BUCKET ?? 'lendry-id';
    await this.getS3Client().send(
      new PutObjectCommand({
        Bucket: bucket,
        Key: payload.storageKey,
        Body: file.buffer,
        ContentType: file.mimetype || payload.contentType || 'application/octet-stream'
      })
    );

    return { ok: true, storageKey: payload.storageKey };
  }

  @Post('avatars/upload-url')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Получить URL для загрузки аватара', description: 'Возвращает presigned URL MinIO для загрузки изображения аватара.' })
  @ApiBody({ type: AvatarUploadDto })
  async createAvatarUploadUrl(@Headers('authorization') authorization: string | undefined, @Body() dto: AvatarUploadDto) {
    const userId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.CreateAvatarUploadUrl({ userId, contentType: dto.contentType }));
  }

  @Post('avatars/confirm')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Подтвердить загрузку аватара', description: 'Сохраняет ключ объекта MinIO как аватар пользователя.' })
  @ApiBody({ type: ConfirmAvatarDto })
  async confirmAvatar(@Headers('authorization') authorization: string | undefined, @Body() dto: ConfirmAvatarDto) {
    const userId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.ConfirmAvatar({ userId, storageKey: dto.storageKey }));
  }

  @Get('avatars/:userId/url')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Получить временную ссылку на аватар', description: 'Возвращает ссылку, действующую 15 минут. Без авторизации ссылка не выдаётся.' })
  @ApiParam({ name: 'userId', description: 'ID пользователя' })
  async getAvatarUrl(@Headers('authorization') authorization: string | undefined, @Param('userId') userId: string) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.GetAvatarAccessUrl({ requesterId, targetUserId: userId }));
  }

  @Post('documents/:documentId/photo/upload-url')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Получить URL для фото документа', description: 'Presigned URL для загрузки скана/фото документа в MinIO.' })
  @ApiBody({ type: DocumentPhotoUploadDto })
  async createDocumentPhotoUploadUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('documentId') documentId: string,
    @Body() dto: DocumentPhotoUploadDto
  ) {
    const userId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.CreateDocumentPhotoUploadUrl({ userId, documentId, contentType: dto.contentType }));
  }

  @Get('users/:userId/documents/photo-url')
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Получить ссылку на фото документа', description: 'Временная ссылка на просмотр фото документа (15 минут).' })
  @ApiParam({ name: 'userId', description: 'ID владельца документа' })
  @ApiQuery({ name: 'storageKey', description: 'Ключ объекта в MinIO' })
  async getDocumentPhotoUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('userId') userId: string,
    @Query('storageKey') storageKey: string
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.GetDocumentPhotoAccessUrl({ requesterId, targetUserId: userId, storageKey })
    );
  }

  @Get('stream/:token')
  @ApiOperation({ summary: 'Потоковая выдача медиа', description: 'Отдаёт файл по временному токену. Для файлов чата требуется Authorization. Токен действует 15 минут.' })
  @ApiParam({ name: 'token', description: 'Временный JWT-токен доступа' })
  @ApiResponse({ status: 403, description: 'Ссылка недействительна или истекла' })
  async stream(
    @Param('token') token: string,
    @Headers('authorization') authorization: string | undefined,
    @Query('download') download: string | undefined,
    @Res({ passthrough: false }) response: StreamResponse
  ) {
    let requesterId: string | undefined;
    if (authorization) {
      try {
        requesterId = await getAuthorizedUserId(this.jwt, this.core, authorization);
      } catch {
        requesterId = undefined;
      }
    }

    const resolved = await firstValueFrom(this.core.media.ResolveStreamToken({ token, requesterId }));
    const payload = resolved as { storageKey: string; contentType: string; fileName?: string };
    const bucket = process.env.MINIO_BUCKET ?? 'lendry-id';
    const object = await this.getS3Client().send(
      new GetObjectCommand({
        Bucket: bucket,
        Key: payload.storageKey
      })
    );

    response.setHeader('Content-Type', payload.contentType);
    response.setHeader('Cache-Control', 'private, no-store');
    if (payload.fileName) {
      const asAttachment = download === '1' || download === 'true';
      response.setHeader(
        'Content-Disposition',
        buildContentDisposition(asAttachment ? 'attachment' : 'inline', payload.fileName)
      );
    }
    const body = object.Body;
    if (!body) {
      response.status(404).json({ message: 'Файл не найден' });
      return;
    }

    const stream = body as NodeJS.ReadableStream;
    stream.pipe(response);
  }

  @Post('families/:groupId/avatar/upload-url')
  @ApiBearerAuth()
  async createFamilyAvatarUploadUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('groupId') groupId: string,
    @Body() dto: AvatarUploadDto
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.CreateFamilyAvatarUploadUrl({ requesterId, groupId, contentType: dto.contentType })
    );
  }

  @Post('families/:groupId/avatar/confirm')
  @ApiBearerAuth()
  async confirmFamilyAvatar(
    @Headers('authorization') authorization: string | undefined,
    @Param('groupId') groupId: string,
    @Body() dto: ConfirmAvatarDto
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.ConfirmFamilyAvatar({ requesterId, groupId, storageKey: dto.storageKey })
    );
  }

  @Get('families/:groupId/avatar/url')
  @ApiBearerAuth()
  async getFamilyAvatarUrl(@Headers('authorization') authorization: string | undefined, @Param('groupId') groupId: string) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.GetFamilyAvatarAccessUrl({ requesterId, groupId }));
  }

  @Post('chat/:roomId/media/upload-url')
  @ApiBearerAuth()
  async createChatMediaUploadUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('roomId') roomId: string,
    @Body() dto: ChatMediaUploadDto
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.CreateChatMediaUploadUrl({
        requesterId,
        roomId,
        contentType: dto.contentType,
        fileName: dto.fileName
      })
    );
  }

  @Get('chat/:roomId/media/url')
  @ApiBearerAuth()
  async getChatMediaUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('roomId') roomId: string,
    @Query('storageKey') storageKey: string,
    @Query('fileName') fileName?: string
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.GetChatMediaAccessUrl({ requesterId, roomId, storageKey, fileName })
    );
  }

  @Post('chat/:roomId/avatar/upload-url')
  @ApiBearerAuth()
  async createChatRoomAvatarUploadUrl(
    @Headers('authorization') authorization: string | undefined,
    @Param('roomId') roomId: string,
    @Body() dto: AvatarUploadDto
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.CreateChatRoomAvatarUploadUrl({ requesterId, roomId, contentType: dto.contentType })
    );
  }

  @Post('chat/:roomId/avatar/confirm')
  @ApiBearerAuth()
  async confirmChatRoomAvatar(
    @Headers('authorization') authorization: string | undefined,
    @Param('roomId') roomId: string,
    @Body() dto: ConfirmAvatarDto
  ) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(
      this.core.media.ConfirmChatRoomAvatar({ requesterId, roomId, storageKey: dto.storageKey })
    );
  }

  @Get('chat/:roomId/avatar/url')
  @ApiBearerAuth()
  async getChatRoomAvatarUrl(@Headers('authorization') authorization: string | undefined, @Param('roomId') roomId: string) {
    const requesterId = await this.authUserId(authorization);
    return firstValueFrom(this.core.media.GetChatRoomAvatarAccessUrl({ requesterId, roomId }));
  }
}
