import env from "./utils/env.js";
import { initializeLogger, getLogger } from "./utils/logger.js";
import express from "express";
import bodyParser from "body-parser";
import { createServer } from "http";
import morgan from "morgan";
import path from "path";
import cookieParser from "cookie-parser";
import { errorHandler } from "./utils/errorHandler.js";
import { initializeCache, closeCache, getCache, isCacheInitialized } from "./utils/cache.js";
import { invalidateEntireCache, getCacheService } from "./services/CacheService.js";
import { loadRoutes, loadSystemRoutes } from "./utils/router.js";
import mailService from "./services/MailService.js";
import permissionService from "./services/PermissionService.js";
import storageService from "./services/StorageService.js";
import { authMiddleware } from "./utils/auth.js";
import { validateSecurityPostureAtStartup } from "./utils/securityPosture.js";
import { requestContextMiddleware } from "./utils/requestContext.js";
import { hooksManager } from "./services/HooksManager.js";
import { db, initializeDatabaseWithCache } from "./utils/db.js";
import { schemaManager } from "./utils/schemaManager.js";
import { startSessionCleanup } from "./utils/sessionCleanup.js";
import { startLogCleanup } from "./utils/logCleanup.js";
import schedule from "node-schedule";
import { rateLimit } from "express-rate-limit";
import socketService from "./services/SocketService.js";
import realtimeService from "./services/RealtimeService.js";
import tasksService from "./services/TasksService.js";
import workflowService from "./services/WorkflowService.js";
import migrationService from "./services/MigrationService.js";
import { registerPageBuilderHooks } from "./services/BlockConfigService.js";
import cors from "cors";
import settingsService from "./services/SettingsService.js";
import { sql } from "drizzle-orm";
import { getBaasixPath } from "./utils/dirname.js";
import { PluginManager } from "./services/PluginManager.js";
// Class-based services for plugin context (instantiated per-request)
import FilesService from "./services/FilesService.js";
import AssetsService from "./services/AssetsService.js";
import NotificationService from "./services/NotificationService.js";
import ReportService from "./services/ReportService.js";
import StatsService from "./services/StatsService.js";
// Get the admin app directory path (inside the package)
const getAdminAppPath = () => getBaasixPath("app");
export const app = express();
// Trust proxy - important for rate limiting and getting real client IP behind reverse proxy
app.set("trust proxy", 1);
// CORS configuration
const getStaticAllowedOrigins = () => {
    return env.get("AUTH_CORS_ALLOWED_ORIGINS")
        ? env
            .get("AUTH_CORS_ALLOWED_ORIGINS")
            .split(",")
            .map((origin) => origin.trim())
        : ["http://localhost:3000", "http://localhost:8056"];
};
let dynamicOriginsCache = [];
let lastDynamicOriginsUpdate = 0;
const DYNAMIC_ORIGINS_CACHE_TTL = 300000; // 5 minutes
export const invalidateCorsCache = () => {
    dynamicOriginsCache = [];
    lastDynamicOriginsUpdate = 0;
    console.info("CORS origins cache invalidated");
};
const getAllowedOrigins = async () => {
    const staticOrigins = getStaticAllowedOrigins();
    const now = Date.now();
    if (now - lastDynamicOriginsUpdate > DYNAMIC_ORIGINS_CACHE_TTL) {
        try {
            const settingsUrls = await settingsService.getAllSettingsUrls();
            dynamicOriginsCache = settingsUrls;
            lastDynamicOriginsUpdate = now;
            console.info(`Updated CORS origins cache with ${settingsUrls.length} dynamic URLs`);
        }
        catch (error) {
            console.error("Error fetching settings URLs for CORS:", error);
        }
    }
    const allOrigins = [...new Set([...staticOrigins, ...dynamicOriginsCache])];
    return allOrigins;
};
const allowAnyPort = env.get("AUTH_CORS_ALLOW_ANY_PORT") !== "false";
const isOriginAllowed = async (origin) => {
    const allowedOrigins = await getAllowedOrigins();
    if (!allowAnyPort) {
        return allowedOrigins.includes(origin);
    }
    try {
        const originUrl = new URL(origin);
        const originHostname = originUrl.hostname;
        const originProtocol = originUrl.protocol;
        return allowedOrigins.some((allowedOrigin) => {
            try {
                const allowedUrl = new URL(allowedOrigin);
                return allowedUrl.hostname === originHostname && allowedUrl.protocol === originProtocol;
            }
            catch {
                return false;
            }
        });
    }
    catch {
        return false;
    }
};
app.use(cors({
    origin: function (origin, callback) {
        if (!origin)
            return callback(null, true);
        isOriginAllowed(origin)
            .then((allowed) => {
            if (allowed) {
                return callback(null, true);
            }
            else {
                console.log(`CORS: Origin "${origin}" not explicitly allowed but permitted for development`);
                return callback(null, true);
            }
        })
            .catch((error) => {
            console.error("Error checking CORS origin:", error);
            return callback(null, true);
        });
    },
    credentials: env.get("AUTH_CORS_CREDENTIALS") !== "false",
    methods: ["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With"],
    exposedHeaders: ["Date"],
}));
app.use(bodyParser.json({ limit: env.get("BODY_SIZE_LIMIT") || "20mb" }));
app.use(cookieParser());
app.use(morgan("combined"));
// Helper to extract userId from JWT token for rate limiting
const extractUserIdFromToken = (req) => {
    try {
        // Check Authorization header first, then cookie
        const authHeader = req.headers.authorization;
        let token = null;
        if (authHeader?.startsWith("Bearer ")) {
            token = authHeader.substring(7);
        }
        else if (req.cookies?.token) {
            token = req.cookies.token;
        }
        if (!token)
            return null;
        // Decode JWT payload without verification (rate limiting doesn't need full verification)
        const parts = token.split(".");
        if (parts.length !== 3)
            return null;
        const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
        return payload.sub || payload.userId || payload.user_id || null;
    }
    catch {
        return null;
    }
};
const limiter = rateLimit({
    windowMs: parseInt(env.get("RATE_LIMIT_INTERVAL") || "5000"),
    max: parseInt(env.get("RATE_LIMIT") || "100"),
    keyGenerator: (req) => {
        // Use userId for authenticated requests if enabled, otherwise use IP
        if (env.get("RATE_LIMIT_BY_USER") === "true") {
            const userId = extractUserIdFromToken(req);
            if (userId)
                return `user:${userId}`;
        }
        return req.ip || "unknown";
    },
    skip: (req, res) => {
        return req.path.startsWith("/admin") || req.path === "/admin";
    },
});
app.use(limiter);
// Serve static files from the React app
const adminAppPath = getAdminAppPath();
app.use("/admin", express.static(adminAppPath));
app.get("/admin/*", (req, res) => {
    res.sendFile(path.join(adminAppPath, "index.html"));
});
let server = null;
/**
 * Initialize the application with all services
 */
