/**
 * Bare Radio Video Chat — Server
 * Roles: admin · moderator · vip · member · guest
 */

'use strict';

const express    = require('express');
const http       = require('http');
const https      = require('https');
const { Server } = require('socket.io');
const session    = require('express-session');
const bcrypt     = require('bcryptjs');
const path       = require('path');
const fs         = require('fs');

const app    = express();
const server = http.createServer(app);
const io     = new Server(server, { cors: { origin: '*' } });

const PORT           = process.env.PORT           || 3000;
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || 'bareradio2024';
const SESSION_SECRET = process.env.SESSION_SECRET || 'bare-radio-s3cr3t-key';
const TENOR_KEY      = process.env.TENOR_KEY      || 'LIVDSRZULELA'; // free demo key — replace with your own from tenor.com
const MAX_USERS      = 20;

const USERS_FILE    = path.join(__dirname, 'users.json');
const SETTINGS_FILE = path.join(__dirname, 'settings.json');
const BANNED_FILE   = path.join(__dirname, 'banned.json');
const RESETS_FILE   = path.join(__dirname, 'password_resets.json');
const AVATARS_DIR   = path.join(__dirname, 'public', 'avatars');
const BACKGROUNDS_DIR = path.join(__dirname, 'public', 'Bakgrunner');
const CHAT_LOG_FILE = path.join(__dirname, 'chat_log.jsonl');

const crypto = require('crypto');

const ROLES = ['admin', 'dj', 'moderator', 'vip', 'member', 'guest'];

const DEFAULT_COLORS = {
  admin:     '#CC1C2E',
  dj:        '#A855F7',
  moderator: '#22C55E',
  vip:       '#EAB308',
  member:    '#FFFFFF',
  guest:     '#9CA3AF'
};

// Default white-label branding. Overridable via admin panel (settings.json).
const DEFAULT_BRANDING = {
  siteName:      process.env.SITE_NAME || 'Pulse Chat',
  tagline:       'Pulse Radio',
  welcomeMessage:'Vi ønsker {name} velkommen til Pulse Chat! 🎙️',
  creditText:    'Designed By Dahl Total',
  logo:          '/pulse_chat_logo.png',
  themeColor:    '#CC1C2E',
  radioEnabled:  true,
  streamUrl:     'https://artemis.streamerr.co/listen/bare_radio/radio.mp3',
  nowPlayingUrl: 'https://artemis.streamerr.co/api/nowplaying/bare_radio'
};

// ── FILE HELPERS ──────────────────────────────────────────────
function readJson(file, fallback) {
  try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
  catch { return fallback; }
}
function writeJson(file, data) {
  fs.writeFileSync(file, JSON.stringify(data, null, 2));
}

function loadUsers()    { return readJson(USERS_FILE,    []); }
function saveUsers(u)   { writeJson(USERS_FILE, u); }
function loadSettings() {
  const s = readJson(SETTINGS_FILE, {});
  const branding = { ...DEFAULT_BRANDING, ...(s.branding || {}) };
  // Credit text is locked — always the fixed default, never editable/overridable.
  branding.creditText = DEFAULT_BRANDING.creditText;
  return {
    roleColors: { ...DEFAULT_COLORS, ...(s.roleColors || {}) },
    branding
  };
}
function saveSettings(s) { writeJson(SETTINGS_FILE, s); }
function loadBans()     { return readJson(BANNED_FILE, []); }
function saveBans(b)    { writeJson(BANNED_FILE, b); }

function findUser(username) {
  return loadUsers().find(u => u.username.toLowerCase() === username.toLowerCase());
}
function findUserByEmail(email) {
  return loadUsers().find(u => u.email && u.email.toLowerCase() === email.toLowerCase());
}
function nextId() {
  const users = loadUsers();
  return users.length ? Math.max(...users.map(u => u.id)) + 1 : 1;
}
function isBanned(username) {
  return loadBans().some(b => b.username && b.username.toLowerCase() === username.toLowerCase());
}

// ── PASSWORD RESET HELPERS ────────────────────────────────────
function loadResets() { return readJson(RESETS_FILE, []); }
function saveResets(r) { writeJson(RESETS_FILE, r); }
function makeResetToken() { return crypto.randomBytes(32).toString('hex'); }

// Ensure avatars directory exists
if (!fs.existsSync(AVATARS_DIR)) fs.mkdirSync(AVATARS_DIR, { recursive: true });

// Ensure uploads directory exists (logo, etc.)
const UPLOADS_DIR = path.join(__dirname, 'public', 'uploads');
if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR, { recursive: true });

// ── MIDDLEWARE ────────────────────────────────────────────────
const sessionMiddleware = session({
  secret: SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { maxAge: 24 * 60 * 60 * 1000 }
});
app.use(sessionMiddleware);
app.use(express.urlencoded({ extended: false, limit: '3mb' }));
app.use(express.json({ limit: '3mb' }));

io.use((socket, next) => {
  sessionMiddleware(socket.request, socket.request.res || {}, next);
});

// ── AUTH GUARDS ───────────────────────────────────────────────
function requireAuth(req, res, next) {
  if (req.session && req.session.username) return next();
  res.redirect('/login');
}
function requireAdmin(req, res, next) {
  if (req.session && req.session.isAdmin) return next();
  res.redirect('/admin');
}

// ── USER AUTH ROUTES ──────────────────────────────────────────
app.get('/login', (req, res) => {
  if (req.session.username) return res.redirect('/');
  res.sendFile(path.join(__dirname, 'public', 'login.html'));
});

app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  if (!username || !password) return res.redirect('/login?error=empty');
  const user = findUser(username.trim());
  if (!user || !bcrypt.compareSync(password, user.passwordHash))
    return res.redirect('/login?error=invalid');
  if (isBanned(user.username))
    return res.redirect('/login?error=banned');

  // Block simultaneous logins from the same account
  const sockets = await io.fetchSockets();
  const alreadyOn = sockets.find(s => s.data.userId === user.id);
  if (alreadyOn) return res.redirect('/login?error=already_online');

  req.session.userId   = user.id;
  req.session.username = user.username;
  req.session.role     = user.role || 'member';
  req.session.isGuest  = false;
  res.redirect('/');
});

// ── SELF-REGISTRATION ─────────────────────────────────────────
app.get('/register', (req, res) => {
  if (req.session.username) return res.redirect('/');
  res.sendFile(path.join(__dirname, 'public', 'register.html'));
});

app.post('/register', (req, res) => {
  const { firstName, lastName, email, gender, username, password, password2 } = req.body;
  // Basic validation
  if (!firstName || !lastName || !email || !gender || !username || !password || !password2)
    return res.redirect('/register?error=missing');
  if (password !== password2)
    return res.redirect('/register?error=mismatch');
  if (password.length < 6)
    return res.redirect('/register?error=short');
  if (findUser(username.trim()))
    return res.redirect('/register?error=username_taken');
  if (findUserByEmail(email.trim()))
    return res.redirect('/register?error=email_taken');
  try {
    const users = loadUsers();
    users.push({
      id: nextId(),
      username: username.trim(),
      firstName: firstName.trim(),
      lastName: lastName.trim(),
      email: email.trim().toLowerCase(),
      gender,
      passwordHash: bcrypt.hashSync(password, 10),
      role: 'member',
      createdAt: new Date().toISOString()
    });
    saveUsers(users);
    res.redirect('/login?success=registered');
  } catch {
    res.redirect('/register?error=unknown');
  }
});

// ── GUEST ROUTE ───────────────────────────────────────────────
app.get('/guest', (req, res) => {
  if (req.session.username) return res.redirect('/');
  res.sendFile(path.join(__dirname, 'public', 'guest.html'));
});

