/**
 * Shield — Main Application Entry
 * CommonJS Express app, cPanel / Phusion Passenger compatible.
 */

'use strict';

require('dotenv').config();

const path     = require('path');
const fs       = require('fs');
const express  = require('express');
const helmet   = require('helmet');
const session  = require('express-session');
const cookieParser = require('cookie-parser');
const cron     = require('node-cron');

const db            = require('./config/db');
const sessionConfig = require('./config/session');
const { assertProductionEnv } = require('./config/assertProductionEnv');
const { publicBaseUrl } = require('./config/publicBaseUrl');
const { logger, requestLogger } = require('./middleware/logger');

// Refuse to boot in production with missing env. Done before anything
// else so we fail fast.
assertProductionEnv();
console.log(`[ncii-shield] public base url = ${publicBaseUrl()}`);

// Middleware
const csrfMiddleware      = require('./middleware/csrf');
const loginRateLimit      = require('./middleware/rateLimit');
const { loadUser, exposeLocals } = require('./middleware/auth');

// Routes
const authRoutes    = require('./routes/auth');
const dashboardRoutes = require('./routes/dashboard');
const caseRoutes    = require('./routes/cases');
const scanRoutes    = require('./routes/scan');
const takedownRoutes = require('./routes/takedown');
const billingRoutes = require('./routes/billing');
const webhookRoutes = require('./routes/webhook');
const adminRoutes   = require('./routes/admin');
const apiRoutes     = require('./routes/api');

// Background jobs
const keyRotator        = require('./services/keyRotator');
const { dailyResetJob, alertOnExhaustion } = require('./jobs/dailyReset');
const autoEscalate      = require('./jobs/autoEscalate');
const takedownWorker    = require('./jobs/takedownWorker');
const caseDiscoveryWorker = require('./jobs/caseDiscoveryWorker');
const evidenceRetention = require('./jobs/evidenceRetention');
const linkHealthWorker  = require('./jobs/linkHealthWorker');
const weeklyDigest      = require('./jobs/weeklyDigest');
const backupWorker      = require('./jobs/backup');
const logRotation       = require('./jobs/logRotation');

const app  = express();
const PORT = parseInt(process.env.PORT, 10) || 3000;

// Trust first proxy (cPanel / Passenger)
app.set('trust proxy', parseInt(process.env.TRUST_PROXY, 10) || 1);

// Views directory — we serve static HTML directly; no engine required.
app.set('views', path.join(__dirname, 'public', 'views'));

// Global request timeout — must come BEFORE everything else so the
// timer wraps every layer (routes, body parser, etc.). Hard deadline
// per request, with longer budgets for the slow /scan/* and
// /cases/* endpoints.
const { timeout: requestTimeout } = require('./middleware/timeout');
app.use(requestTimeout);

// Security headers
app.use(helmet({
  contentSecurityPolicy: {
    useDefaults: true,
    directives: {
      'default-src':     ["'self'"],
      'script-src':      ["'self'", "'unsafe-inline'", 'https://js.stripe.com'],
      'style-src':       ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
      'img-src':         ["'self'", 'data:', 'https:'],
      'connect-src':     ["'self'", 'https://api.stripe.com'],
      'frame-src':       ["'self'", 'https://js.stripe.com', 'https://hooks.stripe.com'],
      'font-src':        ["'self'", 'https:', 'data:'],
      'form-action':     ["'self'"],
      'frame-ancestors': ["'self'"],
      'base-uri':        ["'self'"],
      'object-src':      ["'none'"],
      'script-src-attr': ["'none'"],
      'upgrade-insecure-requests': [],
    },
    reportOnly: false,
  },
  crossOriginEmbedderPolicy: false,
  crossOriginOpenerPolicy:   { policy: 'same-origin' },
  crossOriginResourcePolicy: { policy: 'same-origin' },
  referrerPolicy:            { policy: 'strict-origin-when-cross-origin' },
  hsts: {
    maxAge: 63072000,                       // 2 years
    includeSubDomains: true,
    preload: true,
  },
  noSniff:           true,
  ieNoOpen:          true,
  frameguard:        { action: 'sameorigin' },
  permittedCrossDomainPolicies: { permittedPolicies: 'none' },
}));

