import fs from "node:fs/promises";
import path from "node:path";
import { execFile } from "node:child_process";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import bcrypt from "bcryptjs";
import fetch from "node-fetch";
import jwt from "jsonwebtoken";

import { env } from "../config/env.js";
import { pool } from "../db/pool.js";
import { ApiError } from "../utils/ApiError.js";

const execFileAsync = promisify(execFile);

const MOBILE_TOKEN_TTL = "365d";
const DEFAULT_TRAIL_MINUTES = 10;
const DEFAULT_LOCATION_LIMIT = 500;
const MAX_LOCATION_LIMIT = 1000;
const TRIP_STOP_THRESHOLD_MINUTES = 5;
const DEFAULT_TRACKING_SETTINGS = {
  default_tracking_interval_seconds: 30,
  offline_threshold_minutes: 5,
  retention_days: 90,
  alert_offline_enabled: true,
  alert_offline_threshold_minutes: 5,
  alert_low_battery_enabled: true,
  alert_low_battery_threshold: 15,
  alert_speed_enabled: false,
  alert_speed_threshold_kmh: 120,
  alert_geofence_enabled: true,
  save_locations_enabled: true,
  save_locations_start_time: null,
  save_locations_end_time: null,
};

let ioInstance = null;
let offlineIntervalId = null;
const serviceDir = path.dirname(fileURLToPath(import.meta.url));
const trackingScreensRoot = path.resolve(serviceDir, "../../uploads/tracking-screen");
const trackingScreenshotsRoot = path.resolve(serviceDir, "../../uploads/tracking-screenshots");

const trackingDebugLog = (...args) => {
  if (!env.trackingDebug) {
    return;
  }

  console.log("[tracking]", ...args);
};

const trackingConsoleLog = (label, payload) => {
  console.log(`[tracking] ${label}`, payload);
};

const cleanString = (value) => String(value ?? "").trim();

const normalizeOptionalString = (value) => {
  const normalized = cleanString(value);
  return normalized ? normalized : null;
};

const normalizeOptionalStringWithMax = (value, maxLength) => {
  const normalized = normalizeOptionalString(value);
  if (!normalized) {
    return null;
  }

  return normalized.slice(0, maxLength);
};

const toNumber = (value, fallback = null) => {
  if (value === null || value === undefined || value === "") {
    return fallback;
  }

  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : fallback;
};

const parseTimestamp = (value, fallback = new Date()) => {
  if (!value) {
    return fallback;
  }

  const parsed = new Date(value);
  return Number.isNaN(parsed.getTime()) ? fallback : parsed;
};

const normalizeTimeValue = (value) => {
  const normalized = cleanString(value);
  if (!normalized) {
    return null;
  }

  const match = normalized.match(/^([01]\d|2[0-3]):([0-5]\d)(?::[0-5]\d)?$/);
  if (!match) {
    return null;
  }

  return `${match[1]}:${match[2]}:00`;
};

const timeToMinutes = (value) => {
  const normalized = normalizeTimeValue(value);
  if (!normalized) {
    return null;
  }

  const [hours, minutes] = normalized.split(":").map(Number);
  return hours * 60 + minutes;
};

const isTimestampInsideSaveWindow = (timestamp, settings, timezoneOffsetMinutes = 0) => {
  if (!settings.save_locations_enabled) {
    return false;
  }

  const startMinutes = timeToMinutes(settings.save_locations_start_time);
  const endMinutes = timeToMinutes(settings.save_locations_end_time);

  if (startMinutes === null || endMinutes === null || startMinutes === endMinutes) {
    return true;
  }

  const offset = toNumber(timezoneOffsetMinutes, 0) || 0;
  const utcMinutes = timestamp.getUTCHours() * 60 + timestamp.getUTCMinutes();
  const localMinutes = ((utcMinutes + offset) % 1440 + 1440) % 1440;

  if (startMinutes < endMinutes) {
    return localMinutes >= startMinutes && localMinutes <= endMinutes;
  }

  return localMinutes >= startMinutes || localMinutes <= endMinutes;
};

const getRequestIp = (req) =>
  req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.ip || req.socket?.remoteAddress || null;

const emitCompany = (companyId, event, payload) => {
  if (!ioInstance || !companyId) {
    return;
  }

  trackingDebugLog("emitCompany", {
    room: `company:${companyId}`,
    event,
    payload,
  });
  ioInstance.to(`company:${companyId}`).emit(event, payload);
};

const ensureTrackingSettingsTable = async () => {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS tracking_settings (
      company_id BIGINT PRIMARY KEY REFERENCES companies(id) ON DELETE CASCADE,
      default_tracking_interval_seconds INTEGER NOT NULL DEFAULT 30,
      offline_threshold_minutes INTEGER NOT NULL DEFAULT 5,
      retention_days INTEGER,
      alert_offline_enabled BOOLEAN NOT NULL DEFAULT true,
      alert_offline_threshold_minutes INTEGER NOT NULL DEFAULT 5,
      alert_low_battery_enabled BOOLEAN NOT NULL DEFAULT true,
      alert_low_battery_threshold INTEGER NOT NULL DEFAULT 15,
      alert_speed_enabled BOOLEAN NOT NULL DEFAULT false,
      alert_speed_threshold_kmh INTEGER NOT NULL DEFAULT 120,
      alert_geofence_enabled BOOLEAN NOT NULL DEFAULT true,
      save_locations_enabled BOOLEAN NOT NULL DEFAULT true,
      save_locations_start_time TIME,
      save_locations_end_time TIME,
      created_at TIMESTAMP DEFAULT NOW(),
      updated_at TIMESTAMP DEFAULT NOW()
    )
  `);
  await pool.query(`
    ALTER TABLE tracking_settings
      ADD COLUMN IF NOT EXISTS save_locations_enabled BOOLEAN NOT NULL DEFAULT true
  `);
  await pool.query(`
    ALTER TABLE tracking_settings
      ADD COLUMN IF NOT EXISTS save_locations_start_time TIME
  `);
  await pool.query(`
    ALTER TABLE tracking_settings
      ADD COLUMN IF NOT EXISTS save_locations_end_time TIME
  `);
};

const ensureTrackingCommandsTable = async () => {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS tracking_device_commands (
      id BIGSERIAL PRIMARY KEY,
      company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
      device_id BIGINT NOT NULL REFERENCES tracking_devices(id) ON DELETE CASCADE,
      command_type VARCHAR(50) NOT NULL,
      payload JSONB,
      status VARCHAR(20) NOT NULL DEFAULT 'pending',
      created_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
      created_at TIMESTAMP NOT NULL DEFAULT NOW(),
      delivered_at TIMESTAMP,
      acked_at TIMESTAMP,
      result JSONB
    )
  `);
  await pool.query(
    `CREATE INDEX IF NOT EXISTS idx_tracking_device_commands_device_status
      ON tracking_device_commands(device_id, status, created_at DESC)`,
  );
};

const ensureTrackingDeviceFreeKioskColumns = async () => {
  await pool.query(`
    ALTER TABLE tracking_devices
      ADD COLUMN IF NOT EXISTS freekiosk_enabled BOOLEAN NOT NULL DEFAULT false
  `);
  await pool.query(`
    ALTER TABLE tracking_devices
      ADD COLUMN IF NOT EXISTS freekiosk_base_url VARCHAR(500)
  `);
  await pool.query(`
    ALTER TABLE tracking_devices
      ADD COLUMN IF NOT EXISTS freekiosk_api_key VARCHAR(500)
  `);
  await pool.query(`
    ALTER TABLE tracking_devices
      ADD COLUMN IF NOT EXISTS freekiosk_mqtt_device VARCHAR(200)
  `);
};

const ensureTrackingScreenSessionsTable = async () => {
  await pool.query(`
    CREATE TABLE IF NOT EXISTS tracking_screen_sessions (
      id BIGSERIAL PRIMARY KEY,
      company_id BIGINT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
      device_id BIGINT NOT NULL REFERENCES tracking_devices(id) ON DELETE CASCADE,
      status VARCHAR(30) NOT NULL DEFAULT 'requested',
      requested_by BIGINT REFERENCES users(id) ON DELETE SET NULL,
      requested_at TIMESTAMP NOT NULL DEFAULT NOW(),
      started_at TIMESTAMP,
      stopped_at TIMESTAMP,
      last_frame_at TIMESTAMP,
      viewer_connected_at TIMESTAMP,
      width INTEGER,
      height INTEGER,
      latest_frame_url TEXT,
      stop_reason TEXT
    )
  `);
  await pool.query(
    `CREATE INDEX IF NOT EXISTS idx_tracking_screen_sessions_device_status
      ON tracking_screen_sessions(device_id, status, requested_at DESC)`,
  );
};

const mapTrackingSettingsRow = (row) => ({
  default_tracking_interval_seconds: Number(
    row?.default_tracking_interval_seconds ?? DEFAULT_TRACKING_SETTINGS.default_tracking_interval_seconds,
  ),
  offline_threshold_minutes: Number(
    row?.offline_threshold_minutes ?? DEFAULT_TRACKING_SETTINGS.offline_threshold_minutes,
  ),
  retention_days:
    row?.retention_days === null || row?.retention_days === undefined
      ? DEFAULT_TRACKING_SETTINGS.retention_days
      : Number(row.retention_days),
  alert_offline_enabled: row?.alert_offline_enabled ?? DEFAULT_TRACKING_SETTINGS.alert_offline_enabled,
  alert_offline_threshold_minutes: Number(
    row?.alert_offline_threshold_minutes ?? DEFAULT_TRACKING_SETTINGS.alert_offline_threshold_minutes,
  ),
  alert_low_battery_enabled:
    row?.alert_low_battery_enabled ?? DEFAULT_TRACKING_SETTINGS.alert_low_battery_enabled,
  alert_low_battery_threshold: Number(
    row?.alert_low_battery_threshold ?? DEFAULT_TRACKING_SETTINGS.alert_low_battery_threshold,
  ),
  alert_speed_enabled: row?.alert_speed_enabled ?? DEFAULT_TRACKING_SETTINGS.alert_speed_enabled,
  alert_speed_threshold_kmh: Number(
    row?.alert_speed_threshold_kmh ?? DEFAULT_TRACKING_SETTINGS.alert_speed_threshold_kmh,
  ),
  alert_geofence_enabled: row?.alert_geofence_enabled ?? DEFAULT_TRACKING_SETTINGS.alert_geofence_enabled,
  save_locations_enabled: row?.save_locations_enabled ?? DEFAULT_TRACKING_SETTINGS.save_locations_enabled,
  save_locations_start_time:
    row?.save_locations_start_time === undefined ? DEFAULT_TRACKING_SETTINGS.save_locations_start_time : row.save_locations_start_time,
  save_locations_end_time:
    row?.save_locations_end_time === undefined ? DEFAULT_TRACKING_SETTINGS.save_locations_end_time : row.save_locations_end_time,
});

const mapDeviceRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  device_name: row.device_name,
  login_username: row.login_username,
  owner_name: row.owner_name,
  owner_phone: row.owner_phone,
  owner_department: row.owner_department,
  device_os: row.device_os,
  device_model: row.device_model,
  app_version: row.app_version,
  is_online: row.is_online,
  last_seen: row.last_seen,
  last_lat: row.last_lat === null ? null : Number(row.last_lat),
  last_lng: row.last_lng === null ? null : Number(row.last_lng),
  last_accuracy: row.last_accuracy === null ? null : Number(row.last_accuracy),
  last_speed: row.last_speed === null ? null : Number(row.last_speed),
  last_battery: row.last_battery === null ? null : Number(row.last_battery),
  last_address: row.last_address,
  offline_threshold_minutes:
    row.offline_threshold_minutes === null || row.offline_threshold_minutes === undefined
      ? DEFAULT_TRACKING_SETTINGS.offline_threshold_minutes
      : Number(row.offline_threshold_minutes),
  tracking_interval_seconds:
    row.tracking_interval_seconds === null ? null : Number(row.tracking_interval_seconds),
  is_tracking_enabled: row.is_tracking_enabled,
  is_active: row.is_active,
  freekiosk_enabled: row.freekiosk_enabled === true,
  freekiosk_base_url: row.freekiosk_base_url || null,
  freekiosk_mqtt_device: row.freekiosk_mqtt_device || null,
  freekiosk_configured: Boolean(row.freekiosk_mqtt_device),
  notes: row.notes,
  created_by: row.created_by === null ? null : Number(row.created_by),
  created_at: row.created_at,
  updated_at: row.updated_at,
});

const buildQrPayload = ({ serverUrl, username, password = null, deviceId = null, companyId = null }) => ({
  server: `${serverUrl}/api/tracking/mobile/login`,
  username,
  password,
  device_id: deviceId,
  company_id: companyId,
});

const mapGeofenceRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  name: row.name,
  center_lat: Number(row.center_lat),
  center_lng: Number(row.center_lng),
  radius_meters: Number(row.radius_meters),
  color: row.color,
  alert_on_enter: row.alert_on_enter,
  alert_on_exit: row.alert_on_exit,
  is_active: row.is_active,
  created_by: row.created_by === null ? null : Number(row.created_by),
  created_at: row.created_at,
  device_ids: row.device_ids?.map(Number) || [],
});

const mapAlertRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  device_id: row.device_id === null ? null : Number(row.device_id),
  alert_type: row.alert_type,
  title: row.title,
  message: row.message,
  geofence_id: row.geofence_id === null ? null : Number(row.geofence_id),
  lat: row.lat === null ? null : Number(row.lat),
  lng: row.lng === null ? null : Number(row.lng),
  status: row.status,
  created_at: row.created_at,
  criticality: row.criticality || getAlertCriticality(row.alert_type),
  device_name: row.device_name || null,
  owner_name: row.owner_name || null,
  geofence_name: row.geofence_name || null,
});

const mapCommandRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  device_id: Number(row.device_id),
  command_type: row.command_type,
  payload: row.payload || {},
  status: row.status,
  created_by: row.created_by === null ? null : Number(row.created_by),
  created_at: row.created_at,
  delivered_at: row.delivered_at,
  acked_at: row.acked_at,
  result: row.result || null,
});

const mapScreenSessionRow = (row) => ({
  id: Number(row.id),
  company_id: Number(row.company_id),
  device_id: Number(row.device_id),
  status: row.status,
  requested_by: row.requested_by === null ? null : Number(row.requested_by),
  requested_at: row.requested_at,
  started_at: row.started_at,
  stopped_at: row.stopped_at,
  last_frame_at: row.last_frame_at,
  viewer_connected_at: row.viewer_connected_at,
  width: row.width === null ? null : Number(row.width),
  height: row.height === null ? null : Number(row.height),
  latest_frame_url: row.latest_frame_url || null,
  stop_reason: row.stop_reason || null,
});

const formatKmlTimestamp = (value) =>
  new Date(value).toLocaleTimeString("fr-DZ", {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit",
  });

const getTrackingSettingsInternal = async (companyId) => {
  try {
    await ensureTrackingSettingsTable();
    await pool.query(
      `
        INSERT INTO tracking_settings (company_id, updated_at)
        VALUES ($1, NOW())
        ON CONFLICT (company_id) DO NOTHING
      `,
      [companyId],
    );

    const { rows } = await pool.query(
      `
        SELECT *
        FROM tracking_settings
        WHERE company_id = $1
        LIMIT 1
      `,
      [companyId],
    );

    return mapTrackingSettingsRow(rows[0] || DEFAULT_TRACKING_SETTINGS);
  } catch (error) {
    if (error?.code === "42P01") {
      return { ...DEFAULT_TRACKING_SETTINGS };
    }
    throw error;
  }
};

const getAlertCriticality = (alertType) => {
  if (["offline", "geofence_exit"].includes(alertType)) {
    return "critical";
  }
  if (["low_battery", "speed"].includes(alertType)) {
    return "warning";
  }
  return "info";
};

const ALLOWED_DEVICE_COMMANDS = new Set([
  "ring_device",
  "force_sync",
  "request_location",
  "start_screen_share",
  "lock_device",
  "open_location_settings",
  "restart_tracking",
  "reboot_device",
  "capture_screenshot",
  "capture_freekiosk_screenshot",
  "freekiosk_rest_request",
]);

const FREEKIOSK_ACTIONS = {
  status: { method: "GET", path: "/api/status" },
  battery: { method: "GET", path: "/api/battery" },
  brightness_status: { method: "GET", path: "/api/brightness" },
  screen_status: { method: "GET", path: "/api/screen" },
  sensors: { method: "GET", path: "/api/sensors" },
  storage: { method: "GET", path: "/api/storage" },
  memory: { method: "GET", path: "/api/memory" },
  wifi: { method: "GET", path: "/api/wifi" },
  info: { method: "GET", path: "/api/info" },
  health: { method: "GET", path: "/api/health" },
  screenshot: { method: "GET", path: "/api/screenshot", binary: "png" },
  camera_back: { method: "GET", path: "/api/camera/photo?camera=back&quality=80", binary: "jpg" },
  camera_front: { method: "GET", path: "/api/camera/photo?camera=front&quality=80", binary: "jpg" },
  camera_list: { method: "GET", path: "/api/camera/list" },
  brightness_set: { method: "POST", path: "/api/brightness", bodyFields: ["value"] },
  auto_brightness_enable: { method: "POST", path: "/api/autoBrightness/enable", bodyFields: ["min", "max", "offset"] },
  auto_brightness_disable: { method: "POST", path: "/api/autoBrightness/disable" },
  auto_brightness_status: { method: "GET", path: "/api/autoBrightness" },
  screen_on: { method: "POST", path: "/api/screen/on" },
  screen_off: { method: "POST", path: "/api/screen/off" },
  screensaver_on: { method: "POST", path: "/api/screensaver/on" },
  screensaver_off: { method: "POST", path: "/api/screensaver/off" },
  reload: { method: "POST", path: "/api/reload" },
  wake: { method: "POST", path: "/api/wake" },
  clear_cache: { method: "POST", path: "/api/clearCache" },
  reboot: { method: "POST", path: "/api/reboot" },
  lock: { method: "POST", path: "/api/lock" },
  restart_ui: { method: "POST", path: "/api/restart-ui" },
  url: { method: "POST", path: "/api/url", bodyFields: ["url"] },
  mode_webview: { method: "POST", path: "/api/mode", fixedBody: { mode: "webview" }, bodyFields: ["url"] },
  mode_external_app: { method: "POST", path: "/api/mode", fixedBody: { mode: "external_app" }, bodyFields: ["package"] },
  tts: { method: "POST", path: "/api/tts", bodyFields: ["text", "language"] },
  volume_set: { method: "POST", path: "/api/volume", bodyFields: ["value"] },
  volume_status: { method: "GET", path: "/api/volume" },
  toast: { method: "POST", path: "/api/toast", bodyFields: ["text"] },
  js: { method: "POST", path: "/api/js", bodyFields: ["code"] },
  app_launch: { method: "POST", path: "/api/app/launch", bodyFields: ["package"] },
  audio_play: { method: "POST", path: "/api/audio/play", bodyFields: ["url", "loop", "volume"] },
  audio_stop: { method: "POST", path: "/api/audio/stop" },
  audio_beep: { method: "POST", path: "/api/audio/beep" },
  remote_up: { method: "POST", path: "/api/remote/up" },
  remote_down: { method: "POST", path: "/api/remote/down" },
  remote_left: { method: "POST", path: "/api/remote/left" },
  remote_right: { method: "POST", path: "/api/remote/right" },
  remote_select: { method: "POST", path: "/api/remote/select" },
  remote_back: { method: "POST", path: "/api/remote/back" },
  remote_home: { method: "POST", path: "/api/remote/home" },
  remote_menu: { method: "POST", path: "/api/remote/menu" },
  remote_playpause: { method: "POST", path: "/api/remote/playpause" },
  keyboard_key: { method: "POST", pathBuilder: (payload) => `/api/remote/keyboard/${encodeURIComponent(cleanString(payload.key))}` },
  keyboard_combo: { method: "POST", pathBuilder: (payload) => `/api/remote/keyboard?map=${encodeURIComponent(cleanString(payload.map))}` },
  remote_text: { method: "POST", path: "/api/remote/text", bodyFields: ["text"] },
};

const buildAlertSummary = (items) =>
  items.reduce(
    (accumulator, item) => {
      const criticality = item.criticality || getAlertCriticality(item.alert_type);
      accumulator[criticality] += 1;
      return accumulator;
    },
    { critical: 0, warning: 0, info: 0 },
  );

const ensureTrackingScreenshotsRoot = async () => {
  await fs.mkdir(trackingScreenshotsRoot, { recursive: true });
};

const assertCompanyAccess = (req, companyId) => {
  const allowed = Array.isArray(req.user?.companyIds) ? req.user.companyIds.map(Number) : [];
  const current = Number(req.user?.companyId);

  if (req.user?.role === "super_admin" || req.user?.role === "admin") {
    return;
  }

  if (!allowed.includes(companyId) && current !== companyId) {
    throw new ApiError(403, "Accès refusé.");
  }
};

const logTrackingAudit = async ({
  companyId,
  userId = null,
  action,
  entityType,
  entityId = null,
  details = null,
  ipAddress = null,
}) => {
  await pool.query(
    `
      INSERT INTO tracking_audit_log
      (company_id, user_id, action, entity_type, entity_id, details, ip_address, created_at)
      VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
    `,
    [companyId, userId, action, entityType, entityId, details ? JSON.stringify(details) : null, ipAddress],
  );
};

const normalizeFreeKioskMqttDevice = (value) => {
  const normalized = normalizeOptionalStringWithMax(value, 200);
  if (!normalized) {
    return null;
  }

  return normalized.replace(/^\/+|\/+$/g, "");
};

const normalizeFreeKioskRestBaseUrl = (value) => {
  const normalized = normalizeOptionalStringWithMax(value, 500);
  if (!normalized) {
    return null;
  }

  const withProtocol = /^https?:\/\//i.test(normalized) ? normalized : `http://${normalized}`;
  return withProtocol.replace(/\/+$/, "");
};

const getFreeKioskMqttConfig = () => ({
  host: env.freeKioskMqttHost || process.env.FREEKIOSK_MQTT_HOST || "127.0.0.1",
  port: String(env.freeKioskMqttPort || process.env.FREEKIOSK_MQTT_PORT || "1883"),
  username: env.freeKioskMqttUsername || process.env.FREEKIOSK_MQTT_USERNAME || "",
  password: env.freeKioskMqttPassword || process.env.FREEKIOSK_MQTT_PASSWORD || "",
  baseTopic: (env.freeKioskMqttBaseTopic || process.env.FREEKIOSK_MQTT_BASE_TOPIC || "freekiosk").replace(/^\/+|\/+$/g, ""),
});

const buildFreeKioskMqttMessage = (definition, payload = {}) => {
  if (payload.message !== undefined) {
    return String(payload.message);
  }

  if (definition.message !== undefined) {
    return String(definition.message);
  }

  if (definition.json) {
    const body = {};
    for (const key of definition.json) {
      if (payload[key] !== undefined && payload[key] !== null && payload[key] !== "") {
        body[key] = payload[key];
      }
    }
    return Object.keys(body).length ? JSON.stringify(body) : "";
  }

  if (definition.payloadField) {
    const value = payload[definition.payloadField];
    return value === undefined || value === null ? "" : String(value);
  }

  return "";
};

const publishFreeKioskMqtt = async ({ topic, message }) => {
  const config = getFreeKioskMqttConfig();
  const args = ["-h", config.host, "-p", config.port, "-t", topic];

  if (config.username) {
    args.push("-u", config.username);
  }

  if (config.password) {
    args.push("-P", config.password);
  }

  if (message) {
    args.push("-m", message);
  } else {
    args.push("-n");
  }

  await execFileAsync("mosquitto_pub", args, { timeout: 10000 });

  return {
    broker: `${config.host}:${config.port}`,
    topic,
    message,
  };
};

const normalizeFreeKioskBodyValue = (value) => {
  if (value === undefined || value === null || value === "") {
    return undefined;
  }

  if (value === "true") {
    return true;
  }

  if (value === "false") {
    return false;
  }

  if (typeof value === "number" || typeof value === "boolean") {
    return value;
  }

  const numeric = Number(value);
  if (!Number.isNaN(numeric) && String(value).trim() !== "") {
    return numeric;
  }

  return String(value);
};

const buildFreeKioskRestCommandPayload = ({ definition, payload = {}, device }) => {
  const baseUrl = normalizeFreeKioskRestBaseUrl(device.freekiosk_base_url);
  const path = definition.pathBuilder ? definition.pathBuilder(payload) : definition.path;

  if (!path || path.includes("undefined") || path.endsWith("/")) {
    throw new ApiError(400, "Paramètres FreeKiosk manquants pour cette action.");
  }

  const body = { ...(definition.fixedBody || {}) };

  for (const field of definition.bodyFields || []) {
    const normalized = normalizeFreeKioskBodyValue(payload[field]);
    if (normalized !== undefined) {
      body[field] = normalized;
    }
  }

  const commandPayload = {
    method: definition.method || "POST",
    path,
  };

  if (baseUrl) {
    commandPayload.base_url = baseUrl;
  }

  if (device.freekiosk_api_key) {
    commandPayload.api_key = device.freekiosk_api_key;
  }

  if (definition.binary) {
    commandPayload.binary = definition.binary;
  }

  if (Object.keys(body).length) {
    commandPayload.body_json = JSON.stringify(body);
  }

  return commandPayload;
};

