const fs = require("node:fs");
const https = require("node:https");
const http = require("node:http");
const path = require("node:path");
const { randomUUID } = require("node:crypto");
const { URL } = require("node:url");

const HOST = process.env.HOST || "0.0.0.0";
const PORT = Number(process.env.PORT || 8080);
const DATA_DIR = process.env.DATA_DIR || path.resolve(__dirname, "..", "data");
const WEB_ROOT = process.env.WEB_ROOT || path.resolve(__dirname, "..", "..", "web", "public");
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || "";
const ADMIN_AUTH_REQUIRED = parseBoolean(process.env.ADMIN_AUTH_REQUIRED, false);
const DEFAULT_KAKAO_JAVASCRIPT_KEY = "07f4728599ceaf4299f77c772a135423";
const DEFAULT_KAKAO_REST_API_KEY = "91e5df96769ba97fc83e9ace9a4fec1e";
const KAKAO_REST_API_KEY = process.env.KAKAO_REST_API_KEY || DEFAULT_KAKAO_REST_API_KEY;
const SIMULATOR_ENABLED = parseBoolean(process.env.SIMULATOR_ENABLED, true);
const SIMULATOR_INTERVAL_MS = clampNumber(Number(process.env.SIMULATOR_INTERVAL_MS || 5000), 1000, 60000);
const SIMULATOR_HISTORY_EVERY = clampNumber(Number(process.env.SIMULATOR_HISTORY_EVERY || 3), 1, 30);
const SIMULATOR_SPEED_SCALE = clampNumber(Number(process.env.SIMULATOR_SPEED_SCALE || 1), 0.1, 20);
const KAKAO_DIRECTIONS_URL = "https://apis-navi.kakaomobility.com/affiliate/v1/directions";
const OSRM_ROUTE_URL = process.env.OSRM_ROUTE_URL || "https://router.project-osrm.org/route/v1/driving";

const DRIVERS_FILE = path.join(DATA_DIR, "drivers.json");
const LATEST_FILE = path.join(DATA_DIR, "latest-locations.json");
const HISTORY_FILE = path.join(DATA_DIR, "locations.jsonl");
const SIMULATOR_ROUTES_FILE = path.join(DATA_DIR, "simulator-routes.json");

const VEHICLE_TYPES = new Set([
  "cargo-truck",
  "tractor-trailer",
  "box-truck",
  "van",
  "refrigerated",
  "container",
  "wingbody"
]);

const SIMULATED_FLEET = [
  {
    driverId: "sim-01",
    name: "강민준",
    vehicleNo: "서울 91바 1024",
    token: "sim-token-01",
    vehicleType: "tractor-trailer",
    cargoName: "자동차 부품",
    cargoQuantity: "24팔레트",
    cargoWeight: "18t",
    speedBase: 76,
    loopMs: 16 * 60 * 1000,
    route: [
      point("인천항", 37.4563, 126.7052),
      point("평택 물류단지", 36.9921, 127.1128),
      point("대전 허브", 36.3504, 127.3845),
      point("부산신항", 35.0812, 128.8291)
    ]
  },
  {
    driverId: "sim-02",
    name: "이서준",
    vehicleNo: "부산 82사 7731",
    token: "sim-token-02",
    vehicleType: "container",
    cargoName: "수출 컨테이너",
    cargoQuantity: "40피트 1대",
    cargoWeight: "22t",
    speedBase: 72,
    loopMs: 15 * 60 * 1000,
    route: [
      point("부산항", 35.1028, 129.0403),
      point("대구 물류센터", 35.8714, 128.6014),
      point("대전 터미널", 36.3504, 127.3845),
      point("서울 남부센터", 37.4813, 127.0367)
    ]
  },
  {
    driverId: "sim-03",
    name: "박도윤",
    vehicleNo: "경기 87아 4588",
    token: "sim-token-03",
    vehicleType: "cargo-truck",
    cargoName: "건축 자재",
    cargoQuantity: "철근 320본",
    cargoWeight: "8t",
    speedBase: 68,
    loopMs: 13 * 60 * 1000,
    route: [
      point("남양주 창고", 37.6360, 127.2165),
      point("원주 하차지", 37.3422, 127.9202),
      point("강릉 공사현장", 37.7519, 128.8761),
      point("이천 집하장", 37.2792, 127.4425)
    ]
  },
  {
    driverId: "sim-04",
    name: "최하준",
    vehicleNo: "인천 94자 2190",
    token: "sim-token-04",
    vehicleType: "van",
    cargoName: "소형 택배",
    cargoQuantity: "186박스",
    cargoWeight: "0.9t",
    speedBase: 54,
    loopMs: 9 * 60 * 1000,
    route: [
      point("서울 성수", 37.5446, 127.0557),
      point("수원 권선", 37.2636, 127.0286),
      point("천안 서북", 36.8151, 127.1139),
      point("대전 유성", 36.3622, 127.3563)
    ]
  },
  {
    driverId: "sim-05",
    name: "정지호",
    vehicleNo: "전남 86바 6405",
    token: "sim-token-05",
    vehicleType: "refrigerated",
    cargoName: "냉장 식품",
    cargoQuantity: "72박스",
    cargoWeight: "3.5t",
    speedBase: 62,
    loopMs: 12 * 60 * 1000,
    route: [
      point("평택 냉장창고", 36.9948, 127.0889),
      point("전주 물류센터", 35.8242, 127.1480),
      point("광주 도매시장", 35.1595, 126.8526),
      point("목포 항만", 34.8118, 126.3922)
    ]
  },
  {
    driverId: "sim-06",
    name: "오시우",
    vehicleNo: "광주 80다 3317",
    token: "sim-token-06",
    vehicleType: "wingbody",
    cargoName: "생활용품 팔레트",
    cargoQuantity: "14팔레트",
    cargoWeight: "5t",
    speedBase: 66,
    loopMs: 11 * 60 * 1000,
    route: [
      point("광주 광산", 35.1395, 126.7937),
      point("전주 완산", 35.8160, 127.1530),
      point("논산 집하장", 36.1871, 127.0987),
      point("대전 허브", 36.3504, 127.3845)
    ]
  },
  {
    driverId: "sim-07",
    name: "한유준",
    vehicleNo: "울산 89사 5022",
    token: "sim-token-07",
    vehicleType: "box-truck",
    cargoName: "전자제품",
    cargoQuantity: "96박스",
    cargoWeight: "2.5t",
    speedBase: 61,
    loopMs: 10 * 60 * 1000,
    route: [
      point("울산 공단", 35.5384, 129.3114),
      point("포항 남구", 36.0190, 129.3435),
      point("경주 외동", 35.8401, 129.2124),
      point("대구 성서", 35.8530, 128.5066)
    ]
  },
  {
    driverId: "sim-08",
    name: "문태오",
    vehicleNo: "경남 92자 8816",
    token: "sim-token-08",
    vehicleType: "tractor-trailer",
    cargoName: "철강 코일",
    cargoQuantity: "코일 5롤",
    cargoWeight: "20t",
    speedBase: 58,
    loopMs: 14 * 60 * 1000,
    route: [
      point("여수 산단", 34.7604, 127.6622),
      point("순천 물류장", 34.9506, 127.4872),
      point("진주 하치장", 35.1800, 128.1076),
      point("창원 공단", 35.2279, 128.6811)
    ]
  },
  {
    driverId: "sim-09",
    name: "서은우",
    vehicleNo: "제주 87허 1208",
    token: "sim-token-09",
    vehicleType: "van",
    cargoName: "호텔 린넨",
    cargoQuantity: "38카트",
    cargoWeight: "0.7t",
    speedBase: 46,
    loopMs: 8 * 60 * 1000,
    route: [
      point("제주항", 33.5176, 126.5260),
      point("제주공항", 33.5071, 126.4930),
      point("서귀포 중문", 33.2541, 126.4123),
      point("성산 물류점", 33.4582, 126.9425)
    ]
  },
  {
    driverId: "sim-10",
    name: "유도현",
    vehicleNo: "충북 83바 7344",
    token: "sim-token-10",
    vehicleType: "cargo-truck",
    cargoName: "농산물",
    cargoQuantity: "220상자",
    cargoWeight: "4.5t",
    speedBase: 57,
    loopMs: 12 * 60 * 1000,
    route: [
      point("청주 집하장", 36.6424, 127.4890),
      point("문경 산지센터", 36.5866, 128.1868),
      point("안동 물류장", 36.5684, 128.7294),
      point("경주 하차지", 35.8562, 129.2247)
    ]
  }
];

