// Express Base
import express, { Application } from 'express';
import cors, { CorsOptions } from 'cors';
import autoroutes from 'express-automatic-routes';
import compression from 'compression';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import { engine } from 'express-handlebars';
import { resolve } from 'path';
import { readFileSync } from 'fs';
import { exec } from 'child_process';
// Environments & Constansts
import { Environment } from './interfaces/IEnv';
import { root } from './root';
import constants from './constants/index';
// Middlewares & Schedulers
import responseTemplate from './middlewares/response';
import { apiQueryModifier } from './middlewares/mod/api-query-modifier';
import verify, { requireAdmin } from './middlewares/auth';
import { requestIdMiddleware } from './middlewares/request-id';
import ScheduleClass from './services/scheduler/scheduleJobService';
import RedisService from './services/storage/redisService';
import { createDistributedRateLimit } from './middlewares/rate-limiter';
import { TokenService } from './services/auth/tokenService';
import NotificationWorkerService from './services/notification/notificationWorkerService';
// OutboxPoller moved to separate worker process
// import OutboxPoller from './jobs/outboxPoller';
import PartitionManagementService from '#services/database/partition/partitionManagementService';
import { createAuditStrategy } from '#services/audit/strategies/auditStrategyFactoryService';
import { AuditLogService } from '#services/audit/auditLogService';
import { OidcService } from './oidc/oidcService';
import { MultiPoolService } from '#services/database/multiPoolService';
import oidcRoutes from './oidc/oidcRoutes';
import oidcInteractionsRouter from './oidc/oidcInteractionsController';
// Swagger
import swaggerUI from 'swagger-ui-express';
import Logger, { log } from './utils/logger';
import sequelize from '#services/database/sequelize/sequelizeService';
import Config from '#config';

// Serve Swagger to web
const serveSwagger = async (app: Application, storagePath: string) => {
	// M-08: Guard against missing swagger file — don't crash if not yet generated
	const swaggerPath = `${storagePath}/swagger/openapi.json`;
	let doc: any;
	try {
		doc = JSON.parse(readFileSync(swaggerPath, 'utf8'));
	} catch {
		Logger.warn('[swagger] openapi.json not found — skipping swagger UI');
		return;
	}

	app.use(
		constants.SWAGGER_ROUTER,
		swaggerUI.serve,
		swaggerUI.setup(doc, {
			swaggerOptions: {
				url: '/swagger/openapi.json',
			},
		}),
	);
};

const generateSwagger = async () => {
	// SEC-06: Use async exec instead of execSync to avoid blocking the event loop
	const { exec } = require('child_process');
	const { promisify } = require('util');
	const execAsync = promisify(exec);

	try {
		await execAsync('pnpm swagger:generate', { stdio: 'inherit' });
	} catch {
		Logger.warn('[swagger] Failed to generate openapi.json — continuing without it');
	}
};

// Kill any process using the specified port (development mode only)
const killProcessOnPort = async (port: number): Promise<void> => {
	// eslint-disable-next-line no-restricted-syntax
	if (process.env.NODE_ENV !== 'development') {
		return; // Only auto-kill in development
	}

	return new Promise((resolve) => {
		const command = process.platform === 'win32' ? `netstat -ano | findstr :${port}` : `lsof -ti:${port}`;
		exec(command, (error, stdout) => {
			if (!stdout) {
				resolve();
				return;
			}

			const lines = stdout.trim().split('\n');
			const pids = new Set<string>();

			if (process.platform === 'win32') {
				// Windows: netstat output format
				lines.forEach((line) => {
					const parts = line.trim().split(/\s+/);
					const pid = parts[parts.length - 1];
					if (pid && /^\d+$/.test(pid)) {
						pids.add(pid);
					}
				});
			} else {
				// Unix: lsof output format (just PIDs)
				lines.forEach((pid) => {
					if (pid && /^\d+$/.test(pid)) {
						pids.add(pid);
					}
				});
			}

			if (pids.size === 0) {
				resolve();
				return;
			}

			// Kill each process
			const killCommand = process.platform === 'win32' ? 'taskkill /F /PID' : 'kill -9';
			let killed = 0;
			pids.forEach((pid) => {
				exec(`${killCommand} ${pid}`, (_killError) => {
					killed++;
					if (killed === pids.size) {
						log('INFO', `Killed ${pids.size} process(es) on port ${port}`);
						resolve();
					}
				});
			});
		});
	});
};

