Source: notes-api-server.js

import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import dotenv from 'dotenv';
import path from 'path';
import {fileURLToPath} from 'url';
import {CouchDbNoteRepository} from './db/couchdb-note-repository.js';
import {MongoDbNoteRepository} from './db/mongodb-note-repository.js';
import {createNotesRouter} from './routes/notes-routes.js';

// Load environment variables
dotenv.config();

// Get configuration from environment variables
export const HOST = process.env.HOST || '0.0.0.0';
export const PORT = process.env.PORT || 3000;
export const DB_VENDOR = process.env.DB_VENDOR || 'couchdb';

// CouchDB configuration
export const COUCHDB_URL = process.env.COUCHDB_URL || 'http://admin:password@localhost:5984';
export const COUCHDB_DB_NAME = process.env.COUCHDB_DB_NAME || 'notes_db';

// MongoDB configuration
export const MONGODB_URL = process.env.MONGODB_URL || 'mongodb://localhost:27017';
export const MONGODB_DB_NAME = process.env.MONGODB_DB_NAME || 'notes_db';

// Get the directory name
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

/**
 * NotesServer class encapsulates server lifecycle and eliminates module-level state.
 * Provides a clean, testable interface for the Notes API server with support for
 * both CouchDB and MongoDB backends.
 * 
 * @class
 * @example
 * const server = new NotesServer();
 * await server.initializeApp();
 * server.startServer();
 */
export class NotesServer {
    /**
     * Create a new NotesServer instance
     * @constructor
     * @returns {NotesServer} New NotesServer instance
     * @example
     * const server = new NotesServer();
     */
    constructor() {
        /**
         * HTTP server instance
         * @type {?http.Server}
         * @private
         */
        this.server = null;

        /**
         * Express application instance
         * @type {express.Application}
         */
        this.app = express();
    }

    /**
     * Create the appropriate repository based on the DB_VENDOR environment variable.
     * Supports both CouchDB and MongoDB implementations.
     * 
     * @returns {CouchDbNoteRepository|MongoDbNoteRepository} The configured repository instance
     * @throws {Error} When an unsupported database vendor is specified
     * @example
     * // With DB_VENDOR='mongodb'
     * const repo = server.createNoteRepository(); // Returns MongoDbNoteRepository
     * 
     * // With DB_VENDOR='couchdb' (default)
     * const repo = server.createNoteRepository(); // Returns CouchDbNoteRepository
     */
    createNoteRepository() {
        // Use current environment variable value, not cached constant
        const dbVendor = process.env.DB_VENDOR || 'couchdb';
        if (dbVendor === 'mongodb') {
            console.log('Using MongoDB as the database vendor');
            return new MongoDbNoteRepository(MONGODB_URL, MONGODB_DB_NAME);
        } else {
            console.log('Using CouchDB as the database vendor');
            return new CouchDbNoteRepository(COUCHDB_URL, COUCHDB_DB_NAME);
        }
    }

    /**
     * Graceful shutdown function that handles cleanup and ensures all connections are closed properly.
     * Implements a timeout mechanism to force shutdown if graceful shutdown takes too long.
     * 
     * @param {number} [timeout=10000] - Timeout in milliseconds before forcing shutdown
     * @returns {void}
     * @example
     * // Manual shutdown
     * server.gracefulShutdown();
     * 
     * // With custom timeout
     * server.gracefulShutdown(5000); // 5 second timeout
     */
    gracefulShutdown(timeout = 10000) {
        console.log('Shutting down gracefully...');
        if (this.server) {
            // Store timeout ID so we can clear it if shutdown completes
            const forceShutdownTimeout = setTimeout(() => {
                console.error('Could not close connections in time, forcefully shutting down');
                process.exit(1);
            }, timeout);

            this.server.close(() => {
                console.log('HTTP server closed');
                // Clear the force shutdown timeout since we completed gracefully
                clearTimeout(forceShutdownTimeout);
                // Close database connections, etc.
                process.exit(0);
            });
        } else {
            process.exit(0);
        }
    }

