import { 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 { randomUUID } from 'crypto';
import { NextFunction, Request, Response } from 'express';
import { existsSync, mkdirSync } from 'fs';
import { NetworkInterfaceInfo, networkInterfaces } from 'os';
import { extname, join } from 'path';
import { AppModule } from './app.module';
import { ResponseEnvelopeInterceptor } from './common/interceptors/response-envelope.interceptor';
import { AppLoggerService } from './infrastructure/logging/app-logger.service';
import { RedisService } from './infrastructure/redis/redis.service';
import { RedisIoAdapter } from './infrastructure/socket/redis-io.adapter';

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

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

const getStaticMediaHeaders = (
  extension: string,
): {
  contentType?: string;
  cacheControl?: string;
  acceptRanges?: boolean;
} => {
  switch (extension) {
    case '.jpg':
    case '.jpeg':
      return {
        contentType: 'image/jpeg',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      };
    case '.png':
      return {
        contentType: 'image/png',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      };
    case '.webp':
      return {
        contentType: 'image/webp',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      };
    case '.gif':
      return {
        contentType: 'image/gif',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
      };
    case '.mp3':
      return {
        contentType: 'audio/mpeg',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.wav':
      return {
        contentType: 'audio/wav',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.m4a':
      return {
        contentType: 'audio/mp4',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.aac':
      return {
        contentType: 'audio/aac',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.ogg':
      return {
        contentType: 'audio/ogg',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    case '.mp4':
      return {
        contentType: 'video/mp4',
        cacheControl: IMMUTABLE_MEDIA_CACHE_CONTROL,
        acceptRanges: true,
      };
    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 {};
  }
};

async function bootstrap(): Promise<void> {
  const app = await NestFactory.create(AppModule, { bufferLogs: true });
  const configService = app.get(ConfigService);
  const appLogger = app.get(AppLoggerService);
  app.useLogger(appLogger);
  const corsOrigins = configService.get<string[]>('cors.origins', []);
  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 (storageProvider === 'local' && !existsSync(uploadsDir)) {
    mkdirSync(uploadsDir, { recursive: true });
  }

  app.enableCors({
    origin: corsOrigins.length ? corsOrigins : true,
    credentials: true,
    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 requestId = (req.headers['x-request-id'] as string | undefined) ?? 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 = extname(filePath).toLowerCase();
          const mediaHeaders = getStaticMediaHeaders(extension);

          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 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);
  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',
  );
}

void bootstrap();