// Stripe webhook MUST receive raw body — register BEFORE json parser.
// CSRF exemption: webhook routes never receive the CSRF cookie, so they
// MUST be mounted before csrfMiddleware. If you add a new webhook
// (e.g. Resend inbound, GitHub), mount it here too — never inside
// csrfMiddleware's scope.
app.use('/webhook/stripe', express.raw({ type: 'application/json' }), webhookRoutes);

// Liveness + readiness probe. No auth, no CSRF.
const healthRoutes = require('./routes/health');
app.use('/health', healthRoutes);

// Structured logger + request-id correlation. Must come before any
// route that wants to log with the request id.
app.use(requestLogger);

app.use(express.json({ limit: '2mb' }));
app.use(express.urlencoded({ extended: true, limit: '2mb' }));
app.use(cookieParser());

// Session
app.use(session(sessionConfig));

// Make user/csrf available in all routes
app.use(loadUser);
app.use(exposeLocals);
app.use(csrfMiddleware);  // issue cookie on every request; verify on unsafe methods

// Static — served from /public/* on disk. We resolve the public dir
// against __dirname first (normal case) and fall back to process.cwd()
// (in case cPanel/Passenger ever spawns the app from a different cwd
// than the script's directory, e.g. when started from an admin tool).
// If neither path resolves, we log loud rather than 404-ing silently.
const _publicDirCandidates = [
  path.join(__dirname, 'public'),
  path.join(process.cwd(), 'public'),
];
const _publicDir = _publicDirCandidates.find(p => {
  try { return fs.existsSync(p); } catch { return false; }
});
if (!_publicDir) {
  // eslint-disable-next-line no-console
  console.error(`[fatal] public/ not found in any of: ${_publicDirCandidates.join(', ')}`);
  process.exit(1);
}
if (_publicDir !== _publicDirCandidates[0]) {
  // eslint-disable-next-line no-console
  console.warn(`[boot] serving static from ${_publicDir} (cwd fallback; __dirname had no public/)`);
}
app.use('/public', express.static(_publicDir, {
  maxAge: process.env.NODE_ENV === 'production' ? '7d' : 0,
  etag: true,
  // Don't redirect directory requests — return 404 instead, so missing
  // assets don't 301-loop browsers into odd places.
  redirect: false,
}));

// Public pages (no auth)
app.get('/',          (req, res) => res.sendFile(path.join(__dirname, 'public', 'views', 'index.html')));
app.get('/login',     (req, res) => res.sendFile(path.join(__dirname, 'public', 'views', 'login.html')));
app.get('/register',  (req, res) => res.sendFile(path.join(__dirname, 'public', 'views', 'register.html')));

// Marketing site — public, no auth, no CSRF on GET.
// Helper used by every page below; single source of truth for resolving
// the HTML path under public/views/.
function sendPublicPage(file) {
  return function (_req, res) {
    res.sendFile(path.join(__dirname, 'public', 'views', file));
  };
}

// Top-level marketing pages
app.get('/how-it-works',         sendPublicPage('how-it-works.html'));
app.get('/who-it-helps',         sendPublicPage('who-it-helps.html'));
app.get('/features',             sendPublicPage('features.html'));
app.get('/pricing',              sendPublicPage('pricing.html'));
app.get('/about',                sendPublicPage('about.html'));
app.get('/contact',              sendPublicPage('contact.html'));
app.get('/trust',                sendPublicPage('trust.html'));

// Legacy /article and /blog/article URLs — redirect to the new SEO-friendly
// /blog hub. (These used to be /article?id=1 — now everything lives at
// /blog/:slug. If any external link still points to the old URLs, send
// people to the listing page where they can find the new structure.)
app.get(['/article', '/article/'], (_req, res) => res.redirect(301, '/blog'));
app.get(['/blog/article', '/blog/article/'], (_req, res) => res.redirect(301, '/blog'));

