import { existsSync, unlinkSync } from 'fs';
import path from 'path';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';
import { env } from './config/env';
import { logger } from './utils/logger';
import { prisma } from './config/database';
import { redis } from './config/redis';
import { register, httpRequestDuration, httpRequestsTotal } from './utils/metrics';
import { errorHandler } from './middleware/error-handler';
import { authenticate } from './middleware/auth.middleware';
import { requireRole } from './middleware/rbac.middleware';
import { globalRateLimit, healthMetricsRateLimit } from './middleware/rate-limit';
import { authRouter } from './modules/auth/auth.routes';
import { giteaSsoRouter } from './modules/auth/gitea-sso.routes';
import { identityBridgeRouter } from './modules/auth/identity-bridge.routes';
import { socialLinkBridgeRouter } from './modules/auth/social-link-bridge.routes';
import { donationBridgeRouter } from './modules/payments/donation-bridge.routes';
import { volunteerBridgeRouter } from './modules/volunteer/volunteer-bridge.routes';
import { ssoBridgeRouter } from './modules/auth/sso-bridge.routes';
import { usersRouter } from './modules/users/users.routes';
import { provisioningRouter } from './modules/users/provisioning.routes';
import { campaignsRouter } from './modules/influence/campaigns/campaigns.routes';
import { campaignEmailsPublicRouter, campaignEmailsAdminRouter } from './modules/influence/campaign-emails/campaign-emails.routes';
import { emailQueueRouter } from './modules/influence/email-queue/email-queue.routes';
import { representativesRouter } from './modules/influence/representatives/representatives.routes';
import { campaignPublicRouter } from './modules/influence/campaigns/campaigns-public.routes';
import { campaignUserRouter } from './modules/influence/campaigns/campaigns-user.routes';
import { campaignModerationRouter } from './modules/influence/campaigns/campaigns-moderation.routes';
import { responseCampaignPublicRouter, responsesPublicRouter, responsesAdminRouter } from './modules/influence/responses/responses.routes';
import { locationsAdminRouter, locationsPublicRouter } from './modules/map/locations/locations.routes';
import { bulkGeocodeRouter } from './modules/map/locations/bulk-geocode.routes';
import { cutsAdminRouter, cutsPublicRouter } from './modules/map/cuts/cuts.routes';
import { shiftsAdminRouter, shiftsPublicRouter, shiftsVolunteerRouter } from './modules/map/shifts/shifts.routes';
import shiftSeriesRouter from './modules/map/shifts/shift-series.routes';
import { mapSettingsRouter } from './modules/map/settings/settings.routes';
import { qrRouter } from './modules/qr/qr.routes';
import { listmonkRouter } from './modules/listmonk/listmonk.routes';
import { listmonkWebhookRouter } from './modules/listmonk/listmonk-webhook.routes';
import { meetingPlannerAdminRouter, meetingPlannerPublicRouter } from './modules/meeting-planner/meeting-planner.routes';
import { strawPollAdminRouter } from './modules/polls/polls.routes';
import { strawPollPublicRouter } from './modules/polls/polls-public.routes';
import { strawPollWidgetRouter } from './modules/polls/polls-widget.routes';
import { pagesPublicRouter } from './modules/pages/pages-public.routes';
import { pagesAdminRouter } from './modules/pages/pages-admin.routes';
import { blocksRouter } from './modules/pages/blocks.routes';
import { docsRouter } from './modules/docs/docs.routes';
import { docsAccessRouter } from './modules/docs/docs-access.routes';
import { giteaSetupRouter } from './modules/gitea-setup/gitea-setup.routes';
import { giteaSetupService } from './modules/gitea-setup/gitea-setup.service';
import { servicesRouter } from './modules/services/services.routes';
import { siteSettingsRouter } from './modules/settings/settings.routes';
import { canvassVolunteerRouter, canvassAdminRouter } from './modules/map/canvass/canvass.routes';
import { canvassExportRouter } from './modules/map/canvass/canvass-export.routes';
import { trackingVolunteerRouter, trackingAdminRouter } from './modules/map/tracking/tracking.routes';
import { geocodingRouter } from './modules/map/geocoding/geocoding.routes';
import { eventsPublicRouter } from './modules/map/events/events.routes';
import { pangolinRouter } from './modules/pangolin/pangolin.routes';
import ccpRegistrationRouter from './modules/ccp-registration/ccp-registration.routes';
import { rocketchatRouter } from './modules/rocketchat/rocketchat.routes';
import { rocketchatBridgeRouter } from './modules/rocketchat/rocketchat-bridge.routes';
import { jitsiRouter } from './modules/jitsi/jitsi.routes';
import { rocketchatWebhookService } from './services/rocketchat-webhook.service';
import { gancioClient } from './services/gancio.client';
import { gancioSettingsSyncService } from './services/gancio-settings-sync.service';
import { narImportRouter } from './modules/map/locations/nar-import.routes';
import { areaImportRouter } from './modules/map/locations/area-import.routes';
import emailTemplatesRouter from './modules/email-templates/email-templates-admin.routes';
import { observabilityRouter } from './modules/observability/observability.routes';
import { upgradeRouter } from './modules/upgrade/upgrade.routes';
import { registryRouter } from './modules/registry/registry.routes';
import { dashboardRouter } from './modules/dashboard/dashboard.routes';
import { initEncryption } from './utils/crypto';
import { emailService } from './services/email.service';
import { emailQueueService } from './services/email-queue.service';
import { notificationQueueService } from './services/notification-queue.service';
import { geocodeQueueService } from './services/geocode-queue.service';
import { startProxy, stopProxy } from './services/listmonk-proxy.service';
import { pagesService } from './modules/pages/pages.service';
import { canvassService } from './modules/map/canvass/canvass.service';
import { trackingService } from './modules/map/tracking/tracking.service';
import { verificationTokenService } from './services/verification-token.service';
import { passwordResetTokenService } from './services/password-reset-token.service';
import { paymentsPublicRouter } from './modules/payments/payments-public.routes';
import { paymentsAdminRouter } from './modules/payments/payments-admin.routes';
import { donationPagesPublicRouter } from './modules/payments/donation-pages-public.routes';
import { donationPagesAdminRouter } from './modules/payments/donation-pages-admin.routes';
import { webhookService } from './modules/payments/webhook.service';
import { galleryAdsPublicRouter } from './modules/gallery-ads/gallery-ads-public.routes';
import { galleryAdsAdminRouter } from './modules/gallery-ads/gallery-ads-admin.routes';
import { petitionsPublicRouter, petitionVerifyRouter } from './modules/influence/petitions/petitions-public.routes';
import { petitionsAdminRouter } from './modules/influence/petitions/petitions.routes';
import { effectivenessRouter } from './modules/influence/effectiveness/effectiveness.routes';
import { actionCampaignsRouter, actionCampaignsAdminRouter } from './modules/action-campaigns/action-campaigns.routes';
import { volunteerDashboardRouter } from './modules/volunteer-dashboard/volunteer-dashboard.routes';
import { docsAnalyticsPublicRouter, docsAnalyticsAdminRouter } from './modules/docs-analytics/docs-analytics.routes';
import { analyticsUserRouter } from './modules/analytics/analytics-user.routes';
import { analyticsAdminRouter } from './modules/analytics/analytics.routes';
import { docsCommentsPublicRouter, docsCommentsAdminRouter } from './modules/docs-comments/docs-comments.routes';
import { volunteerInviteRouter } from './modules/volunteer-invite/volunteer-invite.routes';
import { docsAnalyticsService } from './modules/docs-analytics/docs-analytics.service';
import { smsContactsRouter } from './modules/sms/contacts/sms-contacts.routes';
import { smsCampaignsRouter } from './modules/sms/campaigns/sms-campaigns.routes';
import { smsConversationsRouter } from './modules/sms/conversations/sms-conversations.routes';
import { smsMessagesRouter } from './modules/sms/messages/sms-messages.routes';
import { smsDeviceRouter } from './modules/sms/device/sms-device.routes';
import { smsSetupRouter } from './modules/sms/setup/sms-setup.routes';
import { smsTemplatesRouter } from './modules/sms/templates/sms-templates.routes';
import { smsQueueService } from './services/sms-queue.service';
import { smsResponseSyncService } from './services/sms-response-sync.service';
import { smsDeviceMonitorService } from './services/sms-device-monitor.service';
import { reengagementService } from './services/reengagement.service';
import { socialDigestService } from './services/social-digest.service';
import { termuxClient } from './services/termux.client';
import { registerProvisioners } from './services/user-provisioning';
import { peopleRouter } from './modules/people/people.routes';
import { participantNeedsRouter } from './modules/people/participant-needs.routes';
import { profilePublicRouter } from './modules/people/profile-public.routes';
import { searchRouter } from './modules/search/search.routes';
import { activityPublicRouter } from './modules/activity/activity-public.routes';
import { newsletterPublicRouter } from './modules/newsletter/newsletter-public.routes';
import { eventsListPublicRouter } from './modules/events/events-public.routes';
import { homepageRouter } from './modules/homepage/homepage.routes';
import { ogRouter } from './modules/og/og.routes';
import { socialRouter } from './modules/social/social.routes';
import { errorReportRouter } from './modules/reports/error-report.routes';
import calendarRoutes from './modules/calendar/calendar.routes';
import feedRoutes from './modules/calendar/feed.routes';
import sharedCalendarRoutes from './modules/calendar/shared-calendar.routes';
import { adminCalendarRouter } from './modules/calendar/admin-calendar.routes';
import { ticketedEventsPublicRouter } from './modules/ticketed-events/ticketed-events-public.routes';
import { ticketedEventsAdminRouter } from './modules/ticketed-events/ticketed-events-admin.routes';
import { checkinRouter } from './modules/ticketed-events/checkin.routes';
import { sseService } from './modules/social/sse.service';
import { presenceService } from './modules/social/presence.service';
import { upgradeService } from './modules/upgrade/upgrade.service';
import { autoUpgradeService } from './services/auto-upgrade.service';
import { calendarFeedQueueService } from './services/calendar-feed-queue.service';
import { scheduledJobsQueueService } from './services/scheduled-jobs-queue.service';
import { pollAutoFinalizeQueueService } from './services/poll-auto-finalize-queue.service';
import { pollAutoCloseQueueService } from './services/poll-auto-close-queue.service';
import { pollSseService } from './modules/polls/polls-sse.service';
import { agendaRouter } from './modules/meetings/agenda.routes';
import { actionItemsRouter } from './modules/meetings/action-items.routes';
import { WebSocketServer } from 'ws';
import { docsCollabService } from './modules/docs/docs-collab.service';
import { correlationId } from './middleware/correlation-id';
import cookieParser from 'cookie-parser';
import { registerAllEventListeners } from './services/event-listeners';
import { eventBus } from './services/event-bus.service';