const sseClients = new Set();
let simulatorTimer = null;
let simulatorTick = 0;
let simulatorRouteCache = {};

ensureDataFiles();

let drivers = loadDrivers();
let latestLocations = loadLatestLocations();
simulatorRouteCache = loadSimulatorRouteCache();

const server = http.createServer(async (req, res) => {
  const requestUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);

  try {
    addCorsHeaders(res);

    if (req.method === "OPTIONS") {
      res.writeHead(204);
      res.end();
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/health") {
      sendJson(res, 200, {
        status: "ok",
        service: "cargoradar-server",
        time: new Date().toISOString()
      });
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/config.js") {
      sendJavaScript(res, 200, `window.CARGORADAR_CONFIG = ${JSON.stringify({
        kakaoJavaScriptKey: process.env.KAKAO_JAVASCRIPT_KEY || DEFAULT_KAKAO_JAVASCRIPT_KEY
      })};\n`);
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/api/v1/drivers") {
      if (!authorizeAdmin(req, res)) return;
      sendJson(res, 200, {
        drivers: drivers.map(toDriverResponse).sort(compareDrivers)
      });
      return;
    }

    if (req.method === "POST" && requestUrl.pathname === "/api/v1/drivers") {
      if (!authorizeAdmin(req, res)) return;
      const body = await readJsonBody(req);
      const driver = createDriver(body);
      drivers.push(driver);
      writeJsonAtomic(DRIVERS_FILE, drivers);
      sendJson(res, 201, { driver: toDriverResponse(driver) });
      return;
    }

    const driverMatch = requestUrl.pathname.match(/^\/api\/v1\/drivers\/([^/]+)$/);
    if (driverMatch && req.method === "PATCH") {
      if (!authorizeAdmin(req, res)) return;
      const driverId = decodeURIComponent(driverMatch[1]);
      const body = await readJsonBody(req);
      const driver = updateDriver(driverId, body);
      writeJsonAtomic(DRIVERS_FILE, drivers);
      sendJson(res, 200, { driver: toDriverResponse(driver) });
      return;
    }

    if (driverMatch && req.method === "DELETE") {
      if (!authorizeAdmin(req, res)) return;
      const driverId = decodeURIComponent(driverMatch[1]);
      const driverIndex = drivers.findIndex((driver) => driver.driverId === driverId);

      if (driverIndex < 0) {
        sendJson(res, 404, { error: "driver_not_found" });
        return;
      }

      drivers.splice(driverIndex, 1);
      delete latestLocations[driverId];
      writeJsonAtomic(DRIVERS_FILE, drivers);
      writeJsonAtomic(LATEST_FILE, latestLocations);
      broadcastEvent("drivers.updated", { drivers: drivers.map(toDriverResponse).sort(compareDrivers) });
      sendJson(res, 200, { ok: true });
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/api/v1/locations/latest") {
      if (!authorizeAdmin(req, res)) return;
      sendJson(res, 200, {
        locations: Object.values(latestLocations).sort(compareLatestLocations)
      });
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/api/v1/locations/history") {
      if (!authorizeAdmin(req, res)) return;
      const driverId = requestUrl.searchParams.get("driverId");
      const limit = clampNumber(Number(requestUrl.searchParams.get("limit") || 200), 1, 1000);
      sendJson(res, 200, {
        locations: readHistory({ driverId, limit })
      });
      return;
    }

    if (req.method === "GET" && requestUrl.pathname === "/api/v1/events") {
      if (!authorizeAdmin(req, res)) return;
      openEventStream(req, res);
      return;
    }

    if (req.method === "POST" && requestUrl.pathname === "/api/v1/locations") {
      const body = await readJsonBody(req);
      const driver = authorizeDevice(req, body.driverId);

      if (!driver) {
        sendJson(res, 401, { error: "invalid_device_token" });
        return;
      }

      if (body.driverId && body.driverId !== driver.driverId) {
        sendJson(res, 403, { error: "driver_token_mismatch" });
        return;
      }

      const location = createLocationRecord(body, driver);
      storeLocation(location);

      sendJson(res, 201, { location });
      return;
    }

    if (req.method === "GET" || req.method === "HEAD") {
      if (serveStaticFile(req, res, requestUrl.pathname)) return;
    }

    sendJson(res, 404, { error: "not_found" });
  } catch (error) {
    console.error(error);
    sendJson(res, error.statusCode || 500, {
      error: error.publicMessage || "internal_server_error"
    });
  }
});

