// dashboard-server — Phase 4 of CONSOLIDATION_PLAN.md.
// Merges the four per-vendor PM2 apps (klaviyo, meta, google-analytics, typeform)
// into a single ESM service on DASHBOARD_PORT (default 3015).
//
// Mount points (matches Caddy proxy paths):
//   /api/klaviyo/*             → routes/klaviyo (was klaviyo-server :3004)
//   /api/meta/*                → routes/meta    (was meta-server    :3005)
//   /api/dashboard-analytics/* → routes/google  (was google-server  :3007 via Caddy /api/analytics rewrite)
//   /api/typeform/*            → routes/typeform (was typeform-server :3008)
//
// Shared infrastructure (Phase 2 + Phase 6):
//   - shared/auth/middleware.js  authenticate() guards /api/* (Phase 6.1/6.2 — second line of defense)
//   - shared/cors/policy.js      explicit allowed-origins list (Phase 6.6)
//   - shared/logging/request-log.js  pino-http, Authorization/Cookie redacted (Phase 6.5/6.9)
//   - shared/errors/handler.js   consistent error envelope, no leak in prod
//   - shared/db/pg.js / shared/db/redis.js  one Pool + one ioredis client for all vendors
//
// Per-route permission gates (Phase 6.2):
//   - meta_write     PATCH/POST mutations to Meta campaigns
//   - klaviyo_admin  POST /api/klaviyo/events/clearCache (operational maintenance)
// Read-only Google + Typeform endpoints stay authenticated-only.

import { config as loadEnv } from 'dotenv';
import express from 'express';
import cors from 'cors';
import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';

import { authenticate } from '../shared/auth/middleware.js';
import { corsOptions } from '../shared/cors/policy.js';
import { createPool } from '../shared/db/pg.js';
import { createRedis } from '../shared/db/redis.js';
import { errorHandler } from '../shared/errors/handler.js';
import { logger } from '../shared/logging/logger.js';
import { requestLog } from '../shared/logging/request-log.js';

import { createKlaviyoRouter } from './routes/klaviyo/index.js';
import { createMetaRouter } from './routes/meta/index.js';
import { createGoogleRouter } from './routes/google/index.js';
import { createTypeformRouter } from './routes/typeform/index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// Layer envs: shared inventory .env wins on collisions (security-critical vars come
// from one place); vendor-specific keys come from the per-service .env.
//
// dotenv defaults to override:false so the first file loaded wins. Order matters.
const sharedEnvPath = '/var/www/inventory/.env';
const dashboardEnvPath = path.resolve(__dirname, '.env');

if (fs.existsSync(sharedEnvPath)) loadEnv({ path: sharedEnvPath });
if (fs.existsSync(dashboardEnvPath)) loadEnv({ path: dashboardEnvPath });

// Phase 6.4 — refuse to start without JWT_SECRET. Without it authenticate() falls
// back to res.status(401) on every request and the service is useless anyway.
if (!process.env.JWT_SECRET) {
  logger.error('JWT_SECRET is not set; refusing to start (per Phase 6.4)');
  process.exit(1);
}

const app = express();
const PORT = Number(process.env.DASHBOARD_PORT) || 3015;

// Trust X-Forwarded-* only when the immediate hop is loopback (Caddy on the same
// host). Required for the KIOSK_IPS bypass in shared/auth/middleware.js to see
// real client IPs instead of 127.0.0.1.
app.set('trust proxy', 'loopback');

// Single Postgres pool — used by authenticate() to load user permissions.
// All four vendors share this pool (auth lookups are the only DB hits at runtime).
const pool = createPool('DB');

// Single ioredis client shared across all vendors. lazyConnect:true means the
// first .get/.set triggers the actual connect — keeps startup non-blocking even
// if Redis is temporarily unavailable, and aligns with shared/db/redis.js defaults.
const redis = createRedis();

app.use(requestLog());
app.use(cors(corsOptions));
app.use(express.json({ limit: '10mb' }));

// Phase 6.1/6.2: every /api request requires a valid JWT. authenticate() also
// loads user permissions, which the per-route requirePermission() checks rely on.
app.use('/api', authenticate({ pool, secret: process.env.JWT_SECRET }));

app.use('/api/klaviyo', createKlaviyoRouter({ redis }));
app.use('/api/meta', createMetaRouter());
// Note: frontend calls /api/dashboard-analytics (Caddy used to rewrite it to
// /api/analytics for the standalone google-server). Mount at the public path so
// Caddy can drop the rewrite — see Caddyfile.proposed.
app.use('/api/dashboard-analytics', createGoogleRouter({ redis }));
app.use('/api/typeform', createTypeformRouter({ redis }));

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    service: 'dashboard-server',
    redis: redis.status,
  });
});

app.use(errorHandler);

// Connect Redis up front so the first request doesn't pay the connect cost.
// Failures here are non-fatal — vendors degrade to cache-miss → upstream fetch.
redis.connect().catch((err) => {
  logger.error({ err: { message: err.message, code: err.code } }, 'redis lazy-connect failed');
});

const server = app.listen(PORT, '0.0.0.0', () => {
  logger.info({ port: PORT, mode: process.env.NODE_ENV || 'development' }, 'dashboard-server listening');
});

const shutdown = async (signal) => {
  logger.info({ signal }, 'dashboard-server shutting down');
  server.close();
  try { await redis.quit(); } catch { /* ignore */ }
  try { await pool.end(); } catch { /* ignore */ }
  process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));

process.on('uncaughtException', (err) => {
  logger.error({ err: { message: err.message, stack: err.stack } }, 'uncaughtException');
  process.exit(1);
});
process.on('unhandledRejection', (reason) => {
  logger.error({ reason }, 'unhandledRejection');
});