app.post('/guest', (req, res) => {
  if (req.session.username) return res.redirect('/');
  const raw = String(req.body.username || '').trim().slice(0, 24);
  if (!raw) return res.redirect('/guest?error=empty');
  // Only allow letters, numbers, underscore, dash
  if (!/^[A-Za-z0-9_\-æøåÆØÅ]+$/.test(raw))
    return res.redirect('/guest?error=invalid');
  // Block names that look like registered usernames
  if (findUser(raw)) return res.redirect('/guest?error=taken');
  req.session.username = raw;
  req.session.role     = 'guest';
  req.session.isGuest  = true;
  res.redirect('/');
});

app.get('/logout', (req, res) => {
  req.session.destroy(() => res.redirect('/login'));
});

// ── API ───────────────────────────────────────────────────────
app.get('/api/me', requireAuth, (req, res) => {
  const user = req.session.userId ? loadUsers().find(u => u.id === req.session.userId) : null;
  res.json({
    username:  req.session.username,
    role:      req.session.role || 'guest',
    isGuest:   !!req.session.isGuest,
    avatarUrl: user?.avatarUrl || null
  });
});

app.get('/api/settings', (req, res) => {
  res.json(loadSettings());
});

// ── PROFILE ROUTES ────────────────────────────────────────────
// Upload avatar (base64 JSON, registered users only)
app.post('/profile/avatar', requireAuth, (req, res) => {
  if (!req.session.userId) return res.status(403).json({ error: 'Guests cannot upload avatars.' });
  const { base64 } = req.body;
  if (!base64 || typeof base64 !== 'string')
    return res.status(400).json({ error: 'No image data.' });
  const match = base64.match(/^data:image\/(png|jpeg|jpg|gif|webp);base64,(.+)$/);
  if (!match) return res.status(400).json({ error: 'Invalid image format.' });
  const buffer = Buffer.from(match[2], 'base64');
  if (buffer.length > 2 * 1024 * 1024)
    return res.status(400).json({ error: 'Image too large (max 2 MB).' });
  const ext = match[1] === 'jpeg' ? 'jpg' : match[1];
  // Remove any old avatar files for this user
  ['png','jpg','gif','webp'].forEach(e => {
    const old = path.join(AVATARS_DIR, `${req.session.userId}.${e}`);
    if (fs.existsSync(old)) fs.unlinkSync(old);
  });
  const filename = `${req.session.userId}.${ext}`;
  fs.writeFileSync(path.join(AVATARS_DIR, filename), buffer);
  const avatarUrl = `/avatars/${filename}`;
  saveUsers(loadUsers().map(u => u.id === req.session.userId ? { ...u, avatarUrl } : u));
  req.session.avatarUrl = avatarUrl;
  res.json({ avatarUrl });
});

// Change password (must be logged in as registered user)
app.post('/profile/password', requireAuth, (req, res) => {
  if (!req.session.userId) return res.status(403).json({ error: 'Guests cannot change passwords.' });
  const { current, newPassword } = req.body;
  if (!current || !newPassword || newPassword.length < 4)
    return res.status(400).json({ error: 'Invalid input.' });
  const users = loadUsers();
  const user  = users.find(u => u.id === req.session.userId);
  if (!user || !bcrypt.compareSync(current, user.passwordHash))
    return res.status(401).json({ error: 'Current password is wrong.' });
  const updated = users.map(u =>
    u.id === req.session.userId
      ? { ...u, passwordHash: bcrypt.hashSync(newPassword, 10) }
      : u
  );
  saveUsers(updated);
  res.json({ ok: true });
});

// ── FORGOT / RESET PASSWORD ───────────────────────────────────
app.get('/forgot-password', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'forgot-password.html'));
});

app.post('/forgot-password', (req, res) => {
  const username = (req.body.username || '').trim();
  if (!username) return res.redirect('/forgot-password?error=empty');
  const user = findUser(username);
  if (!user) return res.redirect('/forgot-password?error=notfound');
  // Remove any existing reset for this user
  const resets = loadResets().filter(r => r.userId !== user.id);
  const token = makeResetToken();
  resets.push({ token, userId: user.id, username: user.username, requestedAt: new Date().toISOString() });
  saveResets(resets);
  // Always show "sent" to avoid username enumeration
  res.redirect('/forgot-password?sent=1');
});

app.get('/reset-password', (req, res) => {
  const { token } = req.query;
  const resets = loadResets();
  if (!token || !resets.find(r => r.token === token))
    return res.redirect('/login?error=invalid_token');
  res.sendFile(path.join(__dirname, 'public', 'reset-password.html'));
});

app.post('/reset-password', (req, res) => {
  const { token, newPassword } = req.body;
  if (!token || !newPassword || newPassword.length < 4)
    return res.redirect(`/reset-password?token=${token}&error=invalid`);
  const resets = loadResets();
  const entry  = resets.find(r => r.token === token);
  if (!entry) return res.redirect('/login?error=invalid_token');
  saveUsers(loadUsers().map(u =>
    u.id === entry.userId ? { ...u, passwordHash: bcrypt.hashSync(newPassword, 10) } : u
  ));
  saveResets(resets.filter(r => r.token !== token));
  res.redirect('/login?success=reset');
});

// ── CHAT (protected) ──────────────────────────────────────────
app.get('/', requireAuth, (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'index.html'));
});

app.use(express.static(path.join(__dirname, 'public'), { index: false }));

// ── BACKGROUND IMAGES ────────────────────────────────────────
// Lists image files dropped into public/Bakgrunner so users can pick them
// as a chat background. Served statically at /Bakgrunner/<file>.
app.get('/api/backgrounds', (req, res) => {
  try {
    if (!fs.existsSync(BACKGROUNDS_DIR)) return res.json({ images: [] });
    const allowed = /\.(jpe?g|png|gif|webp|avif|svg)$/i;
    const images = fs.readdirSync(BACKGROUNDS_DIR)
      .filter(f => allowed.test(f) && !f.startsWith('.'))
      .sort((a, b) => a.localeCompare(b))
      .map(f => ({
        url: '/Bakgrunner/' + encodeURIComponent(f),
        name: f.replace(/\.[^.]+$/, '')
      }));
    res.json({ images });
  } catch (e) {
    res.json({ images: [] });
  }
});

// ── GIF SEARCH PROXY ─────────────────────────────────────────
app.get('/api/gifs', requireAuth, (req, res) => {
  const q = encodeURIComponent((req.query.q || '').trim().slice(0, 100));
  if (!q) return res.json({ results: [] });
  // Tenor v2 with demo key
  const TENOR_KEY = process.env.TENOR_KEY || 'AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ';
  const url = `https://tenor.googleapis.com/v2/search?q=${q}&key=${TENOR_KEY}&limit=24&client_key=barechat&media_filter=tinygif,gif`;
  https.get(url, (r) => {
    let data = '';
    r.on('data', d => data += d);
    r.on('end', () => {
      try {
        const json = JSON.parse(data);
        const results = (json.results || []).map(item => {
          const fmt = item.media_formats || {};
          const url     = fmt.gif?.url     || fmt.mediumgif?.url || fmt.tinygif?.url || '';
          const preview = fmt.tinygif?.url || fmt.gif?.url || '';
          return { id: item.id, title: item.title || '', url, preview };
        }).filter(r => r.url);
        res.json({ results });
      } catch { res.json({ results: [] }); }
    });
  }).on('error', () => res.json({ results: [] }));
});