server.listen(PORT, HOST, () => {
  console.log(`CargoRadar server listening on http://${HOST}:${PORT}`);
  console.log(`Serving web files from ${WEB_ROOT}`);
  startSimulator();
});

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

function shutdown() {
  if (simulatorTimer) {
    clearInterval(simulatorTimer);
  }

  for (const client of sseClients) {
    client.end();
  }
  server.close(() => process.exit(0));
}

function ensureDataFiles() {
  fs.mkdirSync(DATA_DIR, { recursive: true });

  if (!fs.existsSync(DRIVERS_FILE)) {
    writeJsonAtomic(DRIVERS_FILE, [
      {
        driverId: "demo-driver",
        name: "Demo Driver",
        vehicleNo: "SEOUL-12-3456",
        phone: "",
        token: "demo-token",
        enabled: true
      }
    ]);
  }

  if (!fs.existsSync(LATEST_FILE)) {
    writeJsonAtomic(LATEST_FILE, {});
  }

  if (!fs.existsSync(HISTORY_FILE)) {
    fs.writeFileSync(HISTORY_FILE, "", "utf8");
  }

  if (!fs.existsSync(SIMULATOR_ROUTES_FILE)) {
    writeJsonAtomic(SIMULATOR_ROUTES_FILE, {});
  }
}

function loadDrivers() {
  const parsed = readJsonFile(DRIVERS_FILE, []);
  return Array.isArray(parsed) ? parsed : [];
}

function loadSimulatorRouteCache() {
  const parsed = readJsonFile(SIMULATOR_ROUTES_FILE, {});
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
}

function createDriver(body) {
  const driverId = normalizeDriverId(body.driverId) || generateDriverId();

  if (drivers.some((driver) => driver.driverId === driverId)) {
    validationError("driver_id_exists");
  }

  const name = String(body.name || "").trim();
  const vehicleNo = String(body.vehicleNo || "").trim();

  if (!name) validationError("driver_name_required");
  if (!vehicleNo) validationError("vehicle_no_required");

  const driver = {
    driverId,
    name,
    vehicleNo,
    phone: String(body.phone || "").trim(),
    vehicleType: normalizeVehicleType(body.vehicleType),
    token: String(body.token || "").trim() || generateDeviceToken(),
    enabled: body.enabled === undefined ? true : Boolean(body.enabled),
    simulated: Boolean(body.simulated),
    createdAt: new Date().toISOString(),
    updatedAt: new Date().toISOString()
  };

  broadcastEvent("drivers.updated", { drivers: [...drivers, driver].map(toDriverResponse).sort(compareDrivers) });
  return driver;
}

function updateDriver(driverId, body) {
  const driver = drivers.find((candidate) => candidate.driverId === driverId);
  if (!driver) notFoundError("driver_not_found");

  if (body.name !== undefined) {
    const name = String(body.name || "").trim();
    if (!name) validationError("driver_name_required");
    driver.name = name;
  }

  if (body.vehicleNo !== undefined) {
    const vehicleNo = String(body.vehicleNo || "").trim();
    if (!vehicleNo) validationError("vehicle_no_required");
    driver.vehicleNo = vehicleNo;
  }

  if (body.phone !== undefined) {
    driver.phone = String(body.phone || "").trim();
  }

  if (body.vehicleType !== undefined) {
    driver.vehicleType = normalizeVehicleType(body.vehicleType);
  }

  if (body.enabled !== undefined) {
    driver.enabled = Boolean(body.enabled);
  }

  if (body.regenerateToken) {
    driver.token = generateDeviceToken();
  } else if (body.token !== undefined) {
    const token = String(body.token || "").trim();
    if (!token) validationError("driver_token_required");
    driver.token = token;
  }

  driver.updatedAt = new Date().toISOString();
  broadcastEvent("drivers.updated", { drivers: drivers.map(toDriverResponse).sort(compareDrivers) });
  return driver;
}

function loadLatestLocations() {
  const parsed = readJsonFile(LATEST_FILE, {});
  return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
}

function startSimulator() {
  if (!SIMULATOR_ENABLED) {
    console.log("CargoRadar simulator disabled.");
    return;
  }

  ensureSimulatorDrivers();
  console.log(
    `CargoRadar simulator preparing ${SIMULATED_FLEET.length} vehicles ` +
    `at ${SIMULATOR_INTERVAL_MS}ms updates and ${SIMULATOR_SPEED_SCALE}x speed.`
  );
  primeSimulatorRouteCache()
    .catch((error) => {
      console.warn(`CargoRadar simulator route preload failed: ${error.message}`);
    })
    .finally(startSimulatorPublishing);
}

function startSimulatorPublishing() {
  if (simulatorTimer) return;

  publishSimulatorLocations();
  simulatorTimer = setInterval(publishSimulatorLocations, SIMULATOR_INTERVAL_MS);

  if (typeof simulatorTimer.unref === "function") {
    simulatorTimer.unref();
  }

  console.log("CargoRadar simulator publishing locations.");
}

