require('dotenv').config({ quiet: true });

const crypto = require('crypto');
const { execFile } = require('child_process');
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const express = require('express');
const multer = require('multer');
const repository = require('./src/repository');
const { normalizeDocumentFileName } = require('./src/document-file-name');
const notificationRepository = require('./src/notification-repository');
const mailer = require('./src/mailer');
const { closePool, getPool } = require('./src/db');
const notiflixVersion = require('notiflix/package.json').version;

const app = express();
const port = Number(process.env.PORT || 3000);
const uploadDir = path.join(__dirname, 'uploads', 'packages');
const documentUploadDir = path.join(__dirname, 'uploads', 'documents');
const agentPackageDir = path.resolve(process.env.AGENT_PACKAGE_DIR || path.join(uploadDir, 'agent'));
const agentDebianPackageName = 'local-installer-agent';
const authCookieName = 'robot_installer_session';
const sessionMaxAgeMs = Number(process.env.SESSION_MAX_AGE_MS || 1000 * 60 * 60 * 8);
const authSecret = process.env.AUTH_SECRET || process.env.SESSION_SECRET || 'robot-installer-dev-secret';
const secureSessionCookie = getBooleanEnv(process.env.SESSION_COOKIE_SECURE, process.env.NODE_ENV === 'production');
const publicApiCorsOrigins = getCsvEnv(process.env.WEB_CLIENT_ORIGINS || process.env.PUBLIC_API_CORS_ORIGINS, [
  'https://app.pnkr.cloud',
  'http://localhost:5173',
  'http://localhost:4173'
]);
const installerIdentifierPattern = /^[a-zA-Z0-9._+-]+$/;
const installerVersionPattern = /^[a-zA-Z0-9._:+~=-]+$/;
const installerIdentifierHint = 'Code chi duoc dung chu, so, dau ., _, +, - va khong co khoang trang.';
const installerVersionHint = 'Version chi duoc dung chu, so va cac ky tu . _ : + ~ = -.';
const documentCategories = Object.freeze([
  { id: 'introduction', label: 'Giới thiệu' },
  { id: 'guide', label: 'Hướng dẫn' },
  { id: 'user-guide', label: 'Hướng dẫn sử dụng' },
  { id: 'technical', label: 'Tài liệu kỹ thuật' },
  { id: 'policy', label: 'Quy trình / chính sách' },
  { id: 'other', label: 'Khác' }
]);
const documentCategoryIds = new Set(documentCategories.map((category) => category.id));
const allowedDocumentExtensions = new Set([
  '.pdf', '.doc', '.docx', '.odt', '.rtf', '.txt', '.md',
  '.png', '.jpg', '.jpeg', '.webp',
  '.ppt', '.pptx', '.xls', '.xlsx'
]);
const documentTextExtensions = new Set(['.txt', '.md']);
const documentImageExtensions = new Set(['.png', '.jpg', '.jpeg', '.webp']);
const documentMaxContentChars = Number(process.env.DOCUMENT_MAX_CONTENT_CHARS || 500000);
const agentVersionCollator = new Intl.Collator('en', {
  numeric: true,
  sensitivity: 'base'
});
let didWarnAgentMetadataToolMissing = false;

app.get('/healthz', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

app.get('/readyz', asyncRoute(async (req, res) => {
  const database = await checkDatabaseReadiness();

  res.status(200).json({
    status: 'ready',
    database
  });
}));

fs.mkdirSync(uploadDir, { recursive: true });
fs.mkdirSync(documentUploadDir, { recursive: true });
fs.mkdirSync(agentPackageDir, { recursive: true });

const navItems = [
  { id: 'dashboard', label: 'Tổng quan', href: '/', icon: 'dashboard' },
  { id: 'packages', label: 'Packages', href: '/packages', icon: 'inventory_2' },
  { id: 'applications', label: 'Applications', href: '/applications', icon: 'apps' },
  { id: 'documents', label: 'Tài liệu', href: '/documents', icon: 'library_books' },
  { id: 'builder', label: 'Đóng gói App', href: '/builder', icon: 'deployed_code' },
  { id: 'agent', label: 'Agent', href: '/agent', icon: 'memory', adminOnly: true },
  { id: 'users', label: 'Users', href: '/users', icon: 'group', adminOnly: true }
];

const statusClassMap = {
  Active: 'badge-success',
  Latest: 'badge-primary',
  Stable: 'badge-info',
  Testing: 'badge-warning',
  Draft: 'badge-warning',
  Released: 'badge-success',
  Archived: 'badge-muted',
  Deprecated: 'badge-danger',
  Inactive: 'badge-muted',
  Admin: 'badge-primary',
  User: 'badge-info'
};

const storage = multer.diskStorage({
  destination: uploadDir,
  filename(req, file, callback) {
    const safeName = file.originalname
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-zA-Z0-9._-]/g, '-')
      .replace(/-+/g, '-')
      .toLowerCase();
    const suffix = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;

    callback(null, `${suffix}-${safeName}`);
  }
});

const agentStorage = multer.diskStorage({
  destination: agentPackageDir,
  filename(req, file, callback) {
    const safeName = file.originalname
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-zA-Z0-9._-]/g, '-')
      .replace(/-+/g, '-')
      .toLowerCase();
    const suffix = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;

    callback(null, `${suffix}-${safeName}`);
  }
});

const multipartLimits = Object.freeze({
  fieldNameSize: 64,
  fieldNestingDepth: 0,
  fieldSize: 64 * 1024,
  fields: 16,
  files: 1,
  parts: 17
});

const upload = multer({
  storage,
  limits: {
    ...multipartLimits,
    fileSize: Number(process.env.MAX_UPLOAD_BYTES || 1024 * 1024 * 1024)
  }
});

const documentStorage = multer.diskStorage({
  destination: documentUploadDir,
  filename(req, file, callback) {
    const normalizedName = normalizeDocumentFileName(file.originalname)
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-zA-Z0-9._-]/g, '-')
      .replace(/-+/g, '-')
      .toLowerCase();
    const extension = path.extname(normalizedName).slice(0, 20);
    const baseName = normalizedName.slice(0, normalizedName.length - extension.length).slice(0, 180) || 'document';
    const safeName = `${baseName}${extension}`;
    const suffix = `${Date.now()}-${crypto.randomBytes(4).toString('hex')}`;

    callback(null, `${suffix}-${safeName}`);
  }
});

const agentUpload = multer({
  storage: agentStorage,
  limits: {
    ...multipartLimits,
    fileSize: Number(process.env.AGENT_MAX_UPLOAD_BYTES || process.env.MAX_UPLOAD_BYTES || 1024 * 1024 * 1024)
  }
});

const documentUpload = multer({
  storage: documentStorage,
  limits: {
    ...multipartLimits,
    fieldSize: Number(process.env.DOCUMENT_MAX_CONTENT_BYTES || 2 * 1024 * 1024),
    fileSize: Number(process.env.DOCUMENT_MAX_UPLOAD_BYTES || 50 * 1024 * 1024)
  }
});

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

app.use(express.urlencoded({ extended: true, limit: '32kb', parameterLimit: 100 }));
app.use(express.json({ limit: '32kb' }));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/vendor/notiflix', express.static(path.join(__dirname, 'node_modules/notiflix/dist')));
app.use('/vendor/montserrat', express.static(path.join(__dirname, 'node_modules/@fontsource-variable/montserrat')));
app.use('/vendor/fluent-icons', express.static(path.join(__dirname, 'node_modules/@fluentui/svg-icons/icons')));
app.use(applyPublicApiCors);
app.get('/packages/agent/latest.deb', asyncRoute(async (req, res) => {
  const arch = normalizeAgentArch(req.query.arch);
  const latestPackage = await findLatestAgentPackage(arch);

  if (latestPackage) {
    res.type('application/vnd.debian.binary-package');
    res.setHeader('Content-Disposition', `attachment; filename="${latestPackage.fileName}"`);
    res.setHeader('X-Agent-Version', latestPackage.version);
    res.sendFile(latestPackage.filePath);
    return;
  }

  const latestPackageRecord = await findLatestAgentPackageRecord(arch);
  if (latestPackageRecord) {
    res.setHeader('X-Agent-Version', latestPackageRecord.version);
    res.redirect(latestPackageRecord.filePath);
    return;
  }

  res.status(404).type('text/plain').send(
    `No Local Installer Agent package found${arch ? ` for ${arch}` : ''}. ` +
    'Upload local-installer-agent_<version>_<arch>.deb to web-server/uploads/packages/agent, ' +
    'or upload it as package code local-installer-agent.'
  );
}));

app.get('/api/agent/latest', asyncRoute(async (req, res) => {
  const arch = normalizeAgentArch(req.query.arch) || 'amd64';
  const baseUrl = getBaseUrl(req);
  const latestPackage = await findLatestAgentPackage(arch);

  if (latestPackage) {
    res.json({
      version: latestPackage.version,
      arch: latestPackage.arch,
      fileName: latestPackage.fileName,
      size: latestPackage.size,
      sizeLabel: latestPackage.sizeLabel,
      downloadUrl: `${baseUrl}${latestPackage.downloadPath}`,
      installCommand: `curl -fsSL ${baseUrl}/install-agent.sh | sudo bash`
    });
    return;
  }

  const latestPackageRecord = await findLatestAgentPackageRecord(arch);
  if (latestPackageRecord) {
    const recordPath = latestPackageRecord.filePath || '';
    const downloadUrl = /^https?:\/\//i.test(recordPath)
      ? recordPath
      : `${baseUrl}${recordPath.startsWith('/') ? recordPath : `/${recordPath}`}`;

    res.json({
      version: latestPackageRecord.version,
      arch,
      fileName: '',
      size: 0,
      sizeLabel: '',
      downloadUrl,
      installCommand: `curl -fsSL ${baseUrl}/install-agent.sh | sudo bash`
    });
    return;
  }

  res.status(404).json({
    error: `No Local Installer Agent package found for ${arch}`
  });
}));

app.use('/packages/agent', express.static(agentPackageDir, { fallthrough: false }));
app.use('/packages/agent', staticFileNotFoundHandler('Agent package file'));
app.use(loadCurrentUser);

function helpers() {
  return {
    statusClass(status) {
      return statusClassMap[status] || 'badge-muted';
    },
    packageTypeLabel(type) {
      if (type === 'docker') return 'Docker';
      if (type === 'apt') return 'APT';
      return '.deb';
    },
    packageTypeClass(type) {
      if (type === 'docker') return 'badge-info';
      if (type === 'apt') return 'badge-warning';
      return 'badge-primary';
    },
    documentCategoryLabel(categoryId) {
      return documentCategories.find((category) => category.id === categoryId)?.label || 'Khác';
    }
  };
}

function getCsvEnv(value, fallback) {
  if (!value) return fallback;
  return String(value)
    .split(',')
    .map((item) => item.trim())
    .filter(Boolean);
}

function getBooleanEnv(value, fallback) {
  if (value === undefined || value === null || value === '') return fallback;
  return ['1', 'true', 'yes', 'on'].includes(String(value).toLowerCase());
}