const createTrackingAlert = async ({
  companyId,
  deviceId,
  alertType,
  title,
  message,
  geofenceId = null,
  lat = null,
  lng = null,
}) => {
  const duplicate = await pool.query(
    `
      SELECT id
      FROM tracking_alerts
      WHERE company_id = $1
        AND device_id IS NOT DISTINCT FROM $2
        AND alert_type = $3
        AND geofence_id IS NOT DISTINCT FROM $4
        AND status = 'active'
      ORDER BY id DESC
      LIMIT 1
    `,
    [companyId, deviceId, alertType, geofenceId],
  );

  if (duplicate.rows[0]) {
    return Number(duplicate.rows[0].id);
  }

  const result = await pool.query(
    `
      INSERT INTO tracking_alerts
      (company_id, device_id, alert_type, title, message, geofence_id, lat, lng, status, created_at)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'active', NOW())
      RETURNING id
    `,
    [companyId, deviceId, alertType, title, message, geofenceId, lat, lng],
  );

  emitCompany(companyId, "tracking:alert", {
    id: Number(result.rows[0].id),
    device_id: deviceId,
    alert_type: alertType,
    title,
    message,
    geofence_id: geofenceId,
    lat,
    lng,
    status: "active",
  });

  const alertPayload = {
    id: Number(result.rows[0].id),
    device_id: deviceId,
    alert_type: alertType,
    title,
    message,
    geofence_id: geofenceId,
    lat,
    lng,
    status: "active",
    criticality: getAlertCriticality(alertType),
    created_at: new Date().toISOString(),
  };

  emitCompany(companyId, "alert:new", alertPayload);

  if (alertType === "low_battery") {
    emitCompany(companyId, "alert:battery_low", alertPayload);
  }

  listAlerts({ companyId, status: "active" })
    .then((items) => {
      emitCompany(companyId, "alerts:updated", buildAlertSummary(items));
    })
    .catch(() => {});

  return Number(result.rows[0].id);
};

const resolveActiveAlerts = async (companyId, deviceId, alertType) => {
  await pool.query(
    `
      UPDATE tracking_alerts
      SET status = 'dismissed'
      WHERE company_id = $1
        AND device_id = $2
        AND alert_type = $3
        AND status = 'active'
    `,
    [companyId, deviceId, alertType],
  );

  listAlerts({ companyId, status: "active" })
    .then((items) => {
      emitCompany(companyId, "alerts:updated", buildAlertSummary(items));
    })
    .catch(() => {});
};

export const getCurrentDeviceStates = async (companyId) => {
  await checkOfflineDevices(companyId);

  const { rows } = await pool.query(
    `
      SELECT
        d.*,
        COALESCE(s.offline_threshold_minutes, 5) AS offline_threshold_minutes
      FROM tracking_devices d
      LEFT JOIN tracking_settings s
        ON s.company_id = d.company_id
      WHERE d.company_id = $1
        AND is_active = true
      ORDER BY d.is_online DESC, d.last_seen DESC NULLS LAST, d.device_name ASC
    `,
    [companyId],
  );

  return rows.map(mapDeviceRow);
};

const ensureUsernameAvailable = async (username, excludeDeviceId = null) => {
  const normalizedUsername = cleanString(username).toLowerCase();

  if (!normalizedUsername) {
    throw new ApiError(400, "Nom d'utilisateur requis.");
  }

  const values = [normalizedUsername];
  let where = "LOWER(login_username) = LOWER($1)";

  if (excludeDeviceId) {
    values.push(excludeDeviceId);
    where += ` AND id <> $${values.length}`;
  }

  const { rows } = await pool.query(
    `
      SELECT id
      FROM tracking_devices
      WHERE ${where}
      LIMIT 1
    `,
    values,
  );

  if (rows[0]) {
    throw new ApiError(409, "Nom d'utilisateur déjà utilisé.");
  }

  return normalizedUsername;
};

const getDeviceRowOrThrow = async (companyId, deviceId, includeInactive = true) => {
  await ensureTrackingDeviceFreeKioskColumns();
  const { rows } = await pool.query(
    `
      SELECT *
      FROM tracking_devices
      WHERE company_id = $1
        AND id = $2
        ${includeInactive ? "" : "AND is_active = true"}
      LIMIT 1
    `,
    [companyId, deviceId],
  );

  if (!rows[0]) {
    throw new ApiError(404, "Téléphone introuvable.");
  }

  return rows[0];
};

const validateCompanyDeviceIds = async (companyId, deviceIds) => {
  if (!deviceIds.length) {
    return [];
  }

  const { rows } = await pool.query(
    `
      SELECT id
      FROM tracking_devices
      WHERE company_id = $1
        AND id = ANY($2::INTEGER[])
    `,
    [companyId, deviceIds],
  );

  const foundIds = rows.map((row) => Number(row.id));

  if (foundIds.length !== new Set(deviceIds).size) {
    throw new ApiError(400, "Un ou plusieurs téléphones sélectionnés sont invalides.");
  }

  return foundIds;
};

const getDeviceEvents = async (companyId, deviceId) => {
  const [alertsResult, auditsResult] = await Promise.all([
    pool.query(
      `
        SELECT
          id,
          alert_type,
          title,
          message,
          created_at
        FROM tracking_alerts
        WHERE company_id = $1
          AND device_id = $2
        ORDER BY created_at DESC
        LIMIT 8
      `,
      [companyId, deviceId],
    ),
    pool.query(
      `
        SELECT
          id,
          action,
          details,
          created_at
        FROM tracking_audit_log
        WHERE company_id = $1
          AND entity_type = 'tracking_device'
          AND entity_id = $2
        ORDER BY created_at DESC
        LIMIT 4
      `,
      [companyId, deviceId],
    ),
  ]);

  const alertEvents = alertsResult.rows.map((row) => ({
    id: `alert-${row.id}`,
    type: row.alert_type,
    title: row.title,
    message: row.message,
    created_at: row.created_at,
  }));

  const auditEvents = auditsResult.rows.map((row) => ({
    id: `audit-${row.id}`,
    type: String(row.action || "").toLowerCase(),
    title:
      row.action === "CREATE"
        ? "Téléphone créé"
        : row.action === "UPDATE"
          ? "Paramètres modifiés"
          : row.action === "DELETE"
            ? "Téléphone désactivé"
            : row.action,
    message: row.details?.device_name || null,
    created_at: row.created_at,
  }));

  return [...alertEvents, ...auditEvents]
    .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
    .slice(0, 10);
};

const buildTripsFromLocations = (locations) => {
  if (!locations.length) {
    return [];
  }

  const trips = [];
  let currentTrip = null;

  for (let index = 0; index < locations.length; index += 1) {
    const point = locations[index];
    const previous = index > 0 ? locations[index - 1] : null;
    const timestamp = new Date(point.device_timestamp);
    const previousTimestamp = previous ? new Date(previous.device_timestamp) : null;
    const gapMinutes = previousTimestamp
      ? (timestamp.getTime() - previousTimestamp.getTime()) / (1000 * 60)
      : 0;
    const pointMoving = Boolean(point.is_moving) || Number(point.speed || 0) > 2;

    if (!currentTrip && pointMoving) {
      currentTrip = {
        start: point,
        end: point,
        points: [point],
        distanceKm: 0,
        speedSum: Number(point.speed || 0),
        speedCount: point.speed !== null && point.speed !== undefined ? 1 : 0,
      };
      continue;
    }

    if (!currentTrip) {
      continue;
    }

    if (previous && previous.device_timestamp) {
      currentTrip.distanceKm +=
        haversineDistance(
          Number(previous.latitude),
          Number(previous.longitude),
          Number(point.latitude),
          Number(point.longitude),
        ) / 1000;
    }

    currentTrip.end = point;
    currentTrip.points.push(point);

    if (point.speed !== null && point.speed !== undefined) {
      currentTrip.speedSum += Number(point.speed);
      currentTrip.speedCount += 1;
    }

    const shouldCloseTrip =
      (!pointMoving && gapMinutes >= TRIP_STOP_THRESHOLD_MINUTES) ||
      gapMinutes >= TRIP_STOP_THRESHOLD_MINUTES * 2 ||
      index === locations.length - 1;

    if (shouldCloseTrip) {
      const startTime = new Date(currentTrip.start.device_timestamp);
      const endTime = new Date(currentTrip.end.device_timestamp);
      const durationMinutes = Math.max(
        0,
        Math.round((endTime.getTime() - startTime.getTime()) / (1000 * 60)),
      );

      trips.push({
        id: `${currentTrip.start.id}-${currentTrip.end.id}`,
        start_time: currentTrip.start.device_timestamp,
        end_time: currentTrip.end.device_timestamp,
        start_latitude: Number(currentTrip.start.latitude),
        start_longitude: Number(currentTrip.start.longitude),
        end_latitude: Number(currentTrip.end.latitude),
        end_longitude: Number(currentTrip.end.longitude),
        start_address: currentTrip.start.address || null,
        end_address: currentTrip.end.address || null,
        duration_minutes: durationMinutes,
        distance_km: Number(currentTrip.distanceKm.toFixed(3)),
        avg_speed:
          currentTrip.speedCount > 0
            ? Number((currentTrip.speedSum / currentTrip.speedCount).toFixed(2))
            : 0,
        points_count: currentTrip.points.length,
      });

      currentTrip = pointMoving
        ? {
            start: point,
            end: point,
            points: [point],
            distanceKm: 0,
            speedSum: Number(point.speed || 0),
            speedCount: point.speed !== null && point.speed !== undefined ? 1 : 0,
          }
        : null;
    }
  }

  return trips.filter((trip) => trip.points_count > 1 || trip.distance_km > 0);
};