function ensureSimulatorDrivers() {
  let changed = false;

  for (const vehicle of SIMULATED_FLEET) {
    const existing = drivers.find((driver) => driver.driverId === vehicle.driverId);
    const next = {
      driverId: vehicle.driverId,
      name: vehicle.name,
      vehicleNo: vehicle.vehicleNo,
      phone: "",
      token: vehicle.token,
      vehicleType: normalizeVehicleType(vehicle.vehicleType),
      enabled: true,
      simulated: true
    };

    if (!existing) {
      drivers.push({
        ...next,
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString()
      });
      changed = true;
      continue;
    }

    let driverChanged = false;

    for (const [key, value] of Object.entries(next)) {
      if (existing[key] !== value) {
        existing[key] = value;
        driverChanged = true;
      }
    }

    if (driverChanged) {
      existing.updatedAt = new Date().toISOString();
      changed = true;
    }
  }

  if (changed) {
    writeJsonAtomic(DRIVERS_FILE, drivers);
    broadcastEvent("drivers.updated", { drivers: drivers.map(toDriverResponse).sort(compareDrivers) });
  }
}

function publishSimulatorLocations() {
  simulatorTick += 1;
  const nowMs = Date.now();
  const appendHistory = simulatorTick % SIMULATOR_HISTORY_EVERY === 1;

  for (const vehicle of SIMULATED_FLEET) {
    const driver = drivers.find((candidate) => candidate.driverId === vehicle.driverId);
    if (!driver || driver.enabled === false) continue;

    const body = createSimulatorLocationBody(vehicle, nowMs);
    const location = createLocationRecord(body, driver);
    storeLocation(location, { appendHistory, persistLatest: false });
  }

  writeJsonAtomic(LATEST_FILE, latestLocations);
}

function createSimulatorLocationBody(vehicle, nowMs) {
  const routeState = calculateSimulatorRouteState(vehicle, nowMs);
  const speedWave = Math.sin((nowMs + hashOffset(vehicle.vehicleNo)) / 17000) * 8;
  const speed = Math.max(18, Math.round(vehicle.speedBase + speedWave));
  const remainingDistanceKm = roundNumber(routeState.remainingDistanceKm, 1);
  const etaMinutes = Math.max(3, Math.round((remainingDistanceKm / Math.max(speed, 24)) * 60));

  return {
    driverId: vehicle.driverId,
    vehicleNo: vehicle.vehicleNo,
    vehicleType: vehicle.vehicleType,
    latitude: routeState.latitude,
    longitude: routeState.longitude,
    accuracy: 8,
    speed,
    heading: routeState.heading,
    cargoName: vehicle.cargoName,
    cargoQuantity: vehicle.cargoQuantity,
    cargoWeight: vehicle.cargoWeight,
    origin: routeState.origin,
    destination: routeState.destination,
    destinationLatitude: routeState.destinationPoint.latitude,
    destinationLongitude: routeState.destinationPoint.longitude,
    routePath: routeState.remainingPath,
    routeSource: routeState.routeSource,
    remainingDistanceKm,
    etaMinutes,
    estimatedArrivalAt: new Date(nowMs + etaMinutes * 60 * 1000).toISOString(),
    provider: "simulator",
    simulated: true,
    recordedAt: new Date(nowMs).toISOString()
  };
}

function calculateSimulatorRouteState(vehicle, nowMs) {
  const segments = buildSimulatorSegments(vehicle);
  const totalDistanceKm = segments.reduce((total, segment) => total + segment.distanceKm, 0);

  if (!segments.length || totalDistanceKm <= 0) {
    const fallbackFrom = vehicle.route[0];
    const fallbackTo = vehicle.route[1] || fallbackFrom;
    return {
      latitude: fallbackFrom.latitude,
      longitude: fallbackFrom.longitude,
      heading: Math.round(calculateBearing(fallbackFrom, fallbackTo)),
      origin: fallbackFrom.name,
      destination: fallbackTo.name,
      destinationPoint: fallbackTo,
      remainingPath: [normalizeRoutePoint(fallbackFrom), normalizeRoutePoint(fallbackTo)].filter(Boolean),
      remainingDistanceKm: calculateDistanceKm(fallbackFrom, fallbackTo),
      routeSource: "fallback"
    };
  }

  const kmPerMs = Math.max(vehicle.speedBase, 1) * SIMULATOR_SPEED_SCALE / 3600000;
  const offsetDistanceKm = (hashOffset(vehicle.driverId) % 10000) / 10000 * totalDistanceKm;
  let traveledDistanceKm = (nowMs * kmPerMs + offsetDistanceKm) % totalDistanceKm;

  for (const segment of segments) {
    if (traveledDistanceKm > segment.distanceKm) {
      traveledDistanceKm -= segment.distanceKm;
      continue;
    }

    const position = interpolatePathAtDistance(segment.path, traveledDistanceKm);
    const remainingPath = slicePathFromDistance(segment.path, traveledDistanceKm);
    const remainingDistanceKm = Math.max(0, segment.distanceKm - traveledDistanceKm);

    return {
      latitude: position.point.latitude,
      longitude: position.point.longitude,
      heading: Math.round(position.heading),
      origin: segment.from.name,
      destination: segment.to.name,
      destinationPoint: segment.to,
      remainingPath,
      remainingDistanceKm,
      routeSource: segment.source
    };
  }

  const lastSegment = segments[segments.length - 1];
  const lastPoint = lastSegment.path[lastSegment.path.length - 1];
  const previousPoint = lastSegment.path[lastSegment.path.length - 2] || lastPoint;
  return {
    latitude: lastPoint.latitude,
    longitude: lastPoint.longitude,
    heading: Math.round(calculateBearing(previousPoint, lastPoint)),
    origin: lastSegment.from.name,
    destination: lastSegment.to.name,
    destinationPoint: lastSegment.to,
    remainingPath: [lastPoint],
    remainingDistanceKm: 0,
    routeSource: lastSegment.source
  };
}

