'use strict';
require('dotenv').config();

const express  = require('express');
const session  = require('express-session');
const helmet   = require('helmet');
const { rateLimit } = require('express-rate-limit');
const path = require('path');
const fs   = require('fs');

const { initDb, getDb }        = require('./auth/db');
const { logRequest: _logSyncroRequest } = require('./syncroStats');
const { createSessionStore }   = require('./auth/sessionStore');
const { requireAuth, requireRole } = require('./auth/middleware');
const authRoutes                   = require('./auth/routes');
const adminRoutes                  = require('./auth/admin');

// ── Validate required environment ────────────────────────────────────────────
if (!process.env.SESSION_SECRET || process.env.SESSION_SECRET.length < 32) {
  console.error('FATAL: SESSION_SECRET is missing or too short. Set it in .env');
  process.exit(1);
}
if (!process.env.SYNCRO_BASE_URL || !process.env.SYNCRO_API_KEY) {
  console.error('FATAL: SYNCRO_BASE_URL and SYNCRO_API_KEY must be set in .env');
  process.exit(1);
}

// ── Boot ID — changes on every restart so clients can detect and reload ───────
const SERVER_BOOT_ID = Date.now().toString(36);

// ── Syncro in-memory cache ────────────────────────────────────────────────────
const CACHE_TTL_MS = 5 * 60 * 1000;
const _cache = {
  customers: { data: null, ts: 0 },
  assets:    new Map(),   // customerId (number) → { data: [], ts: number }
  contacts:  new Map(),   // customerId (number) → { data: [], ts: number }
};
let _refreshCooldownUntil = 0;
const _inflight = {
  customers: null,       // Promise | null
  assets:    new Map(),  // customerId → Promise
  contacts:  new Map(),  // customerId → Promise
};

// ── Syncro HTTP helpers ───────────────────────────────────────────────────────
async function syncroGet(path, params = {}) {
  const url = new URL(process.env.SYNCRO_BASE_URL + path);
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
  _logSyncroRequest();
  const res = await fetch(url.toString(), {
    headers: { Authorization: process.env.SYNCRO_API_KEY, Accept: 'application/json' },
  });
  if (!res.ok) throw new Error(`Syncro ${res.status} on GET ${path}`);
  return res.json();
}

async function syncroPut(path, body) {
  const url = new URL(process.env.SYNCRO_BASE_URL + path);
  _logSyncroRequest();
  const res = await fetch(url.toString(), {
    method: 'PUT',
    headers: {
      Authorization:  process.env.SYNCRO_API_KEY,
      'Content-Type': 'application/json',
      Accept:         'application/json',
    },
    body: JSON.stringify(body),
  });
  if (res.status === 204) return {};
  const data = await res.json().catch(() => ({}));
  if (!res.ok) throw new Error(data.error ?? data.message ?? `HTTP ${res.status}`);
  return data;
}

// ── Paginated Syncro fetchers ─────────────────────────────────────────────────
async function _fetchCustomers() {
  let page = 1, all = [];
  while (true) {
    const data = await syncroGet('/customers', { per_page: 100, page });
    all = all.concat(data.customers ?? []);
    if (page >= (data.meta?.total_pages ?? 1)) break;
    page++;
  }
  return all;
}

async function _fetchCustomerAssets(customerId) {
  let page = 1, all = [];
  while (true) {
    const data = await syncroGet('/customer_assets', { customer_id: customerId, per_page: 250, page });
    all = all.concat(data.assets ?? data.customer_assets ?? []);
    if (page >= (data.meta?.total_pages ?? 1)) break;
    page++;
  }
  return all;
}

async function _fetchContacts(customerId) {
  let page = 1, all = [];
  while (true) {
    const data = await syncroGet('/contacts', { customer_id: customerId, per_page: 50, page });
    all = all.concat(data.contacts ?? []);
    if (page >= (data.meta?.total_pages ?? 1)) break;
    page++;
  }
  return all;
}

// ── Cache-with-dedup accessors ────────────────────────────────────────────────
async function cachedCustomers() {
  const now = Date.now();
  if (_cache.customers.data && now - _cache.customers.ts < CACHE_TTL_MS) return _cache.customers.data;
  if (_inflight.customers) return _inflight.customers;
  _inflight.customers = _fetchCustomers().then(data => {
    _cache.customers = { data, ts: Date.now() };
    _inflight.customers = null;
    return data;
  }).catch(err => { _inflight.customers = null; throw err; });
  return _inflight.customers;
}

