import { ShutdownSignal, ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ConfigService } from '@nestjs/config';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import * as express from 'express';
import compression = require('compression');
import { randomUUID } from 'crypto';
import { NextFunction, Request, Response } from 'express';
import { existsSync, mkdirSync } from 'fs';
import { NetworkInterfaceInfo, networkInterfaces } from 'os';
import { join } from 'path';
import { AppModule } from './app.module';
import { ResponseEnvelopeInterceptor } from './common/interceptors/response-envelope.interceptor';
import { getFileExtension, getStaticMediaContentType } from './common/media/allowed-media';
import { AppLoggerService } from './infrastructure/logging/app-logger.service';
import { RedisService } from './infrastructure/redis/redis.service';
import {
  ConfigurableHttpServer,
  configureHttpServer,
} from './infrastructure/reliability/http-server.config';
import { RedisIoAdapter } from './infrastructure/socket/redis-io.adapter';

export const getLocalIpv4Addresses = (): string[] =>
  Object.values(networkInterfaces())
    .flatMap((entries) => entries ?? [])
    .filter((entry): entry is NetworkInterfaceInfo => !!entry)
    .filter((entry) => entry.family === 'IPv4' && !entry.internal)
    .map((entry) => entry.address);

export const isPrivateIpv4Host = (host: string): boolean =>
  /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[0-1])\./.test(host);

const IMMUTABLE_MEDIA_CACHE_CONTROL = 'public, max-age=31536000, immutable';
const SHORT_MANIFEST_CACHE_CONTROL = 'public, max-age=300, stale-while-revalidate=60';