function buildSimulatorSegments(vehicle) {
  const segments = [];

  for (let index = 0; index < vehicle.route.length; index += 1) {
    const from = vehicle.route[index];
    const to = vehicle.route[(index + 1) % vehicle.route.length];
    const cacheKey = getSimulatorSegmentKey(vehicle, index, from, to);
    const cached = simulatorRouteCache[cacheKey];
    const cachedPath = Array.isArray(cached?.path) ? normalizeRoutePath(cached.path) : [];
    const fallbackPath = createSimulatedRoutePath(from, to, `${vehicle.driverId}-${index}`);
    const path = cachedPath.length >= 2 ? cachedPath : fallbackPath;
    const distanceKm = Math.max(0.01, calculatePathDistanceKm(path));

    segments.push({
      from,
      to,
      path,
      distanceKm,
      source: cachedPath.length >= 2 ? cached.source || "cached-route" : "fallback-route"
    });
  }

  return segments;
}

async function primeSimulatorRouteCache() {
  let changed = false;
  let kakaoFailureMessage = "";
  let osrmFailureMessage = "";
  let kakaoAccessBlocked = !KAKAO_REST_API_KEY;

  for (const vehicle of SIMULATED_FLEET) {
    for (let index = 0; index < vehicle.route.length; index += 1) {
      const from = vehicle.route[index];
      const to = vehicle.route[(index + 1) % vehicle.route.length];
      const cacheKey = getSimulatorSegmentKey(vehicle, index, from, to);
      const cachedPath = Array.isArray(simulatorRouteCache[cacheKey]?.path)
        ? normalizeRoutePath(simulatorRouteCache[cacheKey].path)
        : [];

      const cachedSource = simulatorRouteCache[cacheKey]?.source;
      if (cachedPath.length >= 2 && (cachedSource === "kakao-directions" || (kakaoAccessBlocked && cachedSource === "osrm-route"))) {
        continue;
      }

      let route = null;

      if (!kakaoAccessBlocked) {
        try {
          route = await fetchKakaoDirections(from, to);
        } catch (error) {
          kakaoFailureMessage ||= error.message;
          if (isKakaoDirectionsAccessError(error)) {
            kakaoAccessBlocked = true;
          }
        }
      }

      try {
        route ||= await fetchOsrmDirections(from, to);
        if (route.path.length >= 2) {
          simulatorRouteCache[cacheKey] = {
            source: route.source,
            fetchedAt: new Date().toISOString(),
            distanceMeters: route.distanceMeters,
            durationSeconds: route.durationSeconds,
            path: route.path
          };
          changed = true;
        }
      } catch (error) {
        osrmFailureMessage ||= error.message;
      }
    }
  }

  if (changed) {
    writeJsonAtomic(SIMULATOR_ROUTES_FILE, simulatorRouteCache);
    console.log("CargoRadar simulator road routes cached.");
  }

  if (kakaoFailureMessage) {
    console.warn(`CargoRadar simulator could not use Kakao Directions for every segment: ${kakaoFailureMessage}`);
  }

  if (osrmFailureMessage) {
    console.warn(`CargoRadar simulator using internal fallback routes for uncached segments: ${osrmFailureMessage}`);
  }
}

function isKakaoDirectionsAccessError(error) {
  return /http_401|kakao_directions_-401|ip mismatched/i.test(error.message || "");
}

function getSimulatorSegmentKey(vehicle, index, from, to) {
  return [
    vehicle.driverId,
    index,
    roundNumber(from.latitude, 5),
    roundNumber(from.longitude, 5),
    roundNumber(to.latitude, 5),
    roundNumber(to.longitude, 5)
  ].join(":");
}

async function fetchKakaoDirections(from, to) {
  const requestUrl = new URL(KAKAO_DIRECTIONS_URL);
  requestUrl.searchParams.set("origin", `${from.longitude},${from.latitude}`);
  requestUrl.searchParams.set("destination", `${to.longitude},${to.latitude}`);
  requestUrl.searchParams.set("priority", "RECOMMEND");
  requestUrl.searchParams.set("summary", "false");
  requestUrl.searchParams.set("car_fuel", "DIESEL");
  requestUrl.searchParams.set("car_hipass", "true");

  const payload = await requestJson(requestUrl, {
    Authorization: `KakaoAK ${KAKAO_REST_API_KEY}`,
    "Content-Type": "application/json"
  });

  if (payload.code) {
    throw new Error(`kakao_directions_${payload.code}: ${payload.msg || "request_failed"}`);
  }

  return parseKakaoDirections(payload);
}

function parseKakaoDirections(payload) {
  const route = Array.isArray(payload.routes) ? payload.routes[0] : null;
  if (!route || route.result_code !== 0) {
    throw new Error(`kakao_directions_result_${route?.result_code ?? "missing"}`);
  }

  const path = [];
  for (const section of route.sections || []) {
    for (const road of section.roads || []) {
      const vertexes = Array.isArray(road.vertexes) ? road.vertexes : [];
      for (let index = 0; index < vertexes.length - 1; index += 2) {
        pushUniqueRoutePoint(path, {
          latitude: Number(vertexes[index + 1]),
          longitude: Number(vertexes[index])
        });
      }
    }
  }

  return {
    source: "kakao-directions",
    distanceMeters: Number(route.summary?.distance) || Math.round(calculatePathDistanceKm(path) * 1000),
    durationSeconds: Number(route.summary?.duration) || 0,
    path: simplifyRoutePath(path, 240)
  };
}

async function fetchOsrmDirections(from, to) {
  const baseUrl = OSRM_ROUTE_URL.replace(/\/$/, "");
  const requestUrl = new URL(`${baseUrl}/${from.longitude},${from.latitude};${to.longitude},${to.latitude}`);
  requestUrl.searchParams.set("overview", "full");
  requestUrl.searchParams.set("geometries", "geojson");
  requestUrl.searchParams.set("steps", "false");

  const payload = await requestJson(requestUrl, {});
  return parseOsrmDirections(payload);
}