async function initializeApp() {
    try {
        // Single startup security entry point:
        //  1. Hard-fail if SECRET_KEY is missing (nothing can authenticate safely).
        //  2. Print one grouped advisory banner for every security/integrity setting in
        //     a less-safe state (short SECRET_KEY, SCHEMAS_PUBLIC, SSRF/OAuth/rate-limit
        //     overrides, etc.) — helps a new operator who inherited an .env.
        // Advisory banner is skipped under TEST_MODE; the fatal check always runs.
        validateSecurityPostureAtStartup();
        console.info("Initializing system cache...");
        // System cache (permissions, roles, settings) uses SYSTEM_CACHE_* env vars
        const systemCacheAdapter = env.get("SYSTEM_CACHE_ADAPTER") || "memory";
        const systemCacheUri = (systemCacheAdapter === "redis" || systemCacheAdapter === "upstash")
            ? env.get("SYSTEM_CACHE_REDIS_URL")
            : null;
        initializeCache({ ttl: parseInt(env.get("SYSTEM_CACHE_TTL") || "30") * 1000, uri: systemCacheUri });
        // IMPORTANT: Detect Sequelize upgrade BEFORE schema initialization
        // because schemaManager.initialize() will create the baasix_Migration table
        console.info("Checking for Sequelize to Drizzle upgrade...");
        const isUpgrade = await migrationService.detectUpgradeBeforeSchemaInit();
        console.info("Initializing schema registry...");
        await schemaManager.initialize();
        console.info("Initializing migration service...");
        await migrationService.init(isUpgrade);
        // Run pending migrations if auto-run is enabled
        await migrationService.runStartupMigrations();
        console.info("Initializing services in parallel...");
        await Promise.all([
            settingsService.loadSettings(),
            tasksService.init(),
            workflowService.init(),
            registerPageBuilderHooks(),
            permissionService.loadPermissions(),
            mailService.initialize(),
            Promise.resolve(storageService.initialize()),
        ]);
        console.info("Loading routes...");
        // Import ItemsService dynamically to avoid circular dependencies
        const { default: ItemsService } = await import('./services/ItemsService.js');
        const context = {
            db,
            permissionService,
            mailService,
            storageService,
            ItemsService,
            tasksService,
        };
        app.use(requestContextMiddleware);
        app.use(authMiddleware);
        await loadSystemRoutes(app, context);
        await loadRoutes(app, context);
        console.info("Loading hooks...");
        await hooksManager.loadHooksFromDirectory(context);
        await hooksManager.loadSchedulesFromDirectory(context, schedule);
        app.use(errorHandler);
        console.info("App initialization complete");
    }
    catch (error) {
        console.error("Error initializing app:", error);
        throw error;
    }
}
/**
 * Connect to database with retry logic
 * Uses DATABASE_MAX_RETRY and DATABASE_RETRY_INTERVAL env variables
 */
