const path = require('path');
const http = require('http');
const fs = require('fs');
const Database = require('better-sqlite3');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const wsHub = require('./services/wsHub');
const { getEffectiveRolesForUser } = require('./utils/rbac');
const { VALID_GENDERS, normalizeGender } = require('./utils/gender');

// Load environment variables from .env file if it exists
try {
  const envPath = path.join(__dirname, '../.env');
  if (fs.existsSync(envPath)) {
    const envLines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/);
    for (const line of envLines) {
      const trimmed = line.trim();
      if (!trimmed || trimmed.startsWith('#')) continue;
      const match = trimmed.match(/^([\w.-]+)\s*=\s*(.*)$/);
      if (match) {
        const key = match[1];
        let value = match[2] || '';
        if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
          value = value.substring(1, value.length - 1);
        }
        process.env[key] = value.trim();
      }
    }
  }
} catch (e) {
  console.error('Error loading .env file:', e.message);
}

const express = require('express');
const cors = require('cors');

// Import controllers
const usersController = require('./controllers/users.controller');
const departmentsController = require('./controllers/departments.controller');
const classesController = require('./controllers/classes.controller');
const subjectsController = require('./controllers/subjects.controller');
const reportsController = require('./controllers/reports.controller');
const userRolesController = require('./controllers/userRoles.controller');
const settingsController = require('./controllers/settings.controller');
const schoolSettingsController = require('./controllers/schoolSettings.controller');
const noticesController = require('./controllers/notices.controller');
const coursesController = require('./controllers/courses.controller');
const courseGradesController = require('./controllers/courseGrades.controller');
const assignmentsController = require('./controllers/assignments.controller');
const gradesController = require('./controllers/grades.controller');
const attendanceController = require('./controllers/attendance.controller');
const messagesController = require('./controllers/messages.controller');
const calendarController = require('./controllers/calendar.controller');
const examsController = require('./controllers/exams.controller');
const paymentsController = require('./controllers/payments.controller');
const hrController = require('./controllers/hr.controller');
const financeController = require('./controllers/finance.controller');
const teacherController = require('./controllers/teacher.controller');
// §A — File Attachments plan: polymorphic attachments table.
const attachmentsController = require('./controllers/attachments.controller');
const homeworkController = require('./controllers/homework.controller');
const testsController = require('./controllers/tests.controller');
const socialController = require('./controllers/social.controller');
const inventoryController = require('./controllers/inventory.controller');
const hostelController = require('./controllers/hostel.controller');
const transportController = require('./controllers/transport.controller');
const libraryController = require('./controllers/library.controller');
const clubsController = require('./controllers/clubs.controller');
const syncController = require('./controllers/sync.controller');
const feesController = require('./controllers/fees.controller');
// §10a — Finance Phase 2 controllers (operational core).
const financeSuppliersController = require('./controllers/finance-suppliers.controller');
const financeCoaController = require('./controllers/finance-coa.controller');
const financeBankingController = require('./controllers/finance-banking.controller');
const financeInvoicesController = require('./controllers/finance-invoices.controller');
const financeTripsController = require('./controllers/finance-trips.controller');
// §10b — Finance Phase 2 fee engine.
const financeEngineController = require('./controllers/finance-engine.controller');
const frontOfficeController = require('./controllers/front-office.controller');
const auditController = require('./controllers/audit.controller');
const sysadminController = require('./controllers/sysadmin.controller');
const dashboardController = require('./controllers/dashboard.controller');
const clubAttendanceController = require('./controllers/club-attendance.controller');
const hostelAttendanceController = require('./controllers/hostel-attendance.controller');
const transportAttendanceController = require('./controllers/transport-attendance.controller');
const academicRolloverController = require('./controllers/academic-rollover.controller');
const staffAttendanceController = require('./controllers/staff-attendance.controller');
const enrollmentsController = require('./controllers/enrollments.controller');
const courseEnrolmentsController = require('./controllers/course_enrolments.controller');
const marksController = require('./controllers/marks.controller');
const medicalController = require('./controllers/medical.controller');
const classRoomController = require('./controllers/class-room.controller');
const crosswordController = require('./controllers/crossword.controller');
const edutainmentController = require('./controllers/edutainment.controller');
const licensingController = require('./controllers/licensing.controller');
const studentsController = require('./controllers/students.controller');
// §10c — single-student fee profile (joins student_fees + discounts + trips).
const studentsFeesController = require('./controllers/students-fees.controller');
const offboardingController = require('./controllers/offboarding.controller');
const notificationsController = require('./controllers/notifications.controller');
const { getSyncEngine } = require('./services/SyncEngine');
const AuditService = require('./services/AuditService');
const EmailService = require('./services/EmailService');
const setupSwagger = require('./swagger');
const licensingMiddleware = require('./middleware/licensingMiddleware');
const LicenseTimeSyncService = require('./services/LicenseTimeSyncService');
const WeeklyGovPushService = require('./services/WeeklyGovPushService');
// Dev-only controller — mounted only when NODE_ENV !== 'production'. See
// controllers/dev.controller.js for the surface.
const devController = process.env.NODE_ENV !== 'production'
  ? require('./controllers/dev.controller')
  : null;