function parseOsrmDirections(payload) {
  if (payload.code !== "Ok") {
    throw new Error(`osrm_route_${payload.code || "request_failed"}: ${payload.message || "request_failed"}`);
  }

  const route = Array.isArray(payload.routes) ? payload.routes[0] : null;
  const coordinates = Array.isArray(route?.geometry?.coordinates) ? route.geometry.coordinates : [];
  const path = coordinates.map((coordinate) => ({
    latitude: Number(coordinate[1]),
    longitude: Number(coordinate[0])
  }));

  if (path.length < 2) {
    throw new Error("osrm_route_missing_geometry");
  }

  return {
    source: "osrm-route",
    distanceMeters: Number(route.distance) || Math.round(calculatePathDistanceKm(path) * 1000),
    durationSeconds: Number(route.duration) || 0,
    path: simplifyRoutePath(path, 240)
  };
}

function requestJson(requestUrl, headers) {
  return new Promise((resolve, reject) => {
    const req = https.get(requestUrl, { headers, timeout: 8000 }, (res) => {
      const chunks = [];
      res.on("data", (chunk) => chunks.push(chunk));
      res.on("end", () => {
        const body = Buffer.concat(chunks).toString("utf8");
        try {
          const payload = JSON.parse(body || "{}");
          if (res.statusCode < 200 || res.statusCode >= 300) {
            reject(new Error(`http_${res.statusCode}: ${payload.msg || payload.error || body.slice(0, 80)}`));
            return;
          }
          resolve(payload);
        } catch (error) {
          reject(error);
        }
      });
    });

    req.on("timeout", () => {
      req.destroy(new Error("request_timeout"));
    });
    req.on("error", reject);
  });
}

function pushUniqueRoutePoint(path, value) {
  const pointValue = normalizeRoutePoint(value);
  if (!pointValue) return;

  const previous = path[path.length - 1];
  if (previous && calculateDistanceKm(previous, pointValue) < 0.005) return;
  path.push(pointValue);
}

function simplifyRoutePath(path, limit) {
  const normalized = normalizeRoutePath(path);
  if (normalized.length <= limit) return normalized;

  const result = [];
  for (let index = 0; index < limit; index += 1) {
    const sourceIndex = Math.round(index * (normalized.length - 1) / (limit - 1));
    result.push(normalized[sourceIndex]);
  }
  return result;
}

function interpolatePathAtDistance(path, distanceKm) {
  if (path.length < 2) {
    const pointValue = path[0] || { latitude: 0, longitude: 0 };
    return { point: pointValue, heading: 0, index: 0 };
  }

  let traveledKm = 0;
  for (let index = 1; index < path.length; index += 1) {
    const from = path[index - 1];
    const to = path[index];
    const segmentDistanceKm = calculateDistanceKm(from, to);

    if (traveledKm + segmentDistanceKm >= distanceKm) {
      const progress = segmentDistanceKm <= 0 ? 0 : (distanceKm - traveledKm) / segmentDistanceKm;
      return {
        point: {
          latitude: roundNumber(interpolateNumber(from.latitude, to.latitude, progress), 6),
          longitude: roundNumber(interpolateNumber(from.longitude, to.longitude, progress), 6)
        },
        heading: calculateBearing(from, to),
        index
      };
    }

    traveledKm += segmentDistanceKm;
  }

  const last = path[path.length - 1];
  const previous = path[path.length - 2] || last;
  return {
    point: last,
    heading: calculateBearing(previous, last),
    index: path.length - 1
  };
}

function slicePathFromDistance(path, distanceKm) {
  const position = interpolatePathAtDistance(path, distanceKm);
  const rest = path.slice(position.index);
  return simplifyRoutePath([position.point, ...rest], 240);
}

function storeLocation(location, options = {}) {
  const appendHistory = options.appendHistory !== false;
  const persistLatest = options.persistLatest !== false;

  latestLocations[location.driverId] = location;

  if (appendHistory) {
    appendJsonLine(HISTORY_FILE, location);
  }

  if (persistLatest) {
    writeJsonAtomic(LATEST_FILE, latestLocations);
  }

  broadcastEvent("location.updated", location);
}

function readJsonFile(filePath, fallback) {
  try {
    return JSON.parse(fs.readFileSync(filePath, "utf8"));
  } catch {
    return fallback;
  }
}

function writeJsonAtomic(filePath, value) {
  const tempPath = `${filePath}.${process.pid}.tmp`;
  fs.writeFileSync(tempPath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
  fs.renameSync(tempPath, filePath);
}

function appendJsonLine(filePath, value) {
  fs.appendFileSync(filePath, `${JSON.stringify(value)}\n`, "utf8");
}

function sendJson(res, statusCode, payload) {
  const body = JSON.stringify(payload, null, 2);
  res.writeHead(statusCode, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(body)
  });
  res.end(body);
}

function sendJavaScript(res, statusCode, body) {
  res.writeHead(statusCode, {
    "Content-Type": "application/javascript; charset=utf-8",
    "Content-Length": Buffer.byteLength(body)
  });
  res.end(body);
}

function addCorsHeaders(res) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Admin-Token,X-Device-Token");
}

function authorizeAdmin(req, res) {
  if (!ADMIN_AUTH_REQUIRED || !ADMIN_TOKEN) return true;

  const token = extractToken(req, "x-admin-token");
  const requestUrl = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
  const queryToken = requestUrl.searchParams.get("adminToken") || "";

  if (token === ADMIN_TOKEN || queryToken === ADMIN_TOKEN) return true;

  sendJson(res, 401, { error: "invalid_admin_token" });
  return false;
}

function authorizeDevice(req, requestedDriverId) {
  const token = extractToken(req, "x-device-token");
  if (!token) return null;

  return drivers.find((driver) => {
    if (!driver.enabled) return false;
    if (driver.token !== token) return false;
    if (requestedDriverId && driver.driverId !== requestedDriverId) return false;
    return true;
  }) || null;
}