// ── ADMIN ROUTES ──────────────────────────────────────────────
app.get('/admin', (req, res) => {
  if (req.session.isAdmin) return res.redirect('/admin/dashboard');
  res.sendFile(path.join(__dirname, 'public', 'admin-login.html'));
});

app.post('/admin/login', (req, res) => {
  if (req.body.password === ADMIN_PASSWORD) {
    req.session.isAdmin = true;
    return res.redirect('/admin/dashboard');
  }
  res.redirect('/admin?error=1');
});

app.get('/admin/logout', (req, res) => {
  req.session.isAdmin = false;
  res.redirect('/admin');
});

app.get('/admin/dashboard', requireAdmin, (req, res) => {
  const host = `${req.protocol}://${req.get('host')}`;
  res.send(renderAdminDashboard(loadUsers(), loadBans(), loadSettings(), loadResets(), host, req.query));
});

app.post('/admin/users/add', requireAdmin, (req, res) => {
  const { username, password, role } = req.body;
  if (!username || !password) return res.redirect('/admin/dashboard?error=missing');
  if (findUser(username.trim())) return res.redirect('/admin/dashboard?error=exists');
  try {
    const users = loadUsers();
    users.push({
      id: nextId(),
      username: username.trim(),
      firstName: '',
      lastName: '',
      email: '',
      gender: '',
      passwordHash: bcrypt.hashSync(password.trim(), 10),
      role: ROLES.includes(role) ? role : 'member',
      createdAt: new Date().toISOString()
    });
    saveUsers(users);
    res.redirect('/admin/dashboard?success=added');
  } catch { res.redirect('/admin/dashboard?error=unknown'); }
});

app.post('/admin/users/delete', requireAdmin, (req, res) => {
  saveUsers(loadUsers().filter(u => u.id !== Number(req.body.id)));
  res.redirect('/admin/dashboard');
});

app.post('/admin/users/password', requireAdmin, (req, res) => {
  const { id, password } = req.body;
  if (!password) return res.redirect('/admin/dashboard?error=missing');
  saveUsers(loadUsers().map(u =>
    u.id === Number(id) ? { ...u, passwordHash: bcrypt.hashSync(password.trim(), 10) } : u
  ));
  res.redirect('/admin/dashboard?success=password');
});

app.post('/admin/users/role', requireAdmin, async (req, res) => {
  const { id, role } = req.body;
  if (!ROLES.includes(role)) return res.redirect('/admin/dashboard?error=invalid');
  const userId = Number(id);
  saveUsers(loadUsers().map(u => u.id === userId ? { ...u, role } : u));

  // Push role change to any active socket for this user
  const sockets = await io.fetchSockets();
  for (const s of sockets) {
    if (s.data.userId === userId) {
      s.data.role = role;
      const room = s.data.room;
      if (room && rooms.has(room)) {
        const m = rooms.get(room).get(s.id);
        if (m) m.role = role;
        broadcastUserList(room);
      }
      s.emit('role-changed', { role });
    }
  }

  res.redirect('/admin/dashboard?success=role');
});

app.post('/admin/settings/colors', requireAdmin, (req, res) => {
  const settings = loadSettings();
  ROLES.forEach(r => {
    if (req.body[r]) settings.roleColors[r] = req.body[r];
  });
  saveSettings(settings);
  res.redirect('/admin/dashboard?success=colors');
});

// White-label branding (text fields + radio + URLs)
app.post('/admin/settings/branding', requireAdmin, (req, res) => {
  const settings = loadSettings();
  const b = settings.branding || {};
  const textFields = ['siteName', 'tagline', 'welcomeMessage',
                      'themeColor', 'streamUrl', 'nowPlayingUrl'];
  textFields.forEach(f => {
    if (typeof req.body[f] === 'string') b[f] = req.body[f].trim();
  });
  b.radioEnabled = req.body.radioEnabled === 'on' || req.body.radioEnabled === 'true';
  settings.branding = b;
  saveSettings(settings);
  res.redirect('/admin/dashboard?success=branding');
});

// Logo upload (base64 JSON, same pattern as avatars)
app.post('/admin/settings/logo', requireAdmin, (req, res) => {
  const { base64 } = req.body;
  if (!base64 || typeof base64 !== 'string')
    return res.status(400).json({ error: 'No image data.' });
  const match = base64.match(/^data:image\/(png|jpeg|jpg|gif|webp|svg\+xml);base64,(.+)$/);
  if (!match) return res.status(400).json({ error: 'Invalid image format.' });
  const buffer = Buffer.from(match[2], 'base64');
  if (buffer.length > 2 * 1024 * 1024)
    return res.status(400).json({ error: 'Image too large (max 2 MB).' });
  const ext = match[1] === 'jpeg' ? 'jpg' : (match[1] === 'svg+xml' ? 'svg' : match[1]);
  // Remove old logo files
  ['png','jpg','gif','webp','svg'].forEach(e => {
    const old = path.join(UPLOADS_DIR, `logo.${e}`);
    if (fs.existsSync(old)) fs.unlinkSync(old);
  });
  const filename = `logo.${ext}`;
  fs.writeFileSync(path.join(UPLOADS_DIR, filename), buffer);
  const settings = loadSettings();
  settings.branding = settings.branding || {};
  settings.branding.logo = `/uploads/${filename}?t=${Date.now()}`;
  saveSettings(settings);
  res.json({ logo: settings.branding.logo });
});

app.post('/admin/bans/add', requireAdmin, (req, res) => {
  const { username } = req.body;
  if (!username) return res.redirect('/admin/dashboard?error=missing');
  const bans = loadBans();
  if (!bans.some(b => b.username?.toLowerCase() === username.toLowerCase())) {
    bans.push({ id: Date.now(), username: username.trim(), bannedAt: new Date().toISOString() });
    saveBans(bans);
  }
  res.redirect('/admin/dashboard?success=banned');
});

app.post('/admin/bans/remove', requireAdmin, (req, res) => {
  saveBans(loadBans().filter(b => b.id !== Number(req.body.id)));
  res.redirect('/admin/dashboard?success=unbanned');
});

app.post('/admin/resets/delete', requireAdmin, (req, res) => {
  saveResets(loadResets().filter(r => r.token !== req.body.token));
  res.redirect('/admin/dashboard');
});

app.get('/admin/chatlog', requireAdmin, (req, res) => {
  const page    = Math.max(1, parseInt(req.query.page) || 1);
  const perPage = 200;
  const room    = req.query.room || '';
  let entries   = loadChatLog();
  if (room) entries = entries.filter(e => e.room === room);
  entries.reverse(); // newest first
  const total = entries.length;
  const pages = Math.max(1, Math.ceil(total / perPage));
  const slice = entries.slice((page - 1) * perPage, page * perPage);
  const rooms = [...new Set(loadChatLog().map(e => e.room))].filter(Boolean);
  res.send(renderChatLog(slice, { page, pages, total, room, rooms }));
});

app.post('/admin/chatlog/clear', requireAdmin, (req, res) => {
  try { fs.writeFileSync(CHAT_LOG_FILE, '', 'utf8'); } catch {}
  res.redirect('/admin/chatlog?success=cleared');
});

const HISTORY_FILE = path.join(__dirname, 'chat_history.json');