async function cachedAssets(customerId) {
  const id  = Number(customerId);
  const now = Date.now();
  const entry = _cache.assets.get(id);
  if (entry && now - entry.ts < CACHE_TTL_MS) return entry.data;
  if (_inflight.assets.has(id)) return _inflight.assets.get(id);
  const p = _fetchCustomerAssets(id).then(data => {
    _cache.assets.set(id, { data, ts: Date.now() });
    _inflight.assets.delete(id);
    return data;
  }).catch(err => { _inflight.assets.delete(id); throw err; });
  _inflight.assets.set(id, p);
  return p;
}

async function cachedContacts(customerId) {
  const id  = Number(customerId);
  const now = Date.now();
  const entry = _cache.contacts.get(id);
  if (entry && now - entry.ts < CACHE_TTL_MS) return entry.data;
  if (_inflight.contacts.has(id)) return _inflight.contacts.get(id);
  const p = _fetchContacts(id).then(data => {
    _cache.contacts.set(id, { data, ts: Date.now() });
    _inflight.contacts.delete(id);
    return data;
  }).catch(err => { _inflight.contacts.delete(id); throw err; });
  _inflight.contacts.set(id, p);
  return p;
}

// ── Bootstrap DB ─────────────────────────────────────────────────────────────
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
initDb();

// ── App ───────────────────────────────────────────────────────────────────────
const app  = express();
const PORT = parseInt(process.env.PORT ?? '3000', 10);

// Trust CloudPanel's nginx reverse proxy (needed for secure cookies over HTTPS)
app.set('trust proxy', 1);

// ── Security headers ─────────────────────────────────────────────────────────
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc:  ["'self'"],
      scriptSrc:    ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'", 'https://cdn.jsdelivr.net'],  // wasm-unsafe-eval needed for WASM instantiation; jsdelivr for barcode-detector polyfill
      scriptSrcAttr: ["'unsafe-inline'"],           // needed for onclick= handlers in generated popup HTML
      styleSrc:    ["'self'", "'unsafe-inline'"],   // needed for inline styles in JS-rendered HTML
      imgSrc:      ["'self'", 'data:'],
      connectSrc:  ["'self'", 'https://cdn.jsdelivr.net', 'https://fastly.jsdelivr.net'],  // fastly.jsdelivr.net serves the .wasm binary via CDN edge
      fontSrc:     ["'self'"],
      objectSrc:   ["'none'"],
      frameSrc:    ["'none'"],
      baseUri:     ["'self'"],
    },
  },
  crossOriginEmbedderPolicy: false, // not needed for this app
}));

// ── Body parsing ──────────────────────────────────────────────────────────────
app.use(express.json({ limit: '1mb' }));
app.use(express.urlencoded({ extended: false, limit: '1mb' }));

// ── Sessions ──────────────────────────────────────────────────────────────────
app.use(session({
  store:             createSessionStore(getDb()),
  secret:            process.env.SESSION_SECRET,
  name:              'sid',
  resave:            false,
  saveUninitialized: false,
  rolling:           true,   // slide expiry on activity
  cookie: {
    httpOnly: true,
    secure:   process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge:   8 * 60 * 60 * 1000,  // 8 hours
  },
}));

// ── Rate limiting (login endpoint only) ───────────────────────────────────────
const loginLimiter = rateLimit({
  windowMs:              15 * 60 * 1000,  // 15 minutes
  max:                   10,
  standardHeaders:       true,
  legacyHeaders:         false,
  skipSuccessfulRequests: true,
  message: { error: 'Too many login attempts. Try again in 15 minutes.' },
});

// ── Public routes (no auth required) ─────────────────────────────────────────
app.post('/auth/login', loginLimiter);
app.use('/auth', authRoutes);

// Serve login page assets without auth
const pub = path.join(__dirname, 'public');
app.get('/login.html',        (req, res) => res.sendFile(path.join(pub, 'login.html')));
app.get('/login.js',          (req, res) => res.sendFile(path.join(pub, 'login.js')));
app.get('/styles/login.css',      (req, res) => res.sendFile(path.join(pub, 'styles', 'login.css')));
app.get('/styles/variables.css',  (req, res) => res.sendFile(path.join(pub, 'styles', 'variables.css')));
app.use('/assets', express.static(path.join(pub, 'assets')));

// ── Auth wall ─────────────────────────────────────────────────────────────────
app.use(requireAuth);

// ── Admin API (superduperadmin + admin only) ───────────────────────────────────
app.use('/admin', requireRole('superduperadmin', 'admin'), adminRoutes);