const app = express();

// Trust proxy chain: Pangolin/Newt → Nginx → API (2 hops in production)
app.set('trust proxy', 2);

// --- Middleware Stack ---
app.use(correlationId);
app.use(cookieParser());

app.use(helmet({
  contentSecurityPolicy: env.CSP_ENABLED === 'true'
    ? {
        directives: {
          defaultSrc: ["'self'"],
          scriptSrc: ["'self'", "'unsafe-inline'"],
          styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
          fontSrc: ["'self'", 'https://fonts.gstatic.com', 'data:'],
          imgSrc: ["'self'", 'data:', 'blob:', 'https:'],
          connectSrc: ["'self'", 'wss:', 'ws:'],
          frameSrc: ["'self'", `*.${env.DOMAIN}`],
          frameAncestors: ["'self'", `*.${env.DOMAIN}`],
          objectSrc: ["'none'"],
          baseUri: ["'self'"],
          formAction: ["'self'"],
        },
      }
    : false,
}));

app.use(cors({
  origin: env.CORS_ORIGINS.split(',').map(s => s.trim()),
  credentials: true,
}));

app.use(compression());

// Stripe webhook — must receive raw body BEFORE express.json() parses it
app.post('/api/payments/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.headers['stripe-signature'];
  if (!signature || typeof signature !== 'string') {
    res.status(400).json({ error: 'Missing stripe-signature header' });
    return;
  }
  try {
    const event = await webhookService.constructEvent(req.body as Buffer, signature);
    await webhookService.handleEvent(event);
    res.json({ received: true });
  } catch (err: unknown) {
    const message = err instanceof Error ? err.message : 'Webhook error';
    res.status(400).json({ error: message });
  }
});