function escapeShellDoubleQuoted(value) {
  return String(value || '').replace(/(["\\$`])/g, '\\$1');
}

function isPublicApiCorsPath(pathname) {
  return pathname === '/api/apps'
    || pathname.startsWith('/api/apps/')
    || pathname === '/api/agent/latest'
    || pathname.startsWith('/api/package-versions/')
    || pathname === '/install-agent.sh';
}

function applyPublicApiCors(req, res, next) {
  if (!isPublicApiCorsPath(req.path)) {
    next();
    return;
  }

  const origin = req.headers.origin;
  const allowAnyOrigin = publicApiCorsOrigins.includes('*');

  if (origin && (allowAnyOrigin || publicApiCorsOrigins.includes(origin))) {
    res.setHeader('Access-Control-Allow-Origin', allowAnyOrigin ? '*' : origin);
    res.setHeader('Vary', 'Origin');
    res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
    res.setHeader('Access-Control-Allow-Headers', 'Accept, Content-Type');
  }

  if (req.method === 'OPTIONS') {
    res.sendStatus(204);
    return;
  }

  next();
}

function staticFileNotFoundHandler(label) {
  return (error, req, res, next) => {
    if (error.status === 404 || error.statusCode === 404 || error.code === 'ENOENT') {
      res.status(404).type('text/plain').send(`${label} not found.`);
      return;
    }

    next(error);
  };
}

function getVisibleNavItems(user) {
  return navItems.filter((item) => !item.adminOnly || (user && user.role === 'Admin'));
}

function getNotice(req) {
  if (!req.query.notice) return null;

  return {
    type: req.query.noticeType || 'info',
    message: req.query.notice
  };
}

function getCurrentPath(req) {
  const url = new URL(req.originalUrl || '/', 'http://robot-installer.local');
  url.searchParams.delete('notice');
  url.searchParams.delete('noticeType');

  return `${url.pathname}${url.search}` || '/';
}

function toDateInputValue(value = new Date()) {
  const date = value instanceof Date ? value : new Date(value);
  const safeDate = Number.isNaN(date.getTime()) ? new Date() : date;
  const year = safeDate.getFullYear();
  const month = String(safeDate.getMonth() + 1).padStart(2, '0');
  const day = String(safeDate.getDate()).padStart(2, '0');

  return `${year}-${month}-${day}`;
}

function viewModel(req, active, title, pageData, extra = {}) {
  const currentUser = pageData.currentUser || req.currentUser || {
    name: 'Guest',
    username: 'guest',
    role: 'Guest',
    email: ''
  };

  return {
    active,
    title,
    navItems: getVisibleNavItems(currentUser),
    currentUser,
    stats: pageData.stats,
    packages: pageData.packages,
    applications: pageData.applications,
    activity: pageData.activity,
    notiflixVersion,
    notice: getNotice(req),
    currentPath: getCurrentPath(req),
    databaseLabel: getSqlServerDisplayLabel(),
    todayDate: toDateInputValue(),
    helpers: helpers(),
    ...extra
  };
}

function getSqlServerDisplayLabel() {
  const host = process.env.SQLSERVER_HOST || 'localhost';
  const port = process.env.SQLSERVER_PORT || '1433';
  const database = process.env.SQLSERVER_DATABASE || 'RobotInstaller';

  return `${host}:${port}/${database}`;
}

async function checkDatabaseReadiness() {
  const pool = await getPool();
  const result = await pool.request().query(`
    SELECT
      DB_NAME() AS DatabaseName,
      CASE WHEN OBJECT_ID(N'dbo.Users', N'U') IS NULL THEN 0 ELSE 1 END AS HasUsers,
      CASE WHEN OBJECT_ID(N'dbo.vw_PackageList', N'V') IS NULL THEN 0 ELSE 1 END AS HasPackageList,
      CASE WHEN OBJECT_ID(N'dbo.vw_ApplicationList', N'V') IS NULL THEN 0 ELSE 1 END AS HasApplicationList;
  `);

  const row = result.recordset[0] || {};
  const missingObjects = [];

  if (!row.HasUsers) missingObjects.push('dbo.Users');
  if (!row.HasPackageList) missingObjects.push('dbo.vw_PackageList');
  if (!row.HasApplicationList) missingObjects.push('dbo.vw_ApplicationList');

  if (missingObjects.length > 0) {
    const error = new Error(`Missing database objects: ${missingObjects.join(', ')}`);
    error.code = 'DB_SCHEMA_NOT_READY';
    throw error;
  }

  return {
    server: process.env.SQLSERVER_HOST || 'localhost',
    port: Number(process.env.SQLSERVER_PORT || 1433),
    database: row.DatabaseName || process.env.SQLSERVER_DATABASE || 'RobotInstaller'
  };
}

function getErrorDetails(error) {
  if (!getBooleanEnv(process.env.APP_SHOW_ERROR_DETAILS, process.env.NODE_ENV !== 'production')) {
    return null;
  }

  return {
    code: error.code || error.name || 'ERROR',
    message: error.message || 'Unexpected error'
  };
}

function authViewModel(req, mode, values = {}) {
  const formValues = mode === 'register'
    ? {
      username: values.username || String(req.query.username || ''),
      email: values.email || String(req.query.email || ''),
      fullName: values.fullName || String(req.query.fullName || ''),
      notice: values.notice
    }
    : values;

  return {
    title: mode === 'register' ? 'Đăng ký' : 'Đăng nhập',
    mode,
    notiflixVersion,
    notice: values.notice || getNotice(req),
    values: formValues,
    returnTo: sanitizeReturnTo(req.query.returnTo)
  };
}

function confirmEmailViewModel(req, email = '') {
  return {
    title: 'Xác nhận email',
    notiflixVersion,
    notice: getNotice(req),
    email
  };
}

function renderRegisterWithNotice(req, res, type, message) {
  const query = new URLSearchParams();
  const body = req.body || {};

  if (body.username) query.set('username', String(body.username).trim());
  if (body.email) query.set('email', String(body.email).trim());
  if (body.fullName) query.set('fullName', String(body.fullName).trim());

  const url = query.toString() ? `/register?${query.toString()}` : '/register';
  redirectWithNotice(res, url, type, message);
}

function asyncRoute(handler) {
  return (req, res, next) => {
    Promise.resolve(handler(req, res, next)).catch(next);
  };
}

function redirectWithNotice(res, url, type, message) {
  const separator = url.includes('?') ? '&' : '?';
  res.redirect(`${url}${separator}noticeType=${encodeURIComponent(type)}&notice=${encodeURIComponent(message)}`);
}

function parseCookies(cookieHeader) {
  if (!cookieHeader) return {};

  return cookieHeader.split(';').reduce((cookies, pair) => {
    const index = pair.indexOf('=');
    if (index === -1) return cookies;

    const key = pair.slice(0, index).trim();
    const value = pair.slice(index + 1).trim();
    try {
      cookies[key] = decodeURIComponent(value);
    } catch (error) {
      cookies[key] = value;
    }
    return cookies;
  }, {});
}

function signSessionPayload(payload) {
  return crypto
    .createHmac('sha256', authSecret)
    .update(payload)
    .digest('base64url');
}

function createSessionToken(user) {
  const payload = Buffer
    .from(JSON.stringify({
      sub: user.id,
      exp: Date.now() + sessionMaxAgeMs
    }))
    .toString('base64url');
  const signature = signSessionPayload(payload);

  return `${payload}.${signature}`;
}

function verifySessionToken(token) {
  if (!token || !token.includes('.')) return null;

  const [payload, signature] = token.split('.');
  const expectedSignature = signSessionPayload(payload);
  const signatureBuffer = Buffer.from(signature);
  const expectedSignatureBuffer = Buffer.from(expectedSignature);

  if (
    signatureBuffer.length !== expectedSignatureBuffer.length
    || !crypto.timingSafeEqual(signatureBuffer, expectedSignatureBuffer)
  ) {
    return null;
  }

  try {
    const session = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
    if (!session.sub || Number(session.exp) < Date.now()) return null;
    return session;
  } catch (error) {
    return null;
  }
}

function setAuthCookie(res, user) {
  res.cookie(authCookieName, createSessionToken(user), {
    httpOnly: true,
    maxAge: sessionMaxAgeMs,
    sameSite: 'lax',
    secure: secureSessionCookie
  });
}

function clearAuthCookie(res) {
  res.clearCookie(authCookieName, {
    httpOnly: true,
    sameSite: 'lax',
    secure: secureSessionCookie
  });
}

function sanitizeReturnTo(value) {
  const fallback = '/';
  if (!value || typeof value !== 'string') return fallback;
  if (!value.startsWith('/') || value.startsWith('//')) return fallback;

  const [pathname] = value.split('?');
  if (pathname === '/login' || pathname === '/register') return fallback;

  return value;
}

function sanitizeNotificationActionUrl(value) {
  const text = String(value || '').trim();
  if (!text) return '';
  if (
    text.length > 1000
    || !text.startsWith('/')
    || text.startsWith('//')
    || text.includes('\\')
    || /[\u0000-\u001F\u007F]/.test(text)
  ) {
    return null;
  }

  return text;
}

function normalizeNotificationSeverity(value) {
  const severity = String(value || 'info').trim().toLowerCase();
  return ['info', 'success', 'warning', 'error'].includes(severity) ? severity : null;
}

function isUuid(value) {
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
    String(value || '').trim()
  );
}

function getNotificationActorName(user) {
  return String(user?.name || user?.username || '').trim();
}

async function safelyCreateNotification(label, callback) {
  try {
    return await callback();
  } catch (error) {
    console.error(`Cannot create notification (${label}):`, error);
    return null;
  }
}

async function safelyLoadNotificationContext(label, callback) {
  try {
    return await callback();
  } catch (error) {
    console.error(`Cannot load notification context (${label}):`, error);
    return null;
  }
}

function getBaseUrl(req) {
  const requestBaseUrl = getRequestBaseUrl(req);
  if (process.env.APP_BASE_URL) {
    const configuredBaseUrl = process.env.APP_BASE_URL.replace(/\/+$/, '');
    if (!isLoopbackBaseUrl(configuredBaseUrl) || isLoopbackBaseUrl(requestBaseUrl)) {
      return configuredBaseUrl;
    }
  }

  return requestBaseUrl;
}

function getRequestBaseUrl(req) {
  const forwardedProtocol = req.headers['x-forwarded-proto'];
  const forwardedHost = req.headers['x-forwarded-host'];
  const forwardedPort = req.headers['x-forwarded-port'];
  const protocol = forwardedProtocol
    ? String(forwardedProtocol).split(',')[0].trim()
    : req.protocol;
  let host = forwardedHost
    ? String(forwardedHost).split(',')[0].trim()
    : req.get('host');

  if (forwardedPort && host && !hostHasPort(host)) {
    const port = String(forwardedPort).split(',')[0].trim();
    if (port && !isDefaultPort(protocol, port)) {
      host = `${host}:${port}`;
    }
  }

  return `${protocol}://${host}`;
}

function isLoopbackBaseUrl(value) {
  try {
    const hostname = new URL(value).hostname.toLowerCase();
    return hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
  } catch {
    return false;
  }
}

function hostHasPort(host) {
  return /:\d+$/.test(host) || /^\[[^\]]+\]:\d+$/.test(host);
}

function isDefaultPort(protocol, port) {
  return (protocol === 'http' && port === '80') || (protocol === 'https' && port === '443');
}

function normalizeAgentArch(value) {
  const arch = String(value || '').trim().toLowerCase();
  return /^[a-z0-9][a-z0-9._-]*$/.test(arch) ? arch : '';
}

function isValidAgentVersion(value) {
  return /^[a-zA-Z0-9][a-zA-Z0-9._+~=-]*$/.test(String(value || '').trim());
}

function compareAgentPackages(first, second) {
  const versionCompare = agentVersionCollator.compare(first.version, second.version);

  if (versionCompare !== 0) return versionCompare;
  return first.mtimeMs - second.mtimeMs;
}

function formatBytes(bytes) {
  const value = Number(bytes || 0);
  if (value < 1024) return `${value} B`;

  const units = ['KB', 'MB', 'GB'];
  let size = value / 1024;
  let unitIndex = 0;

  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex += 1;
  }

  return `${size.toFixed(size >= 10 ? 1 : 2)} ${units[unitIndex]}`;
}