export const reverseGeocode = async (lat, lng) => {
  try {
    const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lng}&format=json`;
    const response = await fetch(url, {
      headers: {
        "User-Agent": "TrackingApp/1.0",
      },
    });

    if (!response.ok) {
      throw new Error("Reverse geocoding failed.");
    }

    const data = await response.json();
    return data.display_name || `${Number(lat).toFixed(4)}, ${Number(lng).toFixed(4)}`;
  } catch {
    return `${Number(lat).toFixed(4)}, ${Number(lng).toFixed(4)}`;
  }
};

export const haversineDistance = (lat1, lon1, lat2, lon2) => {
  const R = 6371000;
  const dLat = ((lat2 - lat1) * Math.PI) / 180;
  const dLon = ((lon2 - lon1) * Math.PI) / 180;
  const a =
    Math.sin(dLat / 2) ** 2 +
    Math.cos((lat1 * Math.PI) / 180) *
      Math.cos((lat2 * Math.PI) / 180) *
      Math.sin(dLon / 2) ** 2;

  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};

export const checkGeofences = async (deviceId, companyId, lat, lng) => {
  const settings = await getTrackingSettingsInternal(companyId);
  if (!settings.alert_geofence_enabled) {
    return;
  }

  const { rows } = await pool.query(
    `
      SELECT
        g.*,
        COALESCE(gs.is_inside, false) AS was_inside
      FROM tracking_geofences g
      JOIN tracking_geofence_devices gd
        ON gd.geofence_id = g.id
       AND gd.device_id = $1
       AND gd.is_active = true
      LEFT JOIN tracking_geofence_states gs
        ON gs.geofence_id = g.id
       AND gs.device_id = $1
      WHERE g.company_id = $2
        AND g.is_active = true
      ORDER BY g.id ASC
    `,
    [deviceId, companyId],
  );

  for (const fence of rows) {
    const distance = haversineDistance(
      Number(lat),
      Number(lng),
      Number(fence.center_lat),
      Number(fence.center_lng),
    );
    const isInside = distance <= Number(fence.radius_meters);
    const wasInside = Boolean(fence.was_inside);

    await pool.query(
      `
        INSERT INTO tracking_geofence_states
        (geofence_id, device_id, is_inside, last_transition_at, updated_at)
        VALUES ($1, $2, $3, NOW(), NOW())
        ON CONFLICT (geofence_id, device_id)
        DO UPDATE SET
          is_inside = EXCLUDED.is_inside,
          updated_at = NOW(),
          last_transition_at = CASE
            WHEN tracking_geofence_states.is_inside IS DISTINCT FROM EXCLUDED.is_inside
              THEN NOW()
            ELSE tracking_geofence_states.last_transition_at
          END
      `,
      [fence.id, deviceId, isInside],
    );

    if (!wasInside && isInside && fence.alert_on_enter) {
      await createTrackingAlert({
        companyId,
        deviceId,
        alertType: "geofence_enter",
        title: "Entrée dans une zone",
        message: `Le téléphone est entré dans la zone ${fence.name}.`,
        geofenceId: Number(fence.id),
        lat,
        lng,
      });

      emitCompany(companyId, "geofence:enter", {
        device_id: deviceId,
        geofence_id: Number(fence.id),
        geofence_name: fence.name,
        lat,
        lng,
      });
    }

    if (wasInside && !isInside && fence.alert_on_exit) {
      await createTrackingAlert({
        companyId,
        deviceId,
        alertType: "geofence_exit",
        title: "Sortie de zone",
        message: `Le téléphone est sorti de la zone ${fence.name}.`,
        geofenceId: Number(fence.id),
        lat,
        lng,
      });

      emitCompany(companyId, "geofence:exit", {
        device_id: deviceId,
        geofence_id: Number(fence.id),
        geofence_name: fence.name,
        lat,
        lng,
      });
    }
  }
};

export const mobileLogin = async ({ username, password, deviceInfo = {} }) => {
  const normalizedUsername = cleanString(username).toLowerCase();
  const normalizedPassword = String(password ?? "");

  trackingConsoleLog("mobileLogin:received", {
    username: normalizedUsername || null,
    deviceInfo: {
      os: normalizeOptionalStringWithMax(deviceInfo.os, 50),
      model: normalizeOptionalStringWithMax(deviceInfo.model, 200),
      version: normalizeOptionalStringWithMax(deviceInfo.version, 20),
    },
  });

  if (!normalizedUsername || !normalizedPassword) {
    throw new ApiError(400, "Nom d'utilisateur et mot de passe requis.");
  }

  const { rows } = await pool.query(
    `
      SELECT *
      FROM tracking_devices
      WHERE LOWER(login_username) = LOWER($1)
        AND is_active = true
      LIMIT 1
    `,
    [normalizedUsername],
  );

  const device = rows[0];

  if (!device) {
    throw new ApiError(401, "Identifiants incorrects.");
  }

  const valid = await bcrypt.compare(normalizedPassword, device.login_password_hash);

  if (!valid) {
    throw new ApiError(401, "Identifiants incorrects.");
  }

  trackingDebugLog("mobileLogin:success", {
    username: normalizedUsername,
    device_id: Number(device.id),
    company_id: Number(device.company_id),
    device_info: {
      os: normalizeOptionalStringWithMax(deviceInfo.os, 50),
      model: normalizeOptionalStringWithMax(deviceInfo.model, 200),
      version: normalizeOptionalStringWithMax(deviceInfo.version, 20),
    },
  });

  const token = jwt.sign(
    {
      device_id: Number(device.id),
      company_id: Number(device.company_id),
      type: "mobile_tracking",
    },
    env.jwtSecret,
    { expiresIn: MOBILE_TOKEN_TTL },
  );

  await pool.query(
    `
      UPDATE tracking_devices
      SET
        device_os = $1,
        device_model = $2,
        app_version = $3,
        mobile_token = $4,
        mobile_token_expires = NOW() + INTERVAL '365 days',
        is_online = true,
        last_seen = NOW(),
        updated_at = NOW()
      WHERE id = $5
    `,
    [
      normalizeOptionalStringWithMax(deviceInfo.os, 50),
      normalizeOptionalStringWithMax(deviceInfo.model, 200),
      normalizeOptionalStringWithMax(deviceInfo.version, 20),
      token,
      device.id,
    ],
  );

  emitCompany(device.company_id, "device:online", {
    device_id: Number(device.id),
    device_name: device.device_name,
    owner_name: device.owner_name,
  });

  await logTrackingAudit({
    companyId: Number(device.company_id),
    userId: null,
    action: "MOBILE_LOGIN",
    entityType: "tracking_device",
    entityId: Number(device.id),
    details: {
      device_name: device.device_name,
      app_version: normalizeOptionalStringWithMax(deviceInfo.version, 20),
      device_os: normalizeOptionalStringWithMax(deviceInfo.os, 50),
      device_model: normalizeOptionalStringWithMax(deviceInfo.model, 200),
    },
  });

  await resolveActiveAlerts(Number(device.company_id), Number(device.id), "offline");

  return {
    token,
    device_id: Number(device.id),
    tracking_interval: Number(device.tracking_interval_seconds || 30),
    device_name: device.device_name,
    owner_name: device.owner_name,
  };
};

export const verifyMobileToken = async (req, res, next) => {
  const mobileTokenHeader = cleanString(req.headers["x-mobile-token"]);
  console.log("verifyMobileToken:received", {
    authorization: req.headers.authorization ? "present" : "missing",
    xMobileToken: mobileTokenHeader ? "present" : "missing",
  });
  const authHeader = req.headers.authorization;
  const token =
    (authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null) ||
    mobileTokenHeader ||
    null;

  if (!token) {
    return res.status(401).json({ message: "Jeton mobile requis." });
  }

  try {
    const decoded = jwt.verify(token, env.jwtSecret);

    if (decoded.type !== "mobile_tracking" || !decoded.device_id || !decoded.company_id) {
      return res.status(401).json({ message: "Jeton mobile invalide." });
    }

    const { rows } = await pool.query(
      `
        SELECT id, company_id, is_active, is_tracking_enabled, mobile_token, mobile_token_expires
        FROM tracking_devices
        WHERE id = $1
          AND company_id = $2
          AND is_active = true
        LIMIT 1
      `,
      [decoded.device_id, decoded.company_id],
    );

    const device = rows[0];

    if (!device || device.mobile_token !== token) {
      return res.status(401).json({ message: "Jeton mobile invalide." });
    }

    if (!device.is_tracking_enabled) {
      return res.status(403).json({ message: "Le suivi est désactivé pour cet appareil." });
    }

    if (device.mobile_token_expires && new Date(device.mobile_token_expires) <= new Date()) {
      return res.status(401).json({ message: "Jeton mobile expiré." });
    }

    req.mobileDevice = {
      deviceId: Number(device.id),
      companyId: Number(device.company_id),
      token,
    };

    return next();
  } catch {
    return res.status(401).json({ message: "Jeton mobile invalide." });
  }
};

export const receiveLocation = async ({ deviceId, companyId, locationData }) => {
  const latitude = toNumber(locationData.latitude);
  const longitude = toNumber(locationData.longitude);
  const accuracy = toNumber(locationData.accuracy);
  const altitude = toNumber(locationData.altitude);
  const speed = toNumber(locationData.speed);
  const heading = toNumber(locationData.heading);
  const batteryLevel = toNumber(locationData.battery_level);
  const networkType = normalizeOptionalStringWithMax(locationData.network_type, 20);
  const timestamp = parseTimestamp(locationData.device_timestamp);
  const timezoneOffsetMinutes = toNumber(locationData.timezone_offset_minutes, 0) || 0;
  const isMoving = speed !== null && speed > 2;
  const settings = await getTrackingSettingsInternal(companyId);
  const shouldPersistLocation = isTimestampInsideSaveWindow(timestamp, settings, timezoneOffsetMinutes);

  trackingConsoleLog("receiveLocation:received", {
    deviceId,
    companyId,
    latitude,
    longitude,
    accuracy,
    altitude,
    speed,
    heading,
    batteryLevel,
    networkType,
    deviceTimestamp: locationData.device_timestamp || null,
    timezoneOffsetMinutes,
    savedToHistory: shouldPersistLocation,
  });

  if (latitude === null || longitude === null) {
    throw new ApiError(400, "Latitude et longitude requises.");
  }

  trackingDebugLog("receiveLocation", {
    deviceId,
    companyId,
    latitude,
    longitude,
    speed,
    batteryLevel,
    networkType,
    timestamp: timestamp.toISOString(),
  });

  let savedLocationId = null;

  if (shouldPersistLocation) {
    const insertResult = await pool.query(
      `
        INSERT INTO tracking_locations
        (device_id, company_id, latitude, longitude, accuracy, altitude,
         speed, heading, battery_level, network_type, device_timestamp, is_moving)
        VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)
        RETURNING id
      `,
      [
        deviceId,
        companyId,
        latitude,
        longitude,
        accuracy,
        altitude,
        speed,
        heading,
        batteryLevel,
        networkType,
        timestamp,
        isMoving,
      ],
    );
    savedLocationId = Number(insertResult.rows[0].id);
  }

  await pool.query(
    `
      UPDATE tracking_devices
      SET
        is_online = true,
        last_seen = NOW(),
        last_lat = $1,
        last_lng = $2,
        last_accuracy = $3,
        last_speed = $4,
        last_battery = $5,
        updated_at = NOW()
      WHERE id = $6
    `,
    [latitude, longitude, accuracy, speed, batteryLevel, deviceId],
  );

  emitCompany(companyId, "location:update", {
    device_id: deviceId,
    lat: latitude,
    lng: longitude,
    accuracy,
    speed,
    heading,
    battery: batteryLevel,
    timestamp: timestamp.toISOString(),
    is_moving: isMoving,
    saved_to_history: shouldPersistLocation,
  });

  resolveActiveAlerts(companyId, deviceId, "offline").catch(() => {});

  if (
    settings.alert_low_battery_enabled &&
    batteryLevel !== null &&
    batteryLevel <= settings.alert_low_battery_threshold
  ) {
    createTrackingAlert({
      companyId,
      deviceId,
      alertType: "low_battery",
      title: "Batterie faible",
      message: `Batterie à ${batteryLevel}%`,
      lat: latitude,
      lng: longitude,
    }).catch(() => {});
  } else if (
    batteryLevel !== null &&
    batteryLevel > settings.alert_low_battery_threshold
  ) {
    resolveActiveAlerts(companyId, deviceId, "low_battery").catch(() => {});
  }

  if (
    settings.alert_speed_enabled &&
    speed !== null &&
    speed > settings.alert_speed_threshold_kmh
  ) {
    createTrackingAlert({
      companyId,
      deviceId,
      alertType: "speed",
      title: "Vitesse excessive",
      message: `Vitesse détectée: ${Math.round(speed)} km/h`,
      lat: latitude,
      lng: longitude,
    }).catch(() => {});
  } else if (speed !== null && speed <= settings.alert_speed_threshold_kmh) {
    resolveActiveAlerts(companyId, deviceId, "speed").catch(() => {});
  }

  reverseGeocode(latitude, longitude)
    .then(async (address) => {
      await pool.query("UPDATE tracking_devices SET last_address = $1 WHERE id = $2", [address, deviceId]);
      if (savedLocationId) {
        await pool.query(
          `
            UPDATE tracking_locations
            SET address = $1
            WHERE id = $2
          `,
          [address, savedLocationId],
        );
      }
    })
    .catch(() => {});

  checkGeofences(deviceId, companyId, latitude, longitude).catch(() => {});

  return {
    received: true,
    saved: shouldPersistLocation,
    next_ping_seconds: Number(settings.default_tracking_interval_seconds || 30),
  };
};

export const receiveHeartbeat = async ({ deviceId, companyId, payload = {} }) => {
  const batteryLevel = toNumber(payload.battery_level);

  trackingConsoleLog("receiveHeartbeat:received", {
    deviceId,
    companyId,
    batteryLevel,
    payload,
  });

  trackingDebugLog("receiveHeartbeat", {
    deviceId,
    companyId,
    batteryLevel,
    payload,
  });

  const updateResult = await pool.query(
    `
      UPDATE tracking_devices
      SET
        is_online = true,
        last_seen = NOW(),
        last_battery = COALESCE($1, last_battery),
        updated_at = NOW()
      WHERE id = $2
        AND company_id = $3
      RETURNING id, company_id, device_name, owner_name, last_seen, last_battery
    `,
    [batteryLevel, deviceId, companyId],
  );

  const device = updateResult.rows[0];

  if (device) {
    emitCompany(companyId, "device:online", {
      device_id: Number(device.id),
      device_name: device.device_name,
      owner_name: device.owner_name,
      battery: device.last_battery === null ? batteryLevel : Number(device.last_battery),
      timestamp: device.last_seen instanceof Date ? device.last_seen.toISOString() : new Date().toISOString(),
    });
  }

  emitCompany(companyId, "device:heartbeat", {
    device_id: deviceId,
    battery: batteryLevel,
    timestamp: new Date().toISOString(),
  });

  await resolveActiveAlerts(companyId, deviceId, "offline");

  return { received: true };
};

export const checkOfflineDevices = async (companyId = null) => {
  const values = [];
  const companyFilter = companyId ? `AND d.company_id = $1` : "";

  if (companyId) {
    values.push(Number(companyId));
  }

  const { rows } = await pool.query(
    `
      SELECT
        d.id,
        d.company_id,
        d.device_name,
        d.owner_name,
        COALESCE(s.alert_offline_threshold_minutes, s.offline_threshold_minutes, 5) AS offline_threshold_minutes,
        COALESCE(s.alert_offline_enabled, true) AS alert_offline_enabled
      FROM tracking_devices d
      LEFT JOIN tracking_settings s
        ON s.company_id = d.company_id
      WHERE d.is_online = true
        AND d.is_tracking_enabled = true
        AND d.is_active = true
        AND d.last_seen IS NOT NULL
        ${companyFilter}
    `,
    values,
  );

  for (const device of rows) {
    const thresholdMinutes = Math.max(2, Number(device.offline_threshold_minutes || 5));
    const updateResult = await pool.query(
      `
        UPDATE tracking_devices
        SET is_online = false, updated_at = NOW()
        WHERE id = $1
          AND company_id = $2
          AND last_seen < NOW() - ($3::TEXT || ' minutes')::INTERVAL
        RETURNING id, company_id, device_name, owner_name
      `,
      [device.id, device.company_id, String(thresholdMinutes)],
    );

    const justOffline = updateResult.rows[0];
    if (!justOffline) {
      continue;
    }

    trackingDebugLog("checkOfflineDevices:offline", {
      device_id: Number(justOffline.id),
      company_id: Number(justOffline.company_id),
      thresholdMinutes,
    });

    emitCompany(device.company_id, "device:offline", {
      device_id: Number(justOffline.id),
      device_name: justOffline.device_name,
      owner_name: justOffline.owner_name,
    });

    if (device.alert_offline_enabled) {
      await createTrackingAlert({
        companyId: Number(device.company_id),
        deviceId: Number(justOffline.id),
        alertType: "offline",
        title: "Téléphone hors ligne",
        message: `${justOffline.device_name} (${justOffline.owner_name}) est hors ligne.`,
      });
    }
  }
};

export const initializeTrackingService = (io) => {
  ioInstance = io;

  if (!offlineIntervalId) {
    checkOfflineDevices().catch((error) => {
      console.error("Tracking offline detection failed", error);
    });

    offlineIntervalId = setInterval(() => {
      checkOfflineDevices().catch((error) => {
        console.error("Tracking offline detection failed", error);
      });
    }, 30 * 1000);
  }
};

export const listDevices = async (companyId, options = {}) => {
  await checkOfflineDevices(companyId);
  await ensureTrackingDeviceFreeKioskColumns();

  const includeInactive = options.includeInactive === true;
  const { rows } = await pool.query(
    `
      SELECT
        d.*,
        COALESCE(s.offline_threshold_minutes, 5) AS offline_threshold_minutes
      FROM tracking_devices d
      LEFT JOIN tracking_settings s
        ON s.company_id = d.company_id
      WHERE d.company_id = $1
        ${includeInactive ? "" : "AND d.is_active = true"}
      ORDER BY
        CASE
          WHEN d.is_active = false THEN 2
          WHEN d.is_online = true THEN 0
          ELSE 1
        END,
        d.last_seen DESC NULLS LAST,
        d.device_name ASC
    `,
    [companyId],
  );

  return rows.map(mapDeviceRow);
};

export const queueDeviceCommand = async ({
  companyId,
  actorId,
  deviceId,
  commandType,
  payload = {},
  ipAddress,
}) => {
  await ensureTrackingCommandsTable();
  const normalizedCommandType = cleanString(commandType);

  if (!ALLOWED_DEVICE_COMMANDS.has(normalizedCommandType)) {
    throw new ApiError(400, "Commande distante invalide.");
  }

  await getDeviceRowOrThrow(companyId, deviceId);

  const { rows } = await pool.query(
    `
      INSERT INTO tracking_device_commands
      (company_id, device_id, command_type, payload, status, created_by, created_at)
      VALUES ($1, $2, $3, $4, 'pending', $5, NOW())
      RETURNING *
    `,
    [companyId, deviceId, normalizedCommandType, JSON.stringify(payload || {}), actorId || null],
  );

  const item = mapCommandRow(rows[0]);

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "COMMAND_QUEUED",
    entityType: "tracking_device_command",
    entityId: item.id,
    details: {
      device_id: deviceId,
      command_type: normalizedCommandType,
      payload: payload || {},
    },
    ipAddress,
  });

  emitCompany(companyId, "device:command_queued", {
    id: item.id,
    device_id: item.device_id,
    command_type: item.command_type,
    created_at: item.created_at,
  });

  return item;
};

export const requestScreenShareSession = async ({
  companyId,
  actorId,
  deviceId,
  ipAddress,
}) => {
  await ensureTrackingScreenSessionsTable();
  await getDeviceRowOrThrow(companyId, deviceId);

  const { rows } = await pool.query(
    `
      INSERT INTO tracking_screen_sessions
      (company_id, device_id, status, requested_by, requested_at)
      VALUES ($1, $2, 'requested', $3, NOW())
      RETURNING *
    `,
    [companyId, deviceId, actorId || null],
  );

  const session = mapScreenSessionRow(rows[0]);

  await queueDeviceCommand({
    companyId,
    actorId,
    deviceId,
    commandType: "start_screen_share",
    payload: {
      session_id: String(session.id),
    },
    ipAddress,
  });

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "SCREEN_SHARE_REQUESTED",
    entityType: "tracking_screen_session",
    entityId: session.id,
    details: {
      device_id: deviceId,
    },
    ipAddress,
  });

  return session;
};

export const getScreenShareSession = async ({ companyId, sessionId }) => {
  await ensureTrackingScreenSessionsTable();

  const { rows } = await pool.query(
    `
      SELECT *
      FROM tracking_screen_sessions
      WHERE id = $1
        AND company_id = $2
      LIMIT 1
    `,
    [sessionId, companyId],
  );

  const row = rows[0];
  if (!row) {
    throw new ApiError(404, "Session de partage d'écran introuvable.");
  }

  return mapScreenSessionRow(row);
};

export const startScreenShareSession = async ({
  deviceId,
  companyId,
  sessionId,
  width = null,
  height = null,
}) => {
  await ensureTrackingScreenSessionsTable();

  const { rows } = await pool.query(
    `
      UPDATE tracking_screen_sessions
      SET
        status = 'live',
        started_at = COALESCE(started_at, NOW()),
        width = COALESCE($1, width),
        height = COALESCE($2, height)
      WHERE id = $3
        AND device_id = $4
        AND company_id = $5
      RETURNING *
    `,
    [width, height, sessionId, deviceId, companyId],
  );

  const row = rows[0];
  if (!row) {
    throw new ApiError(404, "Session de partage d'écran introuvable.");
  }

  emitCompany(companyId, "screen-share:started", {
    session_id: Number(row.id),
    device_id: Number(row.device_id),
  });

  return mapScreenSessionRow(row);
};

export const uploadScreenShareFrame = async ({
  deviceId,
  companyId,
  sessionId,
  imageBase64,
  capturedAt,
  width = null,
  height = null,
}) => {
  await ensureTrackingScreenSessionsTable();

  const imageBuffer = Buffer.from(String(imageBase64 || ""), "base64");
  if (!imageBuffer.length) {
    throw new ApiError(400, "Image de partage d'écran invalide.");
  }

  const sessionDir = path.join(trackingScreensRoot, `session-${sessionId}`);
  await fs.mkdir(sessionDir, { recursive: true });
  const filePath = path.join(sessionDir, "latest.jpg");
  await fs.writeFile(filePath, imageBuffer);

  const latestFrameUrl = `/uploads/tracking-screen/session-${sessionId}/latest.jpg`;

  const { rows } = await pool.query(
    `
      UPDATE tracking_screen_sessions
      SET
        status = 'live',
        started_at = COALESCE(started_at, NOW()),
        last_frame_at = COALESCE($1::timestamp, NOW()),
        width = COALESCE($2, width),
        height = COALESCE($3, height),
        latest_frame_url = $4
      WHERE id = $5
        AND device_id = $6
        AND company_id = $7
      RETURNING *
    `,
    [capturedAt || new Date().toISOString(), width, height, latestFrameUrl, sessionId, deviceId, companyId],
  );

  const row = rows[0];
  if (!row) {
    throw new ApiError(404, "Session de partage d'écran introuvable.");
  }

  emitCompany(companyId, "screen-share:frame", {
    session_id: Number(row.id),
    device_id: Number(row.device_id),
    latest_frame_url: latestFrameUrl,
    last_frame_at: row.last_frame_at,
  });

  return mapScreenSessionRow(row);
};

export const stopScreenShareSession = async ({
  deviceId,
  companyId,
  sessionId,
  reason = null,
}) => {
  await ensureTrackingScreenSessionsTable();

  const { rows } = await pool.query(
    `
      UPDATE tracking_screen_sessions
      SET
        status = 'stopped',
        stopped_at = NOW(),
        stop_reason = COALESCE($1, stop_reason)
      WHERE id = $2
        AND device_id = $3
        AND company_id = $4
      RETURNING *
    `,
    [normalizeOptionalString(reason), sessionId, deviceId, companyId],
  );

  const row = rows[0];
  if (!row) {
    throw new ApiError(404, "Session de partage d'écran introuvable.");
  }

  emitCompany(companyId, "screen-share:stopped", {
    session_id: Number(row.id),
    device_id: Number(row.device_id),
    reason: row.stop_reason || null,
  });

  return mapScreenSessionRow(row);
};

export const pollDeviceCommands = async ({ deviceId, companyId }) => {
  await ensureTrackingCommandsTable();

  const { rows } = await pool.query(
    `
      WITH picked AS (
        SELECT id
        FROM tracking_device_commands
        WHERE device_id = $1
          AND company_id = $2
          AND status = 'pending'
        ORDER BY created_at ASC
        LIMIT 10
      )
      UPDATE tracking_device_commands command
      SET
        status = 'delivered',
        delivered_at = NOW()
      FROM picked
      WHERE command.id = picked.id
      RETURNING command.*
    `,
    [deviceId, companyId],
  );

  if (rows.length) {
    trackingConsoleLog("pollDeviceCommands:delivered", {
      deviceId,
      companyId,
      commandIds: rows.map((row) => Number(row.id)),
      commandTypes: rows.map((row) => row.command_type),
    });
  }

  return rows.map(mapCommandRow);
};

export const acknowledgeDeviceCommand = async ({
  deviceId,
  companyId,
  commandId,
  status,
  result = null,
}) => {
  await ensureTrackingCommandsTable();
  const normalizedStatus = cleanString(status);

  if (!["completed", "failed"].includes(normalizedStatus)) {
    throw new ApiError(400, "Statut de commande invalide.");
  }

  const { rows } = await pool.query(
    `
      UPDATE tracking_device_commands
      SET
        status = $1,
        acked_at = NOW(),
        result = $2
      WHERE id = $3
        AND device_id = $4
        AND company_id = $5
      RETURNING *
    `,
    [normalizedStatus, result ? JSON.stringify(result) : null, commandId, deviceId, companyId],
  );

  const row = rows[0];
  if (!row) {
    throw new ApiError(404, "Commande distante introuvable.");
  }

  const mappedCommand = mapCommandRow(row);
  emitCompany(companyId, "device:command_ack", {
    device_id: Number(row.device_id),
    command_id: Number(row.id),
    command_type: row.command_type,
    status: row.status,
    result: mappedCommand.result || null,
    acked_at: row.acked_at,
  });

  await logTrackingAudit({
    companyId,
    userId: null,
    action: normalizedStatus === "completed" ? "COMMAND_COMPLETED" : "COMMAND_FAILED",
    entityType: "tracking_device_command",
    entityId: Number(row.id),
    details: {
      device_id: deviceId,
      command_type: row.command_type,
      result: result || null,
    },
  });

  return mappedCommand;
};

export const uploadDeviceScreenshot = async ({
  deviceId,
  companyId,
  capturedAt,
  imageBase64,
}) => {
  if (!imageBase64) {
    throw new ApiError(400, "Capture écran manquante.");
  }

  await ensureTrackingScreenshotsRoot();
  await getDeviceRowOrThrow(companyId, deviceId);

  const timestamp = parseTimestamp(capturedAt, new Date());
  const fileName = `device-${deviceId}-${timestamp.getTime()}.png`;
  const filePath = path.join(trackingScreenshotsRoot, fileName);
  const buffer = Buffer.from(String(imageBase64), "base64");

  await fs.writeFile(filePath, buffer);

  return {
    file_name: fileName,
    captured_at: timestamp.toISOString(),
    file_url: `/uploads/tracking-screenshots/${fileName}`,
  };
};

export const executeFreeKioskAction = async ({
  companyId,
  actorId,
  deviceId,
  action,
  payload = {},
  ipAddress,
}) => {
  const normalizedAction = cleanString(action);
  const definition = FREEKIOSK_ACTIONS[normalizedAction];

  if (!definition) {
    throw new ApiError(400, "Action FreeKiosk invalide.");
  }

  const device = await getDeviceRowOrThrow(companyId, deviceId);

  if (!device.freekiosk_enabled) {
    throw new ApiError(400, "FreeKiosk n'est pas configuré pour ce téléphone.");
  }

  const commandPayload = buildFreeKioskRestCommandPayload({ definition, payload, device });
  const command = await queueDeviceCommand({
    companyId,
    actorId,
    deviceId,
    commandType: "freekiosk_rest_request",
    payload: commandPayload,
    ipAddress,
  });

  const result = {
    message: "Demande FreeKiosk REST envoyée à TrackingApp.",
    command_id: command.id,
    command_status: command.status,
    method: commandPayload.method,
    rest_url: `${commandPayload.base_url || "http://<phone-ip>:8080"}${commandPayload.path}`,
  };

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "FREEKIOSK_REST_REQUESTED",
    entityType: "tracking_device",
    entityId: deviceId,
    details: {
      action: normalizedAction,
      command_id: command.id,
      method: commandPayload.method,
      path: commandPayload.path,
      binary: commandPayload.binary || null,
    },
    ipAddress,
  });

  return {
    action: normalizedAction,
    device_id: deviceId,
    result,
  };
};

export const createDevice = async ({ companyId, actorId, payload, ipAddress, serverUrl }) => {
  await ensureTrackingDeviceFreeKioskColumns();
  const settings = await getTrackingSettingsInternal(companyId);
  const deviceName = cleanString(payload.device_name);
  const loginUsername = await ensureUsernameAvailable(payload.login_username);
  const password = String(payload.password ?? "");
  const ownerName = cleanString(payload.owner_name);
  const trackingIntervalSeconds = Math.max(
    toNumber(payload.tracking_interval_seconds, settings.default_tracking_interval_seconds) ||
      settings.default_tracking_interval_seconds,
    10,
  );

  if (!deviceName || !loginUsername || !password || !ownerName) {
    throw new ApiError(400, "device_name, login_username, password et owner_name sont requis.");
  }

  const passwordHash = await bcrypt.hash(password, 12);
  let result;
  try {
    result = await pool.query(
      `
        INSERT INTO tracking_devices
        (
          company_id, device_name, login_username, login_password_hash, owner_name,
          owner_phone, owner_department, tracking_interval_seconds, is_tracking_enabled,
          is_active, freekiosk_enabled, freekiosk_base_url, freekiosk_api_key, freekiosk_mqtt_device,
          notes, created_by, created_at, updated_at
        )
        VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,true,$10,$11,$12,$13,$14,$15,NOW(),NOW())
        RETURNING *
      `,
      [
        companyId,
        deviceName,
        loginUsername,
        passwordHash,
        ownerName,
        normalizeOptionalString(payload.owner_phone),
        normalizeOptionalString(payload.owner_department),
        trackingIntervalSeconds,
        payload.is_tracking_enabled !== false,
        payload.freekiosk_enabled === true,
        normalizeOptionalStringWithMax(payload.freekiosk_base_url, 500),
        normalizeOptionalStringWithMax(payload.freekiosk_api_key, 500),
        normalizeFreeKioskMqttDevice(payload.freekiosk_mqtt_device),
        normalizeOptionalString(payload.notes),
        actorId,
      ],
    );
  } catch (error) {
    if (error?.code === "23505") {
      throw new ApiError(409, "Nom d'utilisateur déjà utilisé.");
    }
    throw error;
  }

  const row = result.rows[0];

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "CREATE",
    entityType: "tracking_device",
    entityId: Number(row.id),
    details: {
      device_name: row.device_name,
      login_username: row.login_username,
      owner_name: row.owner_name,
    },
    ipAddress,
  });

  return {
    device: mapDeviceRow(row),
    generated_password: password,
    qr_code_data: buildQrPayload({
      serverUrl,
      username: row.login_username,
      password,
      deviceId: Number(row.id),
      companyId: Number(row.company_id),
    }),
  };
};

export const updateDevice = async ({ companyId, actorId, deviceId, payload, ipAddress }) => {
  await ensureTrackingDeviceFreeKioskColumns();
  const existing = await getDeviceRowOrThrow(companyId, deviceId);

  const deviceName = cleanString(payload.device_name || existing.device_name);
  const ownerName = cleanString(payload.owner_name || existing.owner_name);
  const trackingIntervalSeconds = Math.max(
    toNumber(payload.tracking_interval_seconds, Number(existing.tracking_interval_seconds)) ||
      Number(existing.tracking_interval_seconds),
    10,
  );

  if (!deviceName || !ownerName) {
    throw new ApiError(400, "device_name et owner_name sont requis.");
  }

  let passwordHash = existing.login_password_hash;
  if (payload.password) {
    passwordHash = await bcrypt.hash(String(payload.password), 12);
  }

  const loginUsername =
    payload.login_username && cleanString(payload.login_username).toLowerCase() !== cleanString(existing.login_username).toLowerCase()
      ? await ensureUsernameAvailable(payload.login_username, deviceId)
      : existing.login_username;

  let result;
  try {
    result = await pool.query(
      `
        UPDATE tracking_devices
        SET
          device_name = $1,
          owner_name = $2,
          owner_phone = $3,
          owner_department = $4,
          tracking_interval_seconds = $5,
          is_tracking_enabled = $6,
          notes = $7,
          login_password_hash = $8,
          login_username = $9,
          freekiosk_enabled = $10,
          freekiosk_base_url = $11,
          freekiosk_api_key = $12,
          freekiosk_mqtt_device = $13,
          updated_at = NOW()
        WHERE id = $14
        RETURNING *
      `,
      [
        deviceName,
        ownerName,
        normalizeOptionalString(payload.owner_phone ?? existing.owner_phone),
        normalizeOptionalString(payload.owner_department ?? existing.owner_department),
        trackingIntervalSeconds,
        payload.is_tracking_enabled ?? existing.is_tracking_enabled,
        normalizeOptionalString(payload.notes ?? existing.notes),
        passwordHash,
        loginUsername,
        payload.freekiosk_enabled ?? existing.freekiosk_enabled ?? false,
        normalizeOptionalStringWithMax(payload.freekiosk_base_url ?? existing.freekiosk_base_url, 500),
        payload.freekiosk_api_key === undefined
          ? existing.freekiosk_api_key
          : normalizeOptionalStringWithMax(payload.freekiosk_api_key, 500),
        payload.freekiosk_mqtt_device === undefined
          ? existing.freekiosk_mqtt_device
          : normalizeFreeKioskMqttDevice(payload.freekiosk_mqtt_device),
        deviceId,
      ],
    );
  } catch (error) {
    if (error?.code === "23505") {
      throw new ApiError(409, "Nom d'utilisateur déjà utilisé.");
    }
    throw error;
  }

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "UPDATE",
    entityType: "tracking_device",
    entityId: deviceId,
    details: {
      before: mapDeviceRow(existing),
      after: mapDeviceRow(result.rows[0]),
    },
    ipAddress,
  });

  if (
    existing.is_tracking_enabled !== (payload.is_tracking_enabled ?? existing.is_tracking_enabled)
  ) {
    await logTrackingAudit({
      companyId,
      userId: actorId,
      action: payload.is_tracking_enabled ? "TRACKING_ENABLED" : "TRACKING_DISABLED",
      entityType: "tracking_device",
      entityId: deviceId,
      details: { device_name: result.rows[0].device_name },
      ipAddress,
    });
  }

  return mapDeviceRow(result.rows[0]);
};

export const deactivateDevice = async ({ companyId, actorId, deviceId, ipAddress }) => {
  const result = await pool.query(
    `
      UPDATE tracking_devices
      SET
        is_active = false,
        is_online = false,
        mobile_token = NULL,
        mobile_token_expires = NULL,
        updated_at = NOW()
      WHERE id = $1
        AND company_id = $2
        AND is_active = true
      RETURNING *
    `,
    [deviceId, companyId],
  );

  const row = result.rows[0];

  if (!row) {
    throw new ApiError(404, "Téléphone introuvable.");
  }

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "DELETE",
    entityType: "tracking_device",
    entityId: deviceId,
    details: { device_name: row.device_name },
    ipAddress,
  });

  emitCompany(companyId, "device:offline", {
    device_id: deviceId,
    device_name: row.device_name,
    owner_name: row.owner_name,
  });

  return mapDeviceRow(row);
};

export const getDeviceLocations = async ({ companyId, deviceId, from, to, limit }) => {
  await getDeviceRowOrThrow(companyId, deviceId);
  const effectiveLimit = Math.min(Math.max(Number(limit) || DEFAULT_LOCATION_LIMIT, 1), MAX_LOCATION_LIMIT);
  const fromDate = from ? parseTimestamp(from, null) : new Date(Date.now() - 24 * 60 * 60 * 1000);
  const toDate = to ? parseTimestamp(to, null) : new Date();
  const durationMs = Math.max(0, toDate.getTime() - fromDate.getTime());
  const durationHours = durationMs / (1000 * 60 * 60);
  const thinningBucketMinutes = durationHours > 6 ? 5 : durationHours >= 1 ? 1 : null;
  let rows;

  if (thinningBucketMinutes) {
    const result = await pool.query(
      `
        SELECT *
        FROM (
          SELECT DISTINCT ON (bucket_key)
            id,
            latitude,
            longitude,
            accuracy,
            altitude,
            speed,
            heading,
            battery_level,
            network_type,
            address,
            device_timestamp,
            server_timestamp,
            is_moving,
            bucket_key
          FROM (
            SELECT
              *,
              date_trunc('minute', device_timestamp)
              - ((EXTRACT(MINUTE FROM device_timestamp)::INTEGER % $6) * INTERVAL '1 minute') AS bucket_key
            FROM tracking_locations
            WHERE company_id = $1
              AND device_id = $2
              AND device_timestamp BETWEEN $3 AND $4
          ) tl
          ORDER BY bucket_key, device_timestamp DESC
        ) bucketed
        ORDER BY device_timestamp ASC
        LIMIT $5
      `,
      [companyId, deviceId, fromDate, toDate, effectiveLimit, thinningBucketMinutes],
    );
    rows = result.rows;
  } else {
    const result = await pool.query(
      `
        SELECT *
        FROM tracking_locations
        WHERE company_id = $1
          AND device_id = $2
          AND device_timestamp BETWEEN $3 AND $4
        ORDER BY device_timestamp ASC
        LIMIT $5
      `,
      [companyId, deviceId, fromDate, toDate, effectiveLimit],
    );
    rows = result.rows;
  }

  return rows.map((row) => ({
    id: Number(row.id),
    latitude: Number(row.latitude),
    longitude: Number(row.longitude),
    accuracy: row.accuracy === null ? null : Number(row.accuracy),
    altitude: row.altitude === null ? null : Number(row.altitude),
    speed: row.speed === null ? null : Number(row.speed),
    heading: row.heading === null ? null : Number(row.heading),
    battery_level: row.battery_level === null ? null : Number(row.battery_level),
    network_type: row.network_type,
    address: row.address,
    device_timestamp: row.device_timestamp,
    server_timestamp: row.server_timestamp,
    is_moving: row.is_moving,
  }));
};

export const getDeviceLive = async ({ companyId, deviceId }) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);

  const trailFrom = new Date(Date.now() - DEFAULT_TRAIL_MINUTES * 60 * 1000);
  const trail = await getDeviceLocations({
    companyId,
    deviceId,
    from: trailFrom.toISOString(),
    to: new Date().toISOString(),
    limit: 500,
  });

  return {
    device: mapDeviceRow(device),
    trail,
  };
};

export const getTrackingSettings = async (companyId) => ({
  company_id: Number(companyId),
  ...(await getTrackingSettingsInternal(companyId)),
});

export const updateTrackingSettings = async ({ companyId, actorId, payload, ipAddress }) => {
  await ensureTrackingSettingsTable();
  const current = await getTrackingSettingsInternal(companyId);

  const next = {
    default_tracking_interval_seconds: Math.min(
      120,
      Math.max(10, toNumber(payload.default_tracking_interval_seconds, current.default_tracking_interval_seconds)),
    ),
    offline_threshold_minutes: Math.max(
      2,
      toNumber(payload.offline_threshold_minutes, current.offline_threshold_minutes),
    ),
    retention_days:
      payload.retention_days === null || payload.retention_days === undefined || payload.retention_days === ""
        ? current.retention_days
        : Math.max(1, toNumber(payload.retention_days, current.retention_days)),
    alert_offline_enabled: payload.alert_offline_enabled ?? current.alert_offline_enabled,
    alert_offline_threshold_minutes: Math.max(
      2,
      toNumber(payload.alert_offline_threshold_minutes, current.alert_offline_threshold_minutes),
    ),
    alert_low_battery_enabled: payload.alert_low_battery_enabled ?? current.alert_low_battery_enabled,
    alert_low_battery_threshold: Math.min(
      100,
      Math.max(1, toNumber(payload.alert_low_battery_threshold, current.alert_low_battery_threshold)),
    ),
    alert_speed_enabled: payload.alert_speed_enabled ?? current.alert_speed_enabled,
    alert_speed_threshold_kmh: Math.max(
      1,
      toNumber(payload.alert_speed_threshold_kmh, current.alert_speed_threshold_kmh),
    ),
    alert_geofence_enabled: payload.alert_geofence_enabled ?? current.alert_geofence_enabled,
    save_locations_enabled: payload.save_locations_enabled ?? current.save_locations_enabled,
    save_locations_start_time:
      payload.save_locations_start_time === null || payload.save_locations_start_time === ""
        ? null
        : normalizeTimeValue(payload.save_locations_start_time) || current.save_locations_start_time,
    save_locations_end_time:
      payload.save_locations_end_time === null || payload.save_locations_end_time === ""
        ? null
        : normalizeTimeValue(payload.save_locations_end_time) || current.save_locations_end_time,
  };

  await pool.query(
    `
      INSERT INTO tracking_settings
      (
        company_id, default_tracking_interval_seconds, offline_threshold_minutes, retention_days,
        alert_offline_enabled, alert_offline_threshold_minutes,
        alert_low_battery_enabled, alert_low_battery_threshold,
        alert_speed_enabled, alert_speed_threshold_kmh,
        alert_geofence_enabled,
        save_locations_enabled, save_locations_start_time, save_locations_end_time,
        updated_at
      )
      VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,NOW())
      ON CONFLICT (company_id)
      DO UPDATE SET
        default_tracking_interval_seconds = EXCLUDED.default_tracking_interval_seconds,
        offline_threshold_minutes = EXCLUDED.offline_threshold_minutes,
        retention_days = EXCLUDED.retention_days,
        alert_offline_enabled = EXCLUDED.alert_offline_enabled,
        alert_offline_threshold_minutes = EXCLUDED.alert_offline_threshold_minutes,
        alert_low_battery_enabled = EXCLUDED.alert_low_battery_enabled,
        alert_low_battery_threshold = EXCLUDED.alert_low_battery_threshold,
        alert_speed_enabled = EXCLUDED.alert_speed_enabled,
        alert_speed_threshold_kmh = EXCLUDED.alert_speed_threshold_kmh,
        alert_geofence_enabled = EXCLUDED.alert_geofence_enabled,
        save_locations_enabled = EXCLUDED.save_locations_enabled,
        save_locations_start_time = EXCLUDED.save_locations_start_time,
        save_locations_end_time = EXCLUDED.save_locations_end_time,
        updated_at = NOW()
    `,
    [
      companyId,
      next.default_tracking_interval_seconds,
      next.offline_threshold_minutes,
      next.retention_days,
      next.alert_offline_enabled,
      next.alert_offline_threshold_minutes,
      next.alert_low_battery_enabled,
      next.alert_low_battery_threshold,
      next.alert_speed_enabled,
      next.alert_speed_threshold_kmh,
      next.alert_geofence_enabled,
      next.save_locations_enabled,
      next.save_locations_start_time,
      next.save_locations_end_time,
    ],
  );

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "UPDATE",
    entityType: "tracking_settings",
    entityId: Number(companyId),
    details: { before: current, after: next },
    ipAddress,
  });

  return {
    company_id: Number(companyId),
    ...next,
  };
};

export const listGeofences = async (companyId) => {
  const { rows } = await pool.query(
    `
      SELECT
        g.*,
        COALESCE(
          ARRAY_AGG(gd.device_id ORDER BY gd.device_id)
          FILTER (WHERE gd.device_id IS NOT NULL AND gd.is_active = true),
          ARRAY[]::INTEGER[]
        ) AS device_ids
      FROM tracking_geofences g
      LEFT JOIN tracking_geofence_devices gd
        ON gd.geofence_id = g.id
      WHERE g.company_id = $1
      GROUP BY g.id
      ORDER BY g.name ASC
    `,
    [companyId],
  );

  return rows.map(mapGeofenceRow);
};

export const createGeofence = async ({ companyId, actorId, payload, ipAddress }) => {
  const name = cleanString(payload.name);
  const centerLat = toNumber(payload.center_lat);
  const centerLng = toNumber(payload.center_lng);
  const radiusMeters = Math.max(toNumber(payload.radius_meters, 500) || 500, 1);
  const deviceIds = Array.isArray(payload.device_ids)
    ? payload.device_ids.map(Number).filter((value) => Number.isInteger(value) && value > 0)
    : [];

  if (!name || centerLat === null || centerLng === null) {
    throw new ApiError(400, "name, center_lat et center_lng sont requis.");
  }

  await validateCompanyDeviceIds(companyId, deviceIds);

  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    const result = await client.query(
      `
        INSERT INTO tracking_geofences
        (
          company_id, name, center_lat, center_lng, radius_meters, color,
          alert_on_enter, alert_on_exit, is_active, created_by, created_at
        )
        VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,NOW())
        RETURNING *
      `,
      [
        companyId,
        name,
        centerLat,
        centerLng,
        radiusMeters,
        normalizeOptionalString(payload.color) || "#3B82F6",
        payload.alert_on_enter === true,
        payload.alert_on_exit !== false,
        payload.is_active !== false,
        actorId,
      ],
    );

    const geofence = result.rows[0];

    for (const deviceId of deviceIds) {
      await client.query(
        `
          INSERT INTO tracking_geofence_devices (geofence_id, device_id, is_active)
          VALUES ($1, $2, true)
          ON CONFLICT (geofence_id, device_id)
          DO UPDATE SET is_active = true
        `,
        [geofence.id, deviceId],
      );
    }

    await client.query("COMMIT");

    await logTrackingAudit({
      companyId,
      userId: actorId,
      action: "CREATE",
      entityType: "tracking_geofence",
      entityId: Number(geofence.id),
      details: { name, device_ids: deviceIds },
      ipAddress,
    });

    return (await listGeofences(companyId)).find((item) => item.id === Number(geofence.id));
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
};

export const updateGeofence = async ({ companyId, actorId, geofenceId, payload, ipAddress }) => {
  const existingResult = await pool.query(
    `
      SELECT *
      FROM tracking_geofences
      WHERE id = $1
        AND company_id = $2
      LIMIT 1
    `,
    [geofenceId, companyId],
  );

  const existing = existingResult.rows[0];

  if (!existing) {
    throw new ApiError(404, "Zone géographique introuvable.");
  }

  const deviceIds = Array.isArray(payload.device_ids)
    ? payload.device_ids.map(Number).filter((value) => Number.isInteger(value) && value > 0)
    : null;

  if (deviceIds) {
    await validateCompanyDeviceIds(companyId, deviceIds);
  }

  const client = await pool.connect();

  try {
    await client.query("BEGIN");

    await client.query(
      `
        UPDATE tracking_geofences
        SET
          name = $1,
          center_lat = $2,
          center_lng = $3,
          radius_meters = $4,
          color = $5,
          alert_on_enter = $6,
          alert_on_exit = $7,
          is_active = $8
        WHERE id = $9
      `,
      [
        cleanString(payload.name || existing.name),
        toNumber(payload.center_lat, Number(existing.center_lat)),
        toNumber(payload.center_lng, Number(existing.center_lng)),
        Math.max(toNumber(payload.radius_meters, Number(existing.radius_meters)) || Number(existing.radius_meters), 1),
        normalizeOptionalString(payload.color) || existing.color,
        payload.alert_on_enter ?? existing.alert_on_enter,
        payload.alert_on_exit ?? existing.alert_on_exit,
        payload.is_active ?? existing.is_active,
        geofenceId,
      ],
    );

    if (deviceIds) {
      await client.query("UPDATE tracking_geofence_devices SET is_active = false WHERE geofence_id = $1", [
        geofenceId,
      ]);

      for (const deviceId of deviceIds) {
        await client.query(
          `
            INSERT INTO tracking_geofence_devices (geofence_id, device_id, is_active)
            VALUES ($1, $2, true)
            ON CONFLICT (geofence_id, device_id)
            DO UPDATE SET is_active = true
          `,
          [geofenceId, deviceId],
        );
      }
    }

    await client.query("COMMIT");

    await logTrackingAudit({
      companyId,
      userId: actorId,
      action: "UPDATE",
      entityType: "tracking_geofence",
      entityId: geofenceId,
      details: {
        before: existing,
        after: {
          ...existing,
          ...payload,
          device_ids: deviceIds,
        },
      },
      ipAddress,
    });

    return (await listGeofences(companyId)).find((item) => item.id === Number(geofenceId));
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }
};

export const assignGeofenceDevices = async ({ companyId, actorId, geofenceId, deviceIds, ipAddress }) => {
  const normalizedDeviceIds = Array.isArray(deviceIds)
    ? deviceIds.map(Number).filter((value) => Number.isInteger(value) && value > 0)
    : [];

  await validateCompanyDeviceIds(companyId, normalizedDeviceIds);

  const existing = await pool.query(
    `
      SELECT id, name
      FROM tracking_geofences
      WHERE id = $1
        AND company_id = $2
      LIMIT 1
    `,
    [geofenceId, companyId],
  );

  if (!existing.rows[0]) {
    throw new ApiError(404, "Zone géographique introuvable.");
  }

  const client = await pool.connect();

  try {
    await client.query("BEGIN");
    await client.query("UPDATE tracking_geofence_devices SET is_active = false WHERE geofence_id = $1", [geofenceId]);

    for (const deviceId of normalizedDeviceIds) {
      await client.query(
        `
          INSERT INTO tracking_geofence_devices (geofence_id, device_id, is_active)
          VALUES ($1, $2, true)
          ON CONFLICT (geofence_id, device_id)
          DO UPDATE SET is_active = true
        `,
        [geofenceId, deviceId],
      );
    }

    await client.query("COMMIT");
  } catch (error) {
    await client.query("ROLLBACK");
    throw error;
  } finally {
    client.release();
  }

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "ASSIGN_DEVICES",
    entityType: "tracking_geofence",
    entityId: geofenceId,
    details: { geofence_name: existing.rows[0].name, device_ids: normalizedDeviceIds },
    ipAddress,
  });

  return (await listGeofences(companyId)).find((item) => item.id === Number(geofenceId)) || null;
};

export const deleteGeofence = async ({ companyId, actorId, geofenceId, ipAddress }) => {
  const result = await pool.query(
    `
      DELETE FROM tracking_geofences
      WHERE id = $1
        AND company_id = $2
      RETURNING id, name
    `,
    [geofenceId, companyId],
  );

  if (!result.rows[0]) {
    throw new ApiError(404, "Zone géographique introuvable.");
  }

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "DELETE",
    entityType: "tracking_geofence",
    entityId: geofenceId,
    details: { name: result.rows[0].name },
    ipAddress,
  });
};

export const listAlerts = async ({ companyId, status = null, type = null, deviceId = null, from = null, to = null }) => {
  const values = [companyId];
  const where = ["a.company_id = $1"];

  if (status) {
    values.push(status);
    where.push(`a.status = $${values.length}`);
  }

  if (type) {
    values.push(type);
    where.push(`a.alert_type = $${values.length}`);
  }

  if (deviceId) {
    values.push(deviceId);
    where.push(`a.device_id = $${values.length}`);
  }

  if (from) {
    values.push(parseTimestamp(from));
    where.push(`a.created_at >= $${values.length}`);
  }

  if (to) {
    values.push(parseTimestamp(to));
    where.push(`a.created_at <= $${values.length}`);
  }

  const { rows } = await pool.query(
    `
      SELECT
        a.*,
        d.device_name,
        d.owner_name,
        g.name AS geofence_name
      FROM tracking_alerts a
      LEFT JOIN tracking_devices d
        ON d.id = a.device_id
      LEFT JOIN tracking_geofences g
        ON g.id = a.geofence_id
      WHERE ${where.join(" AND ")}
      ORDER BY
        CASE a.status WHEN 'active' THEN 0 WHEN 'read' THEN 1 ELSE 2 END,
        a.created_at DESC
    `,
    values,
  );

  return rows.map(mapAlertRow);
};

export const updateAlertStatus = async ({ companyId, alertId, status, actorId = null, ipAddress = null }) => {
  const allowed = ["active", "read", "dismissed"];

  if (!allowed.includes(status)) {
    throw new ApiError(400, "Statut d'alerte invalide.");
  }

  const result = await pool.query(
    `
      UPDATE tracking_alerts
      SET status = $1
      WHERE id = $2
        AND company_id = $3
      RETURNING *
    `,
    [status, alertId, companyId],
  );

  if (!result.rows[0]) {
    throw new ApiError(404, "Alerte introuvable.");
  }

  const item = mapAlertRow(result.rows[0]);

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: status === "read" ? "ALERT_ACKNOWLEDGED" : "UPDATE",
    entityType: "tracking_alert",
    entityId: alertId,
    details: { status },
    ipAddress,
  });

  const activeItems = await listAlerts({ companyId, status: "active" });
  emitCompany(companyId, "alerts:updated", buildAlertSummary(activeItems));

  return item;
};

export const readAllAlerts = async ({ companyId, actorId = null, ipAddress = null }) => {
  await pool.query(
    `
      UPDATE tracking_alerts
      SET status = 'read'
      WHERE company_id = $1
        AND status = 'active'
    `,
    [companyId],
  );

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "READ_ALL",
    entityType: "tracking_alert",
    entityId: null,
    details: { scope: "company" },
    ipAddress,
  });

  emitCompany(companyId, "alerts:updated", { critical: 0, warning: 0, info: 0 });

  return { success: true };
};

export const getAlertSummary = async (companyId) => {
  const items = await listAlerts({ companyId, status: "active" });
  return buildAlertSummary(items);
};

export const getStats = async ({ companyId, deviceId, from, to }) => {
  const fromDate = from ? parseTimestamp(from, null) : new Date(Date.now() - 24 * 60 * 60 * 1000);
  const toDate = to ? parseTimestamp(to, null) : new Date();
  const values = [companyId, fromDate, toDate];
  let deviceClause = "";

  if (deviceId) {
    values.push(deviceId);
    deviceClause = `AND device_id = $${values.length}`;
  }

  const { rows } = await pool.query(
    `
      SELECT device_id, latitude, longitude, speed, is_moving, device_timestamp
      FROM tracking_locations
      WHERE company_id = $1
        AND device_timestamp BETWEEN $2 AND $3
        ${deviceClause}
      ORDER BY device_id ASC, device_timestamp ASC
    `,
    values,
  );

  let kmTravelled = 0;
  let timeMovingSeconds = 0;
  let timeStoppedSeconds = 0;
  let speedTotal = 0;
  let speedCount = 0;

  for (let index = 0; index < rows.length; index += 1) {
    const current = rows[index];

    if (current.speed !== null) {
      speedTotal += Number(current.speed);
      speedCount += 1;
    }

    if (index === 0) {
      continue;
    }

    const previous = rows[index - 1];

    if (Number(previous.device_id) !== Number(current.device_id)) {
      continue;
    }

    kmTravelled +=
      haversineDistance(
        Number(previous.latitude),
        Number(previous.longitude),
        Number(current.latitude),
        Number(current.longitude),
      ) / 1000;

    const deltaSeconds = Math.max(
      0,
      (new Date(current.device_timestamp).getTime() - new Date(previous.device_timestamp).getTime()) / 1000,
    );

    if (previous.is_moving) {
      timeMovingSeconds += deltaSeconds;
    } else {
      timeStoppedSeconds += deltaSeconds;
    }
  }

  return {
    km_traveled: Number(kmTravelled.toFixed(3)),
    time_moving_seconds: Math.round(timeMovingSeconds),
    time_stopped_seconds: Math.round(timeStoppedSeconds),
    avg_speed: speedCount ? Number((speedTotal / speedCount).toFixed(2)) : 0,
    locations_count: rows.length,
  };
};

export const getDeviceById = async ({ companyId, deviceId }) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);
  const events = await getDeviceEvents(companyId, deviceId);
  const latestLive = await getDeviceLive({ companyId, deviceId });

  return {
    ...latestLive.device,
    trail: latestLive.trail,
    events,
  };
};

export const getDeviceTrips = async ({ companyId, deviceId, from, to }) => {
  const locations = await getDeviceLocations({
    companyId,
    deviceId,
    from,
    to,
    limit: MAX_LOCATION_LIMIT,
  });

  const orderedLocations = [...locations].sort(
    (a, b) => new Date(a.device_timestamp).getTime() - new Date(b.device_timestamp).getTime(),
  );

  return buildTripsFromLocations(orderedLocations);
};

export const getDeviceStats = async ({ companyId, deviceId, from, to }) => {
  const [summary, locations] = await Promise.all([
    getStats({ companyId, deviceId, from, to }),
    getDeviceLocations({
      companyId,
      deviceId,
      from,
      to,
      limit: MAX_LOCATION_LIMIT,
    }),
  ]);

  const distanceByDay = new Map();
  const activityByDay = new Map();
  let maxSpeed = 0;

  const orderedLocations = [...locations].sort(
    (a, b) => new Date(a.device_timestamp).getTime() - new Date(b.device_timestamp).getTime(),
  );

  for (let index = 0; index < orderedLocations.length; index += 1) {
    const current = orderedLocations[index];
    const currentDate = new Date(current.device_timestamp);
    const dayKey = currentDate.toISOString().slice(0, 10);
    const hourKey = currentDate.getHours();

    if (!activityByDay.has(dayKey)) {
      activityByDay.set(dayKey, new Set());
    }
    activityByDay.get(dayKey).add(hourKey);

    maxSpeed = Math.max(maxSpeed, Number(current.speed || 0));

    if (index === 0) {
      if (!distanceByDay.has(dayKey)) {
        distanceByDay.set(dayKey, 0);
      }
      continue;
    }

    const previous = orderedLocations[index - 1];
    const previousDate = new Date(previous.device_timestamp).toISOString().slice(0, 10);
    const deltaKm =
      haversineDistance(
        Number(previous.latitude),
        Number(previous.longitude),
        Number(current.latitude),
        Number(current.longitude),
      ) / 1000;

    distanceByDay.set(previousDate, Number((Number(distanceByDay.get(previousDate) || 0) + deltaKm).toFixed(3)));
  }

  return {
    ...summary,
    max_speed: Number(maxSpeed.toFixed(2)),
    distance_by_day: Array.from(distanceByDay.entries()).map(([date, distance]) => ({
      date,
      distance_km: distance,
    })),
    active_hours_by_day: Array.from(activityByDay.entries()).map(([date, hours]) => ({
      date,
      hours: [...hours].sort((a, b) => a - b),
      active_hours_count: hours.size,
    })),
    movement_breakdown: [
      { label: "En mouvement", value: summary.time_moving_seconds },
      { label: "À l'arrêt", value: summary.time_stopped_seconds },
    ],
  };
};

export const resetDevicePassword = async ({
  companyId,
  actorId,
  deviceId,
  newPassword,
  ipAddress,
  serverUrl,
}) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);
  const password = String(newPassword ?? "");

  if (password.length < 6) {
    throw new ApiError(400, "Le mot de passe doit contenir au moins 6 caractères.");
  }

  const passwordHash = await bcrypt.hash(password, 12);

  await pool.query(
    `
      UPDATE tracking_devices
      SET
        login_password_hash = $1,
        mobile_token = NULL,
        mobile_token_expires = NULL,
        is_online = false,
        updated_at = NOW()
      WHERE id = $2
        AND company_id = $3
    `,
    [passwordHash, deviceId, companyId],
  );

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "RESET_PASSWORD",
    entityType: "tracking_device",
    entityId: deviceId,
    details: { device_name: device.device_name, login_username: device.login_username },
    ipAddress,
  });

  return {
    device: mapDeviceRow({
      ...device,
      is_online: false,
      mobile_token: null,
      mobile_token_expires: null,
    }),
    generated_password: password,
    qr_code_data: buildQrPayload({
      serverUrl,
      username: device.login_username,
      password,
      deviceId,
      companyId,
    }),
  };
};

export const getDeviceQrData = async ({ companyId, deviceId, serverUrl }) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);

  return {
    device: mapDeviceRow(device),
    qr_code_data: buildQrPayload({
      serverUrl,
      username: device.login_username,
      password: null,
      deviceId,
      companyId,
    }),
    requires_password_reset: true,
  };
};

export const getTrackingDevicesQuickCount = async (companyId) => {
  const { rows } = await pool.query(
    `
      SELECT
        COUNT(*) FILTER (WHERE is_active = true)::INTEGER AS total_devices,
        COUNT(*) FILTER (WHERE is_active = true AND is_online = true)::INTEGER AS online_devices
      FROM tracking_devices
      WHERE company_id = $1
    `,
    [companyId],
  );

  return {
    total_devices: Number(rows[0]?.total_devices || 0),
    online_devices: Number(rows[0]?.online_devices || 0),
  };
};

const escapeXml = (value) =>
  String(value ?? "")
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&apos;");

export const getDeviceKmlExport = async ({ companyId, actorId = null, deviceId, date, ipAddress = null }) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);
  const exportDate = date || new Date().toISOString().slice(0, 10);
  const points = await getDeviceLocations({
    companyId,
    deviceId,
    from: `${exportDate}T00:00:00.000Z`,
    to: `${exportDate}T23:59:59.999Z`,
    limit: MAX_LOCATION_LIMIT,
  });

  const content = `<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
  <Document>
    <name>Trajet ${escapeXml(device.device_name)} — ${escapeXml(exportDate)}</name>
    <Placemark>
      <name>Trajet</name>
      <LineString>
        <coordinates>
          ${points.map((point) => `${point.longitude},${point.latitude},0`).join("\n          ")}
        </coordinates>
      </LineString>
    </Placemark>
    ${points
      .map(
        (point) => `
    <Placemark>
      <name>${escapeXml(formatKmlTimestamp(point.device_timestamp))}</name>
      <Point><coordinates>${point.longitude},${point.latitude},0</coordinates></Point>
    </Placemark>`,
      )
      .join("")}
  </Document>