async function connectWithRetry(retries = parseInt(env.get("DATABASE_MAX_RETRY") || "3"), delay = parseInt(env.get("DATABASE_RETRY_INTERVAL") || "5000")) {
    for (let i = 0; i < retries; i++) {
        try {
            // Test database connection
            await db.execute(sql `SELECT 1`);
            console.info("Database connected successfully");
            return;
        }
        catch (err) {
            console.error(`Database connection attempt ${i + 1}/${retries} failed:`, err.message);
            if (i === retries - 1)
                throw err;
            console.info(`Retrying in ${delay}ms...`);
            await new Promise((res) => setTimeout(res, delay));
        }
    }
}
/**
 * Graceful shutdown handler
 */
async function gracefulShutdown(signal) {
    console.info(`Received ${signal}. Shutting down...`);
    try {
        // Shutdown plugins first (in reverse order)
        console.warn("Shutting down plugins...");
        const pluginManager = PluginManager.getInstance();
        await pluginManager.shutdownPlugins();
        console.warn("Shutting down TasksService...");
        const taskWaitTime = parseInt(env.get("TASK_SHUTDOWN_WAIT_TIME") || "30") * 1000;
        await tasksService.shutdown(taskWaitTime);
        console.warn("Shutting down WorkflowService...");
        if (typeof workflowService.shutdown === 'function') {
            await workflowService.shutdown();
        }
        console.warn("Shutting down RealtimeService...");
        if (typeof realtimeService.shutdown === 'function') {
            await realtimeService.shutdown();
        }
        console.warn("Closing cache...");
        await closeCache();
        console.warn("Closing database connection...");
        // await db.close(); // Drizzle doesn't have a close method on db object
        console.warn("Shutting down server...");
        server?.close(() => {
            console.warn("Server shut down successfully");
            process.exit(0);
        });
        setTimeout(() => {
            console.error("Forcefully shutting down");
            process.exit(1);
        }, 10000);
    }
    catch (error) {
        console.error("Error during shutdown:", error);
        process.exit(1);
    }
}
// Graceful shutdown handlers
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
// For nodemon restarts
process.once("SIGUSR2", async () => {
    await gracefulShutdown("SIGUSR2");
    process.kill(process.pid, "SIGUSR2");
});
/**
 * Start the Baasix server
 * @param options - Server startup options including port and logger configuration
 */