app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
app.use(globalRateLimit);

// Request metrics
app.use((req, res, next) => {
  const end = httpRequestDuration.startTimer();
  res.on('finish', () => {
    const route = req.route?.path || req.path;
    const labels = { method: req.method, route, status_code: res.statusCode.toString() };
    end(labels);
    httpRequestsTotal.inc(labels);
  });
  next();
});

// --- Health Check ---
// Public (unauthenticated) for Docker healthcheck compatibility, but the
// `?detailed=true` mode — which exposed disk space and internal service status
// to any unauthenticated caller — was moved to the authenticated `/api/metrics`
// consumers on 2026-04-12. The public path now returns only pass/fail for core
// DB + Redis.
app.get('/api/health', healthMetricsRateLimit, async (_req, res) => {
  const checks: Record<string, string> = {};

  try {
    await prisma.$queryRaw`SELECT 1`;
    checks.database = 'ok';
  } catch {
    checks.database = 'error';
  }

  try {
    await redis.ping();
    checks.redis = 'ok';
  } catch {
    checks.redis = 'error';
  }

  const coreHealthy = checks.database === 'ok' && checks.redis === 'ok';
  res.status(coreHealthy ? 200 : 503).json({
    status: coreHealthy ? 'healthy' : 'degraded',
    version: process.env.npm_package_version || 'unknown',
    checks,
  });
});