const // Server functions
	initServer = async (storagePath: string, env: 'development' | 'staging' | 'production') => {
		const // Setup constant
			app: Application = express(),
			corsOptions = (function (env: 'development' | 'staging' | 'production') {
				// Allow all origins if DEV_CORS_DISABLE is set
				if (Config.cors.devDisable) {
					return {
						origin: true,
						credentials: true,
					} as CorsOptions;
				}

				if (env === 'development') {
					return {
						origin: true,
						credentials: true, // Enable cookies in CORS
					} as CorsOptions;
				}
				// Special gateway origins (hardcoded for gateway proxy)
				const gatewayOrigins = ['https://gateway.dev.meu-solutions.com', 'https://bekind-dev.meucorp.com'];
				const allowedOrigins = [
					...gatewayOrigins,
					Config.server.backendUrl,
					...(Config.server.frontendUrl?.split(',') || []),
				];
				return {
					origin(origin, callback) {
						if (!origin || allowedOrigins.includes(origin)) {
							callback(null, true);
						} else {
							callback(new Error('Not allowed by CORS'));
						}
					},
					credentials: true, // Enable cookies in CORS
				} as CorsOptions;
			})(env);
		// Basic server requirements
		app.set('trust proxy', 1);

		// ── Handlebars view engine for OIDC interactions ──────────────────
		app.engine('hbs', engine({ extname: '.hbs', defaultLayout: false }));
		app.set('view engine', 'hbs');
		app.set('views', resolve(__dirname, './oidc/views'));

		// ── Security headers ────────────────────────────────────────────────
		app.use(
			helmet({
				// Pure REST API — no scripts, no frames needed
				contentSecurityPolicy: {
					directives: {
						defaultSrc: ["'none'"],
						scriptSrc: ["'none'"],
						objectSrc: ["'none'"],
						frameAncestors: ["'none'"],
						formAction: ["'none'"],
						// Allow swagger UI inline scripts and fetch only in dev/staging
						...(env !== 'production' && {
							scriptSrc: ["'self'", "'unsafe-inline'"],
							styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
							fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
							imgSrc: ["'self'", 'data:', 'https:'],
							connectSrc: ["'self'"],
							// Allow OIDC form submissions to SSO origin and local dev apps.
							formAction: [
								"'self'",
								'http://localhost:3001',
								'http://localhost:4001',
								'http://localhost:4002',
								'http://127.0.0.1:3001',
								'http://127.0.0.1:4001',
								'http://127.0.0.1:4002',
							],
						}),
					},
				},
				crossOriginEmbedderPolicy: false, // Not needed for API
				crossOriginOpenerPolicy: { policy: 'same-origin' },
				crossOriginResourcePolicy: { policy: env === 'production' ? 'same-origin' : 'cross-origin' },
				referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
				hsts: env === 'production' ? { maxAge: 31536000, includeSubDomains: true, preload: true } : false,
				xContentTypeOptions: true,
				xFrameOptions: { action: 'deny' },
			}),
		);
		app.use(
			compression({
				filter: (req, res) => {
					const contentType = String(res.getHeader('Content-Type') || '');
					if (contentType.includes('text/event-stream')) {
						return false;
					}

					const accept = String(req.headers.accept || '');
					if (accept.includes('text/event-stream')) {
						return false;
					}

					return compression.filter(req, res);
				},
			}),
		);
		app.use(requestIdMiddleware);
		// Structured access log — IP, method, path, status, ms, requestId
		app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
			const start = Date.now();
			res.on('finish', () => {
				const ms = Date.now() - start;
				// Skip noisy paths
				if (req.path === '/health') return;
				if (/^\/swagger(?:-ui)?\//.test(req.path) || /^\/swagger\/index\//.test(req.path)) return;
				if (req.path === '/favicon.ico') return;

				if (res.statusCode >= 500) {
					const accessLog = {
						type: 'access',
						requestId: (req as any).requestId,
						method: req.method,
						path: req.path,
						status: res.statusCode,
						ms,
						ip: req.ip,
						ua: req.headers['user-agent']?.slice(0, 120),
					};
					Logger.error(JSON.stringify(accessLog));
				}
				// 2xx/3xx/4xx: silent in dev (file logger still records them)
			});
			next();
		});
		app.use(cookieParser()); // Parse cookies
		app.use(express.json({ type: 'application/json', limit: '1mb' })); // Middlewares
		app.use(express.urlencoded({ extended: true, limit: '1mb' }));
		app.use(responseTemplate as express.RequestHandler);
		app.use(apiQueryModifier() as express.RequestHandler);

		// Global fallback rate limit — 600 requests per 15 min per IP
		app.use(
			createDistributedRateLimit({
				windowMs: 15 * 60 * 1000,
				max: 600,
				keyGenerator: (req) => `global:${req.ip || 'unknown'}`,
			}),
		);

		const getRateLimitKey = (req: express.Request) => {
			const userAgent = String(req.headers['user-agent'] || 'unknown').trim();
			const email = typeof (req as any).body?.email === 'string' ? (req as any).body.email.trim().toLowerCase() : '';
			return email ? `${req.ip}:${req.path}:${email}` : `${req.ip}:${req.path}:${userAgent}`;
		};

		const rateLimitRoutes = [
			{
				path: '/api/v1/file',
				windowMs: 60 * 60 * 1000,
				max: 10,
				message: 'Quá nhiều upload, vui lòng thử lại sau.',
			},
			{
				path: '/api/v1/file/upload-multiple',
				windowMs: 60 * 60 * 1000,
				max: 5,
				message: 'Quá nhiều upload nhiều file, vui lòng thử lại sau.',
			},
		];

		rateLimitRoutes.forEach(({ path, windowMs, max }) => {
			app.post(path, createDistributedRateLimit({ windowMs, max, keyGenerator: getRateLimitKey }));
		});

		// Auto import controllers with express-automatic-routes
		app.all('/api/*', cors(corsOptions));
		autoroutes(app, { dir: resolve(__dirname, './controllers/'), log: false });

		// ── OIDC routes (must be before static files) ───────────────────
		app.use('/', oidcRoutes);
		app.use('/oidc/interaction', oidcInteractionsRouter);

		// Serve static files (images, videos, files)
		const uploadPath = Config.uploads.path;
		// Serve storage directory under /api/storage and /storage
		app.use('/api/storage/uploads', express.static(uploadPath));
		app.use('/storage/uploads', express.static(uploadPath));

		// Public folder - admin only access
		app.use('/logs/*', verify as express.RequestHandler, requireAdmin as express.RequestHandler);
		// Health check endpoint
		app.get('/health', async (req, res) => {
			const healthChecks: Record<string, boolean> = {
				database: false,
				redis: false,
				mongodb: false,
			};

			// Database check
			try {
				await sequelize.authenticate();
				healthChecks.database = true;
			} catch (dbError) {
				log('ERR', `Database health check failed: ${dbError}`);
			}

			// Redis check
			if (Config.redis.enabled) {
				try {
					const redis = RedisService.getInstance();
					const isConnected = await redis.isRedisConnected();
					healthChecks.redis = isConnected;
				} catch (redisError) {
					log('ERR', `Redis health check failed: ${redisError}`);
				}
			} else {
				healthChecks.redis = true; // Skip Redis check if disabled
			}

			// MongoDB check (audit logs)
			try {
				const { MongoClientService } = await import('./database/mongo/index.js');
				await MongoClientService.getDb();
				healthChecks.mongodb = true;
			} catch {
				// MongoDB health check failure is non-critical
				healthChecks.mongodb = true; // Don't block health for MongoDB
			}

			const allHealthy = Object.values(healthChecks).every(Boolean);
			const overallStatus = allHealthy ? 'healthy' : 'degraded';

			res.status(overallStatus === 'healthy' ? 200 : 503).json({
				status: overallStatus,
				timestamp: new Date().toISOString(),
				uptime: process.uptime(),
				memory: process.memoryUsage(),
				version: Config.server.projectVersion,
				environment: env,
				checks: healthChecks,
			});
		});

		// Serve openapi.json — development & staging only
		if (env !== 'production') {
			app.get('/openapi.json', (req, res) => {
				const filePath = resolve(storagePath, 'swagger/openapi.json');
				res.sendFile(filePath, (err) => {
					if (err) {
						res.status(404).json({ error: 'OpenAPI file not found' });
					}
				});
			});
			app.get('/swagger/openapi.json', (req, res) => {
				const filePath = resolve(storagePath, 'swagger/openapi.json');
				res.sendFile(filePath, (err) => {
					if (err) {
						res.status(404).json({ error: 'OpenAPI file not found' });
					}
				});
			});
		}

		// Initialize audit strategy at startup (BEFORE app.listen)
		const auditStrategy = createAuditStrategy(Config.audit.mode);
		const auditLogService = new AuditLogService(auditStrategy);

		// Inject audit service into request context
		app.use((req: any, res: any, next: any) => {
			req.auditLogService = auditLogService;
			next();
		});

		log('INFO', `Audit mode: ${Config.audit.mode}`);

		return app;
	},
	startServer = async (env: Environment) => {
		// Dev secrets are auto-generated in src/index.ts BEFORE any module import
		// Production: fail fast if secrets are missing
		if (env !== 'development') {
			TokenService.validateSecrets();
		} else {
			TokenService.validateSecrets();
		}

		const // Path
			port: number = Config.server.port,
			serverHost: string = Config.server.backendUrl,
			storagePath: string = resolve(root, 'storage');
		// Generate swagger output
		switch (env.toLowerCase()) {
			case 'development':
				return await startDevServer(storagePath, serverHost, port);
			default:
				return await startProductionServer(
					storagePath,
					serverHost,
					port,
					env.toLowerCase() as 'staging' | 'production',
				);
		}
	},
	startDevServer = async (storagePath: string, serverHost: string, port: number) => {
		const _time = new Date().toLocaleTimeString('en-US', { hour12: false });
		log('INFO', `${Config.server.projectName} v${Config.server.projectVersion}`);
		log('INFO', 'SSO Backend — OIDC/OAuth2 Authorization Server');
		const app = await initServer(storagePath, 'development');

		// Initialize OIDC provider
		try {
			await OidcService.initialize();
			log('OK', 'OIDC provider initialized');
		} catch (oidcErr) {
			log('WARN', `OIDC provider failed: ${oidcErr}`);
		}

		// Auto-load project DB connections from project_db_connections table.
		// Must happen after OIDC init so any /admin/db-connections activity in
		// tests can be exercised, but before the server starts listening.
		try {
			await MultiPoolService.autoLoadPools();
		} catch (poolErr) {
			log('WARN', `autoLoadPools failed: ${poolErr}`);
		}

		await generateSwagger();
		serveSwagger(app, storagePath);
		log('OK', 'OpenAPI ready');
		log('INFO', `Swagger: ${serverHost}/swagger/index`);

		const redis = RedisService.getInstance();
		if (Config.redis.enabled) {
			try {
				await redis.connect();
				log('OK', 'Redis connected');
			} catch {
				log('WARN', 'Redis fallback (development mode)');
			}
		} else {
			// Redis disabled
		}

		const scheduler = new ScheduleClass();
		scheduler.init();
		log('OK', 'Jobs initialized');

		const partitionManager = PartitionManagementService.getInstance();
		try {
			await partitionManager.ensurePartitions(3);
			log('OK', 'Partitions ready');
		} catch (error) {
			log('ERR', String(error));
		}

		// OutboxPoller moved to separate worker process (audit-worker.ts)
		// Run with: pnpm worker:audit
		/*
		try {
			OutboxPoller.start();
			log('OK', 'OutboxPoller started');
		} catch (error) {
			log('ERR', `OutboxPoller failed to start: ${error}`);
		}
		*/

		// TEMP: Disabled worker startup to debug hang
		// log('INFO', 'Worker disabled (temporarily for debugging)');
		/*
		const notificationWorker = NotificationWorkerService.getInstance();
		try {
			await notificationWorker.start();
			if (notificationWorker.isWorkerAvailable()) {
				log('OK', 'Worker started');
			} else {
				log('INFO', 'Worker disabled');
			}
		} catch (error) {
			log('ERR', String(error));
		}
		*/

		// Kill any existing process on the port before starting (development only)
		await killProcessOnPort(port);

		const devServer = app.listen(port, '0.0.0.0', () => {
			log('OK', `Listening on port ${port} (PID: ${process.pid})`);
		});
		devServer.on('error', (err: any) => {
			if (err.code === 'EADDRINUSE') {
				log(
					'ERR',
					`Port ${port} is already in use. Please kill the process using this port or change the PORT in .env`,
				);
				process.exit(1);
			}
			log('ERR', String(err));
		});
		devServer.headersTimeout = 30_000;
		devServer.requestTimeout = 120_000;
		devServer.timeout = 120_000;

		// Graceful shutdown handler for nodemon
		const gracefulShutdown = async (signal: string) => {
			log('INFO', `${signal} received, shutting down gracefully...`);
			// Only close server if it's actually listening
			if (devServer.listening) {
				devServer.close(async (err) => {
					if (err) {
						log('ERR', `Error closing server: ${err}`);
					} else {
						log('OK', 'Server closed');
					}
					await cleanupConnections();
				});
			} else {
				// Server not listening, just cleanup connections
				await cleanupConnections();
			}
		};

		const cleanupConnections = async () => {
			// Close OIDC provider
			try {
				await OidcService.close();
				log('OK', 'OIDC provider closed');
			} catch (oidcErr) {
				log('ERR', `Error closing OIDC provider: ${oidcErr}`);
			}
			// Close MongoDB
			try {
				const { MongoClientService } = await import('./database/mongo/index.js');
				await MongoClientService.close();
				log('OK', 'MongoDB disconnected');
			} catch (mongoErr) {
				log('ERR', `Error disconnecting MongoDB: ${mongoErr}`);
			}
			// Close MultiPool pools (project DB connections)
			try {
				await MultiPoolService.closeAll();
				log('OK', 'Project pools closed');
			} catch (poolErr) {
				log('ERR', `Error closing project pools: ${poolErr}`);
			}
			// Stop OutboxPoller (moved to separate worker process)
			/*
			try {
				OutboxPoller.stop();
				log('OK', 'OutboxPoller stopped');
			} catch (pollerErr) {
				log('ERR', `Error stopping OutboxPoller: ${pollerErr}`);
			}
			*/
			// Close Redis connection if open
			if (Config.redis.enabled) {
				try {
					await redis.disconnect();
					log('OK', 'Redis disconnected');
				} catch (redisErr) {
					log('ERR', `Error disconnecting Redis: ${redisErr}`);
				}
			}
			// Close database connection
			try {
				await sequelize.close();
				log('OK', 'Database connection closed');
			} catch (dbErr) {
				log('ERR', `Error closing database: ${dbErr}`);
			}
			process.exit(0);
		};

		process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
		process.on('SIGINT', () => gracefulShutdown('SIGINT'));

		return;
	},
	startProductionServer = async (
		storagePath: string,
		serverHost: string,
		port: number,
		env: 'staging' | 'production',
	) => {
		// Disable cluster for Docker production
		const app = await initServer(storagePath, env);

		// Initialize OIDC provider
		try {
			await OidcService.initialize();
			log('OK', 'OIDC provider initialized');
		} catch (oidcErr) {
			log('ERR', `OIDC provider failed: ${oidcErr}`);
		}

		// Auto-load project DB connections from project_db_connections table.
		try {
			await MultiPoolService.autoLoadPools();
		} catch (poolErr) {
			log('WARN', `autoLoadPools failed: ${poolErr}`);
		}

		// TEMP: Disabled swagger generation to debug startup hang
		if (env === 'staging') await generateSwagger();

		// Initialize Redis connection
		const redis = RedisService.getInstance();
		if (Config.redis.enabled) {
			try {
				await redis.connect();
				log('OK', 'Redis connected');
			} catch {
				log('WARN', 'Redis connection failed, using in-memory fallback');
			}
		} else {
			log('INFO', 'Redis disabled by configuration');
		}

		log('INFO', 'Initializing scheduled jobs');
		const scheduler = new ScheduleClass();
		scheduler.init();
		log('OK', 'Scheduled jobs initialized');

		log('INFO', 'Ensuring database partitions');
		const partitionManager = PartitionManagementService.getInstance();
		try {
			await partitionManager.ensurePartitions(3); // Create partitions for current + 3 months
			log('OK', 'Database partitions ensured');
		} catch (error) {
			log('ERR', `Failed to ensure database partitions: ${error}`);
		}

		log('INFO', 'Initializing notification worker');
		// Initialize notification worker (Phase 1)
		const notificationWorker = NotificationWorkerService.getInstance();
		try {
			await notificationWorker.start();
			if (notificationWorker.isWorkerAvailable()) {
				log('OK', 'Notification worker started');
			} else {
				log('WARN', 'Notification worker disabled (Redis unavailable)');
			}
		} catch (error) {
			log('ERR', `Notification worker failed to start: ${error}`);
		}

		// OutboxPoller moved to separate worker process (audit-worker.ts)
		// Run with: pnpm worker:audit
		/*
		log('INFO', 'Initializing audit outbox poller');
		try {
			OutboxPoller.start();
			log('OK', 'OutboxPoller started');
		} catch (error) {
			log('ERR', `OutboxPoller failed to start: ${error}`);
		}
		*/

		// Serve Swagger UI (must be before app.listen)
		if (env == 'staging') serveSwagger(app, storagePath);

		const prodServer = app.listen(port, '0.0.0.0', () => {
			log('OK', 'Server started');
		});
		// Slow loris / idle timeout protection
		prodServer.headersTimeout = 30_000;
		prodServer.requestTimeout = 120_000;
		prodServer.timeout = 120_000;
		log('INFO', `Serving static files from ${storagePath}`);
		log('INFO', `App will be served at ${serverHost}\n`);

		// Graceful shutdown handler
		const gracefulShutdown = async (signal: string) => {
			log('INFO', `${signal} received, shutting down gracefully...`);
			// Only close server if it's actually listening
			if (prodServer.listening) {
				prodServer.close(async (err) => {
					if (err) {
						log('ERR', `Error closing server: ${err}`);
					} else {
						log('OK', 'Server closed');
					}
					await cleanupConnections();
				});
			} else {
				// Server not listening, just cleanup connections
				await cleanupConnections();
			}
		};

		const cleanupConnections = async () => {
			// Close OIDC provider
			try {
				await OidcService.close();
				log('OK', 'OIDC provider closed');
			} catch (oidcErr) {
				log('ERR', `Error closing OIDC provider: ${oidcErr}`);
			}
			// Close MongoDB
			try {
				const { MongoClientService } = await import('./database/mongo/index.js');
				await MongoClientService.close();
				log('OK', 'MongoDB disconnected');
			} catch (mongoErr) {
				log('ERR', `Error disconnecting MongoDB: ${mongoErr}`);
			}
			// Close MultiPool pools (project DB connections)
			try {
				await MultiPoolService.closeAll();
				log('OK', 'Project pools closed');
			} catch (poolErr) {
				log('ERR', `Error closing project pools: ${poolErr}`);
			}
			// Stop OutboxPoller (moved to separate worker process)
			/*
			try {
				OutboxPoller.stop();
				log('OK', 'OutboxPoller stopped');
			} catch (pollerErr) {
				log('ERR', `Error stopping OutboxPoller: ${pollerErr}`);
			}
			*/
			// Close Redis connection if open
			if (Config.redis.enabled) {
				try {
					await redis.disconnect();
					log('OK', 'Redis disconnected');
				} catch (redisErr) {
					log('ERR', `Error disconnecting Redis: ${redisErr}`);
				}
			}
			// Close database connection
			try {
				await sequelize.close();
				log('OK', 'Database connection closed');
			} catch (dbErr) {
				log('ERR', `Error closing database: ${dbErr}`);
			}
			process.exit(0);
		};

		process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
		process.on('SIGINT', () => gracefulShutdown('SIGINT'));

		return;
	};

export { startServer };