// ── CHAT HISTORY HELPERS ──────────────────────────────────────
function loadChatHistory() {
  try {
    const raw = JSON.parse(fs.readFileSync(HISTORY_FILE, 'utf8'));
    // Convert plain object back to Map
    const map = new Map();
    for (const [room, msgs] of Object.entries(raw)) map.set(room, msgs);
    return map;
  } catch { return new Map(); }
}
function saveChatHistory(map) {
  const obj = {};
  for (const [room, msgs] of map.entries()) obj[room] = msgs;
  try { fs.writeFileSync(HISTORY_FILE, JSON.stringify(obj)); } catch {}
}
function appendChatLog(entry) {
  try { fs.appendFileSync(CHAT_LOG_FILE, JSON.stringify(entry) + '\n', 'utf8'); } catch {}
}
function loadChatLog() {
  try {
    return fs.readFileSync(CHAT_LOG_FILE, 'utf8')
      .split('\n').filter(Boolean)
      .map(line => { try { return JSON.parse(line); } catch { return null; } })
      .filter(Boolean);
  } catch { return []; }
}
const rooms       = new Map();
const chatHistory = loadChatHistory(); // persisted to disk
const MAX_HISTORY = 50;

io.on('connection', (socket) => {
  const sess = socket.request.session;
  if (!sess || !sess.username) { socket.disconnect(); return; }
  if (isBanned(sess.username)) {
    socket.emit('banned', { reason: 'You are banned from this server.' });
    socket.disconnect(); return;
  }

  socket.data.username    = sess.username;
  socket.data.userId      = sess.userId || null;
  socket.data.role        = sess.role || 'guest';
  socket.data.isGuest     = !!sess.isGuest;
  // Load avatar from user record (guests have none)
  const socketUser = sess.userId ? loadUsers().find(u => u.id === sess.userId) : null;
  socket.data.avatarUrl   = socketUser?.avatarUrl || null;

  console.log(`[+] ${sess.username} (${socket.data.role}) connected`);

  socket.on('join-room', ({ roomId }) => {
    const room        = String(roomId || 'main').trim().slice(0, 32);
    const displayName = sess.username;
    const role        = socket.data.role;
    const avatarUrl   = socket.data.avatarUrl || null;

    if (!rooms.has(room)) rooms.set(room, new Map());
    const roomUsers = rooms.get(room);
    if (roomUsers.size >= MAX_USERS) { socket.emit('room-full'); return; }

    socket.rooms.forEach(r => { if (r !== socket.id) socket.leave(r); });
    socket.join(room);
    socket.data.room = room;

    roomUsers.set(socket.id, { displayName, role, avatarUrl, camOn: false });

    const existingPeers = [...roomUsers.entries()]
      .filter(([id]) => id !== socket.id)
      .map(([id, m]) => ({ socketId: id, displayName: m.displayName, role: m.role, avatarUrl: m.avatarUrl, camOn: m.camOn || false }));

    socket.emit('room-joined', { room, peers: existingPeers, you: socket.id, history: chatHistory.get(room) || [] });
    socket.to(room).emit('peer-joined', { socketId: socket.id, displayName, role, avatarUrl });
    broadcastUserList(room);
  });

  // WebRTC signaling
  socket.on('offer',         ({ to, offer })     => io.to(to).emit('offer',         { from: socket.id, offer }));
  socket.on('answer',        ({ to, answer })    => io.to(to).emit('answer',        { from: socket.id, answer }));
  socket.on('ice-candidate', ({ to, candidate }) => io.to(to).emit('ice-candidate', { from: socket.id, candidate }));

  // Camera state broadcast
  socket.on('camera-state', ({ on }) => {
    const room = socket.data.room;
    if (!room || !rooms.has(room)) return;
    const roomUsers = rooms.get(room);
    const me = roomUsers.get(socket.id);
    if (me) {
      me.camOn = !!on;
      if (!on) {
        if (me.viewers) me.viewers.clear();          // clear viewers when cam off
        if (me.approvedViewers) me.approvedViewers.clear(); // revoke approvals
      }
    }
    broadcastUserList(room);
  });

  // Camera viewer tracking (used on close / direct removal)
  socket.on('camera-view', ({ targetSocketId, viewing }) => {
    const room = socket.data.room;
    if (!room || !rooms.has(room)) return;
    const roomUsers = rooms.get(room);
    const target = roomUsers.get(targetSocketId);
    if (!target) return;
    if (!target.viewers) target.viewers = new Set();
    if (viewing) target.viewers.add(socket.id);
    else target.viewers.delete(socket.id);
    broadcastUserList(room);
  });

  // ── Camera view PERMISSION flow ──────────────────────────
  // Viewer asks the camera owner for permission to watch.
  socket.on('view-request', ({ targetSocketId }) => {
    const room = socket.data.room;
    if (!room || !rooms.has(room)) return;
    const roomUsers = rooms.get(room);
    const target = roomUsers.get(targetSocketId);
    if (!target) return;
    if (!target.approvedViewers) target.approvedViewers = new Set();

    // Already approved this session → auto-approve, no popup for the owner.
    if (target.approvedViewers.has(socket.id)) {
      io.to(socket.id).emit('view-response', {
        from: targetSocketId, approved: true, displayName: target.displayName
      });
      return;
    }
    // Forward request to the camera owner.
    io.to(targetSocketId).emit('view-request', {
      from: socket.id, displayName: socket.data.username, role: socket.data.role
    });
  });

  // Camera owner answers a pending view request.
  socket.on('view-response', ({ to, approved }) => {
    const room = socket.data.room;
    if (!room || !rooms.has(room)) return;
    const roomUsers = rooms.get(room);
    const me = roomUsers.get(socket.id);
    if (!me) return;
    if (!me.approvedViewers) me.approvedViewers = new Set();
    if (!me.viewers) me.viewers = new Set();

    if (approved) {
      me.approvedViewers.add(to);
      me.viewers.add(to);
      broadcastUserList(room);
    } else {
      me.approvedViewers.delete(to);
      me.viewers.delete(to);
    }
    io.to(to).emit('view-response', {
      from: socket.id, approved: !!approved, displayName: socket.data.username
    });
  });

  // Chat — include role in broadcast
  socket.on('chat-message', (payload) => {
    const room = socket.data.room;
    if (!room) return;
    // Support both plain string (legacy) and object {text?, gifUrl?}
    const isObj = typeof payload === 'object' && payload !== null;
    const text   = isObj ? String(payload.text  || '').trim() : String(payload).trim();
    const gifUrl = isObj && payload.gifUrl ? String(payload.gifUrl).trim() : null;
    // Whitelist GIF domains
    const safeGif = gifUrl && /^https:\/\/(media\.tenor\.com|c\.tenor\.com|media\d*\.giphy\.com|media\.giphy\.com|[\w.-]+\.tenor\.com)\//.test(gifUrl) ? gifUrl : null;
    if (!text && !safeGif) return;
    const msg = {
      from: socket.id, displayName: socket.data.username,
      role: socket.data.role, avatarUrl: socket.data.avatarUrl || null,
      text, gifUrl: safeGif, ts: Date.now()
    };
    // Store in history
    if (!chatHistory.has(room)) chatHistory.set(room, []);
    const hist = chatHistory.get(room);
    hist.push(msg);
    if (hist.length > MAX_HISTORY) hist.shift();
    saveChatHistory(chatHistory);
    // Append to persistent log
    appendChatLog({ room, ...msg });
    socket.to(room).emit('chat-message', msg);
  });

  // Clear chat for everyone in room (admin/dj only)
  socket.on('clear-chat', () => {
    if (!['admin', 'dj'].includes(socket.data.role)) return;
    const room = socket.data.room;
    if (!room) return;
    // Clear in-memory history for this room
    if (chatHistory.has(room)) { chatHistory.set(room, []); saveChatHistory(chatHistory); }
    // Log the clear action
    appendChatLog({ room, type: 'clear', by: socket.data.username, ts: Date.now() });
    // Broadcast to everyone including sender
    io.to(room).emit('chat-cleared', { by: socket.data.username });
  });

  // Language change broadcast (admin/dj only)
  socket.on('lang-changed', ({ lang }) => {
    if (!['admin', 'dj'].includes(socket.data.role)) return;
    const room = socket.data.room;
    if (!room) return;
    // Broadcast to all OTHERS in room (sender already switched locally)
    socket.to(room).emit('lang-changed', { lang });
  });

  // ── KICK (admin only) ────────────────────────────────────
  socket.on('kick-user', ({ targetSocketId }) => {
    if (!['admin','dj'].includes(socket.data.role)) return;
    const target = io.sockets.sockets.get(targetSocketId);
    if (!target) return;
    target.emit('kicked', { by: socket.data.username });
    setTimeout(() => target.disconnect(), 300);
    const room = socket.data.room;
    if (room) io.to(room).emit('system-message',
      `${target.data.username} was kicked by ${socket.data.username}.`);
  });

  // ── BAN (admin only) ─────────────────────────────────────
  socket.on('ban-user', ({ targetSocketId }) => {
    if (!['admin','dj'].includes(socket.data.role)) return;
    const target = io.sockets.sockets.get(targetSocketId);
    if (!target) return;
    const username = target.data.username;
    // Add to ban list
    const bans = loadBans();
    if (!bans.some(b => b.username?.toLowerCase() === username.toLowerCase())) {
      bans.push({ id: Date.now(), username, bannedAt: new Date().toISOString() });
      saveBans(bans);
    }
    target.emit('banned', { by: socket.data.username });
    setTimeout(() => target.disconnect(), 300);
    const room = socket.data.room;
    if (room) io.to(room).emit('system-message',
      `${username} was banned by ${socket.data.username}.`);
  });

  socket.on('disconnecting', () => {
    const room = socket.data.room;
    if (!room || !rooms.has(room)) return;
    const roomUsers = rooms.get(room);
    roomUsers.delete(socket.id);
    // Remove this socket from any viewer / approval sets
    for (const [, m] of roomUsers) {
      if (m.viewers) m.viewers.delete(socket.id);
      if (m.approvedViewers) m.approvedViewers.delete(socket.id);
    }
    socket.to(room).emit('peer-left', { socketId: socket.id });
    if (roomUsers.size === 0) rooms.delete(room);
    else broadcastUserList(room);
  });
});