const app = express();
const config = require('./config');
const PORT = config.port;
const { jwtSecret: JWT_SECRET, allowedOrigins, allowClientOfflineMint } = config;

// Prometheus Metrics Setup (optional telemetry)
let client, register, httpRequestDurationSeconds;
try {
  client = require('prom-client');
  register = new client.Registry();
  client.collectDefaultMetrics({ register });

  httpRequestDurationSeconds = new client.Histogram({
    name: 'http_request_duration_seconds',
    help: 'Duration of HTTP requests in seconds',
    labelNames: ['method', 'route', 'code'],
    buckets: [0.05, 0.1, 0.3, 0.5, 1, 2, 5]
  });
  register.registerMetric(httpRequestDurationSeconds);
} catch (e) {}

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    if (httpRequestDurationSeconds) {
      httpRequestDurationSeconds
        .labels(req.method, req.route ? req.route.path : req.path, res.statusCode)
        .observe(duration);
    }
  });
  next();
});

app.get(['/metrics', '/api/metrics'], async (req, res) => {
  try {
    res.set('Content-Type', register.contentType);
    res.end(await register.metrics());
  } catch (err) {
    res.status(500).end(err);
  }
});

// Database setup
const dbPath = config.dbPath;
const db = new Database(dbPath);
db.pragma('foreign_keys = ON');

// Initialize Sync Engine
getSyncEngine();

// Setup Swagger UI
setupSwagger(app);

// Middleware
// CORS: in production, allow only configured origins. In dev, allow any so
// Vite on a different port (or LAN IP for mobile testing) still works.
app.use(
  cors({
    origin: config.isProd ? allowedOrigins : true,
    credentials: true,
  })
);
// Body limit: keep small for JSON. File uploads go through Multer routes on
// /api/attachments/* with their own per-route limit.
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ limit: '1mb', extended: true }));

// Express body-parser throws PayloadTooLargeError on oversized bodies. The
// default error handler turns that into a 500. Convert to 413 explicitly.
app.use((err, req, res, next) => {
  if (err && err.type === 'entity.too.large') {
    return res.status(413).json({
      error: 'Payload too large',
      message: `Body exceeds the ${err.limit}-byte limit. For larger payloads, use a Multer upload route.`,
    });
  }
  next(err);
});
app.use('/uploads', express.static(path.join(__dirname, '../uploads')));
// Alias the same directory under /api/uploads so axios clients (baseURL: /api)
// can fetch uploads via 'uploads/...' without doubling the path prefix.
// Keep /uploads for any caller that already references it directly.
app.use('/api/uploads', express.static(path.join(__dirname, '../uploads')));

// Licensing & Multi-Tenant Access Control Middleware
app.use(licensingMiddleware);