// Survivor and creator-focused landing pages
app.get('/for-survivors',        sendPublicPage('for-survivors.html'));
app.get('/for-creators',         sendPublicPage('for-creators.html'));

// Policy pages
app.get('/privacy',              sendPublicPage('privacy.html'));
app.get('/terms',                sendPublicPage('terms.html'));
app.get('/cookies',              sendPublicPage('cookies.html'));
app.get('/dmca',                 sendPublicPage('dmca.html'));
app.get('/acceptable-use',       sendPublicPage('acceptable-use.html'));
app.get('/onlyfans',             sendPublicPage('onlyfans.html'));
app.get('/fansly',               sendPublicPage('fansly.html'));
app.get('/webcam-models',        sendPublicPage('webcam-models.html'));

// SEO files — served from /public/ at the root path
app.get('/robots.txt',           (_req, res) => res.type('text/plain').sendFile(path.join(__dirname, 'public', 'robots.txt')));

// Public unauthenticated API — live counters, embed widgets, etc.
// Privacy rule: counts only, no per-user data, no IDs.
app.use('/', require('./routes/public'));
app.get('/sitemap.xml',          (_req, res) => res.type('application/xml').sendFile(path.join(__dirname, 'public', 'sitemap_index.xml')));
app.get('/llms.txt',             (_req, res) => res.type('text/plain').sendFile(path.join(__dirname, 'public', 'llms.txt')));
app.get('/llms-full.txt',        (_req, res) => res.type('text/plain').sendFile(path.join(__dirname, 'public', 'llms-full.txt')));
// SITEMAP-ROUTES-AUTO
app.get('/sitemap_index.xml',    (_req, res) => res.type('application/xml').sendFile(path.join(__dirname, 'public', 'sitemap_index.xml')));
app.get('/sitemap-pages.xml',    (_req, res) => res.type('application/xml').sendFile(path.join(__dirname, 'public', 'sitemap-pages.xml')));
app.get('/sitemap-blog.xml',     (_req, res) => {
  const BlogPost = require('./models/blogPost');
  const base = publicBaseUrl().replace(/\/+$/, '');
  const xmlEsc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
    '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;',
  })[c]);
  const abs = url => /^https?:\/\//i.test(String(url || ''))
    ? String(url)
    : base + (String(url || '').startsWith('/') ? String(url || '') : '/' + String(url || ''));
  const posts = BlogPost.listPublished(500);
  const urls = posts.map(p => `  <url>
    <loc>${xmlEsc(p.canonical_url)}</loc>
    <lastmod>${new Date(p.updated_at).toISOString().slice(0, 10)}</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
    <image:image>
      <image:loc>${xmlEsc(abs(p.og_image_url))}</image:loc>
      <image:title>${xmlEsc(p.title)}</image:title>
    </image:image>
  </url>`).join('\n');
  res.type('application/xml').send(`<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">
${urls}
</urlset>
`);
});
app.get('/llms-manifest.json',   (_req, res) => res.type('application/json').sendFile(path.join(__dirname, 'public', 'llms-manifest.json')));

// Auth
app.use('/auth', loginRateLimit, authRoutes);

// 2FA — enrollment, disable, status. The login-2fa verify endpoint lives
// under /2fa so it's reachable from the login page before the user has
// a "real" session.
const twofaRoutes = require('./routes/twofa');
app.use('/2fa', twofaRoutes);

// Authenticated HTML pages (require session, but no CSRF on GET).
// IMPORTANT: declared BEFORE the API routers so a bare /cases (no trailing
// slash) returns the page, not the JSON list.
const { requireLogin: _rl } = require('./middleware/auth');
function makePageSender(file) {
  return function (_req, res) { res.sendFile(path.join(__dirname, 'public', 'views', file)); };
}
app.get(['/cases',    '/cases/'],    _rl, makePageSender('case.html'));
app.get(['/scan',     '/scan/'],     _rl, makePageSender('scan.html'));
app.get(['/takedown', '/takedown/'], _rl, makePageSender('takedown.html'));
app.get(['/billing',  '/billing/'],  _rl, makePageSender('billing.html'));