function extractToken(req, headerName) {
  const directToken = req.headers[headerName];
  if (typeof directToken === "string" && directToken.trim()) {
    return directToken.trim();
  }

  const authorization = req.headers.authorization || "";
  const match = authorization.match(/^Bearer\s+(.+)$/i);
  return match ? match[1].trim() : "";
}

async function readJsonBody(req) {
  const chunks = [];
  let totalBytes = 0;

  for await (const chunk of req) {
    totalBytes += chunk.length;
    if (totalBytes > 1024 * 1024) {
      const error = new Error("Request body too large.");
      error.statusCode = 413;
      error.publicMessage = "body_too_large";
      throw error;
    }
    chunks.push(chunk);
  }

  try {
    return JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}");
  } catch {
    const error = new Error("Invalid JSON request body.");
    error.statusCode = 400;
    error.publicMessage = "invalid_json";
    throw error;
  }
}

function createLocationRecord(body, driver) {
  const latitude = Number(body.latitude);
  const longitude = Number(body.longitude);

  if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) {
    validationError("invalid_latitude");
  }

  if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) {
    validationError("invalid_longitude");
  }

  return {
    id: randomUUID(),
    driverId: driver.driverId,
    driverName: driver.name || driver.driverId,
    vehicleNo: String(body.vehicleNo || driver.vehicleNo || ""),
    vehicleType: normalizeVehicleType(body.vehicleType || driver.vehicleType),
    latitude,
    longitude,
    accuracy: optionalNumber(body.accuracy),
    speed: optionalNumber(body.speed),
    heading: optionalNumber(body.heading),
    batteryLevel: optionalNumber(body.batteryLevel),
    cargoName: optionalString(body.cargoName || body.cargoItem || body.cargo || body.itemName, 80),
    cargoQuantity: optionalString(body.cargoQuantity || body.quantity || body.qty || body.count, 40),
    cargoWeight: optionalString(body.cargoWeight || body.weight, 40),
    origin: optionalString(body.origin || body.departure, 80),
    destination: optionalString(body.destination || body.arrival, 80),
    destinationLatitude: optionalNumber(body.destinationLatitude || body.destinationLat || body.arrivalLatitude || body.arrivalLat),
    destinationLongitude: optionalNumber(body.destinationLongitude || body.destinationLng || body.destinationLon || body.arrivalLongitude || body.arrivalLng || body.arrivalLon),
    routePath: normalizeRoutePath(body.routePath || body.path || body.polyline),
    routeSource: optionalString(body.routeSource || body.routingProvider, 40),
    remainingDistanceKm: optionalNumber(body.remainingDistanceKm || body.distanceKm || body.routeDistanceKm),
    etaMinutes: optionalNumber(body.etaMinutes || body.estimatedMinutes || body.remainingMinutes),
    estimatedArrivalAt: normalizeOptionalDate(body.estimatedArrivalAt || body.etaAt || body.arrivalAt),
    provider: body.provider ? String(body.provider) : "",
    simulated: Boolean(body.simulated || driver.simulated),
    recordedAt: normalizeDate(body.recordedAt),
    receivedAt: new Date().toISOString()
  };
}

function normalizeVehicleType(value) {
  const vehicleType = String(value || "cargo-truck").trim().toLowerCase();
  return VEHICLE_TYPES.has(vehicleType) ? vehicleType : "cargo-truck";
}

function point(name, latitude, longitude) {
  return { name, latitude, longitude };
}

function createSimulatedRoutePath(from, to, seed) {
  const deltaLatitude = to.latitude - from.latitude;
  const deltaLongitude = to.longitude - from.longitude;
  const length = Math.sqrt(deltaLatitude ** 2 + deltaLongitude ** 2) || 1;
  const offsetDirection = hashOffset(seed) % 2 === 0 ? 1 : -1;
  const offsetSize = Math.min(0.32, Math.max(0.025, length * 0.14));
  const perpendicularLatitude = (-deltaLongitude / length) * offsetSize * offsetDirection;
  const perpendicularLongitude = (deltaLatitude / length) * offsetSize * offsetDirection;

  return [
    normalizeRoutePoint(from),
    normalizeRoutePoint({
      latitude: interpolateNumber(from.latitude, to.latitude, 0.35) + perpendicularLatitude,
      longitude: interpolateNumber(from.longitude, to.longitude, 0.35) + perpendicularLongitude
    }),
    normalizeRoutePoint({
      latitude: interpolateNumber(from.latitude, to.latitude, 0.72) - perpendicularLatitude * 0.35,
      longitude: interpolateNumber(from.longitude, to.longitude, 0.72) - perpendicularLongitude * 0.35
    }),
    normalizeRoutePoint(to)
  ];
}

function normalizeRoutePath(value) {
  if (!Array.isArray(value)) return [];

  return value
    .map((pointValue) => normalizeRoutePoint(pointValue))
    .filter(Boolean)
    .slice(0, 240);
}

function normalizeRoutePoint(value) {
  if (!value) return null;

  const latitude = Array.isArray(value)
    ? Number(value[0])
    : Number(value.latitude ?? value.lat);
  const longitude = Array.isArray(value)
    ? Number(value[1])
    : Number(value.longitude ?? value.lng ?? value.lon);

  if (!Number.isFinite(latitude) || latitude < -90 || latitude > 90) return null;
  if (!Number.isFinite(longitude) || longitude < -180 || longitude > 180) return null;

  return {
    latitude: roundNumber(latitude, 6),
    longitude: roundNumber(longitude, 6)
  };
}

function calculatePathDistanceKm(routePath) {
  let distance = 0;
  for (let index = 1; index < routePath.length; index += 1) {
    distance += calculateDistanceKm(routePath[index - 1], routePath[index]);
  }
  return distance;
}