// Register controller routes
app.use('/api/users', usersController);
app.use('/api/departments', departmentsController);
app.use('/api/classes', classesController);
app.use('/api/subjects', subjectsController);
app.use('/api/reports', reportsController);
// userRoles controller exposes two URL families (user-scoped + grant-scoped);
// mount at /api so internal paths stay absolute.
app.use('/api', userRolesController);
const cohortsController = require('./controllers/cohorts.controller');
app.use('/api/cohorts', cohortsController);
app.use('/api/settings', settingsController);
app.use('/api/school-settings', schoolSettingsController);
app.use('/api/notices', noticesController);
app.use('/api/courses', coursesController);
app.use('/api/course-grades', courseGradesController);
app.use('/api/assignments', assignmentsController);
app.use('/api/grades', gradesController);
app.use('/api/attendance', attendanceController);
app.use('/api/messages', messagesController);
app.use('/api/notifications', notificationsController);
app.use('/api/calendar', calendarController);
app.use('/api/exams', examsController);
app.use('/api/payments', paymentsController);
app.use('/api/hr', hrController);
app.use('/api/finance', financeController);
app.use('/api/teacher', teacherController);
app.use('/api/social', socialController);
app.use('/api/inventory', inventoryController);
app.use('/api/hostels', hostelController);
app.use('/api/transport', transportController);
app.use('/api/library', libraryController);
app.use('/api/clubs', clubsController);
app.use('/api/sync', syncController);
app.use('/api/fees', feesController);
app.use('/api/fees/engine', financeEngineController);
// §10a — Finance Phase 2 controller routes. The finance/ sub-router here
// is the existing one; the new modules mount as separate top-level paths.
app.use('/api/finance/suppliers', financeSuppliersController);
app.use('/api/finance/chart-of-accounts', financeCoaController);
app.use('/api/finance/banking', financeBankingController);
app.use('/api/finance/invoices', financeInvoicesController);
app.use('/api/finance/trips', financeTripsController);
app.use('/api/front-office', frontOfficeController);
app.use('/api/audit', auditController);
app.use('/api/sysadmin', sysadminController);
app.use('/api/dashboard', dashboardController);
app.use('/api/club-attendance', clubAttendanceController);
app.use('/api/hostel-attendance', hostelAttendanceController);
app.use('/api/transport-attendance', transportAttendanceController);
app.use('/api/academic-rollover', academicRolloverController);
app.use('/api/staff-attendance', staffAttendanceController);
app.use('/api/enrollments', enrollmentsController);
app.use('/api/course-enrolments', courseEnrolmentsController);
app.use('/api/marks', marksController);
app.use('/api/medical', medicalController);
app.use('/api/crossword', crosswordController);
app.use('/api/class-room', classRoomController);
app.use('/api/students', studentsController);
// §A — File Attachments: polymorphic storage for syllabuses, course
// materials, supplementary assignment files, homework sheets, test
// papers, student submissions, and teacher-uploaded marked scripts.
app.use('/api/attachments', attachmentsController);
app.use('/api/homework', homeworkController);
// FR-SMS3 — Legacy CSV/Excel import. Mounted at both /api/legacy-import
// (canonical) and /api/students/import (SRS-shaped alias) so the SRS
// path is reachable too. The students controller is mounted later; this
// order means the import sub-paths (e.g. /api/students/import/preview)
// take precedence over the students controller's root, but the
// students controller's other routes (e.g. /api/students/:id) are
// unaffected because /api/students/import/* is a distinct prefix.
const legacyImportController = require('./controllers/legacy-import.controller');
app.use('/api/legacy-import', legacyImportController);
app.use('/api/students/import', legacyImportController);
// §10c — single-student fee profile. Express handles `/api/students/:uid/fees`
// on a separate router so the existing /api/students surface doesn't have
// to learn the new shape.
app.use('/api/students', studentsFeesController);
app.use('/api/tests', testsController);
app.use('/api/license', licensingController);
app.use('/api/offboarding', offboardingController);
// FR-XFER1/2/3 — Interoperability & Transfer APIs (FR-XFER1, FR-XFER2, FR-XFER3).
// The transfers controller is mounted at both `/api/transfers` (canonical,
// per SRS) and `/api/xfer` (short alias) for convenience.
const transfersController = require('./controllers/transfers.controller');
app.use('/api/transfers', transfersController);
app.use('/api/xfer', transfersController);

// FR-SMS1 timetable sub-feature (UAT TC-SMS-008/009/010).
// Mounted at /api/timetable — periods CRUD, entries CRUD, conflict
// detection, and class/teacher weekly views.
const timetableController = require('./controllers/timetable.controller');
app.use('/api/timetable', timetableController);

