import { timingSafeEqual } from 'crypto';
import * as express from 'express';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { ExpressAdapter } from '@bull-board/express';
import { getQueueToken } from '@nestjs/bullmq';
import { ValidationPipe, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { Queue } from 'bullmq';
import { WinstonModule } from 'nest-winston';
import { AppModule } from './app.module';
import { RUBRO_HEADER } from './utils/rubro-context';
import { BigIntSerializerInterceptor } from './common/interceptors/bigint-serializer.interceptor';
import { winstonConfig } from './common/logger/winston.config';
import { envs } from './config';

async function bootstrap() {
  const logger = WinstonModule.createLogger(winstonConfig);
  const app = await NestFactory.create(AppModule, {
    logger,
    bodyParser: false,
  });
  // Body parsers con límite extendido (perfiles con muchos permisos superan el default 100kb).
  // El callback `verify` deja el body crudo accesible como req.rawBody — necesario para verificar
  // la firma HMAC de webhooks entrantes (Novasis Pay) sin reserializar el JSON.
  app.use(
    express.json({
      limit: '5mb',
      verify: (req, _res, buf) => {
        (req as unknown as { rawBody?: string }).rawBody = buf.toString('utf8');
      },
    }),
  );
  app.use(express.urlencoded({ extended: true, limit: '5mb' }));

  // Configurar CORS
  app.enableCors({
    origin: true, // Permite cualquier origen en desarrollo
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
    credentials: true,
    // x-rubro-id: rubro elegido en el selector del sidebar (utils/rubro-context.ts).
    allowedHeaders: ['Content-Type', 'Authorization', 'x-app-key', RUBRO_HEADER],
  });

  // Configurar Bull Board UI (panel de colas). El panel expone la data de los jobs
  // (destinatarios, payloads SIFEN y —si se reactiva la cola de email— códigos OTP en
  // texto plano de los jobs fallidos), así que NUNCA debe quedar público. Montaje:
  //   - Con credenciales (BULL_BOARD_USER/PASSWORD) → detrás de basic-auth (timingSafeEqual).
  //   - En producción SIN credenciales → NO se monta (evita exposición por olvido de config).
  //   - En desarrollo sin credenciales → montado abierto (conveniencia local).
  const bullBoardHasCreds = Boolean(envs.bullBoardUser && envs.bullBoardPassword);
  const bullBoardEnabled = bullBoardHasCreds || process.env.NODE_ENV !== 'production';

  if (bullBoardEnabled) {
    const serverAdapter = new ExpressAdapter();
    serverAdapter.setBasePath('/queues');

    const emailQueue = app.get<Queue>(getQueueToken('email-queue'));
    const sifenQueue = app.get<Queue>(getQueueToken('sifen-queue'));
    const imagenesIaQueue = app.get<Queue>(getQueueToken('imagenes-ia-queue'));

    createBullBoard({
      queues: [new BullMQAdapter(emailQueue), new BullMQAdapter(sifenQueue), new BullMQAdapter(imagenesIaQueue)],
      serverAdapter,
    });

    const expressApp = app.getHttpAdapter().getInstance();

    // Basic-auth para /queues cuando hay credenciales configuradas.
    if (envs.bullBoardUser && envs.bullBoardPassword) {
      const expectedUser = Buffer.from(envs.bullBoardUser, 'utf8');
      const expectedPass = Buffer.from(envs.bullBoardPassword, 'utf8');
      expressApp.use('/queues', (req: express.Request, res: express.Response, next: express.NextFunction) => {
        const header = req.headers.authorization ?? '';
        const [scheme, encoded] = header.split(' ');
        if (scheme === 'Basic' && encoded) {
          const decoded = Buffer.from(encoded, 'base64').toString('utf8');
          const idx = decoded.indexOf(':');
          const user = Buffer.from(decoded.slice(0, idx < 0 ? 0 : idx), 'utf8');
          const pass = Buffer.from(idx < 0 ? '' : decoded.slice(idx + 1), 'utf8');
          const okUser = user.length === expectedUser.length && timingSafeEqual(user, expectedUser);
          const okPass = pass.length === expectedPass.length && timingSafeEqual(pass, expectedPass);
          if (okUser && okPass) return next();
        }
        res.set('WWW-Authenticate', 'Basic realm="queues"');
        return res.status(401).send('Authentication required');
      });
    }

    // Montar Bull Board en /queues (sin prefijo api)
    expressApp.use('/queues', serverAdapter.getRouter());
    logger.log(`📊 Bull Board montado en /queues${bullBoardHasCreds ? ' (protegido con basic-auth)' : ' (ABIERTO — solo dev)'}`);
  } else {
    logger.warn(
      'Bull Board /queues deshabilitado en producción. Configurá BULL_BOARD_USER y BULL_BOARD_PASSWORD para habilitarlo detrás de basic-auth.',
    );
  }

  // Configurar prefijo global
  app.setGlobalPrefix('api');

  // Configurar versionado de API
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1',
  });
  app.useGlobalInterceptors(new BigIntSerializerInterceptor());
  // Configurar validación global
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  if (process.env.NODE_ENV !== 'production' && process.env.DISABLE_SWAGGER !== 'true') {
    try {
      const config = new DocumentBuilder()
        .setTitle('Novasis ERP API')
        .setDescription('Documentación de la API')
        .setVersion('1.0')
        .addApiKey(
          { type: 'apiKey', name: 'x-app-key', in: 'header', description: 'Clave de app' },
          'AppKey',
        )
        .addBearerAuth({ type: 'http', scheme: 'bearer', bearerFormat: 'JWT', in: 'header' }, 'JWT')
        .build();
      const document = SwaggerModule.createDocument(app, config);
      SwaggerModule.setup('docs', app, document, { swaggerOptions: { persistAuthorization: true } });
    } catch (e) {
      logger.warn(`Swagger deshabilitado: ${e.message}`);
    }
  }

  await app.listen(envs.port, '0.0.0.0');
  logger.log(`🚀 Server running on port ${envs.port}`);
}
bootstrap();
