const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const crypto = require('crypto');
const fs = require('fs');
const http = require('http');
const https = require('https');
require('dotenv').config();

// Setup logging with levels
// Levels: debug (0), info (1), warn (2), error (3), silent (4)
const LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
const currentLevel = LOG_LEVELS[(process.env.LOG_LEVEL || 'info').toLowerCase()] || 1;

// Log file lives in DATA_DIR so the non-root container user can write to it
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '../data');
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });

const LOG_PATH = path.join(DATA_DIR, 'server.log');
const LOG_MAX_BYTES = 10 * 1024 * 1024; // 10 MB per file
const LOG_KEEP = 3;                      // keep 3 rotated files

function rotateLogIfNeeded() {
  try {
    const stat = fs.statSync(LOG_PATH);
    if (stat.size < LOG_MAX_BYTES) return;
    for (let i = LOG_KEEP - 1; i >= 1; i--) {
      const src = `${LOG_PATH}.${i}`;
      const dst = `${LOG_PATH}.${i + 1}`;
      if (fs.existsSync(src)) fs.renameSync(src, dst);
    }
    fs.renameSync(LOG_PATH, `${LOG_PATH}.1`);
  } catch { /* ignore rotation errors — don't crash the server */ }
}

rotateLogIfNeeded();
const logFile = fs.createWriteStream(LOG_PATH, { flags: 'a' });
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
const originalConsoleDebug = console.debug;

function shouldLog(level) {
  return level >= currentLevel;
}

console.debug = function(...args) {
  if (!shouldLog(LOG_LEVELS.debug)) return;
  const message = args.join(' ');
  originalConsoleDebug.apply(console, args);
  logFile.write(`[${new Date().toISOString()}] DEBUG: ${message}\n`);
};

console.log = function(...args) {
  if (!shouldLog(LOG_LEVELS.info)) return;
  const message = args.join(' ');
  originalConsoleLog.apply(console, args);
  logFile.write(`[${new Date().toISOString()}] ${message}\n`);
};

console.warn = function(...args) {
  if (!shouldLog(LOG_LEVELS.warn)) return;
  const message = args.join(' ');
  originalConsoleWarn.apply(console, args);
  logFile.write(`[${new Date().toISOString()}] WARN: ${message}\n`);
};

console.error = function(...args) {
  if (!shouldLog(LOG_LEVELS.error)) return;
  const message = args.join(' ');
  originalConsoleError.apply(console, args);
  logFile.write(`[${new Date().toISOString()}] ERROR: ${message}\n`);
};

const sabnzbdRoutes = require('./routes/sabnzbd');
const sonarrRoutes = require('./routes/sonarr');
const radarrRoutes = require('./routes/radarr');
const embyRoutes = require('./routes/emby');
const dashboardRoutes = require('./routes/dashboard');
const historyRoutes = require('./routes/history');
const authRoutes = require('./routes/auth');
const verifyCsrf = require('./middleware/verifyCsrf');
const { startPoller, POLL_INTERVAL, POLLING_ENABLED } = require('./utils/poller');

// ---------------------------------------------------------------------------
// Startup environment validation
// ---------------------------------------------------------------------------
const cookieSecret = process.env.COOKIE_SECRET;
if (!cookieSecret && process.env.NODE_ENV === 'production') {
  console.error('[Security] COOKIE_SECRET is not set in production — aborting.');
  process.exit(1);
} else if (!cookieSecret) {
  console.warn('[Security] COOKIE_SECRET not set — unsigned cookies (dev only)');
}
if (!process.env.EMBY_URL && process.env.NODE_ENV === 'production') {
  console.error('[Config] EMBY_URL is required');
  process.exit(1);
}

const app = express();
const PORT = process.env.PORT || 3001;

// Resolve TLS_ENABLED early — used in Helmet CSP and server startup
const TLS_ENABLED = (process.env.TLS_ENABLED || 'true').toLowerCase() !== 'false';

