import {
  BadRequestException,
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
  Query,
  Res,
  UploadedFile,
  UseGuards,
  UseInterceptors,
  UsePipes,
  ValidationPipe
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { createHash, randomUUID } from 'node:crypto';
import { firstValueFrom } from 'rxjs';
import { CoreGrpcService } from '../core-grpc.service';
import { UpdateAppReleaseDto } from '../dto/app-release.dto';
import { AdminGuard, AdminRequestUser, assertAdminPermission } from '../guards/admin.guard';
import { CurrentAdmin } from '../decorators/current-admin.decorator';
import { buildContentDisposition } from '../media-content-disposition';

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

const releaseWritePipe = new ValidationPipe({
  whitelist: true,
  transform: true,
  forbidNonWhitelisted: false
});

@ApiTags('Релизы приложений (админ)')
@ApiBearerAuth()
@UseGuards(AdminGuard)
@Controller('admin/releases')
export class AdminAppReleaseController {
  private s3Client: S3Client | null = null;

  constructor(private readonly core: CoreGrpcService) {}

  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;
  }

  @Get()
  @ApiOperation({ summary: 'Список релизов приложений', description: 'Возвращает все загруженные версии Android и Windows.' })
  list(@CurrentAdmin() admin: AdminRequestUser, @Query('platform') platform?: string) {
    assertAdminPermission(admin, 'canManageSettings');
    return firstValueFrom(this.core.appRelease.ListAppReleases({ platform }));
  }

  @Post('upload')
  @ApiConsumes('multipart/form-data')
  @ApiOperation({
    summary: 'Загрузить новый релиз',
    description: 'Загружает APK или EXE в MinIO и создаёт запись релиза с SHA-256 подписью.'
  })
  @ApiBody({
    schema: {
      type: 'object',
      properties: {
        platform: { type: 'string', enum: ['ANDROID', 'WINDOWS'] },
        version: { type: 'string', example: '1.2.0' },
        versionCode: { type: 'integer', example: 120 },
        releaseNotes: { type: 'string' },
        file: { type: 'string', format: 'binary' }
      },
      required: ['platform', 'version', 'versionCode', 'file']
    }
  })
  @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 350 * 1024 * 1024 } }))
  async upload(
    @CurrentAdmin() admin: AdminRequestUser,
    @UploadedFile() file: { buffer: Buffer; originalname: string; mimetype: string; size: number } | undefined,
    @Body('platform') platform: string,
    @Body('version') version: string,
    @Body('versionCode') versionCodeRaw: string,
    @Body('releaseNotes') releaseNotes?: string
  ) {
    assertAdminPermission(admin, 'canManageSettings');

    if (!file?.buffer?.length) {
      throw new BadRequestException('Файл релиза не передан');
    }
    if (!platform?.trim() || !version?.trim()) {
      throw new BadRequestException('Укажите платформу и версию');
    }

    const versionCode = Number(versionCodeRaw);
    if (!Number.isInteger(versionCode) || versionCode < 1) {
      throw new BadRequestException('Код версии должен быть положительным целым числом');
    }

    const normalizedPlatform = platform.trim().toUpperCase();
    const fileName = file.originalname?.trim() || 'release.bin';
    const lowerName = fileName.toLowerCase();
    if (normalizedPlatform === 'ANDROID' && !lowerName.endsWith('.apk')) {
      throw new BadRequestException('Для Android загрузите файл .apk');
    }
    if (normalizedPlatform === 'WINDOWS' && !lowerName.endsWith('.exe')) {
      throw new BadRequestException('Для Windows загрузите файл .exe');
    }

    const safeVersion = version.trim().replace(/[^a-zA-Z0-9._-]+/g, '_');
    const safeName = fileName.replace(/[^a-zA-Z0-9._-]+/g, '_');
    const storageKey = `releases/${normalizedPlatform.toLowerCase()}/${safeVersion}/${randomUUID()}-${safeName}`;
    const sha256 = createHash('sha256').update(file.buffer).digest('hex');
    const contentType =
      normalizedPlatform === 'ANDROID'
        ? 'application/vnd.android.package-archive'
        : 'application/vnd.microsoft.portable-executable';

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

    return firstValueFrom(
      this.core.appRelease.CreateAppRelease({
        platform: normalizedPlatform,
        version: version.trim(),
        versionCode,
        fileName,
        storageKey,
        fileSize: String(file.size),
        sha256,
        releaseNotes: releaseNotes?.trim() || undefined,
        createdById: admin.id
      })
    );
  }

  @Patch(':releaseId')
  @UsePipes(releaseWritePipe)
  @ApiOperation({ summary: 'Обновить релиз', description: 'Публикация/снятие с публикации и заметки к релизу.' })
  @ApiParam({ name: 'releaseId', description: 'ID релиза' })
  update(
    @CurrentAdmin() admin: AdminRequestUser,
    @Param('releaseId') releaseId: string,
    @Body() dto: UpdateAppReleaseDto
  ) {
    assertAdminPermission(admin, 'canManageSettings');
    return firstValueFrom(
      this.core.appRelease.UpdateAppRelease({
        releaseId,
        isPublished: dto.isPublished,
        releaseNotes: dto.releaseNotes
      })
    );
  }

  @Delete(':releaseId')
  @ApiOperation({ summary: 'Удалить релиз', description: 'Удаляет запись релиза и файл из хранилища.' })
  @ApiParam({ name: 'releaseId', description: 'ID релиза' })
  remove(@CurrentAdmin() admin: AdminRequestUser, @Param('releaseId') releaseId: string) {
    assertAdminPermission(admin, 'canManageSettings');
    return firstValueFrom(this.core.appRelease.DeleteAppRelease({ releaseId }));
  }
}