function calculateDistanceKm(from, to) {
  const earthRadiusKm = 6371;
  const deltaLatitude = toRadians(to.latitude - from.latitude);
  const deltaLongitude = toRadians(to.longitude - from.longitude);
  const fromLatitude = toRadians(from.latitude);
  const toLatitude = toRadians(to.latitude);
  const a = Math.sin(deltaLatitude / 2) ** 2 +
    Math.cos(fromLatitude) * Math.cos(toLatitude) * Math.sin(deltaLongitude / 2) ** 2;
  return earthRadiusKm * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

function roundNumber(value, decimals) {
  const multiplier = 10 ** decimals;
  return Math.round(value * multiplier) / multiplier;
}

function smoothStep(value) {
  return value * value * (3 - 2 * value);
}

function interpolateNumber(from, to, progress) {
  return from + (to - from) * progress;
}

function calculateBearing(from, to) {
  const fromLatitude = toRadians(from.latitude);
  const toLatitude = toRadians(to.latitude);
  const deltaLongitude = toRadians(to.longitude - from.longitude);
  const y = Math.sin(deltaLongitude) * Math.cos(toLatitude);
  const x = Math.cos(fromLatitude) * Math.sin(toLatitude) -
    Math.sin(fromLatitude) * Math.cos(toLatitude) * Math.cos(deltaLongitude);
  return (Math.atan2(y, x) * 180 / Math.PI + 360) % 360;
}

function toRadians(value) {
  return value * Math.PI / 180;
}

function hashOffset(value) {
  let hash = 0;
  for (const character of String(value)) {
    hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
  }
  return hash;
}

function parseBoolean(value, defaultValue) {
  if (value === undefined || value === null || value === "") return defaultValue;
  return !["0", "false", "no", "off"].includes(String(value).trim().toLowerCase());
}

function validationError(publicMessage) {
  const error = new Error(publicMessage);
  error.statusCode = 400;
  error.publicMessage = publicMessage;
  throw error;
}

function notFoundError(publicMessage) {
  const error = new Error(publicMessage);
  error.statusCode = 404;
  error.publicMessage = publicMessage;
  throw error;
}

function optionalNumber(value) {
  if (value === undefined || value === null || value === "") return null;
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function optionalString(value, maxLength) {
  if (value === undefined || value === null) return "";
  return String(value).trim().slice(0, maxLength);
}

function normalizeDate(value) {
  if (!value) return new Date().toISOString();
  const parsed = new Date(value);
  return Number.isNaN(parsed.getTime()) ? new Date().toISOString() : parsed.toISOString();
}

function normalizeOptionalDate(value) {
  if (!value) return null;
  const parsed = new Date(value);
  return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
}

function readHistory({ driverId, limit }) {
  if (!fs.existsSync(HISTORY_FILE)) return [];

  const lines = fs.readFileSync(HISTORY_FILE, "utf8").trim().split("\n").filter(Boolean);
  const records = [];

  for (let index = lines.length - 1; index >= 0 && records.length < limit; index -= 1) {
    try {
      const record = JSON.parse(lines[index]);
      if (!driverId || record.driverId === driverId) {
        records.push(record);
      }
    } catch {
      continue;
    }
  }

  return records.reverse();
}

function openEventStream(req, res) {
  res.writeHead(200, {
    "Content-Type": "text/event-stream; charset=utf-8",
    "Cache-Control": "no-cache",
    "Connection": "keep-alive"
  });

  res.write(`event: snapshot\n`);
  res.write(`data: ${JSON.stringify({ locations: Object.values(latestLocations) })}\n\n`);
  sseClients.add(res);

  const heartbeat = setInterval(() => {
    res.write(`event: heartbeat\n`);
    res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
  }, 15000);

  req.on("close", () => {
    clearInterval(heartbeat);
    sseClients.delete(res);
  });
}

function broadcastEvent(type, data) {
  const payload = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
  for (const client of sseClients) {
    client.write(payload);
  }
}

function compareLatestLocations(left, right) {
  return new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime();
}

function compareDrivers(left, right) {
  return String(left.vehicleNo || left.driverId).localeCompare(String(right.vehicleNo || right.driverId), "ko");
}

function toDriverResponse(driver) {
  return {
    driverId: driver.driverId,
    name: driver.name || driver.driverId,
    vehicleNo: driver.vehicleNo || "",
    phone: driver.phone || "",
    vehicleType: normalizeVehicleType(driver.vehicleType),
    token: driver.token || "",
    enabled: driver.enabled !== false,
    simulated: Boolean(driver.simulated),
    createdAt: driver.createdAt || null,
    updatedAt: driver.updatedAt || null
  };
}

function normalizeDriverId(value) {
  const rawValue = String(value || "").trim();
  if (!rawValue) return "";

  const normalized = rawValue
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 64);

  if (!normalized) validationError("invalid_driver_id");
  return normalized;
}

function generateDriverId() {
  return `driver-${Date.now().toString(36)}`;
}

function generateDeviceToken() {
  return randomUUID().replace(/-/g, "");
}

function clampNumber(value, min, max) {
  if (!Number.isFinite(value)) return min;
  return Math.min(max, Math.max(min, value));
}

function serveStaticFile(req, res, pathname) {
  const normalizedPath = pathname === "/" ? "/index.html" : pathname;
  const decodedPath = decodeURIComponent(normalizedPath);
  const filePath = path.resolve(WEB_ROOT, `.${decodedPath}`);
  const relativePath = path.relative(WEB_ROOT, filePath);

  if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
    sendJson(res, 403, { error: "forbidden" });
    return true;
  }

  if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
    return false;
  }

  const contentType = getContentType(filePath);
  res.writeHead(200, { "Content-Type": contentType });

  if (req.method === "HEAD") {
    res.end();
    return true;
  }

  fs.createReadStream(filePath).pipe(res);
  return true;
}

function getContentType(filePath) {
  const extension = path.extname(filePath).toLowerCase();
  const types = {
    ".html": "text/html; charset=utf-8",
    ".css": "text/css; charset=utf-8",
    ".js": "application/javascript; charset=utf-8",
    ".json": "application/json; charset=utf-8",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".svg": "image/svg+xml"
  };

  return types[extension] || "application/octet-stream";
}