</kml>`;

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "EXPORT",
    entityType: "tracking_location_kml",
    entityId: deviceId,
    details: { device_name: device.device_name, date: exportDate, format: "kml" },
    ipAddress,
  });

  return {
    filename: `tracking-${deviceId}-${exportDate}.kml`,
    content,
  };
};

export const getDeviceCsvExport = async ({ companyId, actorId = null, deviceId, date, ipAddress = null }) => {
  const device = await getDeviceRowOrThrow(companyId, deviceId);
  const exportDate = date || new Date().toISOString().slice(0, 10);
  const points = await getDeviceLocations({
    companyId,
    deviceId,
    from: `${exportDate}T00:00:00.000Z`,
    to: `${exportDate}T23:59:59.999Z`,
    limit: MAX_LOCATION_LIMIT,
  });

  const rows = [
    ["Heure", "Latitude", "Longitude", "Vitesse", "Adresse", "Batterie", "Precision"].join(";"),
    ...points.map((point) =>
      [
        formatDateTime(point.device_timestamp),
        point.latitude,
        point.longitude,
        point.speed ?? "",
        `"${String(point.address || "").replaceAll('"', '""')}"`,
        point.battery_level ?? "",
        point.accuracy ?? "",
      ].join(";"),
    ),
  ];

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "EXPORT",
    entityType: "tracking_location_csv",
    entityId: deviceId,
    details: { device_name: device.device_name, date: exportDate, format: "csv" },
    ipAddress,
  });

  return {
    filename: `tracking-${deviceId}-${exportDate}.csv`,
    content: rows.join("\n"),
  };
};