function formatLocalDateTime(value) {
  return new Intl.DateTimeFormat('vi-VN', {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
    hour: '2-digit',
    minute: '2-digit'
  }).format(value);
}

function getAgentPackageDownloadPath(fileName) {
  return `/packages/agent/${encodeURIComponent(fileName)}`;
}

function parseAgentPackageFileName(fileName) {
  const match = /^local-installer-agent_([^/\\]+)_([^/\\]+)\.deb$/.exec(fileName);
  if (!match) return null;

  return {
    version: match[1],
    arch: match[2]
  };
}

function parseDebControlOutput(output) {
  return String(output || '')
    .split(/\r?\n/)
    .reduce((metadata, line) => {
      const match = /^([^:]+):\s*(.*)$/.exec(line);
      if (!match) return metadata;

      const key = match[1].trim().toLowerCase();
      const value = match[2].trim();

      if (key === 'package') metadata.package = value;
      if (key === 'version') metadata.version = value;
      if (key === 'architecture') metadata.architecture = value;

      return metadata;
    }, {});
}

function isDebMetadataInspectionUnavailable(error) {
  const message = String(error?.message || '');

  return error?.code === 'ENOENT'
    || /unable to execute decompressing archive/i.test(message)
    || /member 'control\.tar' \([^)]+\): No such file or directory/i.test(message);
}

async function readDebPackageMetadata(filePath) {
  return new Promise((resolve, reject) => {
    execFile(
      'dpkg-deb',
      ['-f', filePath, 'Package', 'Version', 'Architecture'],
      { windowsHide: true, timeout: 10000, maxBuffer: 1024 * 1024 },
      (error, stdout) => {
        if (error) {
          if (isDebMetadataInspectionUnavailable(error)) error.metadataToolMissing = true;
          reject(error);
          return;
        }

        const metadata = parseDebControlOutput(stdout);
        if (!metadata.package || !metadata.version || !metadata.architecture) {
          reject(new Error('Missing Package, Version, or Architecture in deb control metadata.'));
          return;
        }

        resolve(metadata);
      }
    );
  });
}

async function readDebPackageMetadataIfAvailable(filePath) {
  try {
    return await readDebPackageMetadata(filePath);
  } catch (error) {
    if (error.metadataToolMissing) {
      if (!didWarnAgentMetadataToolMissing) {
        console.warn('dpkg-deb metadata inspection is not available; using agent package filename metadata.');
        didWarnAgentMetadataToolMissing = true;
      }
      return null;
    }

    console.warn(`Cannot inspect agent package metadata for ${path.basename(filePath)}:`, error.message);
    return null;
  }
}

async function getAgentPackageFromEntry(entry) {
  const fileNameMetadata = parseAgentPackageFileName(entry.name);
  if (!fileNameMetadata) return null;

  const filePath = path.join(agentPackageDir, entry.name);
  const stat = await fsp.stat(filePath);
  const debMetadata = await readDebPackageMetadataIfAvailable(filePath);

  if (debMetadata?.package && debMetadata.package !== agentDebianPackageName) {
    console.warn(`Ignoring ${entry.name}: deb package is ${debMetadata.package}, expected ${agentDebianPackageName}.`);
    return null;
  }

  if (debMetadata) {
    const metadataArch = normalizeAgentArch(debMetadata.architecture);
    const fileNameArch = normalizeAgentArch(fileNameMetadata.arch);

    if (debMetadata.version !== fileNameMetadata.version || metadataArch !== fileNameArch) {
      console.warn(
        `Ignoring ${entry.name}: filename metadata does not match deb metadata ` +
        `(deb version=${debMetadata.version}, deb arch=${debMetadata.architecture}).`
      );
      return null;
    }
  }

  const version = fileNameMetadata.version;
  const packageArch = fileNameMetadata.arch;

  return {
    fileName: entry.name,
    filePath,
    version,
    arch: packageArch,
    size: stat.size,
    sizeLabel: formatBytes(stat.size),
    uploadedAt: formatLocalDateTime(stat.mtime),
    mtimeMs: stat.mtimeMs,
    downloadPath: getAgentPackageDownloadPath(entry.name)
  };
}

async function listAgentPackages(arch = '') {
  let dirEntries;
  try {
    dirEntries = await fsp.readdir(agentPackageDir, { withFileTypes: true });
  } catch (error) {
    if (error.code === 'ENOENT') return null;
    throw error;
  }

  const packages = (await Promise.all(
    dirEntries
      .filter((entry) => entry.isFile())
      .map(getAgentPackageFromEntry)
  ))
    .filter(Boolean)
    .filter((packageItem) => !arch || packageItem.arch.toLowerCase() === arch);

  const latestByArch = new Map();
  packages
    .sort(compareAgentPackages);

  packages.forEach((packageItem) => {
    latestByArch.set(packageItem.arch.toLowerCase(), packageItem.fileName);
  });

  return packages
    .sort((first, second) => compareAgentPackages(second, first))
    .map((packageItem) => ({
      ...packageItem,
      isLatestForArch: latestByArch.get(packageItem.arch.toLowerCase()) === packageItem.fileName
    }));
}

async function findLatestAgentPackage(arch = '') {
  const packages = await listAgentPackages(arch);
  if (!packages) return null;
  const sortedPackages = packages
    .slice()
    .sort(compareAgentPackages);

  return sortedPackages[sortedPackages.length - 1] || null;
}

function packageVersionMatchesArch(version, arch) {
  if (!arch) return true;
  return String(version.filePath || '').toLowerCase().includes(`_${arch}.deb`);
}

async function findLatestAgentPackageRecord(arch = '') {
  try {
    const packageItem = await repository.getPackageById('local-installer-agent');
    const latestVersion = packageItem?.versions
      ?.filter((version) => version.filePath && packageVersionMatchesArch(version, arch))
      ?.[0];

    if (!latestVersion) return null;

    return {
      filePath: latestVersion.filePath,
      version: latestVersion.version
    };
  } catch (error) {
    console.warn('Cannot read local-installer-agent package from database:', error.message);
    return null;
  }
}

async function loadCurrentUser(req, res, next) {
  try {
    const cookies = parseCookies(req.headers.cookie);
    const session = verifySessionToken(cookies[authCookieName]);

    if (session) {
      const user = await repository.getUserById(session.sub);
      if (user && user.isActive) {
        req.currentUser = user;
        res.locals.currentUser = user;
      } else {
        clearAuthCookie(res);
      }
    }

    next();
  } catch (error) {
    next(error);
  }
}

function requireAuthenticated(req, res, next) {
  if (req.currentUser) {
    next();
    return;
  }

  redirectWithNotice(
    res,
    `/login?returnTo=${encodeURIComponent(req.originalUrl || '/')}`,
    'warning',
    'Vui lòng đăng nhập để tiếp tục.'
  );
}

function requireAdmin(req, res, next) {
  if (req.currentUser && req.currentUser.role === 'Admin') {
    next();
    return;
  }

  redirectWithNotice(res, '/', 'failure', 'Bạn cần quyền Admin để quản lý user.');
}

function requireAdminApi(req, res, next) {
  if (req.currentUser && req.currentUser.role === 'Admin') {
    next();
    return;
  }

  res.status(403).json({ error: 'Bạn cần quyền Admin để thực hiện thao tác này.' });
}

function normalizePackageType(value) {
  const packageType = String(value || 'deb').toLowerCase();
  return ['deb', 'apt', 'docker'].includes(packageType) ? packageType : 'deb';
}

function normalizeApplicationStatus(value) {
  return ['Draft', 'Released', 'Archived'].includes(value) ? value : 'Draft';
}

function normalizeApplicationOpenUrl(value) {
  const text = String(value || '').trim();
  if (!text) return '';

  const candidate = /^https?:\/\//i.test(text) ? text : `http://${text}`;

  try {
    const parsed = new URL(candidate);
    if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname) {
      return null;
    }

    return parsed.href.replace(/\/+$/, '');
  } catch {
    return null;
  }
}

function isInstallerIdentifier(value) {
  return installerIdentifierPattern.test(String(value || '').trim());
}

function isInstallerVersion(value) {
  return installerVersionPattern.test(String(value || '').trim());
}

function getArrayField(value) {
  if (Array.isArray(value)) return value;
  if (value) return [value];
  return [];
}

function getSelectedApplicationPackages(body) {
  const seenPackageIds = new Set();

  return getArrayField(body.packageIds)
    .map((packageId) => String(packageId || '').trim())
    .filter(Boolean)
    .filter((packageId) => {
      if (seenPackageIds.has(packageId)) return false;
      seenPackageIds.add(packageId);
      return true;
    })
    .map((packageId) => ({
      packageId,
      selectedVersionId: String(body[`version_${packageId}`] || '').trim() || null
    }));
}