export const getStaticMediaHeaders = (
  extension: string,
  filePath = '',
): {
  contentType?: string;
  cacheControl?: string;
  acceptRanges?: boolean;
} => {
  const contentType = getStaticMediaContentType(extension, filePath);
  if (contentType) {
    return {
      contentType,
      cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      acceptRanges: contentType.startsWith('audio/') || contentType.startsWith('video/'),
    };
  }

  switch (extension) {
    case '.gif':
      return {
        contentType: 'image/gif',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      };
    case '.m3u8':
      return {
        contentType: 'application/vnd.apple.mpegurl',
        cacheControl: SHORT_MANIFEST_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.m4s':
      return {
        contentType: 'video/iso.segment',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.ts':
      return {
        contentType: 'video/mp2t',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    default:
      return {};
  }
};

export async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule, { bufferLogs: true, bodyParser: false });
  const configService = app.get(ConfigService);
  const appLogger = app.get(AppLoggerService);
  app.useLogger(appLogger);
  app.enableShutdownHooks([ShutdownSignal.SIGINT, ShutdownSignal.SIGTERM]);
  const corsOrigins = configService.get<string[]>('cors.origins', []);
  const nodeEnv = configService.get<string>('nodeEnv', 'development');
  const bodyLimit = configService.get<string>('security.bodyLimit', { infer: true }) ?? '1mb';
  const compressionEnabled =
    configService.get<boolean>('performance.compressionEnabled', { infer: true }) ?? true;
  const compressionThresholdBytes =
    configService.get<number>('performance.compressionThresholdBytes', { infer: true }) ?? 1024;
  const storageProvider = configService.get<string>('storage.provider', { infer: true }) ?? 'local';
  const storageBasePath =
    (configService.get<string>('storage.basePath', { infer: true }) ?? 'uploads').replace(
      /^\/+|\/+$/g,
      '',
    );
  const publicBaseUrl =
    (configService.get<string>('publicBaseUrl', { infer: true }) ?? '').replace(/\/$/, '');
  const emailEnabled = configService.get<boolean>('email.enabled', { infer: true }) ?? false;
  const emailSmtpHost = configService.get<string>('email.smtpHost', { infer: true }) ?? '';
  const emailSmtpUser = configService.get<string>('email.smtpUser', { infer: true }) ?? '';
  const emailFromEmail = configService.get<string>('email.fromEmail', { infer: true }) ?? '';
  const uploadsDir = join(process.cwd(), storageBasePath);

  if (compressionEnabled) {
    app.use(compression({ threshold: compressionThresholdBytes }));
  }
  app.use(express.json({ limit: bodyLimit }));
  app.use(express.urlencoded({ extended: true, limit: bodyLimit }));

  app.use((_req: Request, res: Response, next: NextFunction) => {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    res.setHeader('X-Frame-Options', 'DENY');
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
    res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
    res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
    if (nodeEnv === 'production') {
      res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
    }
    next();
  });

  if (storageProvider === 'local' && !existsSync(uploadsDir)) {
    mkdirSync(uploadsDir, { recursive: true });
  }

  app.enableCors({
    origin: corsOrigins.length ? corsOrigins : nodeEnv === 'production' ? false : true,
    credentials: corsOrigins.length > 0 || nodeEnv !== 'production',
    exposedHeaders: [
      'Accept-Ranges',
      'Content-Length',
      'Content-Range',
      'Content-Type',
      'Cache-Control',
      'ETag',
      'Last-Modified',
    ],
  });

  app.setGlobalPrefix(configService.get<string>('globalPrefix', 'api/v1'));

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  );

  app.use((req: Request, res: Response, next: NextFunction) => {
    const startedAt = Date.now();
    const suppliedRequestId = req.headers['x-request-id'];
    const requestId =
      typeof suppliedRequestId === 'string' && /^[A-Za-z0-9._:-]{1,128}$/.test(suppliedRequestId)
        ? suppliedRequestId
        : randomUUID();
    req.headers['x-request-id'] = requestId;
    res.setHeader('x-request-id', requestId);

    res.on('finish', () => {
      appLogger.logHttp({
        requestId,
        method: req.method,
        path: req.originalUrl,
        statusCode: res.statusCode,
        durationMs: Date.now() - startedAt,
      });
    });

    next();
  });

  const responseEnvelopeEnabled = configService.get<boolean>('responseEnvelopeEnabled', false);
  if (responseEnvelopeEnabled) {
    app.useGlobalInterceptors(new ResponseEnvelopeInterceptor());
  }

  if (storageProvider === 'local') {
    app.use(
      `/${storageBasePath}`,
      express.static(uploadsDir, {
      acceptRanges: true,
      setHeaders: (res, filePath) => {
        const extension = getFileExtension(filePath);
        const mediaHeaders = getStaticMediaHeaders(extension, filePath);

        if (mediaHeaders.contentType) {
          res.setHeader('Content-Type', mediaHeaders.contentType);
        }
        if (mediaHeaders.cacheControl) {
          res.setHeader('Cache-Control', mediaHeaders.cacheControl);
        }
        if (mediaHeaders.acceptRanges) {
          res.setHeader('Accept-Ranges', 'bytes');
        }
        res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
        res.setHeader('X-Content-Type-Options', 'nosniff');
      },
      }),
    );
  }

  const redisEnabled = configService.get<boolean>('redis.enabled', { infer: true }) ?? false;
  const socketAdapterEnabled =
    configService.get<boolean>('redis.socketAdapterEnabled', { infer: true }) ?? false;
  if (redisEnabled && socketAdapterEnabled) {
    const redisIoAdapter = new RedisIoAdapter(app, app.get(RedisService));
    await redisIoAdapter.connectToRedis();
    app.useWebSocketAdapter(redisIoAdapter);
  }

  const swaggerConfig = new DocumentBuilder()
    .setTitle(configService.get<string>('swagger.title', 'Oudelaa API'))
    .setDescription(
      configService.get<string>('swagger.description', 'Social media backend API documentation'),
    )
    .setVersion(configService.get<string>('swagger.version', '1.0.0'))
    .addBearerAuth()
    .build();

  const swaggerEnabled = configService.get<boolean>('swagger.enabled', { infer: true }) ?? false;
  if (swaggerEnabled) {
    const document = SwaggerModule.createDocument(app, swaggerConfig);
    SwaggerModule.setup(configService.get<string>('swagger.path', 'docs'), app, document);
  }

  const port = configService.get<number>('port', 4000);
  const host = configService.get<string>('host', '0.0.0.0');
  if (host === '0.0.0.0') {
    const localIpv4Addresses = getLocalIpv4Addresses();
    let publicBaseHost = '';

    try {
      publicBaseHost = new URL(publicBaseUrl).hostname;
    } catch {
      publicBaseHost = '';
    }

    if (publicBaseHost === 'localhost') {
      appLogger.warn(
        `PUBLIC_BASE_URL is set to "${publicBaseUrl}". Mobile devices on the LAN will not be able to open uploaded files until this is changed to your machine IP, for example http://192.168.x.x:${port}`,
        'Bootstrap',
      );
    } else if (
      publicBaseHost &&
      isPrivateIpv4Host(publicBaseHost) &&
      !localIpv4Addresses.includes(publicBaseHost)
    ) {
      appLogger.warn(
        `PUBLIC_BASE_URL is set to "${publicBaseUrl}" but this IP is not assigned to the current machine. Detected local IPv4 addresses: ${localIpv4Addresses.join(', ') || 'none'}. Uploaded files and avatars may not load until PUBLIC_BASE_URL is updated.`,
        'Bootstrap',
      );
    }
  }

  await app.listen(port, host);
  configureHttpServer(app.getHttpServer() as ConfigurableHttpServer, configService);
  appLogger.log(`Server listening on http://${host}:${port}`, 'Bootstrap');
  appLogger.log(`Resolved PUBLIC_BASE_URL=${publicBaseUrl || `http://localhost:${port}`}`, 'Bootstrap');
  appLogger.log(
    `Email config: enabled=${emailEnabled}, smtpHost=${emailSmtpHost || 'unset'}, smtpUserConfigured=${emailSmtpUser ? 'yes' : 'no'}, fromEmailConfigured=${emailFromEmail ? 'yes' : 'no'}`,
    'Bootstrap',
  );
}

if (require.main === module) {
  void bootstrap();
}