// ---------------------------------------------------------------------------
// Trust proxy — required when behind Nginx/Caddy/Traefik so that
// req.ip reflects the real client IP (not 127.0.0.1) and
// req.secure is true when the upstream TLS is terminated by the proxy.
// Set TRUST_PROXY=1 (or a specific IP/CIDR) via env.
// ---------------------------------------------------------------------------
if (process.env.TRUST_PROXY) {
  const trustValue = /^\d+$/.test(process.env.TRUST_PROXY)
    ? parseInt(process.env.TRUST_PROXY, 10)
    : process.env.TRUST_PROXY;
  app.set('trust proxy', trustValue);
}

// ---------------------------------------------------------------------------
// Helmet v7 — security response headers
// CSP uses a per-request nonce injected into index.html so inline scripts
// and styles are allowed only with a valid nonce, not blanket unsafe-inline.
// ---------------------------------------------------------------------------
app.use((req, res, next) => {
  // Generate a fresh nonce for every request
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
        styleSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
        imgSrc: ["'self'", 'data:', 'blob:'],
        fontSrc: ["'self'", 'data:'],
        connectSrc: ["'self'"],
        objectSrc: ["'none'"],
        baseUri: ["'self'"],
        frameAncestors: ["'none'"],
        formAction: ["'self'"],
        upgradeInsecureRequests: (process.env.TRUST_PROXY || TLS_ENABLED) ? [] : null
      }
    },
    hsts: {
      maxAge: 31536000,       // 1 year
      includeSubDomains: true,
      preload: true
    },
    referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
    crossOriginEmbedderPolicy: false // not needed for this SPA
  })(req, res, next);
});

// Permissions-Policy — disable powerful browser features not needed by the app
app.use((req, res, next) => {
  res.setHeader(
    'Permissions-Policy',
    'camera=(), microphone=(), geolocation=(), payment=(), usb=()'
  );
  next();
});

// ---------------------------------------------------------------------------
// General API rate limiter — applies to all /api/* routes
// More specific limiters (e.g. login) apply on top of this.
// ---------------------------------------------------------------------------
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 minutes
  max: 300,                    // 300 requests per IP per window (generous for polling)
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later' }
});

// ---------------------------------------------------------------------------
// Body parsing & cookies
// ---------------------------------------------------------------------------
app.use(cookieParser(cookieSecret || undefined));
app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads

// ---------------------------------------------------------------------------
// Health / readiness endpoints (no auth, no rate-limit)
// Used by Docker HEALTHCHECK and orchestrators.
// ---------------------------------------------------------------------------
app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() });
});

app.get('/ready', (req, res) => {
  // Confirm critical config is present
  const ready = !!(process.env.EMBY_URL);
  if (ready) {
    res.json({ status: 'ready' });
  } else {
    res.status(503).json({ status: 'not ready', reason: 'EMBY_URL not configured' });
  }
});

// ---------------------------------------------------------------------------
// Static files — served before API routes
// index.html is served manually so we can inject the CSP nonce
// ---------------------------------------------------------------------------
const PUBLIC_DIR = path.join(__dirname, '../public');
const INDEX_HTML = path.join(PUBLIC_DIR, 'index.html');

// Serve all static assets (js, css, images, icons) except index.html.
// JS and CSS get no-cache so browsers revalidate on every load (ETag still
// avoids re-downloading unchanged files; only a deploy changes the ETag).
app.use(express.static(PUBLIC_DIR, {
  index: false,
  setHeaders(res, filePath) {
    if (filePath.endsWith('.js') || filePath.endsWith('.css')) {
      res.setHeader('Cache-Control', 'no-cache');
    }
  }
}));

// Serve index.html with CSP nonce injected into <script> tags
function serveIndex(req, res) {
  fs.readFile(INDEX_HTML, 'utf8', (err, html) => {
    if (err) return res.status(500).send('Internal Server Error');
    const nonce = res.locals.cspNonce;
    // Only inject nonce into <script> tags — style-src 'self' already permits
    // same-origin <link rel=stylesheet> without a nonce, and injecting a nonce
    // onto <link> breaks mobile browsers / caching proxies (stale HTML carries
    // the old nonce which no longer matches the per-request CSP header).
    const patched = html
      .replace(/<script([^>]*)>/gi, `<script nonce="${nonce}"$1>`);
    res.setHeader('Content-Type', 'text/html; charset=utf-8');
    res.send(patched);
  });
}