export const listTrackingAudit = async ({ companyId, userId = null, action = null, from = null, to = null }) => {
  const values = [companyId];
  const where = ["l.company_id = $1"];

  if (userId) {
    values.push(userId);
    where.push(`l.user_id = $${values.length}`);
  }

  if (action) {
    values.push(action);
    where.push(`l.action = $${values.length}`);
  }

  if (from) {
    values.push(parseTimestamp(from));
    where.push(`l.created_at >= $${values.length}`);
  }

  if (to) {
    values.push(parseTimestamp(to));
    where.push(`l.created_at <= $${values.length}`);
  }

  const [itemsResult, usersResult] = await Promise.all([
    pool.query(
      `
        SELECT
          l.*,
          u.full_name AS user_name
        FROM tracking_audit_log l
        LEFT JOIN users u
          ON u.id = l.user_id
        WHERE ${where.join(" AND ")}
        ORDER BY l.created_at DESC
        LIMIT 500
      `,
      values,
    ),
    pool.query(
      `
        SELECT DISTINCT u.id, u.full_name
        FROM tracking_audit_log l
        JOIN users u
          ON u.id = l.user_id
        WHERE l.company_id = $1
        ORDER BY u.full_name ASC
      `,
      [companyId],
    ),
  ]);

  return {
    items: itemsResult.rows.map((row) => ({
      id: Number(row.id),
      created_at: row.created_at,
      action: row.action,
      entity_type: row.entity_type,
      entity_id: row.entity_id === null ? null : Number(row.entity_id),
      user_id: row.user_id === null ? null : Number(row.user_id),
      user_name: row.user_name || "Système",
      ip_address: row.ip_address,
      details: row.details || null,
    })),
    users: usersResult.rows.map((row) => ({
      id: Number(row.id),
      full_name: row.full_name,
    })),
  };
};