export async function startServer(options) {
    // Support legacy call signature: startServer(port)
    const opts = typeof options === "number"
        ? { port: options }
        : options || {};
    const serverPort = opts.port || parseInt(env.get("PORT") || "8055");
    const { plugins = [], pluginOptions = {} } = opts;
    // Initialize logger first with user-provided options
    initializeLogger(opts.logger);
    const logger = getLogger();
    // Initialize plugin manager
    const pluginManager = PluginManager.getInstance();
    pluginManager.setOptions({ verbose: pluginOptions.verbose ?? false });
    try {
        // Register plugins first (before database initialization)
        if (plugins.length > 0) {
            logger.info(`Registering ${plugins.length} plugin(s)...`);
            pluginManager.registerPlugins(plugins);
            // Get plugin schemas to register with schema manager
            const pluginSchemas = pluginManager.getPluginSchemas();
            if (pluginSchemas.length > 0) {
                logger.info(`Found ${pluginSchemas.length} plugin schema(s) to register`);
                schemaManager.registerPluginSchemas(pluginSchemas);
            }
        }
        // Initialize database with cache service FIRST
        logger.info("Initializing database with cache service...");
        await initializeDatabaseWithCache();
        server = createServer(app);
        await connectWithRetry();
        await initializeApp();
        // Initialize plugin services and routes after app initialization
        if (plugins.length > 0) {
            logger.info("Initializing plugin services...");
            // Import ItemsService dynamically to avoid circular dependencies
            const { default: ItemsService } = await import('./services/ItemsService.js');
            // Set plugin context with all available services
            pluginManager.setContext({
                // Database
                db,
                // Core service class (for instantiation)
                ItemsService,
                // Singleton services (pre-initialized)
                permissionService,
                mailService,
                storageService,
                settingsService,
                socketService,
                realtimeService,
                tasksService,
                workflowService,
                migrationService,
                hooksManager,
                // Utility functions
                getCacheService,
                invalidateCache: invalidateEntireCache,
                // Class-based services (for per-request instantiation)
                FilesService,
                AssetsService,
                NotificationService,
                ReportService,
                StatsService,
                // Express app
                app,
            });
            // Initialize plugin services
            await pluginManager.initializeServices();
            // Register plugin routes
            logger.info("Registering plugin routes...");
            pluginManager.registerRoutes(app, authMiddleware);
            // Register plugin hooks
            logger.info("Registering plugin hooks...");
            pluginManager.registerHooks(hooksManager);
            // Register plugin middleware
            pluginManager.registerMiddleware(app);
            // Call onInit for all plugins
            logger.info("Calling plugin onInit hooks...");
            await pluginManager.initPlugins();
        }
        // Start session cleanup (if not disabled)
        if (env.get("DISABLE_SESSION_CLEANUP") !== "true" && env.get("TEST_MODE") !== "true") {
            startSessionCleanup();
        }
        // Start log cleanup (audit logs and email logs)
        if (env.get("TEST_MODE") !== "true") {
            startLogCleanup();
        }
        // Initialize Socket.IO if enabled
        if (env.get("SOCKET_ENABLED") === "true") {
            await socketService.initialize(server);
            // Initialize Realtime Service - connects to Socket.IO for broadcasting
            // This will gracefully handle WAL being unavailable (falls back to hook-based)
            logger.info("Initializing Realtime Service...");
            realtimeService.setSocketService(socketService);
            await realtimeService.initialize();
            // Start WAL consumer if available (non-blocking - runs in background)
            // This is intentionally not awaited as it's a long-running subscription
            realtimeService.startConsuming().catch((error) => {
                logger.error(error, "Failed to start WAL consumer");
            });
            const status = realtimeService.getStatus();
            if (status.walAvailable) {
                logger.info("✅ Realtime Service started with WAL-based change capture");
            }
            else {
                logger.info("✅ Realtime Service started (WAL not available - no automatic broadcasting)");
            }
        }
        server.listen(serverPort, async () => {
            logger.info(`🚀 Baasix Server running on port ${serverPort}`);
            // Call onReady for all plugins after server is listening
            if (plugins.length > 0) {
                logger.info("Calling plugin onReady hooks...");
                await pluginManager.readyPlugins();
                const summary = pluginManager.getSummary();
                logger.info(`✅ ${summary.total} plugin(s) loaded successfully`);
            }
        });
        return app;
    }
    catch (error) {
        logger.error(error, "Failed to start server");
        process.exit(1);
    }
}
/**
 * Start server for testing with environment overrides
 * @param options - Testing options including environment overrides and logger configuration
 */