// --- Metrics Endpoint (authenticated - SUPER_ADMIN only) ---
app.get('/api/metrics', authenticate, requireRole('SUPER_ADMIN'), healthMetricsRateLimit, async (_req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

// --- Internal Metrics Endpoint (for Prometheus scraping within Docker network) ---
// Only accessible from Docker-internal network (nginx doesn't proxy this path externally)
app.get('/api/metrics/internal', async (req, res) => {
  // Basic network-level protection: only allow from Docker bridge / localhost
  const remoteIp = req.ip || req.socket.remoteAddress || '';
  const isInternal = remoteIp === '127.0.0.1' || remoteIp === '::1' ||
    remoteIp.startsWith('172.') || remoteIp.startsWith('10.') || remoteIp.startsWith('192.168.');
  if (!isInternal) {
    res.status(403).json({ error: 'Internal endpoint only' });
    return;
  }
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

// --- API Routes ---
app.use('/api/auth', authRouter);
app.use('/api/auth', identityBridgeRouter);                    // JSN bridge: server-to-server user provisioning
app.use('/api/auth', socialLinkBridgeRouter);                  // JSN bridge: social handle sync
app.use('/api/payments', donationBridgeRouter);                // JSN bridge: donation mirror
app.use('/api/volunteer', volunteerBridgeRouter);              // JSN bridge: volunteer dashboard data
app.use('/api/auth', ssoBridgeRouter);                         // JSN bridge: SSO redirect (issue + consume)
app.use('/api/auth', giteaSsoRouter);                          // Gitea SSO validation (nginx auth_request)
app.use('/api/users', usersRouter);
app.use('/api/users', provisioningRouter);                   // User provisioning management (ADMIN roles)
app.use('/api/campaigns', campaignPublicRouter);          // Public campaign details (no auth)
app.use('/api/campaigns', responseCampaignPublicRouter);  // Public response routes (no auth)
app.use('/api/campaigns', campaignEmailsPublicRouter);    // Public email routes (no auth)
app.use('/api/campaigns', campaignUserRouter);             // User campaign submission (auth, non-temp)
app.use('/api/campaigns', campaignModerationRouter);       // Moderation queue (admin auth)
app.use('/api/campaigns', campaignEmailsAdminRouter);     // Admin email routes (auth required)
app.use('/api/campaigns', campaignsRouter);               // Admin campaign CRUD (auth required)
app.use('/api/responses', responsesPublicRouter);         // Public response actions (no auth)
app.use('/api/responses', responsesAdminRouter);          // Admin response moderation (auth required)
app.use('/api/email-queue', emailQueueRouter);
app.use('/api/representatives', representativesRouter);
app.use('/api/map/locations', locationsPublicRouter);    // Public map data (no auth)
app.use('/api/map/locations', locationsAdminRouter);     // Admin location CRUD (auth required)
app.use('/api/map/locations/bulk-geocode', bulkGeocodeRouter); // Bulk re-geocoding (admin auth required)
app.use('/api/map/nar-import', narImportRouter);         // NAR server-side import (MAP_ADMIN+)
app.use('/api/map/area-import', areaImportRouter);       // Area import from multiple sources (MAP_ADMIN+)
app.use('/api/map/cuts', cutsPublicRouter);              // Public cut polygons (no auth)
app.use('/api/map/cuts', cutsAdminRouter);               // Admin cut CRUD (auth required)
app.use('/api/map/shifts', shiftsPublicRouter);          // Public shift listing + signup (no auth)
app.use('/api/map/shifts', shiftsVolunteerRouter);       // Volunteer shift signup (auth required)
app.use('/api/map/shifts/series', shiftSeriesRouter);    // Shift series management (MAP_ADMIN+)
app.use('/api/map/shifts', shiftsAdminRouter);           // Admin shift CRUD (auth required)
app.use('/api/map/geocoding', geocodingRouter);          // Geocoding search (MAP_ADMIN+)
app.use('/api/map/settings', mapSettingsRouter);         // Map settings (public GET, auth PUT)
app.use('/api/map/events', eventsPublicRouter);          // Public map events from Gancio (no auth)
app.use('/api/meeting-planner', meetingPlannerPublicRouter); // Public poll viewing + voting (no auth)
app.use('/api/meeting-planner', meetingPlannerAdminRouter);  // Admin poll CRUD (auth required)
app.use('/api/straw-polls', strawPollPublicRouter);          // Public straw poll voting + viewing (no auth)
app.use('/api/straw-polls', strawPollWidgetRouter);          // Straw poll widget endpoint (no auth, cached)
app.use('/api/straw-polls', strawPollAdminRouter);           // Admin straw poll CRUD (auth required)
app.use('/api/meetings/agendas', agendaRouter);              // Meeting agendas + minutes (EVENTS roles)
app.use('/api/meetings/action-items', actionItemsRouter);    // Action items CRUD (EVENTS roles / auth)
app.use('/api/qr', qrRouter);                           // QR code generation (public)
app.use('/api/listmonk', listmonkWebhookRouter);         // Listmonk webhook (shared secret, no JWT)
app.use('/api/listmonk', listmonkRouter);               // Listmonk newsletter sync (SUPER_ADMIN)
app.use('/api/email-templates', emailTemplatesRouter);  // Email template management (ADMIN roles)
app.use('/api/pages', pagesPublicRouter);                // Public landing pages (no auth)
app.use('/api/pages', pagesAdminRouter);                 // Admin landing page CRUD (auth required)
app.use('/api/page-blocks', blocksRouter);               // Admin page block library (auth required)
app.use('/api/docs', docsRouter);                        // Docs status + config (auth required)
app.use('/api/docs-access', docsAccessRouter);           // Docs access policies + share links
app.use('/api/gitea/setup', giteaSetupRouter);           // Gitea auto-setup (SUPER_ADMIN)
app.use('/api/services', servicesRouter);                // Platform services status (SUPER_ADMIN)
app.use('/api/map/canvass', canvassVolunteerRouter);       // Volunteer canvass routes (auth required)
app.use('/api/map/canvass', canvassAdminRouter);          // Admin canvass routes (MAP_ADMIN+)
app.use('/api/map/canvass', canvassExportRouter);        // Canvass-to-campaign export (admin roles)
app.use('/api/map/tracking', trackingVolunteerRouter);    // Volunteer GPS tracking (auth required)
app.use('/api/map/tracking', trackingAdminRouter);        // Admin GPS tracking (MAP_ADMIN+)
app.use('/api/settings', siteSettingsRouter);              // Site settings (public GET, SUPER_ADMIN PUT)
app.use('/api/pangolin', pangolinRouter);                  // Pangolin tunnel management (SUPER_ADMIN)
app.use('/api/ccp-registration', ccpRegistrationRouter);  // CCP remote management registration (SUPER_ADMIN)
app.use('/api/rocketchat', rocketchatRouter);              // Rocket.Chat SSO + status (auth required)
app.use('/api/rocketchat', rocketchatBridgeRouter);        // JSN bridge: server-to-server chat-token issuance
app.use('/api/jitsi', jitsiRouter);                        // Jitsi Meet JWT + status (auth required)
app.use('/api/observability', observabilityRouter);        // Observability / monitoring (SUPER_ADMIN)
app.use('/api/upgrade', upgradeRouter);                      // System upgrade management (SUPER_ADMIN)
app.use('/api/registry', registryRouter);                    // Container registry management (SUPER_ADMIN)
app.use('/api/dashboard', dashboardRouter);                  // Dashboard summary (ADMIN roles)
app.use('/api/donation-pages', donationPagesPublicRouter);    // Public donation pages (no auth)
app.use('/api/payments', paymentsPublicRouter);              // Public payment routes (plans, checkout, my subscription)
app.use('/api/payments/admin/donation-pages', donationPagesAdminRouter); // Admin donation page CRUD (SUPER_ADMIN)
app.use('/api/payments/admin', paymentsAdminRouter);         // Admin payment management (SUPER_ADMIN)
app.use('/api/petitions', petitionsPublicRouter);                // Public petition listing + signing (no auth)
app.use('/api/petitions', petitionVerifyRouter);                // Petition email verification (no auth)
app.use('/api/petitions', petitionsAdminRouter);                // Admin petition CRUD (INFLUENCE_ROLES)
app.use('/api/influence/effectiveness', effectivenessRouter);   // Campaign effectiveness analytics (ADMIN)
app.use('/api/action-campaigns', actionCampaignsRouter);        // Public action campaign (optional auth) + step complete (auth)
app.use('/api/admin/action-campaigns', actionCampaignsAdminRouter); // Admin action campaign CRUD (INFLUENCE_ROLES)
app.use('/api/volunteer', volunteerDashboardRouter);            // Volunteer dashboard aggregator (auth required)
app.use('/api/gallery-ads', galleryAdsPublicRouter);          // Public gallery ads (optional auth)
app.use('/api/gallery-ads/admin', galleryAdsAdminRouter);     // Admin gallery ad CRUD (SUPER_ADMIN)
app.use('/api/docs-analytics', docsAnalyticsPublicRouter);    // Public docs page view tracking (no auth)
app.use('/api/docs-analytics', docsAnalyticsAdminRouter);     // Admin docs analytics (ADMIN roles)
app.use('/api/analytics', analyticsUserRouter);                // User self-service analytics (any auth)
app.use('/api/analytics', analyticsAdminRouter);               // Admin unified analytics (ANALYTICS_ROLES)
app.use('/api/docs-comments', docsCommentsPublicRouter);      // Public docs comments (CORS override for docs origin)
app.use('/api/docs-comments', docsCommentsAdminRouter);       // Admin docs comment moderation (ADMIN roles)
app.use('/api/volunteer-invite', volunteerInviteRouter);       // Quick join invite (admin generate + public redeem)
app.use('/api/sms/contacts', smsContactsRouter);               // SMS contact list CRUD + import (ADMIN roles)
app.use('/api/sms/campaigns', smsCampaignsRouter);             // SMS campaign CRUD + execution (ADMIN roles)
app.use('/api/sms/conversations', smsConversationsRouter);     // SMS conversation threads (ADMIN roles)
app.use('/api/sms/messages', smsMessagesRouter);               // SMS message history + ad-hoc send (ADMIN roles)
app.use('/api/sms/device', smsDeviceRouter);                   // SMS device status + sync trigger (ADMIN roles)
app.use('/api/sms/templates', smsTemplatesRouter);               // SMS template CRUD (ADMIN roles)
app.use('/api/sms/setup', smsSetupRouter);                     // SMS setup wizard (SUPER_ADMIN only)
app.use('/api/profile', profilePublicRouter);                     // Self-service contact profile (no auth, token-based)
app.use('/api/people/needs', participantNeedsRouter);           // Participant needs (self-service + EVENTS roles)
app.use('/api/people', peopleRouter);                           // People CRM aggregation (ADMIN roles)
app.use('/api/search', searchRouter);                           // Public unified search (no auth, rate-limited)
app.use('/api/activity', activityPublicRouter);                 // Public activity feed (no auth)
app.use('/api/newsletter', newsletterPublicRouter);             // Public newsletter subscribe (no auth, rate-limited)
app.use('/api/events', eventsListPublicRouter);                  // Public events list from Gancio (no auth, cached)
app.use('/api/homepage', homepageRouter);                        // Public homepage aggregation (no auth, cached)
app.use('/api/og', ogRouter);                                    // OG meta tags for social sharing bots (no auth, cached)
app.use('/api/social', socialRouter);                              // Social connections (auth required)
app.use('/api/ticketed-events/admin', ticketedEventsAdminRouter);   // Admin ticketed event CRUD (auth + permission) — MUST be before public /:slug
app.use('/api/ticketed-events/checkin', checkinRouter);             // Check-in scanner routes (auth required)
app.use('/api/ticketed-events', ticketedEventsPublicRouter);         // Public ticketed event listing + checkout (no auth)
app.use('/api/public/error-report', errorReportRouter);            // Public 404 error reporting (rate-limited)
app.use('/api/admin/calendar/shared', adminCalendarRouter);          // Admin calendar views (SUPER_ADMIN/MAP_ADMIN)
app.use('/api/calendar/shared', sharedCalendarRoutes);              // Shared calendar views + collaboration (public share + auth)
app.use('/api/calendar', feedRoutes);                                // ICS feed subscriptions + export (public .ics + auth)
app.use('/api/calendar', calendarRoutes);                            // Personal calendar layers + items (auth required)

// --- API 404 Handler (catch unmatched /api/* routes) ---
app.use('/api/*', (_req, res) => {
  res.status(404).json({ error: { message: 'Route not found', code: 'NOT_FOUND' } });
});

// --- Error Handler (must be last) ---
app.use(errorHandler);

// --- Start Server ---
async function start() {
  try {
    await prisma.$connect();
    logger.info('Database connected');

    // Initialize encryption for DB-stored secrets (SMTP password, etc.)
    initEncryption(env.ENCRYPTION_KEY);

    // Warn if Listmonk sync is enabled but webhook secret is not configured
    if (env.LISTMONK_SYNC_ENABLED === 'true' && !env.LISTMONK_WEBHOOK_SECRET) {
      logger.warn('LISTMONK_SYNC_ENABLED is true but LISTMONK_WEBHOOK_SECRET is not set. Unsubscribe events from Listmonk will not be processed.');
    }

    // Register user provisioning framework
    registerProvisioners();

    // Register EventBus listeners (Listmonk, RC, CRM, Calendar, n8n, Gancio)
    registerAllEventListeners();

    // Rebuild SMTP transporter from DB settings (env fallback for empty fields)
    await emailService.rebuildTransporter();

    emailQueueService.startWorker();
    notificationQueueService.startWorker();
    geocodeQueueService.startWorker();
    calendarFeedQueueService.startWorker();
    scheduledJobsQueueService.startWorker();
    pollAutoFinalizeQueueService.startWorker();
    pollAutoCloseQueueService.startWorker();
    startProxy();

    // Load SMS config from DB (env fallback for empty fields)
    await termuxClient.configureFromDb();

    // Load Tailscale config from DB (for SMS setup device discovery)
    const { tailscaleClient } = await import('./services/tailscale.client');
    await tailscaleClient.configureFromDb();

    // Start SMS services if enabled
    if (termuxClient.enabled) {
      smsQueueService.startWorker();
      smsResponseSyncService.start();
      smsDeviceMonitorService.start();
      logger.info('SMS integration enabled (Termux API)');
    }

    // One-time startup calls (recurring runs handled by scheduled-jobs queue)
    verificationTokenService.cleanupExpiredTokens().catch(() => {});
    passwordResetTokenService.cleanupExpiredTokens().catch(() => {});
    canvassService.closeAbandonedSessions().catch(() => {});
    trackingService.cleanupOldData(30).catch(() => {});
    trackingService.closeStaleTrackingSessions(120).catch(() => {});
    docsAnalyticsService.cleanupOldData(90).catch(() => {});
    reengagementService.scan().catch(() => {});
    socialDigestService.scan().catch(() => {});

    // Gitea auto-setup (if admin password is provided, auto-configure token + repos)
    giteaSetupService.autoSetupIfNeeded().catch(() => {});

    // SSE + Presence: mark all users offline on startup, start heartbeat + stale cleanup
    presenceService.markAllOffline().catch(() => {});
    sseService.startHeartbeat();
    pollSseService.startHeartbeat();
    setInterval(() => presenceService.cleanupStale().catch(() => {}), 60 * 1000); // every 1 min

    // Challenge lifecycle: activate/complete/score every 5 minutes
    import('./services/challenge-scoring.service').then(({ challengeScoringService }) => {
      challengeScoringService.processLifecycle().catch(() => {});
      setInterval(() => challengeScoringService.processLifecycle().catch(() => {}), 5 * 60 * 1000);
      logger.info('Challenge lifecycle processor started (every 5min)');
    }).catch(() => {});

    // Clean up stale upgrade progress on startup
    upgradeService.clearStaleProgress();

    // Archive any pending upgrade result to history + send notifications
    autoUpgradeService.handlePostRestartResult().catch(() => {});
    // Start auto-upgrade scheduler if enabled
    autoUpgradeService.start().catch(() => {});

    // Setup Rocket.Chat notification channels (non-blocking)
    rocketchatWebhookService.setupChannels().catch(() => {});

    // Sync CML site settings → Gancio (with retry for slow Gancio startup)
    if (gancioClient.enabled) {
      const maxAttempts = 3;
      const retryDelay = 20000; // 20s — covers Gancio's 60s start_period
      const attemptSync = async (attempt: number): Promise<void> => {
        try {
          const available = await gancioClient.isAvailable();
          if (!available) throw new Error('Gancio not available');
          await gancioSettingsSyncService.syncAll();
        } catch {
          if (attempt < maxAttempts) {
            logger.debug(`Gancio settings sync attempt ${attempt}/${maxAttempts} failed, retrying in ${retryDelay / 1000}s`);
            setTimeout(() => attemptSync(attempt + 1).catch(() => {}), retryDelay);
          } else {
            logger.warn(`Gancio settings sync failed after ${maxAttempts} attempts — will sync on next settings update`);
          }
        }
      };
      attemptSync(1).catch(() => {});
    }

    // Sync MkDocs overrides on startup
    pagesService.syncOverrides()
      .then(({ imported, updated }) => {
        if (imported > 0 || updated > 0) {
          logger.info(`Startup sync: imported ${imported}, updated ${updated} MkDocs overrides`);
        }
      })
      .catch((err) => {
        logger.warn('Startup sync of MkDocs overrides failed:', err);
      });

    // Check for docs reset flag (set by config.sh during setup)
    const docsResetFlagPath = path.resolve(path.dirname(env.MKDOCS_CONFIG_PATH), '.reset-docs-on-startup');
    if (existsSync(docsResetFlagPath)) {
      const { docsResetService } = await import('./modules/docs/docs-reset.service');
      docsResetService.resetToBaseline()
        .then((result) => {
          logger.info(`Docs reset completed: ${result.filesReset} files reset, ${result.filesPreserved} preserved`);
          unlinkSync(docsResetFlagPath);
        })
        .catch((err) => logger.warn('Docs reset from config flag failed:', err));
    }

    // Validate MkDocs exports on startup (recurring runs handled by scheduled-jobs queue)
    pagesService.validateExports()
      .then(({ validated, repaired, errors }) => {
        if (repaired > 0 || errors.length > 0) {
          logger.info(`Validation: ${validated} checked, ${repaired} repaired, ${errors.length} errors`);
        }
      })
      .catch((err) => logger.warn('Validation failed:', err));

    const server = app.listen(env.PORT, () => {
      logger.info(`API server running on port ${env.PORT} [${env.NODE_ENV}]`);
    });

    // --- WebSocket upgrade handler for docs collaboration ---
    const wss = new WebSocketServer({ noServer: true });
    server.on('upgrade', (request, socket, head) => {
      const url = new URL(request.url || '', `http://${request.headers.host}`);
      // HocuspocusProvider connects to the base URL; document name is sent via protocol
      const collabPath = '/api/docs/collaborate';
      if (url.pathname !== collabPath && !url.pathname.startsWith(collabPath + '/')) {
        socket.destroy();
        return;
      }
      wss.handleUpgrade(request, socket, head, (ws) => {
        // Document name may be in the URL path (y-websocket) or sent via protocol (HocuspocusProvider)
        const pathSuffix = url.pathname.slice(collabPath.length + 1); // strip /api/docs/collaborate/
        const documentName = pathSuffix ? decodeURIComponent(pathSuffix) : '';
        const token = url.searchParams.get('token') || '';
        docsCollabService.handleConnection(ws, request, { documentName, token });
      });
    });

    // Clean stale collab states on startup (recurring runs handled by scheduled-jobs queue)
    docsCollabService.cleanupStaleStates().catch(() => {});
  } catch (err) {
    logger.error('Failed to start server:', err);
    process.exit(1);
  }
}

start();

// Graceful shutdown
for (const signal of ['SIGTERM', 'SIGINT']) {
  process.on(signal, async () => {
    logger.info(`${signal} received, shutting down...`);
    sseService.closeAll();
    pollSseService.closeAll();
    await docsCollabService.shutdown();
    await stopProxy();
    await emailQueueService.close();
    await notificationQueueService.close();
    await geocodeQueueService.close();
    await smsQueueService.close();
    await calendarFeedQueueService.close();
    await scheduledJobsQueueService.close();
    await prisma.$disconnect();
    redis.disconnect();
    process.exit(0);
  });
}

export { app };