// Dev-only echo + liveness endpoints. Mounted only when
// NODE_ENV !== 'production' so the surface disappears in prod with no
// extra guard rails. Used to smoke-test the WeeklyGovPushService
// against a local listener before the Ministry publishes the real URL.
if (devController) {
  app.use('/api/admin/dev', devController);
  console.log('[dev] echo endpoints mounted at /api/admin/dev');
}

// Serve static files from the React app
app.use(express.static(path.join(__dirname, '../../client/dist')));

// Health endpoint for Docker / monitoring / smoke tests. DB ping confirms
// the SQLite connection is open; the response is JSON so it's easy to
// assert on in CI.
app.get('/api/health', (req, res) => {
  try {
    db.prepare('SELECT 1').get();
    res.json({ status: 'ok', db: 'up', ts: new Date().toISOString() });
  } catch (e) {
    res.status(503).json({ status: 'degraded', db: 'down', error: e.message });
  }
});

// Auth middleware
const auth = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token provided' });
  try {
    req.user = jwt.verify(token, JWT_SECRET);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

// ============ AUTH ROUTES ============

/**
 * @openapi
 * /api/auth/login:
 *   post:
 *     summary: Log in a user
 *     description: Authenticate user credentials and return a JWT token.
 *     tags:
 *       - Authentication
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - email
 *               - password
 *             properties:
 *               email:
 *                 type: string
 *                 format: email
 *                 example: admin@school.com
 *               password:
 *                 type: string
 *                 format: password
 *                 example: admin123
 *     responses:
 *       200:
 *         description: Login successful. Returns JWT token and user info.
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 token:
 *                   type: string
 *                 user:
 *                   type: object
 *                   properties:
 *                     id:
 *                       type: integer
 *                     email:
 *                       type: string
 *                     role:
 *                       type: string
 *                     first_name:
 *                       type: string
 *                     last_name:
 *                       type: string
 *       400:
 *         description: Missing email or password
 *       401:
 *         description: Invalid credentials
 *       500:
 *         description: Internal Server Error
 */
app.post('/api/auth/login', async (req, res) => {
  try {
    const { email, password } = req.body;
    console.log(`Login attempt for: ${email}`);
    
    if (!email || !password) {
      return res.status(400).json({ error: 'Email and password are required' });
    }

    const user = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
    
    if (!user) {
      console.log(`User not found: ${email}`);
      await AuditService.logLogin(req, false, email);
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    if (!bcrypt.compareSync(password, user.password)) {
      console.log(`Invalid password for: ${email}`);
      await AuditService.logLogin(req, false, email);
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    await AuditService.logLogin(req, true, email);

    // Phase 1 PR 1: embed effective_roles in the JWT so route gating can
    // check auxiliary roles without a per-request DB hit. Old clients
    // that don't read effective_roles still work — `role` is unchanged.
    const effective_roles = getEffectiveRolesForUser(user.id);

    const token = jwt.sign(
      { id: user.id, email: user.email, role: user.role, effective_roles },
      JWT_SECRET,
      { expiresIn: '7d' }
    );
    console.log(`Login successful for: ${email}`);

    // Optional: ship the JWT secret to the client so it can mint tokens
    // when the server is unreachable (offline mode). Only when the env
    // explicitly allows it — defaults to true in dev, false in prod.
    const offline_jwt_secret = allowClientOfflineMint ? JWT_SECRET : null;

    res.json({
      token,
      offline_jwt_secret,
      user: {
        id: user.id,
        email: user.email,
        role: user.role,
        effective_roles,
        first_name: user.first_name,
        last_name: user.last_name,
        gender: user.gender,
        profile_image: user.profile_image,
      },
    });
  } catch (err) {
    console.error('LOGIN ERROR:', err);
    res.status(500).json({ error: 'Internal Server Error', message: err.message });
  }
});

/**
 * @openapi
 * /api/auth/refresh-roles:
 *   get:
 *     summary: Re-sign the JWT with the latest effective_roles
 *     description: |
 *       Reads the user_roles table and returns a new JWT + user payload
 *       reflecting the current set of active auxiliary roles and covers.
 *       Clients call this on app load and after every user_roles
 *       grant/revoke mutation so a freshly-granted role is visible
 *       without waiting 7 days for the original JWT to expire.
 *     tags:
 *       - Authentication
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: New token and refreshed user payload
 *       401:
 *         description: Not authenticated
 */
app.get('/api/auth/refresh-roles', auth, async (req, res) => {
  try {
    const userRow = db.prepare('SELECT id, email, role, first_name, last_name, gender, profile_image, is_active FROM users WHERE id = ? AND is_deleted = 0').get(req.user.id);
    if (!userRow) return res.status(401).json({ error: 'User not found' });
    if (!userRow.is_active) return res.status(403).json({ error: 'Account is inactive' });

    const effective_roles = getEffectiveRolesForUser(userRow.id);
    const token = jwt.sign(
      { id: userRow.id, email: userRow.email, role: userRow.role, effective_roles },
      JWT_SECRET,
      { expiresIn: '7d' }
    );
    res.json({
      token,
      user: {
        id: userRow.id,
        email: userRow.email,
        role: userRow.role,
        effective_roles,
        first_name: userRow.first_name,
        last_name: userRow.last_name,
        gender: userRow.gender,
        profile_image: userRow.profile_image,
      },
    });
  } catch (err) {
    console.error('REFRESH-ROLES ERROR:', err);
    res.status(500).json({ error: 'Internal Server Error', message: err.message });
  }
});

// Authenticated endpoint that returns only the caller's own password hash.
// The client uses this after a successful login to populate its offline
// auth cache so it can verify passwords when the server is unreachable.
// Returns ONLY the caller's own hash (looked up by JWT id), never an
// arbitrary email lookup, so it cannot be used to enumerate hashes.
app.get('/api/auth/me/credentials', auth, async (req, res) => {
  try {
    const row = db.prepare('SELECT password FROM users WHERE id = ? AND is_deleted = 0').get(req.user.id);
    if (!row) {
      return res.status(404).json({ error: 'User not found' });
    }
    res.json({ email: req.user.email, password_hash: row.password });
  } catch (err) {
    console.error('CREDENTIALS ERROR:', err);
    res.status(500).json({ error: 'Internal Server Error', message: err.message });
  }
});

/**
 * @openapi
 * /api/auth/register:
 *   post:
 *     summary: Register a new user
 *     description: Create a new user account (default role 'student').
 *     tags:
 *       - Authentication
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - email
 *               - password
 *             properties:
 *               email:
 *                 type: string
 *                 format: email
 *               password:
 *                 type: string
 *                 format: password
 *               role:
 *                 type: string
 *                 enum: [school_admin, principal, hr, bursar, teacher, student, parent]
 *               first_name:
 *                 type: string
 *               last_name:
 *                 type: string
 *               phone:
 *                 type: string
 *     responses:
 *       200:
 *         description: Registration successful
 *       400:
 *         description: Email already exists or validation error
 */
app.post('/api/auth/register', async (req, res) => {
  const { email, password, role, first_name, last_name, phone, gender } = req.body;
  const { v4: uuidv4 } = require('uuid');
  const normalizedGender = normalizeGender(gender);

  if (normalizedGender && !VALID_GENDERS.includes(normalizedGender)) {
    return res.status(400).json({ error: `Invalid gender. Must be one of: ${VALID_GENDERS.join(', ')}` });
  }
  
  try {
    const hashedPassword = bcrypt.hashSync(password, 10);
    const uid = uuidv4();
    const result = db.prepare(`
      INSERT INTO users (uid, email, password, role, first_name, last_name, phone, gender)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).run(uid, email, hashedPassword, role || 'student', first_name, last_name, phone, normalizedGender);
    
    await AuditService.log(req, 'REGISTER_USER', {
      userEmail: email,
      entityType: 'user',
      entityId: uid,
      newData: { email, role: role || 'student', first_name, last_name, gender: normalizedGender }
    });
    
    res.json({ id: result.lastInsertRowid, email, role: role || 'student', gender: normalizedGender });
  } catch (err) {
    res.status(400).json({ error: 'Email already exists' });
  }
});

/**
 * @openapi
 * /api/auth/forgot-password:
 *   post:
 *     summary: Request password recovery
 *     description: |
 *       Issues a temporary password for the given email and sends it via
 *       `EmailService`. Always returns 200 even if no user matches (the
 *       controller returns 404 when the email is unknown).
 *     tags:
 *       - Authentication
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required: [email]
 *             properties:
 *               email:
 *                 type: string
 *                 format: email
 *                 example: admin@school.com
 *     responses:
 *       200:
 *         description: Recovery email queued (or attempted)
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 message:
 *                   type: string
 *                 previewUrl:
 *                   type: string
 *                   description: Ethersoft preview URL when EmailService runs in dev mode
 *       400:
 *         description: Missing email
 *       404:
 *         description: No account found with that email
 */
app.post('/api/auth/forgot-password', async (req, res) => {
  try {
    const { email } = req.body;
    if (!email) {
      return res.status(400).json({ error: 'Email is required' });
    }
    const user = db.prepare('SELECT * FROM users WHERE email = ? AND is_deleted = 0').get(email);
    if (!user) {
      return res.status(404).json({ error: 'No account found with this email' });
    }
    
    // Generate temporary password
    const tempPassword = `NextGen-${Math.random().toString(36).substring(2, 8).toUpperCase()}`;
    const hashedPassword = bcrypt.hashSync(tempPassword, 10);
    
    // Update the database
    db.prepare('UPDATE users SET password = ?, updated_at = datetime(\'now\', \'localtime\') WHERE id = ?').run(hashedPassword, user.id);
    
    console.log(`[PASSWORD RECOVERY] Reset password for ${email} to ${tempPassword}`);
    
    // Construct login link
    const host = req.get('origin') || req.get('referer') || 'http://localhost:3000';
    const loginLink = `${host}/login`;
    
    // Send email
    const emailResult = await EmailService.sendPasswordRecovery(email, tempPassword, loginLink);
    
    await AuditService.log(req, 'FORGOT_PASSWORD', {
      userEmail: email,
      entityType: 'user',
      entityId: user.uid,
      newData: { email, msg: 'Temporary password generated and email sent' }
    });

    res.json({ 
      message: 'A password recovery email has been sent to your inbox.',
      ...(emailResult.previewUrl ? { previewUrl: emailResult.previewUrl } : {})
    });
  } catch (err) {
    console.error('FORGOT PASSWORD ERROR:', err);
    res.status(500).json({ error: 'Internal Server Error', message: err.message });
  }
});


// Start server
// Global error handler
app.use((err, req, res, next) => {
  console.error('SERVER ERROR:', err);
  res.status(500).json({ error: 'Internal Server Error', message: err.message, stack: process.env.NODE_ENV === 'development' ? err.stack : undefined });
});

// Catch-all 404 handler for unmatched /api routes to prevent HTML fallthrough
app.use('/api/*', (req, res) => {
  res.status(404).json({ error: 'API endpoint not found', path: req.originalUrl });
});

// The "catchall" handler: for any request that doesn't
// match one above, send back React's index.html file.
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, '../../client/dist/index.html'));
});

// Trigger reload
// Wrap Express in a raw http.Server so we can attach the WebSocket hub on
// the SAME port. This avoids opening a second listener for WS clients.
const server = http.createServer(app);

// Attach WebSocket hub (JWT-authenticated pub/sub on `/`, `/ws`, `/socket`).
// The hub reads JWT_SECRET from process.env so it MUST be the same secret
// Express uses above.
wsHub.attach(server);

// Expose wsHub on the Express app so other controllers can broadcast
// (e.g. announcements, marks, attendance updates).
app.set('wsHub', wsHub);

// Only bind a port when run directly (e.g. `node src/index.js`). Tests
// import this file and rely on supertest's ephemeral listener, so we
// must not pre-bind a port here.
if (require.main === module) {
  server.listen(PORT, () => {
    console.log(`🚀 Africa Alert Server running on http://localhost:${PORT}`);
    console.log(`   WebSocket hub: ws://localhost:${PORT}/ws (token via query or Sec-WebSocket-Protocol)`);
    // Auto-publish ticker — flips draft schedules to active once
    // their start_time has arrived. See services/ScheduleAutoPublish.js.
    // Gated by EXAM_AUTO_PUBLISH=0 (default: enabled) so tests can
    // suppress the timer cleanly.
    const { start: startAutoPublish } = require('./services/ScheduleAutoPublish');
    startAutoPublish();
    LicenseTimeSyncService.start();
    WeeklyGovPushService.start();
  });
}

module.exports = app;
// Export the http.Server too — useful for tests / graceful shutdown scripts.
module.exports.server = server;