export const cleanOldLocations = async () => {
  await ensureTrackingSettingsTable();
  const { rows } = await pool.query(
    `
      SELECT company_id, retention_days
      FROM tracking_settings
      WHERE retention_days IS NOT NULL
        AND retention_days > 0
    `,
  );

  let deletedCount = 0;

  for (const row of rows) {
    const result = await pool.query(
      `
        DELETE FROM tracking_locations
        WHERE company_id = $1
          AND device_timestamp < NOW() - ($2::TEXT || ' days')::INTERVAL
      `,
      [row.company_id, String(Number(row.retention_days))],
    );
    deletedCount += result.rowCount || 0;
  }

  return { deleted_count: deletedCount };
};

export const clearTrackingLocations = async ({ companyId, actorId, month, deviceId = null, ipAddress = null }) => {
  const normalizedMonth = cleanString(month);
  const monthMatch = normalizedMonth.match(/^(\d{4})-(\d{2})$/);

  if (!monthMatch) {
    throw new ApiError(400, "Mois invalide. Format attendu: YYYY-MM.");
  }

  const year = Number(monthMatch[1]);
  const monthNumber = Number(monthMatch[2]);

  if (monthNumber < 1 || monthNumber > 12) {
    throw new ApiError(400, "Mois invalide.");
  }

  const from = new Date(Date.UTC(year, monthNumber - 1, 1, 0, 0, 0));
  const to = new Date(Date.UTC(year, monthNumber, 1, 0, 0, 0));
  const values = [companyId, from, to];
  let deviceFilter = "";

  if (deviceId) {
    values.push(deviceId);
    deviceFilter = ` AND device_id = $${values.length}`;
  }

  const result = await pool.query(
    `
      DELETE FROM tracking_locations
      WHERE company_id = $1
        AND device_timestamp >= $2
        AND device_timestamp < $3
        ${deviceFilter}
    `,
    values,
  );

  const deletedCount = result.rowCount || 0;

  await logTrackingAudit({
    companyId,
    userId: actorId,
    action: "CLEAR_LOCATIONS",
    entityType: "tracking_locations",
    entityId: Number(companyId),
    details: {
      month: normalizedMonth,
      device_id: deviceId || null,
      deleted_count: deletedCount,
    },
    ipAddress,
  });

  return {
    month: normalizedMonth,
    device_id: deviceId || null,
    deleted_count: deletedCount,
  };
};