    /**
     * Initialize the Express application with middleware, routes, and error handling.
     * Sets up the complete application stack including security, CORS, static files,
     * and API routes.
     * 
     * @param {?NoteRepository} [noteRepository=null] - Optional repository instance for dependency injection (useful for testing)
     * @returns {Promise<{app: express.Application, repository: NoteRepository}>} Initialized app and repository
     * @throws {Error} When repository initialization fails
     * @example
     * // Default initialization
     * const { app, repository } = await server.initializeApp();
     * 
     * // With custom repository (for testing)
     * const mockRepo = new MockNoteRepository();
     * const { app, repository } = await server.initializeApp(mockRepo);
     */
    async initializeApp(noteRepository = null) {
        try {
            // Clear any existing middleware/routes for testing
            this.app._router = undefined;

            // Security middleware
            this.app.use(helmet());

            // CORS middleware for cross-origin requests
            this.app.use(cors());

            // Middleware for parsing JSON bodies
            this.app.use(express.json());

            // Serve static files from the public directory
            this.app.use(express.static(path.join(__dirname, 'public')));

            // Use provided repository or create default one
            const repository = noteRepository || this.createNoteRepository();

            // Initialize the repository
            await repository.init();
            console.log('Repository initialized successfully');

            // Create and mount the notes router
            const notesRouter = createNotesRouter(repository);
            this.app.use('/api/notes', notesRouter);

            // Add a simple health check endpoint
            this.app.get('/health', (req, res) => {
                res.status(200).json({status: 'ok'});
            });

            // Serve the index.html file for the root route
            this.app.get('/', (req, res) => {
                res.sendFile(path.join(__dirname, 'public', 'index.html'));
            });

            // 404 handler for undefined routes
            this.app.use((req, res) => {
                res.status(404).json({ error: 'Not found' });
            });

            // Error handling middleware
            this.app.use((err, req, res, next) => {
                console.error('Unhandled error:', err);

                // Handle JSON parsing errors
                if (err.type === 'entity.parse.failed') {
                    return res.status(400).json({ error: 'Invalid JSON' });
                }

                // Default to 500 for other errors
                res.status(500).json({error: 'Internal server error'});
            });

            return { app: this.app, repository };
        } catch (error) {
            console.error('Failed to initialize application:', error);
            throw error;
        }
    }

    /**
     * Start the HTTP server and set up signal handlers for graceful shutdown.
     * The server will listen on the configured HOST and PORT.
     * 
     * @returns {http.Server} The started HTTP server instance
     * @throws {Error} When server fails to start
     * @example
     * const server = new NotesServer();
     * await server.initializeApp();
     * const httpServer = server.startServer();
     * console.log('Server started successfully');
     */
    startServer() {
        // Convert PORT to number to ensure correct type for the test
        const port = parseInt(PORT, 10);
        this.server = this.app.listen(port, HOST, () => {
            console.log(`Notes API server is running at http://${HOST}:${port}`);
            console.log('Available endpoints:');
            console.log('  GET    /                - Web UI for notes management');
            console.log('  GET    /api/notes       - Get all notes');
            console.log('  GET    /api/notes/:id   - Get a note by ID');
            console.log('  POST   /api/notes       - Create a new note');
            console.log('  PUT    /api/notes/:id   - Update a note');
            console.log('  DELETE /api/notes/:id   - Delete a note');
            console.log('  GET    /health          - Health check');
            console.log('\nOpen your browser at http://localhost:' + port + ' to use the Notes UI');
        });

        // Handle graceful shutdown
        process.on('SIGTERM', () => this.gracefulShutdown());
        process.on('SIGINT', () => this.gracefulShutdown());

        return this.server;
    }
}

// Create global instance for backward compatibility
const globalNotesServer = new NotesServer();

/*
 * Convenience exports for backward compatibility
 * These provide direct access to a global NotesServer instance.
 * For better testability and control, use the NotesServer class directly.
 */

/**
 * Global Express application instance for backward compatibility
 * @type {express.Application}
 * @deprecated Use NotesServer class for better control
 * @example
 * import { app } from './notes-api-server.js';
 * // Note: Prefer using NotesServer class directly
 */
export const app = globalNotesServer.app;

/**
 * Create repository instance using global server (backward compatibility)
 * @returns {CouchDbNoteRepository|MongoDbNoteRepository} The configured repository instance
 * @deprecated Use NotesServer.createNoteRepository() for better testability
 * @example
 * import { createNoteRepository } from './notes-api-server.js';
 * const repo = createNoteRepository();
 */
export const createNoteRepository = () => globalNotesServer.createNoteRepository();

/**
 * Graceful shutdown using global server (backward compatibility)
 * @param {number} [timeout=10000] - Timeout in milliseconds before forcing shutdown
 * @returns {void}
 * @deprecated Use NotesServer.gracefulShutdown() for better control
 * @example
 * import { gracefulShutdown } from './notes-api-server.js';
 * gracefulShutdown();
 */
export const gracefulShutdown = () => globalNotesServer.gracefulShutdown();

/**
 * Initialize application using global server (backward compatibility)
 * @param {?NoteRepository} [noteRepository=null] - Optional repository instance for dependency injection
 * @returns {Promise<{app: express.Application, repository: NoteRepository}>} Initialized app and repository
 * @deprecated Use NotesServer.initializeApp() for better testability
 * @example
 * import { initializeApp } from './notes-api-server.js';
 * const { app, repository } = await initializeApp();
 */
export const initializeApp = (noteRepository = null) => globalNotesServer.initializeApp(noteRepository);

/**
 * Start server using global server instance (backward compatibility)
 * @returns {http.Server} The started HTTP server instance
 * @deprecated Use NotesServer.startServer() for better control
 * @example
 * import { startServer } from './notes-api-server.js';
 * const server = startServer();
 */
export const startServer = () => globalNotesServer.startServer();

// Only start the server if this file is run directly (not imported)
if (import.meta.url === `file://${process.argv[1]}`) {
    const notesServer = new NotesServer();
    notesServer.initializeApp()
        .then(() => {
            notesServer.startServer();
        })
        .catch(error => {
            console.error('Application startup failed:', error);
            process.exit(1);
        });
}