// ---------------------------------------------------------------------------
// API routes (rate-limited; auth routes exempt CSRF for login/csrf endpoints)
// CSRF protection applies to all state-changing /api/* requests except
// /api/auth/login (pre-auth) and /api/auth/csrf (issues the token).
// ---------------------------------------------------------------------------
app.use('/api', apiLimiter);
app.use('/api/auth', authRoutes);

// All routes below this point require CSRF validation on mutating methods
app.use('/api', verifyCsrf);
app.use('/api/sabnzbd', sabnzbdRoutes);
app.use('/api/sonarr', sonarrRoutes);
app.use('/api/radarr', radarrRoutes);
app.use('/api/emby', embyRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use('/api/history', historyRoutes);

// SPA catch-all — serve index.html for any unmatched path
app.get('*', serveIndex);

// ---------------------------------------------------------------------------
// Global error handler — never leak stack traces to clients
// ---------------------------------------------------------------------------
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, next) => {
  console.error('[Server] Unhandled error:', err.message);
  res.status(500).json({ error: 'Internal server error' });
});

// ---------------------------------------------------------------------------
// TLS / HTTPS support
// Set TLS_CERT and TLS_KEY to paths of your certificate and private key.
// If unset, defaults to the bundled snakeoil self-signed certificate
// (localhost/127.0.0.1 only — suitable for local testing).
// Set TLS_ENABLED=false to force plain HTTP even if cert files exist.
// ---------------------------------------------------------------------------
const CERTS_DIR = path.join(__dirname, '../certs');
const TLS_CERT_PATH = process.env.TLS_CERT || path.join(CERTS_DIR, 'snakeoil.crt');
const TLS_KEY_PATH  = process.env.TLS_KEY  || path.join(CERTS_DIR, 'snakeoil.key');

function loadTlsCredentials() {
  if (!TLS_ENABLED) return null;
  try {
    return {
      cert: fs.readFileSync(TLS_CERT_PATH),
      key:  fs.readFileSync(TLS_KEY_PATH)
    };
  } catch (err) {
    console.warn(`[TLS] Could not load certificate files — falling back to HTTP. (${err.message})`);
    return null;
  }
}

const tlsCredentials = loadTlsCredentials();
const server = tlsCredentials
  ? https.createServer(tlsCredentials, app)
  : http.createServer(app);

const protocol = tlsCredentials ? 'https' : 'http';
const isSnakeoil = TLS_ENABLED &&
  (!process.env.TLS_CERT || process.env.TLS_CERT === TLS_CERT_PATH);

server.listen(PORT, () => {
  console.log(`=================================`);
  console.log(`  sofarr - Your Downloads Dashboard`);
  console.log(`  Server running on ${protocol}://localhost:${PORT}`);
  if (tlsCredentials && isSnakeoil) {
    console.warn(`  [TLS] Using bundled snakeoil certificate (self-signed).`);
    console.warn(`  [TLS] Set TLS_CERT and TLS_KEY for a trusted certificate.`);
    console.warn(`  [TLS] Set TLS_ENABLED=false to disable TLS entirely.`);
  } else if (tlsCredentials) {
    console.log(`  [TLS] Certificate: ${TLS_CERT_PATH}`);
  } else {
    console.warn(`  [TLS] Running in plain HTTP mode — not suitable for production.`);
  }
  console.log(`  Log level: ${process.env.LOG_LEVEL || 'info'}`);
  console.log(`  Polling: ${POLLING_ENABLED ? POLL_INTERVAL + 'ms' : 'disabled (on-demand)'}`);
  console.log(`  Trust proxy: ${process.env.TRUST_PROXY || 'disabled'}`);
  console.log(`=================================`);
  startPoller();
});