function csvCell(value) {
  const text = String(value ?? '');
  if (!/[",\r\n]/.test(text)) return text;
  return `"${text.replace(/"/g, '""')}"`;
}

function sendCsv(res, filename, rows) {
  const csv = rows
    .map((row) => row.map(csvCell).join(','))
    .join('\r\n');

  res.setHeader('Content-Type', 'text/csv; charset=utf-8');
  res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
  res.send(`\uFEFF${csv}\r\n`);
}

async function checksumFile(filePath) {
  const hash = crypto.createHash('sha256');
  const stream = fs.createReadStream(filePath);

  for await (const chunk of stream) {
    hash.update(chunk);
  }

  return hash.digest('hex');
}

async function removeUploadedFile(file) {
  if (!file) return;

  try {
    await fsp.unlink(file.path);
  } catch (error) {
    if (error.code !== 'ENOENT') {
      console.warn(`Cannot remove uploaded file ${file.path}:`, error.message);
    }
  }
}

async function getArtifactFromUpload(file) {
  if (!file) {
    return {
      filePath: null,
      checksum: null,
      fileSizeBytes: null
    };
  }

  return {
    filePath: `/uploads/packages/${file.filename}`,
    checksum: await checksumFile(file.path),
    fileSizeBytes: file.size
  };
}

function normalizeDocumentCategory(value) {
  const category = String(value || '').trim().toLowerCase();
  return documentCategoryIds.has(category) ? category : '';
}

function getDocumentUploadValidationMessage(file) {
  if (!file) return '';

  const originalFileName = normalizeDocumentFileName(file.originalname);

  if (originalFileName.length > 260) {
    return 'Tên file tài liệu không được vượt quá 260 ký tự.';
  }

  const extension = path.extname(originalFileName).toLowerCase();
  if (!allowedDocumentExtensions.has(extension)) {
    return 'Định dạng tài liệu chưa được hỗ trợ. Hãy dùng PDF, Word, OpenDocument, text/Markdown, ảnh, PowerPoint hoặc Excel.';
  }

  return '';
}

function getDocumentArtifact(file) {
  if (!file) {
    return {
      filePath: null,
      originalFileName: null,
      mimeType: null,
      fileSizeBytes: null
    };
  }

  return {
    filePath: `/uploads/documents/${file.filename}`,
    originalFileName: normalizeDocumentFileName(file.originalname),
    mimeType: file.mimetype || 'application/octet-stream',
    fileSizeBytes: file.size
  };
}

function getLocalDocumentFilePath(filePath) {
  const storedPath = String(filePath || '').trim();
  const prefix = '/uploads/documents/';

  if (!storedPath.startsWith(prefix)) return null;

  let relativePath;
  try {
    relativePath = decodeURIComponent(storedPath.slice(prefix.length));
  } catch {
    return null;
  }

  if (!relativePath || relativePath.includes('\0')) return null;

  const documentRoot = path.resolve(documentUploadDir);
  const localPath = path.resolve(documentRoot, relativePath);
  const pathDelta = path.relative(documentRoot, localPath);

  if (!pathDelta || pathDelta.startsWith('..') || path.isAbsolute(pathDelta)) {
    return null;
  }

  return localPath;
}

async function removeStoredDocumentFile(filePath) {
  const localPath = getLocalDocumentFilePath(filePath);
  if (!localPath) return;

  try {
    await fsp.unlink(localPath);
  } catch (error) {
    if (error.code !== 'ENOENT') {
      console.warn(`Cannot remove document file ${localPath}:`, error.message);
    }
  }
}

function getDocumentPreviewKind(document) {
  if (!document?.filePath) return '';

  const extension = path.extname(document.originalFileName || document.filePath).toLowerCase();
  if (extension === '.pdf') return 'pdf';
  if (documentImageExtensions.has(extension)) return 'image';
  if (documentTextExtensions.has(extension)) return 'text';
  return '';
}

function getDocumentResponseMimeType(document) {
  const extension = path.extname(document.originalFileName || document.filePath).toLowerCase();
  const knownTypes = {
    '.pdf': 'application/pdf',
    '.txt': 'text/plain; charset=utf-8',
    '.md': 'text/plain; charset=utf-8',
    '.png': 'image/png',
    '.jpg': 'image/jpeg',
    '.jpeg': 'image/jpeg',
    '.webp': 'image/webp'
  };

  return knownTypes[extension] || document.mimeType || 'application/octet-stream';
}

function setDocumentContentDisposition(res, disposition, fileName) {
  const originalName = String(fileName || 'document')
    .replace(/[\r\n]/g, '')
    .slice(0, 260);
  const asciiName = originalName
    .normalize('NFD')
    .replace(/[\u0300-\u036f]/g, '')
    .replace(/[^\x20-\x7E]/g, '_')
    .replace(/["\\;]/g, '_') || 'document';
  const encodedName = encodeURIComponent(originalName)
    .replace(/['()]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);

  res.setHeader(
    'Content-Disposition',
    `${disposition}; filename="${asciiName}"; filename*=UTF-8''${encodedName}`
  );
}

async function getDebUploadMetadataValidationMessage(file, packageCode, version) {
  if (!file || path.extname(file.originalname).toLowerCase() !== '.deb') return null;

  let debMetadata = null;
  try {
    debMetadata = await readDebPackageMetadata(file.path);
  } catch (error) {
    if (error.metadataToolMissing) {
      console.warn('dpkg-deb metadata inspection is not available; skipping package metadata validation.');
      return null;
    }

    return `Khong doc duoc metadata trong file .deb: ${error.message}`;
  }

  const metadataErrors = [];
  if (debMetadata.package !== packageCode) {
    metadataErrors.push(`package=${debMetadata.package}`);
  }

  if (debMetadata.version !== version) {
    metadataErrors.push(`version=${debMetadata.version}`);
  }

  return metadataErrors.length > 0
    ? `File .deb khong khop thong tin package (${metadataErrors.join(', ')}). Package code va version tren web phai khop Package/Version trong file .deb.`
    : null;
}

async function listMissingManifestPackageFiles(manifest) {
  const missingPackageFiles = [];

  for (const component of manifest.components || []) {
    if (component.type !== 'deb') continue;

    const versionId = getPackageVersionIdFromDownloadUrl(component.downloadUrl);
    const packageVersion = versionId
      ? await repository.getPackageVersionDownload(versionId)
      : null;
    const localPath = packageVersion
      ? getLocalPackageFilePath(packageVersion.filePath)
      : null;

    if (!localPath) continue;

    try {
      await fsp.access(localPath, fs.constants.R_OK);
    } catch (error) {
      if (error.code !== 'ENOENT') throw error;

      missingPackageFiles.push({
        componentId: component.componentId,
        packageName: component.packageName,
        version: component.version,
        filePath: packageVersion?.filePath || '',
        downloadUrl: component.downloadUrl
      });
    }
  }

  return missingPackageFiles;
}

function getPackageVersionIdFromDownloadUrl(downloadUrl) {
  let pathname;

  try {
    pathname = new URL(downloadUrl).pathname;
  } catch {
    return null;
  }

  const match = pathname.match(/^\/api\/package-versions\/([^/]+)\/download$/);
  return match ? decodeURIComponent(match[1]) : null;
}

function getLocalPackageFilePath(filePath) {
  let pathname = String(filePath || '').trim();

  if (!pathname) return null;

  if (/^https?:\/\//i.test(pathname)) {
    try {
      pathname = new URL(pathname).pathname;
    } catch {
      return null;
    }
  }

  const prefix = '/uploads/packages/';
  if (!pathname.startsWith(prefix)) return null;

  const relativePath = decodeURIComponent(pathname.slice(prefix.length));
  if (!relativePath || relativePath.includes('\0')) return null;

  const uploadRoot = path.resolve(uploadDir);
  const localPath = path.resolve(uploadRoot, relativePath);
  const pathDelta = path.relative(uploadRoot, localPath);

  if (!pathDelta || pathDelta.startsWith('..') || path.isAbsolute(pathDelta)) {
    return null;
  }

  return localPath;
}

function getDownloadFileName(packageVersion, localPath) {
  const storedName = path.basename(localPath || '');
  if (storedName) {
    const uploadedName = storedName
      .replace(/"/g, '')
      .replace(/^\d+-[a-f0-9]{8}-/, '');

    if (uploadedName) return uploadedName;
  }

  const packageCode = String(packageVersion.packageCode || 'package').replace(/[^a-zA-Z0-9._+-]/g, '-');
  const version = String(packageVersion.version || 'latest').replace(/[^a-zA-Z0-9._:+~=-]/g, '-');
  return `${packageCode}_${version}.deb`;
}

async function sendEmailConfirmation(req, user) {
  const token = await repository.createEmailConfirmationToken(user.id);
  const confirmUrl = `${getBaseUrl(req)}/confirm-email?token=${encodeURIComponent(token)}`;

  try {
    return await mailer.sendConfirmationEmail({
      to: user.email,
      name: user.name,
      confirmUrl
    });
  } catch (emailError) {
    console.error('Cannot send confirmation email:', emailError);
    console.warn(`Email confirmation link for ${user.email}: ${confirmUrl}`);
    return { sent: false, reason: 'SMTP_SEND_FAILED' };
  }
}

app.get('/login', (req, res) => {
  if (req.currentUser) {
    res.redirect(sanitizeReturnTo(req.query.returnTo));
    return;
  }

  res.render('auth', authViewModel(req, 'login'));
});

app.post('/login', asyncRoute(async (req, res) => {
  const returnTo = sanitizeReturnTo(req.body.returnTo);
  const identifier = String(req.body.identifier || '').trim();
  const password = String(req.body.password || '');

  if (!identifier || !password) {
    redirectWithNotice(res, `/login?returnTo=${encodeURIComponent(returnTo)}`, 'warning', 'Vui lòng nhập tài khoản và mật khẩu.');
    return;
  }

  const user = await repository.authenticateUser(identifier, password);
  if (!user) {
    redirectWithNotice(res, `/login?returnTo=${encodeURIComponent(returnTo)}`, 'failure', 'Tài khoản hoặc mật khẩu không đúng.');
    return;
  }

  setAuthCookie(res, user);
  res.redirect(returnTo);
}));

app.get('/register', (req, res) => {
  if (req.currentUser) {
    res.redirect('/');
    return;
  }

  res.render('auth', authViewModel(req, 'register'));
});

app.get('/register/check', asyncRoute(async (req, res) => {
  const field = String(req.query.field || '').trim();
  const value = String(req.query.value || '').trim();

  if (!['username', 'email'].includes(field) || !value) {
    res.json({
      available: true,
      status: 'empty',
      message: ''
    });
    return;
  }

  const conflict = await repository.getRegistrationConflict({
    username: field === 'username' ? value : '',
    email: field === 'email' ? value : ''
  });

  if (!conflict) {
    res.json({
      available: true,
      status: 'available',
      message: field === 'email' ? 'Email này có thể sử dụng.' : 'Username này có thể sử dụng.'
    });
    return;
  }

  if (field === 'email' && !conflict.user.isActive) {
    res.json({
      available: false,
      status: 'pending',
      message: 'Email này đã đăng ký nhưng chưa xác nhận.',
      resendUrl: `/confirm-email-sent?email=${encodeURIComponent(conflict.user.email)}`
    });
    return;
  }

  res.json({
    available: false,
    status: 'taken',
    message: field === 'email'
      ? 'Email này đã được sử dụng.'
      : 'Username này đã được sử dụng.'
  });
}));

app.post('/register', asyncRoute(async (req, res) => {
  if (req.currentUser) {
    res.redirect('/');
    return;
  }

  const username = String(req.body.username || '').trim();
  const email = String(req.body.email || '').trim();
  const fullName = String(req.body.fullName || '').trim();
  const password = String(req.body.password || '');
  const confirmPassword = String(req.body.confirmPassword || '');

  if (!username || !email || !password) {
    renderRegisterWithNotice(req, res, 'warning', 'Vui lòng nhập username, email và mật khẩu.');
    return;
  }

  if (password.length < 8) {
    renderRegisterWithNotice(req, res, 'warning', 'Mật khẩu cần tối thiểu 8 ký tự.');
    return;
  }

  if (password !== confirmPassword) {
    renderRegisterWithNotice(req, res, 'warning', 'Xác nhận mật khẩu chưa khớp.');
    return;
  }

  try {
    const conflict = await repository.getRegistrationConflict({ username, email });

    if (conflict) {
      if (conflict.field === 'email' && !conflict.user.isActive) {
        redirectWithNotice(
          res,
          `/confirm-email-sent?email=${encodeURIComponent(conflict.user.email)}`,
          'warning',
          'Email này đã đăng ký nhưng chưa xác nhận. Bạn có thể gửi lại email confirm.'
        );
        return;
      }

      renderRegisterWithNotice(
        req,
        res,
        'warning',
        conflict.field === 'email'
          ? 'Email này đã được sử dụng. Vui lòng dùng email khác.'
          : 'Username này đã được sử dụng. Vui lòng chọn username khác.'
      );
      return;
    }

    const user = await repository.createUser({
      username,
      email,
      fullName,
      password,
      isActive: false
    });
    const emailResult = await sendEmailConfirmation(req, user);
    const sentUrl = `/confirm-email-sent?email=${encodeURIComponent(user.email)}`;

    if (!emailResult.sent) {
      redirectWithNotice(res, sentUrl, 'warning', 'Đã tạo tài khoản nhưng chưa gửi được email xác nhận. Bạn có thể gửi lại email hoặc kiểm tra log server.');
      return;
    }

    redirectWithNotice(res, sentUrl, 'success', 'Đăng ký thành công. Vui lòng kiểm tra email để xác nhận tài khoản.');
  } catch (error) {
    if (error.code === 'DUPLICATE_USER') {
      renderRegisterWithNotice(req, res, 'warning', 'Username hoặc email đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.get('/confirm-email-sent', (req, res) => {
  if (req.currentUser) {
    res.redirect('/');
    return;
  }

  res.render('confirm-email-sent', confirmEmailViewModel(req, String(req.query.email || '').trim()));
});

app.post('/resend-confirmation', asyncRoute(async (req, res) => {
  if (req.currentUser) {
    res.redirect('/');
    return;
  }

  const email = String(req.body.email || '').trim();
  const sentUrl = `/confirm-email-sent?email=${encodeURIComponent(email)}`;

  if (!email) {
    redirectWithNotice(res, '/confirm-email-sent', 'warning', 'Vui lòng nhập email đã đăng ký.');
    return;
  }

  const pendingUser = await repository.getPendingUserByEmail(email);

  if (!pendingUser) {
    redirectWithNotice(res, sentUrl, 'warning', 'Email này không có tài khoản đang chờ xác nhận hoặc tài khoản đã active.');
    return;
  }

  const emailResult = await sendEmailConfirmation(req, pendingUser);
  if (!emailResult.sent) {
    redirectWithNotice(res, sentUrl, 'warning', 'Chưa gửi được email xác nhận. Vui lòng thử lại sau.');
    return;
  }

  redirectWithNotice(res, sentUrl, 'success', 'Đã gửi lại email xác nhận. Vui lòng kiểm tra Inbox hoặc Spam.');
}));

app.get('/confirm-email', asyncRoute(async (req, res) => {
  const token = String(req.query.token || '').trim();

  if (!token) {
    redirectWithNotice(res, '/login', 'warning', 'Link xác nhận email không hợp lệ.');
    return;
  }

  const user = await repository.confirmEmailToken(token);
  if (!user) {
    redirectWithNotice(res, '/login', 'failure', 'Link xác nhận email không hợp lệ hoặc đã hết hạn.');
    return;
  }

  setAuthCookie(res, user);
  redirectWithNotice(res, '/', 'success', 'Email đã được xác nhận. Tài khoản đã được kích hoạt.');
}));

app.post('/logout', (req, res) => {
  clearAuthCookie(res);
  redirectWithNotice(res, '/login', 'success', 'Đã đăng xuất.');
});

app.get('/api/apps', asyncRoute(async (req, res) => {
  const applications = await repository.listApplications();
  res.json({
    apps: applications
      .filter((application) => application.status === 'Released')
      .map((application) => ({
        appId: application.id,
        appCode: application.code,
        appName: application.name,
        version: application.version,
        status: application.status,
        packageCount: application.packageCount,
        openUrl: application.openUrl
      }))
  });
}));

app.get('/api/apps/:appCode', asyncRoute(async (req, res) => {
  const application = await repository.getApplicationById(req.params.appCode);
  if (!application || application.status !== 'Released') {
    res.status(404).json({ error: 'Application not found' });
    return;
  }

  res.json({
    appId: application.id,
    appCode: application.code,
    appName: application.name,
    version: application.version,
    status: application.status,
    packageCount: application.packageCount,
    openUrl: application.openUrl,
    packages: application.packages
  });
}));

app.get('/api/package-versions/:versionId/download', asyncRoute(async (req, res) => {
  const packageVersion = await repository.getPackageVersionDownload(req.params.versionId);
  if (!packageVersion || packageVersion.packageType !== 'deb') {
    res.status(404).type('text/plain').send('Package version not found.');
    return;
  }

  const localPath = getLocalPackageFilePath(packageVersion.filePath);
  if (!localPath) {
    res.status(409).type('text/plain').send('Package file path is not available on this package server.');
    return;
  }

  try {
    await fsp.access(localPath, fs.constants.R_OK);
  } catch (error) {
    if (error.code === 'ENOENT') {
      res.status(404).type('text/plain').send('Package file not found.');
      return;
    }

    throw error;
  }

  res.type('application/vnd.debian.binary-package');
  res.setHeader('Content-Disposition', `attachment; filename="${getDownloadFileName(packageVersion, localPath)}"`);
  if (packageVersion.checksum) {
    res.setHeader('X-Package-Sha256', packageVersion.checksum);
  }

  res.sendFile(localPath);
}));

app.get('/api/apps/:appCode/versions/:version/manifest', asyncRoute(async (req, res) => {
  const manifest = await repository.getApplicationManifest(
    req.params.appCode,
    req.params.version,
    getBaseUrl(req)
  );

  if (!manifest) {
    res.status(404).json({ error: 'Application manifest not found' });
    return;
  }

  const missingPackageFiles = await listMissingManifestPackageFiles(manifest);
  if (missingPackageFiles.length > 0) {
    res.status(409).json({
      error: 'Package file is missing on the package server',
      detail: 'Upload the package file to this web-server storage or re-upload the same package version from the Packages page.',
      missingPackageFiles
    });
    return;
  }

  res.json(manifest);
}));

app.get('/install-agent.sh', (req, res) => {
  const baseUrl = getBaseUrl(req);
  const agentUrl = `${baseUrl}/packages/agent/latest.deb`;
  const agentAllowedOrigins = Array.from(new Set([
    baseUrl,
    ...publicApiCorsOrigins.filter((origin) => origin !== '*'),
    'http://localhost:3000',
    'http://127.0.0.1:3000',
    'http://localhost:8080',
    'http://127.0.0.1:8080'
  ])).join(',');
  res.type('text/x-shellscript').send(`#!/usr/bin/env bash
set -euo pipefail

ARCH="$(dpkg --print-architecture)"
PACKAGE_BASE_URL="${baseUrl}"
AGENT_URL="${agentUrl}?arch=$ARCH"
AGENT_ENV="/etc/local-installer-agent/agent.env"
PACKAGE_HOST="$(printf '%s' "$PACKAGE_BASE_URL" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/:]+).*$#\\1#')"
PACKAGE_REGISTRY="$(printf '%s' "$PACKAGE_BASE_URL" | sed -E 's#^[a-zA-Z][a-zA-Z0-9+.-]*://([^/]+).*$#\\1#')"
TMP_DEB="/tmp/local-installer-agent.deb"
AGENT_BIND_HOST="\${AGENT_HOST:-0.0.0.0}"
AGENT_BIND_PORT="\${AGENT_PORT:-5010}"

set_agent_env() {
  KEY="$1"
  VALUE="$2"

  if grep -q "^$KEY=" "$AGENT_ENV"; then
    sed -i "s|^$KEY=.*|$KEY=$VALUE|" "$AGENT_ENV"
  else
    echo "$KEY=$VALUE" >> "$AGENT_ENV"
  fi
}

echo "Downloading Local Installer Agent..."
curl -fL "$AGENT_URL" -o "$TMP_DEB"

echo "Installing Local Installer Agent..."
apt install -y "$TMP_DEB"

echo "Configuring Local Installer Agent..."
mkdir -p /etc/local-installer-agent
touch "$AGENT_ENV"
set_agent_env AGENT_HOST "$AGENT_BIND_HOST"
set_agent_env AGENT_PORT "$AGENT_BIND_PORT"
set_agent_env ROBOT_PACKAGE_BASE_URL "$PACKAGE_BASE_URL"
set_agent_env ALLOWED_ORIGINS "${escapeShellDoubleQuoted(agentAllowedOrigins)}"
set_agent_env ALLOWED_DOWNLOAD_HOSTS "$PACKAGE_HOST,localhost,127.0.0.1"
set_agent_env ALLOWED_DOCKER_REGISTRIES "$PACKAGE_REGISTRY,$PACKAGE_HOST,localhost,127.0.0.1,registry.robot.package,docker.io"
set_agent_env ALLOW_DOCKER true
set_agent_env AUTO_INSTALL_DOCKER true

echo "Starting Local Installer Agent..."
systemctl enable local-installer-agent
systemctl restart local-installer-agent

echo "Checking Agent..."
for attempt in $(seq 1 20); do
  if curl -fsSL "http://127.0.0.1:$AGENT_BIND_PORT/health"; then
    echo ""
    echo "Local Installer Agent installed successfully."
    exit 0
  fi
  sleep 1
done

echo ""
echo "Local Installer Agent did not become healthy. Recent service logs:"
journalctl -u local-installer-agent -n 80 --no-pager || true
exit 1
`);
});

app.use(requireAuthenticated);
app.use('/uploads/packages', express.static(uploadDir));

app.get('/api/notifications', asyncRoute(async (req, res) => {
  const countOnly = String(req.query.countOnly || '').toLowerCase() === 'true';
  const result = countOnly
    ? await notificationRepository.getUnreadCount(req.currentUser.id)
    : await notificationRepository.listNotifications(req.currentUser.id, {
      limit: req.query.limit,
      unreadOnly: String(req.query.unreadOnly || '').toLowerCase() === 'true'
    });

  res.setHeader('Cache-Control', 'no-store');
  res.json(result);
}));

app.post('/api/notifications/read-all', asyncRoute(async (req, res) => {
  const result = await notificationRepository.markAllNotificationsRead(req.currentUser.id);
  if (!result.available) {
    res.status(503).json({ error: 'Tính năng thông báo chưa được kích hoạt trong database.' });
    return;
  }

  res.json(result);
}));

app.post('/api/notifications/:id/read', asyncRoute(async (req, res) => {
  if (!isUuid(req.params.id)) {
    res.status(400).json({ error: 'Notification id không hợp lệ.' });
    return;
  }

  const result = await notificationRepository.markNotificationRead(req.currentUser.id, req.params.id);
  if (!result.available) {
    res.status(503).json({ error: 'Tính năng thông báo chưa được kích hoạt trong database.' });
    return;
  }

  if (!result.updated) {
    res.status(404).json({ error: 'Không tìm thấy thông báo.' });
    return;
  }

  res.json(result);
}));

app.post('/api/notifications/broadcasts', requireAdminApi, asyncRoute(async (req, res) => {
  const title = String(req.body.title || '').trim();
  const message = String(req.body.message || '').trim();
  const severity = normalizeNotificationSeverity(req.body.severity);
  const actionUrl = sanitizeNotificationActionUrl(req.body.actionUrl);

  if (!title || title.length > 200) {
    res.status(400).json({ error: 'Tiêu đề bắt buộc và không được vượt quá 200 ký tự.' });
    return;
  }

  if (!message || message.length > 1000) {
    res.status(400).json({ error: 'Nội dung bắt buộc và không được vượt quá 1000 ký tự.' });
    return;
  }

  if (!severity) {
    res.status(400).json({ error: 'Mức độ thông báo không hợp lệ.' });
    return;
  }

  if (actionUrl === null) {
    res.status(400).json({ error: 'Đường dẫn phải là đường dẫn nội bộ bắt đầu bằng /.' });
    return;
  }

  const result = await notificationRepository.createSystemBroadcast({
    actorUserId: req.currentUser.id,
    actorName: getNotificationActorName(req.currentUser),
    severity,
    title,
    message,
    actionUrl
  });

  if (!result.available) {
    res.status(503).json({ error: 'Chưa có bảng dbo.Notifications. Hãy chạy migration 04_notifications.sql trước.' });
    return;
  }

  res.status(201).json(result);
}));

app.get('/agent', requireAdmin, asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  const agentPackages = await listAgentPackages();
  const preferredArch = normalizeAgentArch(req.query.arch) || 'amd64';
  const latestAgentPackage = await findLatestAgentPackage(preferredArch);
  const baseUrl = getBaseUrl(req);

  res.render('agent', viewModel(req, 'agent', 'Agent packages', pageData, {
    agentPackages,
    latestAgentPackage,
    agentPackageDir,
    preferredArch,
    installCommand: `curl -fsSL ${baseUrl}/install-agent.sh | sudo bash`,
    latestAgentUrl: `${baseUrl}/packages/agent/latest.deb?arch=${preferredArch}`
  }));
}));

app.post('/agent/packages', requireAdmin, agentUpload.single('agentFile'), asyncRoute(async (req, res) => {
  const version = String(req.body.version || '').trim();
  const arch = normalizeAgentArch(req.body.arch);

  try {
    if (!req.file || !version || !arch) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/agent', 'warning', 'Vui lòng chọn file .deb, nhập version và architecture.');
      return;
    }

    if (!isValidAgentVersion(version)) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/agent', 'warning', 'Version chỉ nên chứa chữ, số, dấu chấm, gạch ngang hoặc gạch dưới.');
      return;
    }

    if (path.extname(req.file.originalname).toLowerCase() !== '.deb') {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/agent', 'warning', 'Agent package phải là file .deb.');
      return;
    }

    let debMetadata = null;
    try {
      debMetadata = await readDebPackageMetadata(req.file.path);
    } catch (error) {
      if (!error.metadataToolMissing) {
        await removeUploadedFile(req.file);
        redirectWithNotice(res, '/agent', 'warning', `Khong doc duoc metadata trong file .deb: ${error.message}`);
        return;
      }

      console.warn('dpkg-deb metadata inspection is not available; skipping uploaded agent package metadata validation.');
    }

    if (debMetadata) {
      const metadataArch = normalizeAgentArch(debMetadata.architecture);
      const metadataErrors = [];

      if (debMetadata.package !== agentDebianPackageName) {
        metadataErrors.push(`package=${debMetadata.package}`);
      }

      if (debMetadata.version !== version) {
        metadataErrors.push(`version=${debMetadata.version}`);
      }

      if (metadataArch !== arch) {
        metadataErrors.push(`arch=${debMetadata.architecture}`);
      }

      if (metadataErrors.length > 0) {
        await removeUploadedFile(req.file);
        redirectWithNotice(
          res,
          '/agent',
          'warning',
          `File .deb khong khop thong tin upload (${metadataErrors.join(', ')}).`
        );
        return;
      }
    }

    const targetFileName = `local-installer-agent_${version}_${arch}.deb`;
    const targetPath = path.join(agentPackageDir, targetFileName);
    const isUpdate = fs.existsSync(targetPath);

    await fsp.mkdir(agentPackageDir, { recursive: true });
    await fsp.rename(req.file.path, targetPath);

    redirectWithNotice(
      res,
      '/agent',
      'success',
      isUpdate ? 'Đã cập nhật Agent package.' : 'Đã upload Agent package mới.'
    );
  } catch (error) {
    await removeUploadedFile(req.file);
    throw error;
  }
}));

app.post('/profile', asyncRoute(async (req, res) => {
  const returnTo = sanitizeReturnTo(req.body.returnTo);
  const fullName = String(req.body.fullName || '').trim();
  const email = String(req.body.email || '').trim();
  const confirmEmail = String(req.body.confirmEmail || '').trim();
  const newPassword = String(req.body.newPassword || '');
  const confirmPassword = String(req.body.confirmPassword || '');
  const emailChanged = email.toLowerCase() !== String(req.currentUser.email || '').trim().toLowerCase();

  if (!email) {
    redirectWithNotice(res, returnTo, 'warning', 'Vui lòng nhập email mới.');
    return;
  }

  if (email.toLowerCase() !== confirmEmail.toLowerCase()) {
    redirectWithNotice(res, returnTo, 'warning', 'Confirm email mới chưa khớp.');
    return;
  }

  if ((newPassword || confirmPassword) && newPassword !== confirmPassword) {
    redirectWithNotice(res, returnTo, 'warning', 'Xác nhận mật khẩu mới chưa khớp.');
    return;
  }

  if (newPassword && newPassword.length < 8) {
    redirectWithNotice(res, returnTo, 'warning', 'Mật khẩu mới cần tối thiểu 8 ký tự.');
    return;
  }

  try {
    const updatedUser = await repository.updateUser({
      userId: req.currentUser.id,
      username: req.currentUser.username,
      email,
      fullName,
      role: req.currentUser.role,
      isActive: emailChanged ? false : req.currentUser.isActive,
      password: newPassword || null
    });

    if (emailChanged) {
      const sentUrl = `/confirm-email-sent?email=${encodeURIComponent(updatedUser.email)}`;
      const emailResult = await sendEmailConfirmation(req, updatedUser);
      clearAuthCookie(res);

      if (!emailResult.sent) {
        redirectWithNotice(res, sentUrl, 'warning', 'Đã cập nhật email mới nhưng chưa gửi được email xác nhận. Bạn có thể gửi lại email hoặc kiểm tra log server.');
        return;
      }

      redirectWithNotice(res, sentUrl, 'success', 'Đã cập nhật email mới. Vui lòng kiểm tra email để xác nhận lại tài khoản.');
      return;
    }

    redirectWithNotice(
      res,
      returnTo,
      'success',
      newPassword ? 'Đã cập nhật thông tin cá nhân và mật khẩu.' : 'Đã cập nhật thông tin cá nhân.'
    );
  } catch (error) {
    if (error.code === 'DUPLICATE_USER') {
      redirectWithNotice(res, returnTo, 'warning', 'Email này đã được sử dụng. Vui lòng dùng email khác.');
      return;
    }

    throw error;
  }
}));

app.get('/', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  res.render('dashboard', viewModel(req, 'dashboard', 'Tổng quan', pageData));
}));

app.get('/packages', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  res.render('packages', viewModel(req, 'packages', 'Packages', pageData));
}));

app.get('/packages/export.csv', asyncRoute(async (req, res) => {
  const packages = await repository.listPackages();
  const rows = [
    ['PackageCode', 'PackageName', 'PackageType', 'LatestVersion', 'LatestReleaseDate', 'Status', 'Owner', 'Description', 'Artifact'],
    ...packages.map((packageItem) => [
      packageItem.code,
      packageItem.name,
      packageItem.type,
      packageItem.latestVersion,
      packageItem.latestReleaseDate,
      packageItem.status,
      packageItem.owner,
      packageItem.description,
      packageItem.artifact
    ])
  ];

  sendCsv(res, 'packages.csv', rows);
}));

app.post('/packages', upload.single('packageFile'), asyncRoute(async (req, res) => {
  try {
    const packageType = normalizePackageType(req.body.packageType);
    if (packageType === 'apt' && req.file) {
      await removeUploadedFile(req.file);
    }
    const artifact = packageType === 'apt'
      ? { filePath: null, checksum: null, fileSizeBytes: null }
      : await getArtifactFromUpload(req.file);
    const packageCode = String(req.body.packageCode || '').trim();
    const packageName = String(req.body.packageName || '').trim();
    const version = String(req.body.version || '').trim();

    if (!packageCode || !packageName || !version) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', 'Vui lòng nhập Package code, Package name và Version.');
      return;
    }

    if (!isInstallerIdentifier(packageCode)) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', `Package code khong hop le. ${installerIdentifierHint}`);
      return;
    }

    if (!isInstallerVersion(version)) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', `Package version khong hop le. ${installerVersionHint}`);
      return;
    }

    if (packageType === 'deb') {
      const metadataMessage = await getDebUploadMetadataValidationMessage(req.file, packageCode, version);
      if (metadataMessage) {
        await removeUploadedFile(req.file);
        redirectWithNotice(res, '/packages', 'warning', metadataMessage);
        return;
      }
    }

    await repository.createPackageWithVersion({
      packageCode,
      packageName,
      packageType,
      description: req.body.description,
      version,
      releaseDate: req.body.releaseDate,
      filePath: artifact.filePath,
      dockerImage: req.body.dockerImage && req.body.dockerImage.trim(),
      fileSizeBytes: artifact.fileSizeBytes,
      checksum: artifact.checksum,
      changeLog: req.body.changeLog,
      createdByUserId: req.currentUser.id
    });

    redirectWithNotice(
      res,
      '/packages',
      'success',
      packageType === 'apt'
        ? 'Đã tạo APT package metadata; không cần upload artifact.'
        : 'Đã upload package và lưu vào database.'
    );
  } catch (error) {
    await removeUploadedFile(req.file);
    if (error.code === 'DUPLICATE_PACKAGE') {
      redirectWithNotice(res, '/packages', 'warning', 'Package code hoặc version đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.post('/package-versions', upload.single('packageFile'), asyncRoute(async (req, res) => {
  let versionInput = null;

  try {
    const version = String(req.body.version || '').trim();

    if (!req.body.packageId || !version) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', 'Vui lòng chọn package và nhập version mới.');
      return;
    }

    if (!isInstallerVersion(version)) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', `Package version khong hop le. ${installerVersionHint}`);
      return;
    }

    const packageItem = await repository.getPackageById(req.body.packageId);
    if (!packageItem) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/packages', 'warning', 'Khong tim thay package can cap nhat.');
      return;
    }

    if (packageItem.type === 'apt' && req.file) {
      await removeUploadedFile(req.file);
    }
    const artifact = packageItem.type === 'apt'
      ? { filePath: null, checksum: null, fileSizeBytes: null }
      : await getArtifactFromUpload(req.file);

    if (packageItem.type === 'deb') {
      const metadataMessage = await getDebUploadMetadataValidationMessage(req.file, packageItem.code, version);
      if (metadataMessage) {
        await removeUploadedFile(req.file);
        redirectWithNotice(res, `/packages/${req.body.packageId}`, 'warning', metadataMessage);
        return;
      }
    }

    versionInput = {
      packageId: req.body.packageId,
      version,
      releaseDate: req.body.releaseDate,
      filePath: artifact.filePath,
      dockerImage: req.body.dockerImage && req.body.dockerImage.trim(),
      fileSizeBytes: artifact.fileSizeBytes,
      checksum: artifact.checksum,
      changeLog: req.body.changeLog
    };

    await repository.addPackageVersion(versionInput);
    await safelyCreateNotification('package version available', () => (
      notificationRepository.notifyPackageVersionAvailable({
        packageId: versionInput.packageId,
        version: versionInput.version,
        actorUserId: req.currentUser.id,
        actorName: getNotificationActorName(req.currentUser)
      })
    ));

    redirectWithNotice(res, `/packages/${req.body.packageId}`, 'success', 'Đã cập nhật version mới và đặt làm latest.');
  } catch (error) {
    if (error.code === 'DUPLICATE_PACKAGE') {
      if (versionInput && (versionInput.filePath || versionInput.dockerImage)) {
        try {
          const replacedVersionId = await repository.replacePackageVersionArtifact(versionInput);

          if (replacedVersionId) {
            await safelyCreateNotification('package version artifact updated', () => (
              notificationRepository.notifyPackageVersionAvailable({
                packageId: versionInput.packageId,
                version: versionInput.version,
                isArtifactUpdate: true,
                actorUserId: req.currentUser.id,
                actorName: getNotificationActorName(req.currentUser)
              })
            ));
            redirectWithNotice(
              res,
              `/packages/${versionInput.packageId}`,
              'success',
              'Da re-upload package version hien co va cap nhat artifact.'
            );
            return;
          }
        } catch (replaceError) {
          await removeUploadedFile(req.file);
          throw replaceError;
        }
      }

      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/packages/${req.body.packageId || ''}`, 'warning', 'Version này đã tồn tại trong package.');
      return;
    }

    throw error;
  }
}));

app.post('/packages/:id/delete', asyncRoute(async (req, res) => {
  const notificationImpact = await safelyLoadNotificationContext('package delete', () => (
    notificationRepository.getPackageImpact(req.params.id)
  ));
  const deleted = await repository.deletePackage(req.params.id);

  if (deleted && notificationImpact) {
    await safelyCreateNotification('package deleted', () => (
      notificationRepository.createNotificationsForUsers({
        recipientUserIds: notificationImpact.recipientUserIds,
        actorUserId: req.currentUser.id,
        actorName: getNotificationActorName(req.currentUser),
        eventType: 'package.deleted',
        severity: 'error',
        title: `Package ${notificationImpact.packageCode} đã bị xóa`,
        message: 'Các liên kết application sử dụng package này đã bị gỡ. Hãy kiểm tra lại cấu hình application liên quan.',
        actionUrl: '/applications',
        entityType: 'package',
        entityId: notificationImpact.packageId
      })
    ));
  }

  redirectWithNotice(
    res,
    '/packages',
    deleted ? 'success' : 'warning',
    deleted ? 'Đã xóa package và các liên kết database.' : 'Không tìm thấy package cần xóa.'
  );
}));

app.post('/package-versions/:id/latest', asyncRoute(async (req, res) => {
  const returnTo = sanitizeReturnTo(req.body.returnTo || '/packages');

  await repository.setLatestPackageVersion(req.params.id);
  redirectWithNotice(res, returnTo, 'success', 'Đã đặt version là latest trong database.');
}));

app.post('/package-versions/:id/delete', asyncRoute(async (req, res) => {
  const returnTo = sanitizeReturnTo(req.body.returnTo || '/packages');
  const notificationImpact = await safelyLoadNotificationContext('package version delete', () => (
    notificationRepository.getPackageVersionImpact(req.params.id)
  ));
  const result = await repository.deletePackageVersion(req.params.id);

  if (result.deleted && notificationImpact) {
    await safelyCreateNotification('package version deleted', () => (
      notificationRepository.createNotificationsForUsers({
        recipientUserIds: notificationImpact.recipientUserIds,
        actorUserId: req.currentUser.id,
        actorName: getNotificationActorName(req.currentUser),
        eventType: 'package.version_deleted',
        severity: 'warning',
        title: `Version ${notificationImpact.version} của ${notificationImpact.packageCode} đã bị xóa`,
        message: 'Các liên kết application sử dụng chính version này đã bị gỡ. Hãy kiểm tra lại application liên quan.',
        actionUrl: '/applications',
        entityType: 'package',
        entityId: notificationImpact.packageId
      })
    ));
  }

  redirectWithNotice(
    res,
    result.packageId ? `/packages/${result.packageId}` : returnTo,
    result.deleted ? 'success' : 'warning',
    result.deleted ? 'Đã xóa version và các liên kết app đang dùng version đó.' : 'Không tìm thấy version cần xóa.'
  );
}));

app.get('/packages/:id', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  const packageItem = await repository.getPackageById(req.params.id);

  if (!packageItem) {
    res.status(404).render('not-found', viewModel(req, 'packages', 'Không tìm thấy', pageData));
    return;
  }

  res.render(
    'package-detail',
    viewModel(req, 'packages', packageItem.name, pageData, { packageItem })
  );
}));

app.get('/applications', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  res.render('applications', viewModel(req, 'applications', 'Applications', pageData));
}));

app.get('/applications/export.csv', asyncRoute(async (req, res) => {
  const applications = await repository.listApplications();
  const rows = [
    ['AppCode', 'AppName', 'AppVersion', 'Status', 'CreatedAt', 'CreatedBy', 'PackageCount', 'Packages', 'OpenUrl', 'Notes'],
    ...applications.map((application) => [
      application.code,
      application.name,
      application.version,
      application.status,
      application.createdAt,
      application.createdBy,
      application.packageCount,
      application.packages.map((packageItem) => `${packageItem.code}@${packageItem.selectedVersion}`).join('; '),
      application.openUrl,
      application.notes
    ])
  ];

  sendCsv(res, 'applications.csv', rows);
}));

app.post('/applications', asyncRoute(async (req, res) => {
  const appCode = String(req.body.appCode || '').trim();
  const appName = String(req.body.appName || '').trim();
  const appVersion = String(req.body.appVersion || '').trim();
  const openUrl = normalizeApplicationOpenUrl(req.body.openUrl);

  if (!appCode || !appName || !appVersion) {
    redirectWithNotice(res, '/builder', 'warning', 'Vui lòng nhập App code, App name và App version.');
    return;
  }

  if (openUrl === null) {
    redirectWithNotice(res, '/builder', 'warning', 'Open URL khong hop le. Hay nhap URL http/https, vi du http://127.0.0.1:5000.');
    return;
  }

  if (!isInstallerIdentifier(appCode)) {
    redirectWithNotice(res, '/builder', 'warning', `App code khong hop le. ${installerIdentifierHint}`);
    return;
  }

  if (!isInstallerVersion(appVersion)) {
    redirectWithNotice(res, '/builder', 'warning', `App version khong hop le. ${installerVersionHint}`);
    return;
  }

  try {
    const applicationId = await repository.createApplication({
      appCode,
      appName,
      appVersion,
      notes: req.body.notes,
      openUrl,
      status: normalizeApplicationStatus(req.body.status),
      createdByUserId: req.currentUser.id,
      packages: getSelectedApplicationPackages(req.body)
    });

    redirectWithNotice(res, `/applications/${applicationId}`, 'success', 'Đã tạo app và lưu package đã chọn vào database.');
  } catch (error) {
    if (error.code === 'DUPLICATE_APPLICATION') {
      redirectWithNotice(res, '/builder', 'warning', 'App code đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.post('/applications/:id/edit', asyncRoute(async (req, res) => {
  const applicationId = String(req.params.id || '').trim();
  const returnTo = sanitizeReturnTo(req.body.returnTo || `/applications/${applicationId}`);
  const appCode = String(req.body.appCode || '').trim();
  const appName = String(req.body.appName || '').trim();
  const appVersion = String(req.body.appVersion || '').trim();
  const openUrl = normalizeApplicationOpenUrl(req.body.openUrl);

  if (!appCode || !appName || !appVersion) {
    redirectWithNotice(res, returnTo, 'warning', 'Vui lòng nhập App code, App name và App version.');
    return;
  }

  if (openUrl === null) {
    redirectWithNotice(res, returnTo, 'warning', 'Open URL khong hop le. Hay nhap URL http/https, vi du http://127.0.0.1:5000.');
    return;
  }

  if (!isInstallerIdentifier(appCode)) {
    redirectWithNotice(res, returnTo, 'warning', `App code khong hop le. ${installerIdentifierHint}`);
    return;
  }

  if (!isInstallerVersion(appVersion)) {
    redirectWithNotice(res, returnTo, 'warning', `App version khong hop le. ${installerVersionHint}`);
    return;
  }

  try {
    const updatedApplication = await repository.updateApplication({
      applicationId,
      appCode,
      appName,
      appVersion,
      notes: req.body.notes,
      openUrl,
      status: normalizeApplicationStatus(req.body.status),
      packages: getSelectedApplicationPackages(req.body)
    });

    redirectWithNotice(
      res,
      updatedApplication ? returnTo : '/applications',
      updatedApplication ? 'success' : 'warning',
      updatedApplication ? 'Đã cập nhật application trong database.' : 'Không tìm thấy app cần cập nhật.'
    );
  } catch (error) {
    if (error.code === 'DUPLICATE_APPLICATION') {
      redirectWithNotice(res, returnTo, 'warning', 'App code đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.post('/applications/:id/release', asyncRoute(async (req, res) => {
  const applicationId = String(req.params.id || '').trim();
  const returnTo = sanitizeReturnTo(req.body.returnTo || `/applications/${applicationId}`);
  const updated = await repository.updateApplicationStatus(applicationId, 'Released');

  if (updated) {
    await safelyCreateNotification('application released', () => (
      notificationRepository.notifyApplicationReleased({
        applicationId,
        actorUserId: req.currentUser.id,
        actorName: getNotificationActorName(req.currentUser)
      })
    ));
  }

  redirectWithNotice(
    res,
    returnTo,
    updated ? 'success' : 'warning',
    updated ? 'Đã chuyển application sang Released trong database.' : 'Không tìm thấy app cần release.'
  );
}));

app.post('/applications/:id/delete', asyncRoute(async (req, res) => {
  const deleted = await repository.deleteApplication(req.params.id);

  redirectWithNotice(
    res,
    '/applications',
    deleted ? 'success' : 'warning',
    deleted ? 'Đã xóa application và các package link.' : 'Không tìm thấy app cần xóa.'
  );
}));

app.post('/applications/:id/packages/:packageId/delete', asyncRoute(async (req, res) => {
  const applicationId = String(req.params.id || '').trim();
  const removed = await repository.removeApplicationPackage(applicationId, req.params.packageId);

  redirectWithNotice(
    res,
    `/applications/${applicationId}`,
    removed ? 'success' : 'warning',
    removed ? 'Đã gỡ package khỏi application.' : 'Không tìm thấy package link cần gỡ.'
  );
}));

app.get('/applications/:id', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  const application = await repository.getApplicationById(req.params.id);

  if (!application) {
    res.status(404).render('not-found', viewModel(req, 'applications', 'Không tìm thấy', pageData));
    return;
  }

  res.render(
    'application-detail',
    viewModel(req, 'applications', application.name, pageData, { application })
  );
}));

app.get('/documents', asyncRoute(async (req, res) => {
  const [pageData, documents] = await Promise.all([
    repository.getPageData(req.currentUser),
    repository.listDocuments()
  ]);

  res.render(
    'documents',
    viewModel(req, 'documents', 'Tài liệu', pageData, { documents, documentCategories })
  );
}));

app.post('/documents', documentUpload.single('documentFile'), asyncRoute(async (req, res) => {
  try {
    const title = String(req.body.title || '').trim();
    const category = normalizeDocumentCategory(req.body.category);
    const summary = String(req.body.summary || '').trim();
    const content = String(req.body.content || '').trim();
    const fileValidationMessage = getDocumentUploadValidationMessage(req.file);

    if (!title || title.length > 200) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', 'Tiêu đề tài liệu là bắt buộc và không được vượt quá 200 ký tự.');
      return;
    }

    if (!category) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', 'Vui lòng chọn nhóm tài liệu hợp lệ.');
      return;
    }

    if (summary.length > 1000 || content.length > documentMaxContentChars) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', 'Mô tả hoặc nội dung tài liệu vượt quá độ dài cho phép.');
      return;
    }

    if (fileValidationMessage) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', fileValidationMessage);
      return;
    }

    if (!content && !req.file) {
      redirectWithNotice(res, '/documents', 'warning', 'Hãy nhập nội dung hoặc đính kèm ít nhất một file tài liệu.');
      return;
    }

    const artifact = getDocumentArtifact(req.file);
    await repository.createDocument({
      title,
      category,
      summary,
      content,
      ...artifact,
      createdByUserId: req.currentUser.id
    });

    redirectWithNotice(res, '/documents', 'success', 'Đã lưu tài liệu mới.');
  } catch (error) {
    await removeUploadedFile(req.file);
    throw error;
  }
}));

app.get('/documents/:id/file', asyncRoute(async (req, res) => {
  const document = await repository.getDocumentById(req.params.id);
  const localPath = document ? getLocalDocumentFilePath(document.filePath) : null;

  if (!document || !localPath) {
    res.status(404).type('text/plain').send('Không tìm thấy file tài liệu.');
    return;
  }

  try {
    await fsp.access(localPath, fs.constants.R_OK);
  } catch (error) {
    if (error.code === 'ENOENT') {
      res.status(404).type('text/plain').send('File tài liệu không còn tồn tại trên máy chủ.');
      return;
    }
    throw error;
  }

  const previewKind = getDocumentPreviewKind(document);
  const shouldDownload = req.query.download === '1' || !previewKind;

  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('Content-Type', getDocumentResponseMimeType(document));
  setDocumentContentDisposition(
    res,
    shouldDownload ? 'attachment' : 'inline',
    document.originalFileName || path.basename(localPath)
  );
  res.sendFile(localPath);
}));

app.post('/documents/:id/edit', documentUpload.single('documentFile'), asyncRoute(async (req, res) => {
  const documentId = String(req.params.id || '').trim();

  try {
    const existingDocument = await repository.getDocumentById(documentId);
    if (!existingDocument) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', 'Không tìm thấy tài liệu cần cập nhật.');
      return;
    }

    const title = String(req.body.title || '').trim();
    const category = normalizeDocumentCategory(req.body.category);
    const summary = String(req.body.summary || '').trim();
    const content = String(req.body.content || '').trim();
    const removeAttachment = req.body.removeAttachment === '1';
    const fileValidationMessage = getDocumentUploadValidationMessage(req.file);
    const willHaveAttachment = Boolean(req.file) || (Boolean(existingDocument.filePath) && !removeAttachment);

    if (!title || title.length > 200) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/documents/${documentId}`, 'warning', 'Tiêu đề tài liệu là bắt buộc và không được vượt quá 200 ký tự.');
      return;
    }

    if (!category) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/documents/${documentId}`, 'warning', 'Vui lòng chọn nhóm tài liệu hợp lệ.');
      return;
    }

    if (summary.length > 1000 || content.length > documentMaxContentChars) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/documents/${documentId}`, 'warning', 'Mô tả hoặc nội dung tài liệu vượt quá độ dài cho phép.');
      return;
    }

    if (fileValidationMessage) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/documents/${documentId}`, 'warning', fileValidationMessage);
      return;
    }

    if (!content && !willHaveAttachment) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, `/documents/${documentId}`, 'warning', 'Tài liệu phải có nội dung hoặc file đính kèm.');
      return;
    }

    const artifact = getDocumentArtifact(req.file);
    const replaceAttachment = Boolean(req.file) || removeAttachment;
    const result = await repository.updateDocument({
      documentId,
      title,
      category,
      summary,
      content,
      replaceAttachment,
      ...artifact
    });

    if (!result) {
      await removeUploadedFile(req.file);
      redirectWithNotice(res, '/documents', 'warning', 'Không tìm thấy tài liệu cần cập nhật.');
      return;
    }

    if (
      result.attachmentChanged
      && result.previousFilePath
      && result.previousFilePath !== result.currentFilePath
    ) {
      await removeStoredDocumentFile(result.previousFilePath);
    }

    redirectWithNotice(res, `/documents/${documentId}`, 'success', 'Đã cập nhật tài liệu.');
  } catch (error) {
    await removeUploadedFile(req.file);
    throw error;
  }
}));

app.post('/documents/:id/delete', asyncRoute(async (req, res) => {
  const documentId = String(req.params.id || '').trim();

  if (!isUuid(documentId)) {
    redirectWithNotice(res, '/documents', 'warning', 'Không tìm thấy tài liệu cần xóa.');
    return;
  }

  const deletedDocument = await repository.deleteDocument(documentId);
  if (deletedDocument?.filePath) {
    await removeStoredDocumentFile(deletedDocument.filePath);
  }

  redirectWithNotice(
    res,
    '/documents',
    deletedDocument ? 'success' : 'warning',
    deletedDocument ? 'Đã xóa tài liệu và file đính kèm.' : 'Không tìm thấy tài liệu cần xóa.'
  );
}));

app.get('/documents/:id', asyncRoute(async (req, res) => {
  const [pageData, document] = await Promise.all([
    repository.getPageData(req.currentUser),
    repository.getDocumentById(req.params.id)
  ]);

  if (!document) {
    res.status(404).render('not-found', viewModel(req, 'documents', 'Không tìm thấy', pageData));
    return;
  }

  res.render(
    'document-detail',
    viewModel(req, 'documents', document.title, pageData, {
      document,
      documentCategories,
      documentPreviewKind: getDocumentPreviewKind(document)
    })
  );
}));

app.get('/users', requireAdmin, asyncRoute(async (req, res) => {
  const [pageData, users] = await Promise.all([
    repository.getPageData(req.currentUser),
    repository.listUsers()
  ]);

  res.render('users', viewModel(req, 'users', 'Users', pageData, { users }));
}));

app.post('/users', requireAdmin, asyncRoute(async (req, res) => {
  const username = String(req.body.username || '').trim();
  const email = String(req.body.email || '').trim();
  const fullName = String(req.body.fullName || '').trim();
  const password = String(req.body.password || '');
  const role = String(req.body.role || 'User') === 'Admin' ? 'Admin' : 'User';

  if (!username || !email || !password) {
    redirectWithNotice(res, '/users', 'warning', 'Vui lòng nhập username, email và mật khẩu.');
    return;
  }

  if (password.length < 8) {
    redirectWithNotice(res, '/users', 'warning', 'Mật khẩu cần tối thiểu 8 ký tự.');
    return;
  }

  try {
    await repository.createUser({
      username,
      email,
      fullName,
      password,
      role
    });

    redirectWithNotice(res, '/users', 'success', 'Đã tạo user mới.');
  } catch (error) {
    if (error.code === 'DUPLICATE_USER') {
      redirectWithNotice(res, '/users', 'warning', 'Username hoặc email đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.post('/users/:id', requireAdmin, asyncRoute(async (req, res) => {
  const userId = String(req.params.id || '').trim();
  const role = String(req.body.role || 'User') === 'Admin' ? 'Admin' : 'User';
  const isActive = req.body.isActive === 'on';

  if (userId === req.currentUser.id && (!isActive || role !== 'Admin')) {
    redirectWithNotice(res, '/users', 'warning', 'Bạn không thể tự hạ quyền hoặc khóa tài khoản Admin đang đăng nhập.');
    return;
  }

  await repository.updateUserAccess({
    userId,
    role,
    isActive
  });

  redirectWithNotice(res, '/users', 'success', 'Đã cập nhật quyền user.');
}));

app.post('/users/:id/edit', requireAdmin, asyncRoute(async (req, res) => {
  const userId = String(req.params.id || '').trim();
  const role = String(req.body.role || 'User') === 'Admin' ? 'Admin' : 'User';
  const isActive = req.body.isActive === 'on';
  const newPassword = String(req.body.newPassword || '');
  const confirmPassword = String(req.body.confirmPassword || '');

  if (!req.body.username || !req.body.email) {
    redirectWithNotice(res, '/users', 'warning', 'Vui lòng nhập username và email.');
    return;
  }

  if (userId === req.currentUser.id && (!isActive || role !== 'Admin')) {
    redirectWithNotice(res, '/users', 'warning', 'Bạn không thể tự hạ quyền hoặc khóa tài khoản Admin đang đăng nhập.');
    return;
  }

  if ((newPassword || confirmPassword) && newPassword !== confirmPassword) {
    redirectWithNotice(res, '/users', 'warning', 'Xác nhận mật khẩu mới chưa khớp.');
    return;
  }

  if (newPassword && newPassword.length < 8) {
    redirectWithNotice(res, '/users', 'warning', 'Mật khẩu mới cần tối thiểu 8 ký tự.');
    return;
  }

  try {
    await repository.updateUser({
      userId,
      username: req.body.username,
      email: req.body.email,
      fullName: req.body.fullName,
      role,
      isActive,
      password: newPassword || null
    });

    redirectWithNotice(
      res,
      '/users',
      'success',
      newPassword ? 'Đã cập nhật thông tin user và mật khẩu.' : 'Đã cập nhật thông tin user.'
    );
  } catch (error) {
    if (error.code === 'DUPLICATE_USER') {
      redirectWithNotice(res, '/users', 'warning', 'Username hoặc email đã tồn tại.');
      return;
    }

    throw error;
  }
}));

app.post('/users/:id/delete', requireAdmin, asyncRoute(async (req, res) => {
  const userId = String(req.params.id || '').trim();

  if (userId === req.currentUser.id) {
    redirectWithNotice(res, '/users', 'warning', 'Bạn không thể xóa tài khoản đang đăng nhập.');
    return;
  }

  try {
    const deleted = await repository.deleteUser(userId);

    redirectWithNotice(
      res,
      '/users',
      deleted ? 'success' : 'warning',
      deleted ? 'Đã xóa user.' : 'Không tìm thấy user cần xóa.'
    );
  } catch (error) {
    if (error.code === 'USER_HAS_OWNED_DATA') {
      redirectWithNotice(res, '/users', 'warning', 'Không thể xóa user đang sở hữu package, application hoặc tài liệu. Hãy khóa tài khoản nếu cần.');
      return;
    }

    throw error;
  }
}));

app.get('/builder', asyncRoute(async (req, res) => {
  const pageData = await repository.getPageData(req.currentUser);
  res.render('builder', viewModel(req, 'builder', 'Đóng gói App', pageData));
}));

app.use(async (error, req, res, next) => {
  if (error instanceof multer.MulterError) {
    const uploadErrorMessages = {
      LIMIT_FIELD_COUNT: 'Biểu mẫu upload có quá nhiều trường.',
      LIMIT_FIELD_KEY: 'Tên trường upload quá dài.',
      LIMIT_FIELD_NESTING: 'Tên trường upload không đúng định dạng.',
      LIMIT_FIELD_VALUE: 'Nội dung một trường upload quá dài.',
      LIMIT_FILE_COUNT: 'Chỉ được upload một file mỗi lần.',
      LIMIT_FILE_SIZE: 'File upload vượt quá dung lượng cho phép.',
      LIMIT_PART_COUNT: 'Biểu mẫu upload có quá nhiều thành phần.',
      LIMIT_UNEXPECTED_FILE: 'Trường file upload không hợp lệ.'
    };
    const returnPath = req.path.startsWith('/agent/')
      ? '/agent'
      : (req.path.startsWith('/documents') ? '/documents' : '/packages');

    console.warn(`Rejected multipart upload (${error.code || 'UNKNOWN'}):`, error.message);
    redirectWithNotice(
      res,
      returnPath,
      'warning',
      uploadErrorMessages[error.code] || 'Biểu mẫu upload không hợp lệ.'
    );
    return;
  }

  console.error(error);

  if (req.path.startsWith('/api/notifications')) {
    res.status(500).json({
      error: 'Không thể xử lý thông báo lúc này. Vui lòng thử lại sau.'
    });
    return;
  }

  const pageData = await repository.getPageData(req.currentUser).catch(() => ({
    currentUser: req.currentUser || { name: 'Guest', role: 'Guest', email: '' },
    stats: {
      totalPackages: 0,
      activePackages: 0,
      totalVersions: 0,
      totalApplications: 0,
      releasedApplications: 0
    },
    packages: [],
    applications: [],
    activity: []
  }));

  res.status(500).render(
    'error',
    viewModel(req, 'dashboard', 'Có lỗi xảy ra', pageData, {
      errorTitle: 'Không thể tải dữ liệu',
      errorMessage: 'Web server không đọc được dữ liệu từ SQL Server. Kiểm tra env kết nối, network tới SQL Server và schema/views trong database.',
      errorDetails: getErrorDetails(error),
      notice: {
        type: 'failure',
        message: 'Không thể xử lý thao tác. Kiểm tra log server để xem chi tiết.'
      }
    })
  );
});

const server = app.listen(port, () => {
  console.log(`Robot Installer UI is running at http://localhost:${port}`);
});

async function shutdown() {
  server.close(async () => {
    await closePool();
    process.exit(0);
  });
}

process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