export const checkDeviceUsernameAvailability = async ({ username, excludeDeviceId = null }) => {
  const normalizedUsername = cleanString(username).toLowerCase();

  if (!normalizedUsername) {
    return { available: false };
  }

  const values = [normalizedUsername];
  let where = "LOWER(login_username) = LOWER($1)";

  if (excludeDeviceId) {
    values.push(excludeDeviceId);
    where += ` AND id <> $${values.length}`;
  }

  const { rows } = await pool.query(
    `
      SELECT id
      FROM tracking_devices
      WHERE ${where}
      LIMIT 1
    `,
    values,
  );

  return {
    available: !rows[0],
    normalized_username: normalizedUsername,
  };
};

export const registerTrackingSocketHandlers = (socket, token, companyId) => {
  try {
    const decoded = jwt.verify(token, env.jwtSecret);
    const requestedCompanyId = Number(companyId);
    const allowedCompanies = Array.isArray(decoded.companyIds)
      ? decoded.companyIds.map(Number)
      : [Number(decoded.companyId)].filter(Boolean);

    if (!allowedCompanies.includes(requestedCompanyId) && Number(decoded.companyId) !== requestedCompanyId) {
      return false;
    }

    socket.join(`company:${requestedCompanyId}`);
    getCurrentDeviceStates(requestedCompanyId)
      .then((devices) => {
        socket.emit("devices:initial_state", devices);
      })
      .catch(() => {});

    return true;
  } catch {
    return false;
  }
};

export const resolveCompanyIdFromRequest = (req) => {
  const companyId = Number(req.query.company_id || req.body.company_id || req.user?.companyId);

  if (!companyId) {
    throw new ApiError(400, "Société requise.");
  }

  assertCompanyAccess(req, companyId);
  return companyId;
};

export const getServerBaseUrl = (req) => `${req.protocol}://${req.get("host")}`;