export async function startServerForTesting(options) {
    const { envOverrides, logger: loggerOptions } = options || {};
    // Initialize logger for testing (default to silent or minimal logging in tests)
    initializeLogger(loggerOptions || { level: "warn" });
    try {
        // If there's an existing server instance, close it first
        if (server) {
            console.log("Closing existing server instance before starting new one");
            await new Promise((resolve) => server.close(resolve));
            server = null;
        }
        // Set the environment for testing
        if (envOverrides && typeof envOverrides === "object") {
            // Clear env cache to ensure overrides take effect
            env.clearCache();
            for (const [key, value] of Object.entries(envOverrides)) {
                env.set(key, value);
                console.info(`Environment override for testing: ${key}=${value}`);
            }
        }
        // Initialize database with cache service FIRST
        console.info("Initializing database with cache service for testing...");
        await initializeDatabaseWithCache();
        // Create a new server instance
        server = createServer(app);
        // Connect to the database with retry logic
        await connectWithRetry();
        if (env.get("TEST_MODE") === "true") {
            // Ensure tables are destroyed and recreated for a clean test environment
            console.log("Destroying all tables before test");
            await destroyAllTablesInDB();
        }
        // Initialize the app with fresh state
        console.log("Initializing app for testing");
        await initializeApp();
        // Start session cleanup
        if (env.get("DISABLE_SESSION_CLEANUP") !== "true" && env.get("TEST_MODE") !== "true") {
            startSessionCleanup();
        }
        // Start log cleanup (audit logs and email logs)
        if (env.get("TEST_MODE") !== "true") {
            startLogCleanup();
        }
        // Attach the server to the app for proper cleanup later
        app.server = server;
        console.log("Test server ready");
        return app;
    }
    catch (error) {
        console.error("Failed to start server for testing:", error);
        throw error;
    }
}
/**
 * Destroy all tables in the database (for testing)
 */
export async function destroyAllTablesInDB() {
    try {
        console.log("Destroying all tables in database...");
        // Clear cache first to remove stale data (permissions, settings, auth)
        // Clear utility cache (permissions, settings, auth)
        if (isCacheInitialized()) {
            try {
                const cache = getCache();
                await cache.clear();
                console.log("Utility cache cleared successfully");
            }
            catch (cacheError) {
                console.warn("Failed to clear utility cache:", cacheError);
            }
        }
        // Clear CacheService (query results cache)
        try {
            await invalidateEntireCache();
            console.log("CacheService cleared successfully");
        }
        catch (cacheError) {
            console.warn("Failed to clear CacheService:", cacheError);
        }
        // Disable foreign key constraints for PostgreSQL (session-level setting)
        await db.execute(sql.raw("SET session_replication_role = 'replica';"));
        try {
            // Get all table names from the database, excluding PostGIS system tables
            const result = await db.execute(sql `
        SELECT tablename
        FROM pg_tables
        WHERE schemaname = 'public'
        AND tablename NOT IN ('spatial_ref_sys', 'geometry_columns', 'geography_columns')
      `);
            const tables = result.map((row) => row.tablename);
            console.log(`Found ${tables.length} tables to drop`);
            // Drop all tables with CASCADE
            for (const table of tables) {
                try {
                    console.log(`Dropping table: ${table}`);
                    await db.execute(sql.raw(`DROP TABLE IF EXISTS "${table}" CASCADE`));
                    console.log(`Successfully dropped table: ${table}`);
                }
                catch (error) {
                    console.error(`Error dropping table ${table}:`, error.message);
                    // Continue with other tables rather than failing entirely
                }
            }
            console.log("Successfully dropped all tables");
        }
        finally {
            // Always re-enable foreign key constraints
            await db.execute(sql.raw("SET session_replication_role = 'origin';"));
        }
    }
    catch (error) {
        console.error("Failed to destroy all tables in DB:", error);
        throw error;
    }
}
// Export app as default for testing
export default app;
//# sourceMappingURL=app.js.map