// ── Health / boot ID — lets clients detect a restart and reload ───────────────
app.get('/api/ping', (_req, res) => {
  res.json({ ok: true, bootId: SERVER_BOOT_ID });
});

// ── Syncro API cache routes (auth-gated) ──────────────────────────────────────

app.get('/api/customers', async (req, res) => {
  try {
    const user = req.session.user;
    if (user.role === 'client') {
      if (!user.syncro_customer_id) return res.json({ customers: [] });
      const all = await cachedCustomers();
      return res.json({ customers: all.filter(c => c.id === user.syncro_customer_id) });
    }
    res.json({ customers: await cachedCustomers() });
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

app.get('/api/customer_assets/:customerId', async (req, res) => {
  const user = req.session.user;
  if (user.role === 'client') {
    if (!user.syncro_customer_id) return res.status(403).json({ error: 'Forbidden.' });
    if (Number(req.params.customerId) !== user.syncro_customer_id)
      return res.status(403).json({ error: 'Forbidden.' });
  }
  try {
    const assets = await cachedAssets(req.params.customerId);
    res.json({ assets });
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

app.get('/api/contacts/:customerId', async (req, res) => {
  const user = req.session.user;
  if (user.role === 'client') {
    if (!user.syncro_customer_id) return res.status(403).json({ error: 'Forbidden.' });
    if (Number(req.params.customerId) !== user.syncro_customer_id)
      return res.status(403).json({ error: 'Forbidden.' });
  }
  try {
    const contacts = await cachedContacts(req.params.customerId);
    res.json({ contacts });
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

// Write-through: forward PUT to Syncro then invalidate that customer's asset cache
app.put('/api/customer_assets/:id', async (req, res) => {
  if (req.session.user.role === 'client') return res.status(403).json({ error: 'Forbidden.' });
  const { customer_id, ...assetBody } = req.body;
  try {
    const result = await syncroPut(`/customer_assets/${req.params.id}`, assetBody);
    if (customer_id) _cache.assets.delete(Number(customer_id));
    res.json(result);
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

// ── Label Center queue ────────────────────────────────────────────────────────

function getLabelQueue(userId) {
  return getDb()
    .prepare('SELECT * FROM label_queue WHERE user_id = ? ORDER BY added_at ASC')
    .all(userId);
}

app.get('/api/label-queue', (req, res) => {
  try {
    res.json({ queue: getLabelQueue(req.session.user.id) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.post('/api/label-queue', (req, res) => {
  const { asset_id, asset_name, asset_serial, customer_name, customer_phone, custom_line } = req.body;
  if (!asset_id || !asset_name || !customer_name) {
    return res.status(400).json({ error: 'asset_id, asset_name, and customer_name are required' });
  }
  const uid = req.session.user.id;
  try {
    getDb()
      .prepare(`INSERT OR REPLACE INTO label_queue
        (user_id, asset_id, asset_name, asset_serial, customer_name, customer_phone, custom_line, sheet_position, added_at)
        VALUES (?, ?, ?, ?, ?, ?, ?,
          (SELECT sheet_position FROM label_queue WHERE user_id = ? AND asset_id = ?),
          COALESCE(
            (SELECT added_at FROM label_queue WHERE user_id = ? AND asset_id = ?),
            datetime('now')
          ))`)
      .run(uid, asset_id, asset_name, asset_serial ?? null,
           customer_name, customer_phone ?? null, custom_line ?? null,
           uid, asset_id,
           uid, asset_id);
    res.json({ queue: getLabelQueue(uid) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.patch('/api/label-queue/:assetId', (req, res) => {
  const assetId = parseInt(req.params.assetId, 10);
  if (isNaN(assetId)) return res.status(400).json({ error: 'Invalid assetId' });
  const { sheet_position } = req.body;
  if (sheet_position !== null && (typeof sheet_position !== 'number' || !Number.isInteger(sheet_position) || sheet_position < 1)) {
    return res.status(400).json({ error: 'sheet_position must be a positive integer or null' });
  }
  const uid = req.session.user.id;
  try {
    getDb()
      .prepare('UPDATE label_queue SET sheet_position = ? WHERE user_id = ? AND asset_id = ?')
      .run(sheet_position ?? null, uid, assetId);
    res.json({ queue: getLabelQueue(uid) });
  } catch (err) {
    // UNIQUE constraint on (user_id, sheet_position) — position already taken
    if (err.message.includes('UNIQUE')) {
      return res.status(409).json({ error: 'That position is already occupied' });
    }
    res.status(500).json({ error: err.message });
  }
});

app.put('/api/label-queue/:assetId', (req, res) => {
  const assetId = parseInt(req.params.assetId, 10);
  if (isNaN(assetId)) return res.status(400).json({ error: 'Invalid assetId' });
  const { asset_name, asset_serial, customer_name, customer_phone, custom_line } = req.body;
  if (!asset_name || !customer_name) return res.status(400).json({ error: 'asset_name and customer_name are required' });
  const uid = req.session.user.id;
  try {
    getDb()
      .prepare('UPDATE label_queue SET asset_name = ?, asset_serial = ?, customer_name = ?, customer_phone = ?, custom_line = ? WHERE user_id = ? AND asset_id = ?')
      .run(asset_name, asset_serial ?? null, customer_name, customer_phone ?? null, custom_line ?? null, uid, assetId);
    res.json({ queue: getLabelQueue(uid) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.delete('/api/label-queue/:assetId', (req, res) => {
  const assetId = parseInt(req.params.assetId, 10);
  if (isNaN(assetId)) return res.status(400).json({ error: 'Invalid assetId' });
  const uid = req.session.user.id;
  try {
    getDb()
      .prepare('DELETE FROM label_queue WHERE user_id = ? AND asset_id = ?')
      .run(uid, assetId);
    res.json({ queue: getLabelQueue(uid) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

app.delete('/api/label-queue', (req, res) => {
  const uid = req.session.user.id;
  try {
    if (req.body?.reset_sheet_only) {
      getDb()
        .prepare('UPDATE label_queue SET sheet_position = NULL WHERE user_id = ?')
        .run(uid);
    } else {
      getDb()
        .prepare('DELETE FROM label_queue WHERE user_id = ?')
        .run(uid);
    }
    res.json({ queue: getLabelQueue(uid) });
  } catch (err) {
    res.status(500).json({ error: err.message });
  }
});

// Cache refresh with 30s global cooldown
app.post('/api/cache/refresh', (req, res) => {
  if (req.session.user.role === 'client') return res.status(403).json({ error: 'Forbidden.' });
  const now = Date.now();
  if (now < _refreshCooldownUntil) {
    return res.status(429).json({
      error: 'Refresh cooldown active',
      retryAfter: Math.ceil((_refreshCooldownUntil - now) / 1000),
    });
  }
  _refreshCooldownUntil = now + 30_000;
  _cache.customers = { data: null, ts: 0 };
  _cache.assets.clear();
  _cache.contacts.clear();
  res.json({ ok: true });
});

// ── Syncro single-asset proxy routes (ownership-checked, replaces /syncro-api/ bypass) ──
// Must register /api/asset/search BEFORE /api/asset/:id or "search" matches the :id param.

app.get('/api/asset/search', async (req, res) => {
  const user = req.session.user;
  try {
    const params = { per_page: req.query.per_page ?? 10 };
    if (req.query.query) params.query = req.query.query;
    if (user.role === 'client') {
      if (!user.syncro_customer_id) return res.json({ assets: [], customer_assets: [] });
      params.customer_id = user.syncro_customer_id;
    }
    res.json(await syncroGet('/customer_assets', params));
  } catch (err) {
    res.status(502).json({ error: err.message });
  }
});

app.get('/api/asset/:id', async (req, res) => {
  try {
    const data = await syncroGet(`/customer_assets/${req.params.id}`);
    const asset = data.asset ?? data;
    const user = req.session.user;
    if (user.role === 'client') {
      if (!user.syncro_customer_id) return res.status(403).json({ error: 'Forbidden.' });
      if (asset.customer_id !== user.syncro_customer_id)
        return res.status(403).json({ error: 'Forbidden.' });
    }
    res.json(data);
  } catch (err) {
    const status = err.message.includes('404') ? 404 : 502;
    res.status(status).json({ error: err.message });
  }
});

// ── Static app files (auth-gated) ─────────────────────────────────────────────
// JS and CSS: no-cache so browsers always revalidate (ETags mean no wasted
// bandwidth when files haven't changed — just a fast 304).
app.use((req, res, next) => {
  if (/\.(js|css)$/.test(req.path)) {
    res.setHeader('Cache-Control', 'no-cache');
  }
  next();
});
app.use(express.static(pub));

// Catch-all: return index.html for any unmatched GET (SPA)
app.get('*', (req, res) => res.sendFile(path.join(pub, 'index.html')));

// ── Start ─────────────────────────────────────────────────────────────────────
app.listen(PORT, '127.0.0.1', () => {
  console.log(`Asset Browser listening on port ${PORT}`);
});