// Authenticated API (must come AFTER the HTML page routes above).
// /scan and /cases/:id/discover are the most expensive endpoints
// (Scrape.do + MiniMax + sharp + RDAP). Cap them harder.
const { api: apiLimiter, scan: scanLimiter } = require('./middleware/rateLimit');
app.use('/dashboard', dashboardRoutes);
app.use('/cases',     scanLimiter, caseRoutes);
app.use('/scan',      scanLimiter, scanRoutes);
app.use('/takedown',  takedownRoutes);
// Public blog (DB-backed, slug-based URLs, server-rendered SEO meta)
app.use('/blog', require('./routes/blog'));

app.use('/',          require('./routes/takedownCheckNow'));   // POST /takedown/:id/check-now
app.use('/',          require('./routes/takedownNotes'));      // GET/POST /takedown/:id/notes, /takedown/notes/:noteId, /takedown/notes-counts
app.use('/billing',   billingRoutes);
app.use('/',          require('./routes/findings'));   // /findings/:id/{page,save.mhtml,classify,classification}, /findings/search

// Superadmin
app.use('/admin', adminRoutes);
app.use('/admin', require('./routes/adminWebhooks'));   // /admin/webhooks + /admin/digest-subscriptions

// Admin → support contact form. IDOR-safe by construction (UUID PKs,
// all reads scoped to req.user.id). Mounted at /api/admin/contact.
app.use('/api/admin/contact', require('./routes/contact'));

// Agency REST API
app.use('/api/v1', apiRoutes);

// 404
app.use((req, res) => {
  res.status(404).sendFile(path.join(__dirname, 'public', 'views', '404.html'), err => {
    if (err) res.status(404).send('Not Found');
  });
});

// Error handler
app.use((err, req, res, _next) => {
  logger.error('http.error', {
    method: req.method, path: req.originalUrl,
    status: err.status || 500,
    message: err.message,
    stack: err.stack && err.stack.slice(0, 1500),
  });
  if (res.headersSent) return;
  res.status(err.status || 500).json({
    error: err.code || 'INTERNAL_ERROR',
    message: process.env.NODE_ENV === 'production'
      ? 'Something went wrong.'
      : err.message,
  });
});

// Initialise DB schema & cron jobs
db.init();
keyRotator.bootstrapFromEnv(); // seed env-level keys if api_keys table is empty
alertOnExhaustion.start();     // periodic check for exhausted services
dailyResetJob.start();         // reset counters at midnight
// Register worker state for /health (see routes/health.js)
global.__ncii_workers = {
  caseDiscovery: 'pending',
  takedown: 'pending',
  autoEscalate: 'pending',
  linkHealth: 'pending',
  weeklyDigest: 'pending',
  backup: 'pending',
};
autoEscalate.start();          // auto-escalate stale takedowns every 30 min
takedownWorker.start();        // async takedown processor (queue)
caseDiscoveryWorker.start();   // async deep discovery (Google → validate → persist)
evidenceRetention.start();     // prune old scan rows + audit JSONs daily
linkHealthWorker.start();      // re-probe every sent takedown's URL every 4h
weeklyDigest.start();           // send weekly/daily digest emails at the configured hour
backupWorker.start();           // daily sqlite backup → backups/prod-data-<timestamp>/
logRotation.start();            // rotate server.log / server-err.log when they hit 10MB
global.__ncii_workers.autoEscalate = 'running';
global.__ncii_workers.takedown = 'running';
global.__ncii_workers.caseDiscovery = 'running';
global.__ncii_workers.linkHealth = 'running';
global.__ncii_workers.weeklyDigest = 'running';
global.__ncii_workers.backup = 'running';

// Cron: daily reset at 00:00
cron.schedule('0 0 * * *', () => {
  console.log('[cron] daily reset of API key counters');
  db.resetDailyKeyCounters();
});

// Start server — Passenger will set port; for local dev use PORT
if (require.main === module && !process.env.PASSENGER_APP_ENV) {
  app.listen(PORT, () => {
    console.log(`[ncii-shield] listening on :${PORT} (env=${process.env.NODE_ENV})`);
  });
}

module.exports = app;