function broadcastUserList(room) {
  if (!rooms.has(room)) return;
  const list = [...rooms.get(room).entries()].map(([id, m]) => ({
    socketId: id, displayName: m.displayName, role: m.role, avatarUrl: m.avatarUrl || null,
    camOn: m.camOn || false, viewerCount: m.viewers?.size || 0
  }));
  io.to(room).emit('user-list', list);
}

// ── ADMIN DASHBOARD HTML ──────────────────────────────────────
function renderAdminDashboard(users, bans, settings, resets, host, query = {}) {
  const { success, error } = query;
  const flash = success === 'added'    ? ['✔ Bruker lagt til.',              'green']
              : success === 'password' ? ['✔ Passord oppdatert.',             'green']
              : success === 'role'     ? ['✔ Rolle oppdatert.',               'green']
              : success === 'colors'   ? ['✔ Rollefarger lagret.',            'green']
              : success === 'branding' ? ['✔ Branding lagret.',               'green']
              : success === 'banned'   ? ['✔ Bruker utestengt.',              'green']
              : success === 'unbanned' ? ['✔ Utestengelse opphevet.',         'green']
              : error   === 'exists'   ? ['✖ Brukernavnet er allerede i bruk.','red']
              : error   === 'missing'  ? ['✖ Obligatoriske felt mangler.',    'red']
              : error   === 'invalid'  ? ['✖ Ugyldig verdi.',                 'red']
              : null;

  const ROLE_LABELS_ADMIN = { admin:'Admin', dj:'DJ', moderator:'Mod', vip:'VIP', member:'Medlem', guest:'Gjest' };
  const roleLbl = r => ROLE_LABELS_ADMIN[r] || r.charAt(0).toUpperCase()+r.slice(1);

  const colors = settings.roleColors;
  const brand  = settings.branding || {};
  const esc = (s) => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  const roleOptions = (selected) => ROLES.map(r =>
    `<option value="${r}"${r===selected?' selected':''}>${roleLbl(r)}</option>`
  ).join('');

  const userRows = users.map(u => {
    const uRole = u.role || 'member';
    const rolePills = ROLES.map(r => {
      const isActive = r === uRole;
      const col = colors[r] || DEFAULT_COLORS[r];
      return `<button
        class="role-pill${isActive ? ' active' : ''}"
        style="${isActive ? `background:${col};border-color:${col};color:#fff` : `border-color:${col};color:${col}`}"
        name="role" value="${r}" form="rf-${u.id}" type="submit"
        title="Sett rolle til ${roleLbl(r)}">${roleLbl(r)}</button>`;
    }).join('');

    return `<tr>
    <td>
      <span class="role-dot" style="color:${colors[uRole]||'#fff'}">●</span>
      <strong>${esc(u.username)}</strong>
    </td>
    <td style="color:#8a8da8;font-size:11px">${u.email || '—'}</td>
    <td style="color:var(--grey-lt);font-size:11px">${new Date(u.createdAt).toLocaleDateString()}</td>
    <td>
      <form id="rf-${u.id}" method="POST" action="/admin/users/role" style="display:none">
        <input type="hidden" name="id" value="${u.id}"/>
      </form>
      <div class="role-pills">${rolePills}</div>
    </td>
    <td class="actions-cell">
      <form method="POST" action="/admin/users/password" class="inline-form">
        <input type="hidden" name="id" value="${u.id}"/>
        <input type="text" name="password" class="admin-input sm" placeholder="Nytt passord" required/>
        <button class="admin-btn grey sm" type="submit">Nullstill PW</button>
      </form>
      <form method="POST" action="/admin/users/delete" class="inline-form"
            onsubmit="return confirm('Slett ${esc(u.username)}?')">
        <input type="hidden" name="id" value="${u.id}"/>
        <button class="admin-btn red sm" type="submit">Slett</button>
      </form>
    </td>
  </tr>`;
  }).join('');

  const banRows = bans.length ? bans.map(b => `<tr>
    <td>${esc(b.username || '—')}</td>
    <td>${new Date(b.bannedAt).toLocaleDateString()}</td>
    <td>
      <form method="POST" action="/admin/bans/remove" class="inline-form">
        <input type="hidden" name="id" value="${b.id}"/>
        <button class="admin-btn blue sm" type="submit">Opphev utestengelse</button>
      </form>
    </td>
  </tr>`).join('') : '<tr><td colspan="3" style="color:var(--grey);font-size:12px">Ingen aktive utestengelser.</td></tr>';

  const colorInputs = ROLES.map(r => `
    <div class="color-row">
      <input type="color" name="${r}" value="${colors[r]||DEFAULT_COLORS[r]}" class="color-swatch-input"/>
      <span class="color-label" style="color:${colors[r]||DEFAULT_COLORS[r]}">${roleLbl(r)}</span>
    </div>`).join('');

  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
  <title>${esc(brand.siteName)} — Admin</title>
  <link rel="icon" type="image/png" href="${esc(brand.logo)}"/>
  <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=IBM+Plex+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet"/>
  <style>
    *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
    :root{--bg:#0C0D14;--bg2:#10111C;--bg3:#181924;--red:#CC1C2E;--red-b:#E8223B;
      --blue:#1A3AA0;--blue-b:#2148C8;--cream:#EEF0F8;--grey:#5a5c70;--grey-lt:#8a8da8;
      --green:#22C55E;--glow-red:0 0 16px rgba(204,28,46,.6)}
    body{background:var(--bg);color:var(--cream);font-family:'Space Grotesk',sans-serif;min-height:100vh}
    header{display:flex;align-items:center;justify-content:space-between;padding:0 28px;
      height:56px;background:var(--bg2);border-bottom:1px solid rgba(204,28,46,.25)}
    .hdr-brand{display:flex;align-items:center;gap:12px}
    .hdr-logo{height:36px;filter:drop-shadow(0 2px 8px rgba(204,28,46,.5))}
    .hdr-tag{font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:700;
      letter-spacing:.25em;color:var(--red-b);border:1px solid var(--red);
      padding:2px 8px;border-radius:2px;box-shadow:var(--glow-red)}
    .hdr-actions{display:flex;gap:12px;align-items:center}
    .logout-link{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.12em;
      color:var(--grey-lt);text-decoration:none;border:1px solid var(--grey);
      padding:5px 12px;border-radius:3px;transition:all .18s}
    .logout-link:hover{border-color:var(--red);color:var(--red)}
    main{max-width:960px;margin:0 auto;padding:28px 20px;display:flex;flex-direction:column;gap:20px}
    h1{font-family:'Bebas Neue',sans-serif;font-size:30px;letter-spacing:.04em;color:var(--cream)}
    .subtitle{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;color:var(--grey-lt);margin-top:2px;margin-bottom:0}
    .flash{padding:10px 16px;border-radius:4px;font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:.1em}
    .flash.green{background:rgba(0,180,80,.1);border:1px solid rgba(0,180,80,.4);color:#3ddc84}
    .flash.red{background:rgba(204,28,46,.08);border:1px solid rgba(204,28,46,.35);color:var(--red-b)}
    .card{background:var(--bg2);border:1px solid rgba(255,255,255,.07);border-radius:6px;padding:20px 22px}
    .card-title{font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:700;
      letter-spacing:.25em;color:var(--grey);margin-bottom:14px}
    .add-form{display:flex;gap:8px;flex-wrap:wrap;align-items:flex-end}
    .admin-input{background:var(--bg3);border:1px solid var(--grey);border-radius:4px;
      color:var(--cream);font-family:'IBM Plex Mono',monospace;font-size:13px;
      padding:8px 12px;outline:none;transition:border-color .2s}
    .admin-input:focus{border-color:var(--blue-b)}
    .admin-input.sm{padding:5px 9px;font-size:11px;width:130px}
    .admin-select{background:var(--bg3);border:1px solid var(--grey);border-radius:4px;
      color:var(--cream);font-family:'IBM Plex Mono',monospace;font-size:11px;
      padding:5px 8px;outline:none;cursor:pointer}
    .admin-btn{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:700;
      letter-spacing:.12em;border:none;border-radius:4px;cursor:pointer;
      padding:8px 16px;transition:all .18s;text-transform:uppercase}
    .admin-btn.sm{padding:5px 10px;font-size:10px}
    .admin-btn.red{background:var(--red);color:#fff}.admin-btn.red:hover{background:var(--red-b)}
    .admin-btn.blue{background:var(--blue);color:#fff}.admin-btn.blue:hover{background:var(--blue-b)}
    .admin-btn.grey{background:var(--bg3);color:var(--grey-lt);border:1px solid var(--grey)}
    .admin-btn.grey:hover{background:rgba(255,255,255,.06);color:var(--cream)}
    table{width:100%;border-collapse:collapse}
    thead tr{border-bottom:1px solid rgba(255,255,255,.1)}
    th{font-family:'IBM Plex Mono',monospace;font-size:9px;font-weight:700;
      letter-spacing:.2em;color:var(--grey);text-align:left;padding:8px 10px}
    td{padding:9px 10px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:middle}
    tr:hover td{background:rgba(255,255,255,.02)}
    .actions-cell{display:flex;gap:6px;align-items:center;flex-wrap:wrap}
    .inline-form{display:flex;gap:5px;align-items:center}
    .role-dot{font-size:12px;margin-right:4px}
    .count-badge{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--blue-b);
      background:rgba(26,58,160,.15);border:1px solid rgba(26,58,160,.35);
      padding:2px 10px;border-radius:2px}
    /* Color picker section */
    .color-grid{display:flex;gap:16px;flex-wrap:wrap;align-items:center}
    .color-row{display:flex;align-items:center;gap:8px}
    .color-swatch-input{width:40px;height:32px;padding:2px;border:1px solid var(--grey);
      border-radius:4px;background:var(--bg3);cursor:pointer}
    .color-label{font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:700;letter-spacing:.15em;min-width:80px}
    /* Branding fields */
    .brand-field{display:flex;flex-direction:column;gap:6px;width:100%;max-width:520px}
    .brand-field>span{font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:700;
      letter-spacing:.18em;color:var(--grey);text-transform:uppercase}
    .brand-field .admin-input{width:100%}
    /* Role pills in user table */    .role-pills{display:flex;gap:5px;flex-wrap:wrap}
    .role-pill{font-family:'IBM Plex Mono',monospace;font-size:9px;font-weight:700;
      letter-spacing:.14em;padding:3px 9px;border-radius:3px;border:1px solid;
      background:transparent;cursor:pointer;transition:all .15s;text-transform:uppercase;
      opacity:.55}
    .role-pill:hover{opacity:1;transform:translateY(-1px)}
    .role-pill.active{opacity:1;font-weight:900}
    /* Ban add form */
    .ban-add-form{display:flex;gap:8px;margin-bottom:16px}
    .reset-link-row{display:flex;gap:6px;align-items:center}
  </style>
</head>
<body>
<header>
  <div class="hdr-brand">
    <img src="${esc(brand.logo)}" alt="${esc(brand.siteName)}" class="hdr-logo"/>
    <span class="hdr-tag">ADMIN PANEL</span>
  </div>
  <div class="hdr-actions">
    <span class="count-badge">${users.length} BRUKER${users.length!==1?'E':''}</span>
    <a href="/admin/chatlog" class="logout-link">📋 Chat-logg</a>
    <a href="/admin/logout" class="logout-link">LOGG UT</a>
  </div>
</header>
<main>
  <div><h1>BRUKERADMINISTRASJON</h1><p class="subtitle">${esc((brand.siteName || 'PULSE CHAT').toUpperCase())} · KONTROLLPANEL</p></div>
  ${flash ? `<div class="flash ${flash[1]}">${flash[0]}</div>` : ''}

  <!-- LEGG TIL BRUKER -->
  <div class="card">
    <div class="card-title">LEGG TIL NY BRUKER</div>
    <form method="POST" action="/admin/users/add" class="add-form">
      <input class="admin-input" type="text" name="username" placeholder="Brukernavn" maxlength="24" required autocomplete="off"/>
      <input class="admin-input" type="password" name="password" placeholder="Passord" required/>
      <select name="role" class="admin-select">${roleOptions('member')}</select>
      <button class="admin-btn blue" type="submit">+ LEGG TIL BRUKER</button>
    </form>
  </div>

  <!-- BRUKERLISTE -->
  <div class="card">
    <div class="card-title">REGISTRERTE BRUKERE (${users.length})</div>
    ${users.length===0 ? '<p style="color:var(--grey);font-size:13px">Ingen brukere ennå.</p>' : `
    <table>
      <thead><tr><th>BRUKERNAVN</th><th>E-POST</th><th>REGISTRERT</th><th>ROLLE</th><th>HANDLINGER</th></tr></thead>
      <tbody>${userRows}</tbody>
    </table>`}
  </div>

  <!-- BRANDING / WHITE-LABEL -->
  <div class="card">
    <div class="card-title">BRANDING</div>
    <form method="POST" action="/admin/settings/branding" class="add-form" style="flex-direction:column;align-items:stretch;gap:14px">
      <label class="brand-field"><span>Sidenavn</span>
        <input class="admin-input" type="text" name="siteName" value="${esc(brand.siteName)}" maxlength="40" placeholder="Pulse Chat"/></label>
      <label class="brand-field"><span>Undertittel / radionavn</span>
        <input class="admin-input" type="text" name="tagline" value="${esc(brand.tagline)}" maxlength="60" placeholder="Pulse Radio"/></label>
      <label class="brand-field"><span>Velkomstmelding <small style="opacity:.6">(bruk {name} for brukernavn)</small></span>
        <input class="admin-input" type="text" name="welcomeMessage" value="${esc(brand.welcomeMessage)}" maxlength="120" placeholder="Vi ønsker {name} velkommen til Pulse Chat! 🎙️"/></label>
      <label class="brand-field"><span>Krediteringstekst (låst)</span>
        <input class="admin-input" type="text" value="${esc(brand.creditText)}" maxlength="80" disabled style="opacity:.6;cursor:not-allowed"/></label>
      <label class="brand-field"><span>Temafarge (aksent)</span>
        <input class="admin-input" type="color" name="themeColor" value="${esc(brand.themeColor || '#CC1C2E')}" style="height:42px;padding:4px;width:80px"/></label>
      <label class="brand-field" style="flex-direction:row;align-items:center;gap:10px">
        <input type="checkbox" name="radioEnabled" ${brand.radioEnabled ? 'checked' : ''} style="width:auto"/>
        <span style="margin:0">Vis radiospiller</span></label>
      <label class="brand-field"><span>Stream-URL (radio)</span>
        <input class="admin-input" type="url" name="streamUrl" value="${esc(brand.streamUrl)}" placeholder="https://.../radio.mp3"/></label>
      <label class="brand-field"><span>Now-playing-URL (radio)</span>
        <input class="admin-input" type="url" name="nowPlayingUrl" value="${esc(brand.nowPlayingUrl)}" placeholder="https://.../api/nowplaying/..."/></label>
      <button class="admin-btn blue" type="submit">LAGRE BRANDING</button>
    </form>

    <div style="margin-top:18px;border-top:1px solid var(--border);padding-top:16px">
      <div class="card-title" style="margin-bottom:10px">LOGO</div>
      <div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap">
        <img id="logo-preview" src="${esc(brand.logo)}" alt="Logo" style="height:48px;width:auto;background:#0008;border-radius:8px;padding:6px"/>
        <input id="logo-file" type="file" accept="image/*" style="color:var(--grey);font-size:13px"/>
        <button id="logo-upload-btn" class="admin-btn blue" type="button">LAST OPP LOGO</button>
        <span id="logo-status" style="font-size:13px;color:var(--grey)"></span>
      </div>
    </div>
  </div>
  <script>
    (function(){
      var fileInput=document.getElementById('logo-file');
      var btn=document.getElementById('logo-upload-btn');
      var status=document.getElementById('logo-status');
      var preview=document.getElementById('logo-preview');
      if(!btn)return;
      btn.addEventListener('click',function(){
        var f=fileInput.files&&fileInput.files[0];
        if(!f){status.textContent='Velg en fil først.';return;}
        if(f.size>2*1024*1024){status.textContent='Filen er for stor (maks 2 MB).';return;}
        var reader=new FileReader();
        reader.onload=function(){
          status.textContent='Laster opp…';
          fetch('/admin/settings/logo',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({base64:reader.result})})
            .then(function(r){return r.json();})
            .then(function(d){
              if(d.logo){preview.src=d.logo;status.textContent='✔ Logo oppdatert.';}
              else{status.textContent='✖ '+(d.error||'Feil ved opplasting.');}
            }).catch(function(){status.textContent='✖ Feil ved opplasting.';});
        };
        reader.readAsDataURL(f);
      });
    })();
  </script>

  <!-- ROLLEFARGER -->
  <div class="card">
    <div class="card-title">ROLLEFARGER</div>
    <form method="POST" action="/admin/settings/colors" class="add-form" style="flex-direction:column;align-items:flex-start;gap:14px">
      <div class="color-grid">${colorInputs}</div>
      <button class="admin-btn blue" type="submit">LAGRE FARGER</button>
    </form>
  </div>

  <!-- UTESTENGINGSLISTE -->
  <div class="card">
    <div class="card-title">UTESTENGINGSLISTE (${bans.length})</div>
    <div class="ban-add-form">
      <form method="POST" action="/admin/bans/add" class="inline-form">
        <input class="admin-input" type="text" name="username" placeholder="Brukernavn å utestenge" required/>
        <button class="admin-btn red" type="submit">UTESTENG BRUKER</button>
      </form>
    </div>
    <table>
      <thead><tr><th>BRUKERNAVN</th><th>UTESTENGT</th><th>HANDLING</th></tr></thead>
      <tbody>${banRows}</tbody>
    </table>
  </div>

  <!-- PASSORDTILBAKESTILLING -->
  <div class="card">
    <div class="card-title">PASSORDTILBAKESTILLINGER (${resets.length})</div>
    ${resets.length === 0
      ? '<p style="color:var(--grey);font-size:13px">Ingen ventende forespørsler.</p>'
      : `<table>
      <thead><tr><th>BRUKERNAVN</th><th>FORESPURT</th><th>TILBAKESTILLINGSLENKE</th><th>HANDLING</th></tr></thead>
      <tbody>${resets.map(r => {
        const resetUrl = `${host}/reset-password?token=${r.token}`;
        return `<tr>
        <td><strong>${esc(r.username)}</strong></td>
        <td style="color:var(--grey-lt);font-size:11px">${new Date(r.requestedAt).toLocaleString()}</td>
        <td>
          <div class="reset-link-row">
            <input class="admin-input sm" style="width:220px" readonly value="${esc(resetUrl)}" onclick="this.select()"/>
            <button class="admin-btn grey sm" type="button"
              onclick="navigator.clipboard.writeText('${esc(resetUrl)}').then(()=>this.textContent='Kopiert!');return false">Kopier</button>
          </div>
        </td>
        <td>
          <form method="POST" action="/admin/resets/delete" class="inline-form"
                onsubmit="return confirm('Slett denne tilbakestillingsforespørselen?')">
            <input type="hidden" name="token" value="${esc(r.token)}"/>
            <button class="admin-btn red sm" type="submit">Slett</button>
          </form>
        </td>
      </tr>`;
      }).join('')}</tbody>
    </table>`}
  </div>
</main>
<script>
  // Replace placeholder with real origin
  document.querySelectorAll('.reset-link-row input').forEach(inp => {
    inp.value = inp.value.replace('__HOST__', window.location.origin);
  });
  document.querySelectorAll('.reset-link-row button[onclick]').forEach(btn => {
    btn.setAttribute('onclick', btn.getAttribute('onclick').replace(/__HOST__/g, window.location.origin));
  });
</script>
</body>
</html>`;
}

function esc(s) {
  return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}

function renderChatLog(entries, { page, pages, total, room, rooms }) {
  const brand = loadSettings().branding || {};
  const esc2 = (s) => String(s == null ? '' : s).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  const ROLE_COLORS_LOG = { admin:'#CC1C2E', dj:'#1A3AA0', moderator:'#7B3FE4', vip:'#D4A017', member:'#8a8da8', guest:'#5a5c70' };
  const fmt = ts => {
    if (!ts) return '—';
    const d = new Date(ts);
    return d.toLocaleDateString('no-NO') + ' ' + d.toTimeString().slice(0,8);
  };
  const rows = entries.map(e => {
    if (e.type === 'clear') return `
      <tr class="clear-row">
        <td>${fmt(e.ts)}</td>
        <td>${esc(e.room||'—')}</td>
        <td colspan="2" style="color:#e8563a;font-style:italic">⚠ Chat tømt av <strong>${esc(e.by||'?')}</strong></td>
      </tr>`;
    const col = ROLE_COLORS_LOG[e.role] || '#8a8da8';
    return `<tr>
      <td style="font-size:11px;color:#5a5c70;white-space:nowrap">${fmt(e.ts)}</td>
      <td style="font-size:11px;color:#5a5c70">${esc(e.room||'—')}</td>
      <td style="color:${col};font-weight:600;white-space:nowrap">${esc(e.displayName||'?')}</td>
      <td class="msg-cell">${e.gifUrl ? `<em style="color:#5a5c70">[GIF]</em>` : esc(e.text||'')}</td>
    </tr>`;
  }).join('');

  const roomOptions = rooms.map(r => `<option value="${esc(r)}"${r===room?' selected':''}>${esc(r)}</option>`).join('');
  const pagerPrev = page > 1 ? `<a href="/admin/chatlog?page=${page-1}&room=${esc(room)}" class="admin-btn grey sm">← Forrige</a>` : '';
  const pagerNext = page < pages ? `<a href="/admin/chatlog?page=${page+1}&room=${esc(room)}" class="admin-btn grey sm">Neste →</a>` : '';

  return `<!DOCTYPE html>
<html lang="no">
<head>
  <meta charset="UTF-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
  <title>Chat-logg — Admin</title>
  <link rel="icon" type="image/png" href="${esc2(brand.logo)}"/>
  <link href="https://fonts.googleapis.com/css2?family=Bebas+Neue&family=IBM+Plex+Mono:wght@400;500;700&family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet"/>
  <style>
    *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
    :root{--bg:#0C0D14;--bg2:#10111C;--bg3:#181924;--red:#CC1C2E;--red-b:#E8223B;
      --cream:#EEF0F8;--grey:#5a5c70;--grey-lt:#8a8da8;--green:#22C55E}
    body{background:var(--bg);color:var(--cream);font-family:'Space Grotesk',sans-serif;min-height:100vh}
    header{display:flex;align-items:center;justify-content:space-between;padding:0 28px;
      height:56px;background:var(--bg2);border-bottom:1px solid rgba(204,28,46,.25)}
    .hdr-brand{display:flex;align-items:center;gap:12px}
    .hdr-logo{height:36px;filter:drop-shadow(0 2px 8px rgba(204,28,46,.5))}
    .hdr-tag{font-family:'IBM Plex Mono',monospace;font-size:10px;font-weight:700;
      letter-spacing:.25em;color:var(--red-b);border:1px solid var(--red);padding:2px 8px;border-radius:2px}
    .hdr-actions{display:flex;gap:12px;align-items:center}
    .logout-link{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.12em;
      color:var(--grey-lt);text-decoration:none;border:1px solid var(--grey);
      padding:5px 12px;border-radius:3px;transition:all .18s}
    .logout-link:hover{border-color:var(--red);color:var(--red)}
    main{max-width:1100px;margin:0 auto;padding:28px 20px;display:flex;flex-direction:column;gap:18px}
    h1{font-family:'Bebas Neue',sans-serif;font-size:30px;letter-spacing:.04em}
    .subtitle{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.18em;color:var(--grey-lt)}
    .card{background:var(--bg2);border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:20px}
    .toolbar{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
    select{background:var(--bg3);color:var(--cream);border:1px solid rgba(255,255,255,.12);
      padding:6px 10px;border-radius:4px;font-family:'Space Grotesk',sans-serif;font-size:13px}
    table{width:100%;border-collapse:collapse;font-size:13px}
    th{font-family:'IBM Plex Mono',monospace;font-size:10px;letter-spacing:.12em;
      color:var(--grey-lt);text-align:left;padding:8px 10px;border-bottom:1px solid rgba(255,255,255,.07)}
    td{padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.04);vertical-align:top}
    .msg-cell{word-break:break-word;max-width:500px}
    tr:hover td{background:rgba(255,255,255,.02)}
    .clear-row td{background:rgba(232,86,58,.04)}
    .admin-btn{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:4px;
      font-family:'IBM Plex Mono',monospace;font-size:11px;font-weight:700;letter-spacing:.1em;
      cursor:pointer;text-decoration:none;border:none;transition:all .15s}
    .admin-btn.grey{background:#23243a;color:var(--grey-lt);border:1px solid rgba(255,255,255,.1)}
    .admin-btn.grey:hover{border-color:var(--cream);color:var(--cream)}
    .admin-btn.red{background:var(--red);color:#fff}
    .admin-btn.red:hover{background:var(--red-b)}
    .admin-btn.sm{padding:5px 10px;font-size:10px}
    .pager{display:flex;align-items:center;gap:10px;justify-content:center;padding-top:4px}
    .pager-info{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--grey-lt)}
    .flash{padding:10px 16px;border-radius:4px;font-family:'IBM Plex Mono',monospace;font-size:12px}
    .flash.green{background:rgba(0,180,80,.1);border:1px solid rgba(0,180,80,.4);color:#3ddc84}
  </style>
</head>
<body>
<header>
  <div class="hdr-brand">
    <img src="${esc2(brand.logo)}" class="hdr-logo" alt="${esc2(brand.siteName)}"/>
    <span class="hdr-tag">CHAT-LOGG</span>
  </div>
  <div class="hdr-actions">
    <a href="/admin/dashboard" class="logout-link">← Dashboard</a>
    <a href="/admin/logout" class="logout-link">Logg ut</a>
  </div>
</header>
<main>
  <div>
    <h1>Chat-logg</h1>
    <p class="subtitle">${total} meldinger totalt</p>
  </div>
  <div class="card">
    <div class="toolbar">
      <form method="GET" action="/admin/chatlog" style="display:flex;gap:8px;align-items:center">
        <select name="room">
          <option value="">Alle rom</option>
          ${roomOptions}
        </select>
        <button class="admin-btn grey sm" type="submit">Filtrer</button>
      </form>
      <form method="POST" action="/admin/chatlog/clear"
            onsubmit="return confirm('Slette hele chat-loggen permanent?')" style="margin-left:auto">
        <button class="admin-btn red sm" type="submit">🗑 Tøm loggfil</button>
      </form>
    </div>
  </div>
  <div class="card" style="padding:0;overflow:hidden">
    <table>
      <thead><tr>
        <th>Tidspunkt</th><th>Rom</th><th>Bruker</th><th>Melding</th>
      </tr></thead>
      <tbody>${rows || '<tr><td colspan="4" style="color:var(--grey);padding:20px;text-align:center">Ingen meldinger i loggen.</td></tr>'}</tbody>
    </table>
  </div>
  <div class="pager">
    ${pagerPrev}
    <span class="pager-info">Side ${page} av ${pages}</span>
    ${pagerNext}
  </div>
</main>
</body>
</html>`;
}

// ── START ─────────────────────────────────────────────────────
server.listen(PORT, () => {
  const siteName = (loadSettings().branding || {}).siteName || 'Pulse Chat';
  console.log(`\n🔴  ${siteName.toUpperCase()}  →  http://localhost:${PORT}`);
  console.log(`🔧  Admin panel  →  http://localhost:${PORT}/admin`);
  console.log(`    Admin PW: ${ADMIN_PASSWORD}\n`);
});
