const state = {
  map: null,
  mapProvider: "",
  markers: new Map(),
  infoOverlay: null,
  routeOverlay: null,
  routeDestinationOverlay: null,
  markerAnimationMs: 4800,
  hasFittedInitialLocations: false,
  isProgrammaticMapChange: false,
  programmaticMapChangeTimer: 0,
  pendingLocationUpdates: new Map(),
  locationUpdateBatchTimer: 0,
  drivers: new Map(),
  locations: new Map(),
  selectedDriverId: "",
  activeDetailMode: "",
  vehicleSearch: "",
  vehicleStatusFilter: "all",
  eventSource: null,
  launchTimer: 0,
  activeView: "locations",
  adminToken: new URLSearchParams(window.location.search).get("adminToken") || ""
};

const LEGACY_LOGIN_STORAGE_KEYS = [
  "cargoRadarLoginReady",
  "cargoRadarLoginName",
  "cargoRadarAdminToken"
];
const LAUNCH_STEPS = [
  {
    title: "관제 데이터 연결 중",
    message: "운행 차량의 최신 위치를 불러오고 있습니다."
  },
  {
    title: "차량 위치 불러오는 중",
    message: "수신 중인 차량과 화물 정보를 정리합니다."
  },
  {
    title: "경로 데이터 확인 중",
    message: "목적지 경로와 도착 시간을 확인합니다."
  },
  {
    title: "관제 화면 준비 완료",
    message: "실시간 관제 화면으로 이동합니다."
  }
];

const elements = {
  loginScreen: document.getElementById("loginScreen"),
  loginForm: document.getElementById("loginForm"),
  loginNameInput: document.getElementById("loginNameInput"),
  loginCodeInput: document.getElementById("loginCodeInput"),
  launchScreen: document.getElementById("launchScreen"),
  launchTitle: document.getElementById("launchTitle"),
  launchMessage: document.getElementById("launchMessage"),
  launchProgress: document.getElementById("launchProgress"),
  launchSteps: [...document.querySelectorAll("[data-launch-step]")],
  connectionStatus: document.getElementById("connectionStatus"),
  systemClock: document.getElementById("systemClock"),
  refreshButton: document.getElementById("refreshButton"),
  demoButton: document.getElementById("demoButton"),
  logoutButton: document.getElementById("logoutButton"),
  locationsTab: document.getElementById("locationsTab"),
  driversTab: document.getElementById("driversTab"),
  locationsPanel: document.getElementById("locationsPanel"),
  driversPanel: document.getElementById("driversPanel"),
  mapPanel: document.querySelector(".map-panel"),
  vehicleList: document.getElementById("vehicleList"),
  mapMessage: document.getElementById("mapMessage"),
  totalCount: document.getElementById("totalCount"),
  activeCount: document.getElementById("activeCount"),
  staleCount: document.getElementById("staleCount"),
  lastUpdated: document.getElementById("lastUpdated"),
  opsHeadline: document.getElementById("opsHeadline"),
  opsSubline: document.getElementById("opsSubline"),
  averageEta: document.getElementById("averageEta"),
  cargoTypeCount: document.getElementById("cargoTypeCount"),
  routePreviewCount: document.getElementById("routePreviewCount"),
  vehicleSearchInput: document.getElementById("vehicleSearchInput"),
  statusFilterButtons: [...document.querySelectorAll("[data-status-filter]")],
  mapActiveCount: document.getElementById("mapActiveCount"),
  mapEtaSummary: document.getElementById("mapEtaSummary"),
  mapUpdatedAt: document.getElementById("mapUpdatedAt"),
  routeFocusPanel: document.getElementById("routeFocusPanel"),
  routeFocusTitle: document.getElementById("routeFocusTitle"),
  routeFocusMeta: document.getElementById("routeFocusMeta"),
  markerActionPanel: document.getElementById("markerActionPanel"),
  markerActionEyebrow: document.getElementById("markerActionEyebrow"),
  markerActionTitle: document.getElementById("markerActionTitle"),
  markerActionMeta: document.getElementById("markerActionMeta"),
  markerActionSummary: document.getElementById("markerActionSummary"),
  markerActionCloseButton: document.getElementById("markerActionCloseButton"),
  driverForm: document.getElementById("driverForm"),
  driverNameInput: document.getElementById("driverNameInput"),
  vehicleNoInput: document.getElementById("vehicleNoInput"),
  vehicleTypeInput: document.getElementById("vehicleTypeInput"),
  phoneInput: document.getElementById("phoneInput"),
  driverIdInput: document.getElementById("driverIdInput"),
  driverCount: document.getElementById("driverCount"),
  driverList: document.getElementById("driverList"),
  historyDrawer: document.getElementById("historyDrawer"),
  historyCloseButton: document.getElementById("historyCloseButton"),
  historyTitle: document.getElementById("historyTitle"),
  historySummary: document.getElementById("historySummary"),
  historyList: document.getElementById("historyList")
};

const VEHICLE_TYPES = {
  "cargo-truck": {
    label: "카고트럭",
    shortLabel: "카고",
    className: "type-cargo"
  },
  "tractor-trailer": {
    label: "트레일러",
    shortLabel: "트레",
    className: "type-trailer"
  },
  "box-truck": {
    label: "탑차",
    shortLabel: "탑차",
    className: "type-box"
  },
  "van": {
    label: "용달차",
    shortLabel: "용달",
    className: "type-van"
  },
  "refrigerated": {
    label: "냉동탑차",
    shortLabel: "냉동",
    className: "type-cold"
  },
  "container": {
    label: "컨테이너",
    shortLabel: "컨",
    className: "type-container"
  },
  "wingbody": {
    label: "윙바디",
    shortLabel: "윙",
    className: "type-wing"
  }
};

document.addEventListener("DOMContentLoaded", () => {
  initializeLoginState();
  startSystemClock();
  initializeMap();
  bindActions();
});

function startDataServices() {
  refreshDrivers();
  refreshLatestLocations();
  connectEvents();
}

async function initializeMap() {
  const kakaoJavaScriptKey = window.CARGORADAR_CONFIG?.kakaoJavaScriptKey || "";

  if (kakaoJavaScriptKey) {
    try {
      await loadKakaoMapsSdk(kakaoJavaScriptKey);
      const center = new kakao.maps.LatLng(36.45, 127.85);
      state.map = new kakao.maps.Map(document.getElementById("map"), {
        center,
        level: 13
      });
      state.mapProvider = "kakao";

      state.map.addControl(new kakao.maps.ZoomControl(), kakao.maps.ControlPosition.RIGHT);
      state.map.addControl(new kakao.maps.MapTypeControl(), kakao.maps.ControlPosition.TOPRIGHT);
      bindMapInteractionTracking();
      hideMapMessage();
      updateInitialMapFit();
      return;
    } catch (error) {
      console.warn("Kakao map unavailable. Falling back to OpenStreetMap.", error);
    }
  }

  try {
    await initializeLeafletMap();
  } catch (error) {
    console.error(error);
    showMapMessage("지도 로딩에 실패했습니다. 네트워크 상태를 확인하세요.");
  }
}

function loadKakaoMapsSdk(appKey) {
  return new Promise((resolve, reject) => {
    if (window.kakao?.maps) {
      kakao.maps.load(resolve);
      return;
    }

    const script = document.createElement("script");
    script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${encodeURIComponent(appKey)}&autoload=false`;
    script.async = true;
    script.onload = () => {
      if (!window.kakao?.maps) {
        reject(new Error("kakao_maps_not_authorized"));
        return;
      }
      kakao.maps.load(resolve);
    };
    script.onerror = () => reject(new Error("kakao_maps_load_failed"));
    document.head.appendChild(script);
  });
}

async function initializeLeafletMap() {
  await loadLeafletAssets();
  state.mapProvider = "leaflet";
  state.map = L.map("map", {
    attributionControl: true,
    zoomControl: true
  }).setView([36.45, 127.85], 7);

  L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
    maxZoom: 19,
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  }).addTo(state.map);

  bindMapInteractionTracking();
  showMapMessage("카카오맵 권한 전까지 OpenStreetMap으로 표시 중");
  window.setTimeout(hideMapMessage, 4500);
  updateInitialMapFit();
}

function loadLeafletAssets() {
  if (window.L?.map) return Promise.resolve();

  const version = "1.9.4";
  const stylesheet = loadStylesheet(
    "leaflet-stylesheet",
    `https://unpkg.com/leaflet@${version}/dist/leaflet.css`
  );
  const script = loadScript(
    "leaflet-script",
    `https://unpkg.com/leaflet@${version}/dist/leaflet.js`
  );

  return Promise.all([stylesheet, script]).then(() => {
    if (!window.L?.map) throw new Error("leaflet_load_failed");
  });
}

function loadStylesheet(id, href) {
  return new Promise((resolve, reject) => {
    if (document.getElementById(id)) {
      resolve();
      return;
    }

    const link = document.createElement("link");
    link.id = id;
    link.rel = "stylesheet";
    link.href = href;
    link.onload = resolve;
    link.onerror = () => reject(new Error(`${id}_load_failed`));
    document.head.appendChild(link);
  });
}

function loadScript(id, src) {
  return new Promise((resolve, reject) => {
    if (document.getElementById(id)) {
      resolve();
      return;
    }

    const script = document.createElement("script");
    script.id = id;
    script.src = src;
    script.async = true;
    script.onload = resolve;
    script.onerror = () => reject(new Error(`${id}_load_failed`));
    document.head.appendChild(script);
  });
}

function bindActions() {
  elements.loginForm.addEventListener("submit", submitLoginForm);
  elements.refreshButton.addEventListener("click", refreshLatestLocations);
  elements.demoButton.addEventListener("click", postDemoLocation);
  elements.logoutButton.addEventListener("click", showLoginScreen);
  elements.locationsTab.addEventListener("click", () => setActiveView("locations"));
  elements.driversTab.addEventListener("click", () => setActiveView("drivers"));
  elements.driverForm.addEventListener("submit", submitDriverForm);
  elements.historyCloseButton.addEventListener("click", closeHistoryDrawer);
  elements.markerActionCloseButton.addEventListener("click", closeMarkerActionPanel);
  elements.markerActionPanel.addEventListener("click", (event) => {
    const actionButton = event.target.closest("[data-marker-action]");
    if (!actionButton) return;

    const location = state.locations.get(state.selectedDriverId);
    if (!location) {
      closeMarkerActionPanel();
      return;
    }

    runMarkerAction(actionButton.dataset.markerAction, location);
  });
  elements.vehicleSearchInput.addEventListener("input", () => {
    state.vehicleSearch = elements.vehicleSearchInput.value.trim();
    render();
  });
  for (const button of elements.statusFilterButtons) {
    button.addEventListener("click", () => {
      state.vehicleStatusFilter = button.dataset.statusFilter || "all";
      render();
    });
  }
}

function initializeLoginState() {
  clearStoredLoginState();
  elements.loginNameInput.value = "";
  showLoginScreen({ instant: true });
}

function submitLoginForm(event) {
  event.preventDefault();

  const code = elements.loginCodeInput.value.trim();
  state.adminToken = code || state.adminToken || "demo";

  elements.loginCodeInput.value = "";
  beginLaunchSequence();
  startDataServices();
}

function showLoginScreen(options = {}) {
  clearLaunchSequence();
  clearStoredLoginState();
  if (state.eventSource) {
    state.eventSource.close();
    state.eventSource = null;
  }
  document.body.classList.remove("is-login-closing");
  document.body.classList.add("is-login-active");
  if (options.instant) {
    document.body.classList.add("is-login-ready");
  }
  window.setTimeout(() => elements.loginNameInput.focus(), options.instant ? 80 : 220);
}

function clearStoredLoginState() {
  for (const key of LEGACY_LOGIN_STORAGE_KEYS) {
    window.localStorage.removeItem(key);
  }
}

function hideLoginScreen(options = {}) {
  if (options.instant) {
    document.body.classList.remove("is-login-active", "is-login-closing");
    document.body.classList.add("is-login-ready");
    return;
  }

  document.body.classList.add("is-login-closing");
  window.setTimeout(() => {
    document.body.classList.remove("is-login-active", "is-login-closing");
    document.body.classList.add("is-login-ready");
  }, 620);
}

function beginLaunchSequence() {
  clearLaunchSequence();
  document.body.classList.add("is-launch-active");
  setLaunchStep(0);
  hideLoginScreen();

  let stepIndex = 0;
  const advance = () => {
    stepIndex += 1;
    if (stepIndex >= LAUNCH_STEPS.length) {
      finishLaunchSequence();
      return;
    }

    setLaunchStep(stepIndex);
    state.launchTimer = window.setTimeout(advance, stepIndex === LAUNCH_STEPS.length - 1 ? 460 : 620);
  };

  state.launchTimer = window.setTimeout(advance, 620);
}

function setLaunchStep(index) {
  const step = LAUNCH_STEPS[index] || LAUNCH_STEPS[0];
  elements.launchTitle.textContent = step.title;
  elements.launchMessage.textContent = step.message;
  elements.launchProgress.style.width = `${Math.min(100, ((index + 1) / LAUNCH_STEPS.length) * 100)}%`;

  for (const item of elements.launchSteps) {
    const itemIndex = Number(item.dataset.launchStep);
    item.classList.toggle("is-active", itemIndex === index);
    item.classList.toggle("is-complete", itemIndex < index);
  }
}

function finishLaunchSequence() {
  document.body.classList.add("is-launch-closing");
  state.launchTimer = window.setTimeout(() => {
    document.body.classList.remove("is-launch-active", "is-launch-closing");
    state.launchTimer = 0;
  }, 360);
}

function clearLaunchSequence() {
  if (state.launchTimer) {
    window.clearTimeout(state.launchTimer);
    state.launchTimer = 0;
  }
  document.body.classList.remove("is-launch-active", "is-launch-closing");
}

function startSystemClock() {
  const updateClock = () => {
    const now = new Date();
    elements.systemClock.dateTime = now.toISOString();
    elements.systemClock.textContent = new Intl.DateTimeFormat("ko-KR", {
      hour: "2-digit",
      minute: "2-digit"
    }).format(now);
  };

  updateClock();
  window.setInterval(updateClock, 30000);
}

function setActiveView(view) {
  state.activeView = view;
  elements.locationsTab.classList.toggle("is-active", view === "locations");
  elements.driversTab.classList.toggle("is-active", view === "drivers");
  elements.locationsPanel.classList.toggle("is-hidden", view !== "locations");
  elements.driversPanel.classList.toggle("is-hidden", view !== "drivers");
}

async function refreshLatestLocations() {
  try {
    setConnectionStatus("연결됨", "online");
    const data = await fetchJson("/api/v1/locations/latest", {
      headers: adminHeaders()
    });
    replaceLocations(data.locations || []);
  } catch (error) {
    console.error(error);
    if (error.message === "invalid_admin_token") {
      requestAdminToken();
      return;
    }
    setConnectionStatus("조회 실패", "error");
  }
}

async function refreshDrivers() {
  try {
    const data = await fetchJson("/api/v1/drivers", {
      headers: adminHeaders()
    });
    replaceDrivers(data.drivers || []);
  } catch (error) {
    console.error(error);
    if (error.message === "invalid_admin_token") {
      requestAdminToken();
    }
  }
}

function connectEvents() {
  if (state.eventSource) {
    state.eventSource.close();
  }

  state.eventSource = new EventSource(withAdminToken("/api/v1/events"));
  state.eventSource.addEventListener("open", () => setConnectionStatus("실시간 연결", "online"));
  state.eventSource.addEventListener("snapshot", (event) => {
    const payload = JSON.parse(event.data);
    replaceLocations(payload.locations || []);
  });
  state.eventSource.addEventListener("location.updated", (event) => {
    const location = JSON.parse(event.data);
    queueLocationUpdate(location);
  });
  state.eventSource.addEventListener("drivers.updated", (event) => {
    const payload = JSON.parse(event.data);
    replaceDrivers(payload.drivers || []);
  });
  state.eventSource.addEventListener("error", () => setConnectionStatus("재연결 중", "error"));
}

async function postDemoLocation() {
  const driver = getDemoDriver();
  const now = Date.now();
  const latitude = 37.5665 + Math.sin(now / 60000) * 0.03;
  const longitude = 126.978 + Math.cos(now / 60000) * 0.04;

  try {
    await fetchJson("/api/v1/locations", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Device-Token": driver.token
      },
      body: JSON.stringify({
        driverId: driver.driverId,
        vehicleNo: driver.vehicleNo,
        vehicleType: driver.vehicleType || "cargo-truck",
        latitude,
        longitude,
        accuracy: 12,
        speed: 42,
        heading: Math.round((now / 1000) % 360),
        cargoName: "전자부품",
        cargoQuantity: "36박스",
        cargoWeight: "1.2t",
        origin: "서울 상차지",
        destination: "평택 물류센터",
        provider: "demo",
        recordedAt: new Date().toISOString()
      })
    });
  } catch (error) {
    console.error(error);
    setConnectionStatus("전송 실패", "error");
  }
}

function getDemoDriver() {
  const enabledDriver = [...state.drivers.values()].find((driver) => driver.enabled && driver.token);
  return enabledDriver || {
    driverId: "demo-driver",
    vehicleNo: "SEOUL-12-3456",
    token: "demo-token"
  };
}

function replaceLocations(locations) {
  state.locations.clear();
  for (const location of locations) {
    state.locations.set(location.driverId, location);
  }
  render();
  updateMarkers();
  updateInitialMapFit();

  if (state.selectedDriverId) {
    const selectedLocation = state.locations.get(state.selectedDriverId);
    if (selectedLocation) {
      refreshSelectedDetail(selectedLocation);
    } else {
      closeMarkerActionPanel();
      clearRouteOverlay();
      clearInfoOverlay();
      renderRouteFocusPanel(null);
    }
  }
}

function replaceDrivers(drivers) {
  state.drivers.clear();
  for (const driver of drivers) {
    state.drivers.set(driver.driverId, driver);
  }
  renderDrivers();
}

function queueLocationUpdate(location) {
  state.pendingLocationUpdates.set(location.driverId, location);

  if (state.locationUpdateBatchTimer) return;

  state.locationUpdateBatchTimer = window.setTimeout(() => {
    state.locationUpdateBatchTimer = 0;
    flushLocationUpdates();
  }, 180);
}

function flushLocationUpdates() {
  if (!state.pendingLocationUpdates.size) return;

  const locations = [...state.pendingLocationUpdates.values()];
  state.pendingLocationUpdates.clear();
  applyLocationUpdates(locations);
}

function applyLocationUpdates(locations) {
  if (!locations.length) return;

  for (const location of locations) {
    state.locations.set(location.driverId, location);
  }

  render();
  updateMarkers();

  const selectedLocation = state.selectedDriverId
    ? state.locations.get(state.selectedDriverId)
    : null;

  if (selectedLocation) {
    refreshSelectedDetail(selectedLocation);
  }
}

function render() {
  const vehicleListScrollTop = elements.vehicleList.scrollTop;
  const locations = [...state.locations.values()].sort((left, right) => {
    return new Date(right.receivedAt).getTime() - new Date(left.receivedAt).getTime();
  });

  const counts = locations.reduce(
    (accumulator, location) => {
      const status = getLocationStatus(location);
      accumulator.total += 1;
      if (status.key === "active") accumulator.active += 1;
      if (status.key !== "active") accumulator.stale += 1;
      return accumulator;
    },
    { total: 0, active: 0, stale: 0 }
  );

  elements.totalCount.textContent = String(counts.total);
  elements.activeCount.textContent = String(counts.active);
  elements.staleCount.textContent = String(counts.stale);
  elements.lastUpdated.textContent = locations[0] ? `${locations.length}대 · ${formatTime(locations[0].receivedAt)}` : "수신 대기";
  renderOperationsSummary(locations, counts);
  renderMapStatus(locations, counts);
  syncStatusFilterButtons();

  elements.vehicleList.replaceChildren();

  const visibleLocations = locations.filter(matchesVehicleFilters);

  if (!visibleLocations.length) {
    const empty = document.createElement("div");
    empty.className = "empty-state";
    empty.textContent = locations.length ? "조건에 맞는 차량이 없습니다." : "수신 위치가 없습니다.";
    elements.vehicleList.append(empty);
    elements.vehicleList.scrollTop = vehicleListScrollTop;
    return;
  }

  for (const location of visibleLocations) {
    elements.vehicleList.append(createVehicleItem(location));
  }

  elements.vehicleList.scrollTop = vehicleListScrollTop;
}

function renderOperationsSummary(locations, counts) {
  const activeText = counts.active > 0 ? `${counts.active}대 운행 중` : "운행 대기";
  const staleText = counts.stale > 0 ? `주의 ${counts.stale}대` : "정상 수신";
  const averageEtaMinutes = calculateAverageEtaMinutes(locations);
  const cargoTypes = new Set(locations.map(getCargoName).filter(Boolean));
  const routeCount = locations.filter((location) => getRoutePath(location).length >= 2).length;

  elements.opsHeadline.textContent = `전국 ${activeText}`;
  elements.opsSubline.textContent = locations.length
    ? `${staleText} · ${locations.length}대 관제 중`
    : "첫 위치를 기다리는 중입니다.";
  elements.averageEta.textContent = averageEtaMinutes === null ? "-" : formatDuration(averageEtaMinutes).replace("약 ", "");
  elements.cargoTypeCount.textContent = cargoTypes.size ? `${cargoTypes.size}종` : "-";
  elements.routePreviewCount.textContent = routeCount ? `${routeCount}건` : "-";
}

function renderMapStatus(locations, counts) {
  const latestLocation = locations[0];
  const averageEtaMinutes = calculateAverageEtaMinutes(locations);

  elements.mapActiveCount.textContent = String(counts.active);
  elements.mapEtaSummary.textContent = averageEtaMinutes === null ? "-" : formatDuration(averageEtaMinutes).replace("약 ", "");
  elements.mapUpdatedAt.textContent = latestLocation ? formatTime(latestLocation.receivedAt) : "-";
}

function syncStatusFilterButtons() {
  for (const button of elements.statusFilterButtons) {
    button.classList.toggle("is-active", button.dataset.statusFilter === state.vehicleStatusFilter);
  }
}

function matchesVehicleFilters(location) {
  const status = getLocationStatus(location);
  if (state.vehicleStatusFilter === "active" && status.key !== "active") return false;
  if (state.vehicleStatusFilter === "stale" && status.key === "active") return false;

  if (!state.vehicleSearch) return true;

  const vehicleType = getVehicleType(location);
  const haystack = [
    location.vehicleNo,
    location.driverName,
    location.driverId,
    vehicleType.label,
    getCargoSummary(location),
    getRouteSummary(location),
    getRouteProgressSummary(location)
  ].join(" ").toLowerCase();

  return haystack.includes(state.vehicleSearch.toLowerCase());
}

function calculateAverageEtaMinutes(locations) {
  const etaValues = locations
    .map((location) => getRouteMetrics(location)?.etaMinutes)
    .filter((value) => Number.isFinite(value));

  if (!etaValues.length) return null;
  return Math.round(etaValues.reduce((total, value) => total + value, 0) / etaValues.length);
}

function renderDrivers() {
  const drivers = [...state.drivers.values()].sort((left, right) => {
    return String(left.vehicleNo || left.driverId).localeCompare(String(right.vehicleNo || right.driverId), "ko");
  });

  elements.driverCount.textContent = `${drivers.length}명`;
  elements.driverList.replaceChildren();

  if (!drivers.length) {
    const empty = document.createElement("div");
    empty.className = "empty-state";
    empty.textContent = "등록된 기사가 없습니다.";
    elements.driverList.append(empty);
    return;
  }

  for (const driver of drivers) {
    elements.driverList.append(createDriverItem(driver));
  }
}

function createVehicleItem(location) {
  const status = getLocationStatus(location);
  const vehicleType = getVehicleType(location);
  const item = document.createElement("article");
  item.className = "vehicle-item";
  item.classList.toggle("is-selected", state.selectedDriverId === location.driverId);
  item.tabIndex = 0;

  const main = document.createElement("div");
  main.className = "vehicle-main";

  const title = document.createElement("div");
  title.className = "vehicle-title";

  const badge = document.createElement("span");
  badge.className = `badge ${status.key}`;
  badge.textContent = status.label;

  const typeBadge = document.createElement("span");
  typeBadge.className = `vehicle-type-chip ${vehicleType.className}`;
  typeBadge.textContent = vehicleType.label;

  const vehicleNo = document.createElement("strong");
  vehicleNo.textContent = location.vehicleNo || location.driverId;

  title.append(badge, typeBadge, vehicleNo);

  const meta = document.createElement("div");
  meta.className = "vehicle-meta";
  meta.textContent = [
    location.driverName || location.driverId,
    formatDateTime(location.receivedAt)
  ].filter(Boolean).join(" · ");

  const routeLine = document.createElement("div");
  routeLine.className = "vehicle-route-line";
  routeLine.textContent = getRouteSummary(location);

  const cargoLine = document.createElement("div");
  cargoLine.className = "vehicle-cargo-line";
  cargoLine.textContent = getCargoSummary(location);

  const progress = document.createElement("div");
  progress.className = "vehicle-progress";
  const progressBar = document.createElement("span");
  progressBar.style.width = `${getRouteProgressPercent(location)}%`;
  progress.append(progressBar);

  const speed = document.createElement("div");
  speed.className = "speed";
  const speedValue = document.createElement("strong");
  speedValue.textContent = location.speed === null || location.speed === undefined ? "-" : `${Math.round(location.speed)}`;
  const speedUnit = document.createElement("span");
  speedUnit.textContent = "km/h";
  const etaValue = document.createElement("small");
  etaValue.textContent = getRouteProgressSummary(location) || "도착 대기";
  speed.append(speedValue, speedUnit, etaValue);

  main.append(title, meta, routeLine, cargoLine, progress);
  item.append(main, speed);

  item.addEventListener("click", () => focusLocation(location));
  item.addEventListener("keydown", (event) => {
    if (event.key === "Enter" || event.key === " ") {
      event.preventDefault();
      focusLocation(location);
    }
  });

  return item;
}

function createDriverItem(driver) {
  const vehicleType = getVehicleType(driver);
  const item = document.createElement("article");
  item.className = "driver-item";

  const head = document.createElement("div");
  head.className = "driver-head";

  const title = document.createElement("div");
  title.className = "driver-title";

  const name = document.createElement("strong");
  name.textContent = `${driver.vehicleNo || driver.driverId} · ${driver.name}`;

  const meta = document.createElement("div");
  meta.className = "vehicle-meta";
  meta.textContent = [vehicleType.label, driver.driverId, driver.phone].filter(Boolean).join(" · ");

  title.append(name, meta);

  const badge = document.createElement("span");
  badge.className = `badge ${driver.enabled ? "active" : "offline"}`;
  badge.textContent = driver.enabled ? "사용" : "중지";

  head.append(title, badge);

  const tokenRow = document.createElement("div");
  tokenRow.className = "driver-token";

  const token = document.createElement("code");
  token.textContent = driver.token || "-";

  const copyButton = document.createElement("button");
  copyButton.type = "button";
  copyButton.textContent = "복사";
  copyButton.addEventListener("click", () => copyToken(driver.token));

  tokenRow.append(token, copyButton);

  const actions = document.createElement("div");
  actions.className = "driver-actions";

  const toggleButton = document.createElement("button");
  toggleButton.type = "button";
  toggleButton.textContent = driver.enabled ? "중지" : "사용";
  toggleButton.addEventListener("click", () => updateDriver(driver.driverId, { enabled: !driver.enabled }));

  const tokenButton = document.createElement("button");
  tokenButton.type = "button";
  tokenButton.textContent = "토큰 재발급";
  tokenButton.addEventListener("click", () => updateDriver(driver.driverId, { regenerateToken: true }));

  const deleteButton = document.createElement("button");
  deleteButton.type = "button";
  deleteButton.textContent = "삭제";
  deleteButton.addEventListener("click", () => deleteDriver(driver));

  actions.append(toggleButton, tokenButton, deleteButton);
  item.append(head, tokenRow, actions);
  return item;
}

async function submitDriverForm(event) {
  event.preventDefault();

  const payload = {
    name: elements.driverNameInput.value.trim(),
    vehicleNo: elements.vehicleNoInput.value.trim(),
    vehicleType: elements.vehicleTypeInput.value,
    phone: elements.phoneInput.value.trim(),
    driverId: elements.driverIdInput.value.trim()
  };

  try {
    await fetchJson("/api/v1/drivers", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...adminHeaders()
      },
      body: JSON.stringify(payload)
    });
    elements.driverForm.reset();
    await refreshDrivers();
  } catch (error) {
    console.error(error);
    window.alert(`등록 실패: ${error.message}`);
  }
}

async function updateDriver(driverId, patch) {
  try {
    const data = await fetchJson(`/api/v1/drivers/${encodeURIComponent(driverId)}`, {
      method: "PATCH",
      headers: {
        "Content-Type": "application/json",
        ...adminHeaders()
      },
      body: JSON.stringify(patch)
    });
    state.drivers.set(data.driver.driverId, data.driver);
    renderDrivers();
  } catch (error) {
    console.error(error);
    window.alert(`수정 실패: ${error.message}`);
  }
}

async function deleteDriver(driver) {
  const confirmed = window.confirm(`${driver.vehicleNo || driver.driverId} 삭제`);
  if (!confirmed) return;

  try {
    await fetchJson(`/api/v1/drivers/${encodeURIComponent(driver.driverId)}`, {
      method: "DELETE",
      headers: adminHeaders()
    });
    state.drivers.delete(driver.driverId);
    renderDrivers();
  } catch (error) {
    console.error(error);
    window.alert(`삭제 실패: ${error.message}`);
  }
}

async function copyToken(token) {
  if (!token) return;

  try {
    await navigator.clipboard.writeText(token);
    setConnectionStatus("토큰 복사됨", "online");
  } catch {
    window.prompt("기기 토큰", token);
  }
}

function updateMarkers({ fitBounds = false } = {}) {
  if (!state.map || !state.mapProvider) return;

  const activeIds = new Set(state.locations.keys());

  for (const [driverId, marker] of state.markers) {
    if (!activeIds.has(driverId)) {
      removeMarker(marker);
      state.markers.delete(driverId);
    }
  }

  for (const location of state.locations.values()) {
    updateMarker(location);
  }

  updateMarkerSelectionClasses();

  if (fitBounds && state.markers.size > 0) {
    fitMapToLocations();
  }
}

function updateMarkerSelectionClasses() {
  for (const [driverId, marker] of state.markers) {
    const element = marker.provider === "leaflet"
      ? marker.marker.getElement?.()
      : marker.element;

    if (element) {
      element.classList.toggle("is-selected", state.selectedDriverId === driverId);
    }
  }
}

function updateInitialMapFit() {
  if (state.hasFittedInitialLocations || !state.map || !state.locations.size) return;

  updateMarkers({ fitBounds: true });
  state.hasFittedInitialLocations = true;
}

function bindMapInteractionTracking() {
  if (!state.map || !state.mapProvider) return;

  const markUserInteraction = () => {
    if (!state.isProgrammaticMapChange) {
      state.hasFittedInitialLocations = true;
    }
  };

  if (state.mapProvider === "kakao") {
    kakao.maps.event.addListener(state.map, "dragstart", markUserInteraction);
    kakao.maps.event.addListener(state.map, "zoom_changed", markUserInteraction);
    return;
  }

  if (state.mapProvider === "leaflet") {
    state.map.on("dragstart", markUserInteraction);
    state.map.on("zoomstart", markUserInteraction);
  }
}

function runProgrammaticMapChange(callback) {
  state.isProgrammaticMapChange = true;
  window.clearTimeout(state.programmaticMapChangeTimer);
  try {
    return callback();
  } finally {
    state.programmaticMapChangeTimer = window.setTimeout(() => {
      state.isProgrammaticMapChange = false;
    }, 350);
  }
}

function updateMarker(location) {
  if (!state.map || !state.mapProvider) return;

  if (state.mapProvider === "leaflet") {
    updateLeafletMarker(location);
    return;
  }

  updateKakaoMarker(location);
}

function updateKakaoMarker(location) {
  const targetPosition = createPosition(location.latitude, location.longitude);
  const kakaoPosition = new kakao.maps.LatLng(targetPosition.latitude, targetPosition.longitude);
  const status = getLocationStatus(location);
  let marker = state.markers.get(location.driverId);
  const markerHeading = resolveMarkerHeading(location, marker?.currentPosition, targetPosition);
  const markerElement = createTruckMarkerElement(location, status, markerHeading);

  if (!marker) {
    marker = {
      provider: "kakao",
      overlay: new kakao.maps.CustomOverlay({
        position: kakaoPosition,
        content: markerElement,
        yAnchor: 1
      }),
      element: markerElement,
      currentPosition: targetPosition
    };
    marker.overlay.setMap(state.map);
    state.markers.set(location.driverId, marker);
  } else {
    marker.overlay.setContent(markerElement);
    marker.element = markerElement;
    animateMarkerPosition(marker, targetPosition);
  }
}

function updateLeafletMarker(location) {
  const status = getLocationStatus(location);
  const vehicleType = getVehicleType(location);
  const targetPosition = createPosition(location.latitude, location.longitude);
  const leafletPosition = [targetPosition.latitude, targetPosition.longitude];
  const existingMarker = state.markers.get(location.driverId);
  const markerHeading = resolveMarkerHeading(location, existingMarker?.currentPosition, targetPosition);
  const selectedClass = state.selectedDriverId === location.driverId ? "is-selected" : "";
  const icon = L.divIcon({
    className: `leaflet-truck-marker truck-marker ${status.key} ${vehicleType.className} ${selectedClass}`,
    html: createTruckMarkerHtml(location, vehicleType, markerHeading),
    iconSize: [104, 68],
    iconAnchor: [52, 64],
    popupAnchor: [0, -50]
  });

  let marker = existingMarker;

  if (!marker) {
    marker = {
      provider: "leaflet",
      marker: L.marker(leafletPosition, { icon, title: location.vehicleNo || location.driverId }),
      currentPosition: targetPosition
    };
    marker.marker.on("click", () => openMarkerActionPanel(location));
    marker.marker.addTo(state.map);
    state.markers.set(location.driverId, marker);
  } else {
    marker.marker.setIcon(icon);
    marker.marker.off("click");
    marker.marker.on("click", () => openMarkerActionPanel(location));
    animateMarkerPosition(marker, targetPosition);
  }
}

function createTruckMarkerElement(location, status, heading) {
  const vehicleType = getVehicleType(location);
  const markerElement = document.createElement("button");
  const selectedClass = state.selectedDriverId === location.driverId ? "is-selected" : "";
  markerElement.className = `truck-marker ${status.key} ${vehicleType.className} ${selectedClass}`;
  markerElement.type = "button";
  markerElement.title = `${vehicleType.label} ${location.vehicleNo || location.driverId}`;
  markerElement.setAttribute("aria-label", `${vehicleType.label} ${location.vehicleNo || location.driverId} 위치 보기`);
  markerElement.innerHTML = createTruckMarkerHtml(location, vehicleType, heading);
  markerElement.addEventListener("click", (event) => {
    event.stopPropagation();
    openMarkerActionPanel(location);
  });
  return markerElement;
}

function createTruckMarkerHtml(location, vehicleType = getVehicleType(location), heading = getLocationHeading(location)) {
  const label = location.vehicleNo ? location.vehicleNo.slice(-4) : location.driverId.slice(0, 4);
  const rotation = normalizeMarkerRotation(heading);
  const vehiclePictogram = createVehiclePictogramSvg(vehicleType.className);
  return `
    <span class="truck-marker__beacon" style="--truck-heading: ${rotation}deg" aria-hidden="true">
      <svg class="truck-marker__beacon-icon" viewBox="0 0 44 52" focusable="false" aria-hidden="true">
        <path class="beacon-shadow" d="M22 4 39 45 22 36 5 45z"></path>
        <path class="beacon-body" d="M22 4 39 45 22 36 5 45z"></path>
        <path class="beacon-spine" d="M22 13v25"></path>
      </svg>
    </span>
    <span class="truck-marker__body-icon" aria-hidden="true">${vehiclePictogram}</span>
    <span class="truck-marker__status" aria-hidden="true"></span>
    <span class="truck-marker__identity">
      <span class="truck-marker__label-icon" aria-hidden="true">${vehiclePictogram}</span>
      <span class="truck-marker__plate">${escapeHtml(label)}</span>
      <span class="truck-marker__type">${escapeHtml(vehicleType.shortLabel)}</span>
    </span>
  `;
}

function createVehiclePictogramSvg(typeClass) {
  if (typeClass === "type-trailer") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <rect class="icon-load" x="3" y="7" width="23" height="7" rx="2"></rect>
        <path class="icon-cab" d="M28 9h5l4 4v4h-9z"></path>
        <path class="icon-line" d="M26 13h3"></path>
        <circle class="icon-wheel" cx="9" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="23" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="34" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  if (typeClass === "type-container") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <rect class="icon-load" x="3" y="6" width="24" height="9" rx="1.5"></rect>
        <path class="icon-cab" d="M29 9h5l4 4v4h-9z"></path>
        <path class="icon-line" d="M8 7v7M13 7v7M18 7v7M23 7v7"></path>
        <circle class="icon-wheel" cx="10" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="24" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="35" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  if (typeClass === "type-box") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <rect class="icon-load" x="6" y="4" width="20" height="12" rx="2"></rect>
        <path class="icon-cab" d="M28 9h5l4 4v4h-9z"></path>
        <path class="icon-line" d="M11 8h10M11 12h8"></path>
        <circle class="icon-wheel" cx="12" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="34" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  if (typeClass === "type-van") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <path class="icon-load" d="M6 16v-4c1-5 5-8 10-8h9c5 0 8 4 9 9l3 2v2H6z"></path>
        <path class="icon-window" d="M15 6h7v6H11c1-3 2-5 4-6zM24 6h3c3 1 5 3 6 6h-9z"></path>
        <circle class="icon-wheel" cx="14" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="30" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  if (typeClass === "type-cold") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <rect class="icon-load" x="6" y="5" width="20" height="11" rx="2"></rect>
        <path class="icon-cab" d="M28 9h5l4 4v4h-9z"></path>
        <path class="icon-snow" d="M16 7v7M12 9l8 4M20 9l-8 4"></path>
        <circle class="icon-wheel" cx="12" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="34" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  if (typeClass === "type-wing") {
    return `
      <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
        <path class="icon-wing" d="M8 6h21l-6 8H4z"></path>
        <rect class="icon-load" x="8" y="10" width="18" height="6" rx="2"></rect>
        <path class="icon-cab" d="M28 9h5l4 4v4h-9z"></path>
        <circle class="icon-wheel" cx="13" cy="17" r="2.2"></circle>
        <circle class="icon-wheel" cx="34" cy="17" r="2.2"></circle>
      </svg>
    `;
  }

  return `
    <svg class="truck-marker__vehicle-icon" viewBox="0 0 40 22" focusable="false" aria-hidden="true">
      <rect class="icon-load" x="6" y="7" width="20" height="9" rx="2"></rect>
      <path class="icon-cab" d="M28 9h5l4 4v4h-9z"></path>
      <path class="icon-line" d="M11 10h10M11 13h8"></path>
      <circle class="icon-wheel" cx="12" cy="17" r="2.2"></circle>
      <circle class="icon-wheel" cx="34" cy="17" r="2.2"></circle>
    </svg>
  `;
}

function resolveMarkerHeading(location, fromPosition, targetPosition) {
  const routeHeading = getRouteStartHeading(location);
  if (routeHeading !== null) return routeHeading;

  if (fromPosition && targetPosition && calculateDistanceKm(fromPosition, targetPosition) >= 0.003) {
    return calculateBearing(fromPosition, targetPosition);
  }

  return getLocationHeading(location);
}

function getRouteStartHeading(location) {
  const routePath = getRoutePath(location);
  if (routePath.length < 2) return null;

  const from = routePath[0];
  for (let index = 1; index < routePath.length; index += 1) {
    const to = routePath[index];
    if (calculateDistanceKm(from, to) >= 0.003) {
      return calculateBearing(from, to);
    }
  }

  return null;
}

function getLocationHeading(location) {
  const heading = parseFiniteNumber(location.heading);
  return heading === null ? 90 : heading;
}

function normalizeMarkerRotation(heading) {
  const normalizedHeading = ((Number(heading) % 360) + 360) % 360;
  return Math.round(normalizedHeading - 90);
}

function animateMarkerPosition(marker, targetPosition) {
  const fromPosition = marker.currentPosition || getMarkerPosition(marker) || targetPosition;

  if (calculateDistanceKm(fromPosition, targetPosition) < 0.01) {
    setMarkerPosition(marker, targetPosition);
    marker.currentPosition = targetPosition;
    return;
  }

  cancelMarkerAnimation(marker);

  const startedAt = window.performance.now();
  const duration = state.markerAnimationMs;

  const tick = (timestamp) => {
    const progress = Math.min(1, (timestamp - startedAt) / duration);
    const easedProgress = easeInOutCubic(progress);
    const nextPosition = {
      latitude: interpolateNumber(fromPosition.latitude, targetPosition.latitude, easedProgress),
      longitude: interpolateNumber(fromPosition.longitude, targetPosition.longitude, easedProgress)
    };

    setMarkerPosition(marker, nextPosition);
    marker.currentPosition = nextPosition;

    if (progress < 1) {
      marker.animationFrame = window.requestAnimationFrame(tick);
    } else {
      marker.animationFrame = null;
      marker.currentPosition = targetPosition;
      setMarkerPosition(marker, targetPosition);
    }
  };

  marker.animationFrame = window.requestAnimationFrame(tick);
}

function cancelMarkerAnimation(marker) {
  if (!marker?.animationFrame) return;
  window.cancelAnimationFrame(marker.animationFrame);
  marker.animationFrame = null;
}

function getMarkerPosition(marker) {
  if (marker.provider === "leaflet" && marker.marker?.getLatLng) {
    const position = marker.marker.getLatLng();
    return createPosition(position.lat, position.lng);
  }

  return marker.currentPosition || null;
}

function setMarkerPosition(marker, position) {
  if (marker.provider === "kakao") {
    marker.overlay.setPosition(new kakao.maps.LatLng(position.latitude, position.longitude));
    return;
  }

  if (marker.provider === "leaflet") {
    marker.marker.setLatLng([position.latitude, position.longitude]);
  }
}

function createPosition(latitude, longitude) {
  return {
    latitude: Number(latitude),
    longitude: Number(longitude)
  };
}

function easeInOutCubic(value) {
  return value < 0.5
    ? 4 * value * value * value
    : 1 - ((-2 * value + 2) ** 3) / 2;
}

function removeMarker(marker) {
  cancelMarkerAnimation(marker);

  if (marker.provider === "kakao") {
    marker.overlay.setMap(null);
    return;
  }

  if (marker.provider === "leaflet") {
    marker.marker.remove();
  }
}

function fitMapToLocations() {
  if (state.mapProvider === "kakao") {
    const bounds = new kakao.maps.LatLngBounds();
    for (const location of state.locations.values()) {
      bounds.extend(new kakao.maps.LatLng(location.latitude, location.longitude));
    }
    runProgrammaticMapChange(() => state.map.setBounds(bounds));
    return true;
  }

  if (state.mapProvider === "leaflet") {
    const bounds = L.latLngBounds([...state.locations.values()].map((location) => {
      return [location.latitude, location.longitude];
    }));
    runProgrammaticMapChange(() => {
      state.map.fitBounds(bounds, {
        maxZoom: 14,
        padding: [40, 40]
      });
    });
    return true;
  }

  return false;
}

function centerMapOnLocation(location, options = {}) {
  if (!state.map || !state.mapProvider) return;

  if (state.mapProvider === "kakao") {
    const position = new kakao.maps.LatLng(location.latitude, location.longitude);
    runProgrammaticMapChange(() => {
      state.map.setCenter(position);
      if (options.zoom && state.map.getLevel() > 4) {
        state.map.setLevel(4);
      }
    });
    return;
  }

  if (state.mapProvider === "leaflet") {
    const nextZoom = options.zoom ? Math.max(state.map.getZoom(), 14) : state.map.getZoom();
    runProgrammaticMapChange(() => {
      state.map.setView([location.latitude, location.longitude], nextZoom, { animate: true });
    });
  }
}

function renderRoutePreview(location, options = {}) {
  if (!state.map || !state.mapProvider) return false;

  clearRouteOverlay();

  const routePath = getRoutePath(location);
  if (routePath.length < 2) return false;

  if (state.mapProvider === "kakao") {
    const path = routePath.map((point) => new kakao.maps.LatLng(point.latitude, point.longitude));
    state.routeOverlay = new kakao.maps.Polyline({
      path,
      strokeWeight: 5,
      strokeColor: "#1f6feb",
      strokeOpacity: 0.86,
      strokeStyle: "solid"
    });
    state.routeOverlay.setMap(state.map);

    state.routeDestinationOverlay = new kakao.maps.CustomOverlay({
      position: path[path.length - 1],
      content: createDestinationMarkerElement(location),
      yAnchor: 1.1
    });
    state.routeDestinationOverlay.setMap(state.map);
  }

  if (state.mapProvider === "leaflet") {
    const path = routePath.map((point) => [point.latitude, point.longitude]);
    state.routeOverlay = L.polyline(path, {
      color: "#1f6feb",
      weight: 5,
      opacity: 0.86,
      lineCap: "round",
      lineJoin: "round"
    }).addTo(state.map);
    state.routeDestinationOverlay = L.marker(path[path.length - 1], {
      icon: L.divIcon({
        className: "route-destination-leaflet",
        html: createDestinationMarkerHtml(location),
        iconSize: [116, 42],
        iconAnchor: [58, 42]
      })
    }).addTo(state.map);
  }

  if (options.fit !== false) {
    fitMapToRoute(location);
  }

  return true;
}

function clearRouteOverlay() {
  if (state.routeOverlay) {
    if (typeof state.routeOverlay.setMap === "function") {
      state.routeOverlay.setMap(null);
    } else if (typeof state.routeOverlay.remove === "function") {
      state.routeOverlay.remove();
    }
  }

  if (state.routeDestinationOverlay) {
    if (typeof state.routeDestinationOverlay.setMap === "function") {
      state.routeDestinationOverlay.setMap(null);
    } else if (typeof state.routeDestinationOverlay.remove === "function") {
      state.routeDestinationOverlay.remove();
    }
  }

  state.routeOverlay = null;
  state.routeDestinationOverlay = null;
}

function fitMapToRoute(location) {
  const routePath = getRoutePath(location);
  if (!state.map || !state.mapProvider || routePath.length < 2) return false;

  if (state.mapProvider === "kakao") {
    const bounds = new kakao.maps.LatLngBounds();
    for (const point of routePath) {
      bounds.extend(new kakao.maps.LatLng(point.latitude, point.longitude));
    }
    runProgrammaticMapChange(() => state.map.setBounds(bounds));
    return true;
  }

  if (state.mapProvider === "leaflet") {
    runProgrammaticMapChange(() => {
      state.map.fitBounds(
        L.latLngBounds(routePath.map((point) => [point.latitude, point.longitude])),
        {
          maxZoom: 13,
          padding: [48, 48]
        }
      );
    });
    return true;
  }

  return false;
}

function createDestinationMarkerElement(location) {
  const marker = document.createElement("div");
  marker.className = "route-destination-marker";
  marker.innerHTML = createDestinationMarkerHtml(location);
  return marker;
}

function createDestinationMarkerHtml(location) {
  const metrics = getRouteMetrics(location);
  const destinationName = getDestinationName(location) || "목적지";
  const etaText = metrics ? formatEtaTime(metrics.estimatedArrivalAt) : "도착시간 미정";

  return `
    <span class="route-destination-pin">도착</span>
    <strong>${escapeHtml(destinationName)}</strong>
    <small>${escapeHtml(etaText)}</small>
  `;
}

function refreshSelectedDetail(location) {
  if (!elements.markerActionPanel.classList.contains("is-hidden")) {
    renderMarkerActionPanel(location);
  }

  if (state.activeDetailMode === "route") {
    renderRouteFocusPanel(location);
    renderRoutePreview(location, { fit: false });
    return;
  }

  if (state.activeDetailMode === "cargo") {
    hideRouteFocusPanel();
    showInfoOverlay(location);
    return;
  }

  if (state.activeDetailMode === "history") {
    renderRouteFocusPanel(location);
    renderRoutePreview(location, { fit: false });
    if (state.infoOverlay) {
      showInfoOverlay(location);
    }
    return;
  }

  if (state.activeDetailMode === "center") {
    renderCurrentLocationPanel(location);
    showInfoOverlay(location);
  }
}

function openMarkerActionPanel(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "menu";
  elements.historyDrawer.classList.add("is-hidden");
  clearRouteOverlay();
  clearInfoOverlay();
  renderRouteFocusPanel(null);
  renderMarkerActionPanel(location);
  elements.markerActionPanel.classList.remove("is-hidden");
  render();
  updateMarkerSelectionClasses();
}

function hideMarkerActionPanel() {
  elements.markerActionPanel.classList.add("is-hidden");
}

function closeMarkerActionPanel() {
  hideMarkerActionPanel();
  if (state.activeDetailMode === "menu") {
    state.activeDetailMode = "";
  }
  state.selectedDriverId = "";
  render();
  updateMarkerSelectionClasses();
}

function renderMarkerActionPanel(location) {
  const status = getLocationStatus(location);
  const vehicleType = getVehicleType(location);
  const metrics = getRouteMetrics(location);
  const driver = location.driverName || location.driverId;
  const title = location.vehicleNo || location.driverId;
  const distance = metrics ? formatDistance(metrics.remainingDistanceKm) : "-";
  const eta = metrics ? formatEtaTime(metrics.estimatedArrivalAt) : "-";

  elements.markerActionEyebrow.textContent = "차량";
  elements.markerActionTitle.textContent = `${title} · ${vehicleType.label}`;
  elements.markerActionMeta.textContent = [driver, status.label, formatSpeed(location.speed)].filter(Boolean).join(" · ");
  elements.markerActionSummary.replaceChildren(
    createMarkerActionSummaryItem("화물", getCargoSummary(location)),
    createMarkerActionSummaryItem("목적지", getRouteSummary(location)),
    createMarkerActionSummaryItem("남은 거리", distance),
    createMarkerActionSummaryItem("도착 시간", eta)
  );
}

function createMarkerActionSummaryItem(label, value) {
  const item = document.createElement("span");
  const labelElement = document.createElement("b");
  const valueElement = document.createElement("em");
  labelElement.textContent = label;
  valueElement.textContent = value || "-";
  item.append(labelElement, valueElement);
  return item;
}

function runMarkerAction(action, location) {
  if (action === "route") {
    showVehicleRoute(location);
    return;
  }

  if (action === "cargo") {
    showVehicleCargo(location);
    return;
  }

  if (action === "history") {
    showVehicleHistory(location);
    return;
  }

  if (action === "center") {
    showVehicleCenter(location);
  }
}

function showVehicleRoute(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "route";
  hideMarkerActionPanel();
  elements.historyDrawer.classList.add("is-hidden");
  clearInfoOverlay();
  renderRouteFocusPanel(location);
  render();
  updateMarkerSelectionClasses();

  if (!state.map || !state.mapProvider) return;

  if (!renderRoutePreview(location, { fit: true })) {
    centerMapOnLocation(location, { zoom: true });
  }
}

function showVehicleCargo(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "cargo";
  hideMarkerActionPanel();
  elements.historyDrawer.classList.add("is-hidden");
  clearRouteOverlay();
  hideRouteFocusPanel();
  render();
  updateMarkerSelectionClasses();
  showInfoOverlay(location);
}

function showVehicleHistory(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "history";
  hideMarkerActionPanel();
  clearInfoOverlay();
  renderRouteFocusPanel(location);
  render();
  updateMarkerSelectionClasses();
  renderRoutePreview(location, { fit: false });
  loadLocationHistory(location);
}

function showVehicleCenter(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "center";
  hideMarkerActionPanel();
  elements.historyDrawer.classList.add("is-hidden");
  clearRouteOverlay();
  renderCurrentLocationPanel(location);
  render();
  updateMarkerSelectionClasses();

  if (state.map && state.mapProvider) {
    centerMapOnLocation(location, { zoom: true });
  }
  showInfoOverlay(location);
}

function setRouteFocusEyebrow(label) {
  const eyebrow = elements.routeFocusPanel.querySelector(".section-eyebrow");
  if (eyebrow) {
    eyebrow.textContent = label;
  }
}

function setRouteFocusContent(eyebrow, title, meta) {
  setRouteFocusEyebrow(eyebrow);
  elements.routeFocusPanel.classList.add("is-active");
  elements.routeFocusTitle.textContent = title;
  elements.routeFocusMeta.textContent = meta;
}

function hideRouteFocusPanel() {
  elements.routeFocusPanel.classList.remove("is-active");
  setRouteFocusEyebrow("차량 정보");
  elements.routeFocusTitle.textContent = "";
  elements.routeFocusMeta.textContent = "";
}

function renderCurrentLocationPanel(location) {
  const vehicleType = getVehicleType(location);
  const coordinate = `${formatCoordinate(location.latitude)}, ${formatCoordinate(location.longitude)}`;
  setRouteFocusContent(
    "지도 중심",
    `${location.vehicleNo || location.driverId} · ${vehicleType.label}`,
    `${formatSpeed(location.speed)} · ${coordinate} · ${formatDateTime(location.receivedAt)}`
  );
}

function renderRouteFocusPanel(location) {
  setRouteFocusEyebrow("차량 정보");
  if (!location) {
    hideRouteFocusPanel();
    return;
  }

  const metrics = getRouteMetrics(location);
  const vehicleType = getVehicleType(location);
  const cargo = getCargoSummary(location);
  const progress = metrics
    ? `${formatDistance(metrics.remainingDistanceKm)} 남음 · ${formatClockTime(metrics.estimatedArrivalAt)} 도착`
    : "경로 데이터 대기";
  const destination = getDestinationName(location) || "목적지 미등록";

  elements.routeFocusPanel.classList.add("is-active");
  elements.routeFocusTitle.textContent = `${location.vehicleNo || location.driverId} · ${vehicleType.label}`;
  elements.routeFocusMeta.textContent = `${cargo} · ${destination} · ${progress}`;
}

function focusLocation(location) {
  state.activeDetailMode = "history";
  hideMarkerActionPanel();
  loadLocationHistory(location);
  renderRouteFocusPanel(location);
  render();
  updateMarkerSelectionClasses();

  if (!state.map || !state.mapProvider) return;

  if (!renderRoutePreview(location, { fit: true })) {
    centerMapOnLocation(location, { zoom: true });
  }
  showInfoOverlay(location);
}

async function loadLocationHistory(location) {
  state.selectedDriverId = location.driverId;
  state.activeDetailMode = "history";
  hideMarkerActionPanel();
  elements.historyDrawer.classList.remove("is-hidden");
  elements.historyTitle.textContent = `${location.vehicleNo || location.driverId} 수신 이력`;
  elements.historySummary.textContent = "조회 중";
  elements.historyList.replaceChildren();

  try {
    const data = await fetchJson(
      `/api/v1/locations/history?driverId=${encodeURIComponent(location.driverId)}&limit=80`,
      { headers: adminHeaders() }
    );
    renderHistory(location, data.locations || []);
  } catch (error) {
    console.error(error);
    elements.historySummary.textContent = `조회 실패: ${error.message}`;
  }
}

function renderHistory(location, records) {
  elements.historyList.replaceChildren();
  elements.historySummary.textContent = `${records.length}건 · ${location.driverName || location.driverId}`;

  if (!records.length) {
    const empty = document.createElement("div");
    empty.className = "empty-state";
    empty.textContent = "위치 이력이 없습니다.";
    elements.historyList.append(empty);
    return;
  }

  for (const record of records.reverse()) {
    const row = document.createElement("div");
    row.className = "history-row";

    const time = document.createElement("time");
    time.textContent = formatTime(record.receivedAt);

    const coordinate = document.createElement("span");
    coordinate.textContent = `${formatCoordinate(record.latitude)}, ${formatCoordinate(record.longitude)}`;

    const speed = document.createElement("span");
    speed.textContent = record.speed === null || record.speed === undefined ? "-" : `${Math.round(record.speed)} km/h`;

    row.append(time, coordinate, speed);
    elements.historyList.append(row);
  }
}

function closeHistoryDrawer() {
  state.selectedDriverId = "";
  state.activeDetailMode = "";
  elements.historyDrawer.classList.add("is-hidden");
  hideMarkerActionPanel();
  clearRouteOverlay();
  clearInfoOverlay();
  renderRouteFocusPanel(null);
  render();
  updateMarkerSelectionClasses();
}

function showInfoOverlay(location) {
  if (!elements.mapPanel) return;

  if (shouldSuppressInfoPanel()) {
    clearInfoOverlay();
    return;
  }

  const status = getLocationStatus(location);
  const content = createPopupElement(location, status);
  const placement = getInfoPanelPlacement(location);

  if (!(state.infoOverlay instanceof HTMLElement)) {
    clearInfoOverlay();
    state.infoOverlay = document.createElement("section");
    state.infoOverlay.setAttribute("aria-label", "화물 상세");
    elements.mapPanel.append(state.infoOverlay);
  }

  state.infoOverlay.className = `map-info-panel ${placement}`;
  state.infoOverlay.replaceChildren(content);
}

function clearInfoOverlay() {
  if (!state.infoOverlay) return;

  if (state.infoOverlay instanceof HTMLElement) {
    state.infoOverlay.remove();
  } else if (typeof state.infoOverlay.setMap === "function") {
    state.infoOverlay.setMap(null);
  } else if (state.mapProvider === "leaflet" && state.map?.closePopup) {
    state.map.closePopup(state.infoOverlay);
  }

  state.infoOverlay = null;
}

function shouldSuppressInfoPanel() {
  return window.matchMedia("(max-width: 560px)").matches;
}

function getInfoPanelPlacement(location) {
  const center = getCurrentMapCenter();
  if (!center) return "is-top-right";

  return location.longitude >= center.longitude ? "is-bottom-left" : "is-top-right";
}

function getCurrentMapCenter() {
  if (!state.map || !state.mapProvider) return null;

  if (state.mapProvider === "kakao" && typeof state.map.getCenter === "function") {
    const center = state.map.getCenter();
    return {
      latitude: center.getLat(),
      longitude: center.getLng()
    };
  }

  if (state.mapProvider === "leaflet" && typeof state.map.getCenter === "function") {
    const center = state.map.getCenter();
    return {
      latitude: center.lat,
      longitude: center.lng
    };
  }

  return null;
}

function createPopupElement(location, status) {
  const vehicleType = getVehicleType(location);
  const container = document.createElement("div");
  container.className = "map-info-card";

  const title = document.createElement("strong");
  title.className = "popup-title";
  title.textContent = location.vehicleNo || location.driverId;

  const driver = document.createElement("div");
  driver.textContent = `${vehicleType.label} · ${location.driverName || location.driverId} · ${status.label}`;

  const cargo = document.createElement("div");
  cargo.className = "popup-cargo";
  cargo.textContent = `제품: ${getCargoName(location) || "미등록"}`;

  const quantity = document.createElement("div");
  quantity.textContent = `수량: ${getCargoQuantity(location) || "미등록"}`;

  const weight = document.createElement("div");
  weight.textContent = `중량: ${getCargoWeight(location) || "미등록"}`;

  const destination = document.createElement("div");
  destination.textContent = getRouteSummary(location);

  const routeInfo = createRouteInfoElement(location);

  const receivedAt = document.createElement("div");
  receivedAt.textContent = `수신: ${formatDateTime(location.receivedAt)}`;

  const movement = document.createElement("div");
  movement.textContent = `속도: ${formatSpeed(location.speed)}`;

  container.append(title, driver, cargo, quantity, weight, destination, routeInfo, movement, receivedAt);
  return container;
}

function createRouteInfoElement(location) {
  const routeInfo = document.createElement("div");
  routeInfo.className = "popup-route";

  const metrics = getRouteMetrics(location);
  if (!metrics) {
    routeInfo.textContent = "경로: 목적지 좌표 대기";
    return routeInfo;
  }

  routeInfo.append(
    createPopupRouteLine("남은 거리", formatDistance(metrics.remainingDistanceKm)),
    createPopupRouteLine("도착 시간", formatEtaTime(metrics.estimatedArrivalAt)),
    createPopupRouteLine("남은 시간", formatDuration(metrics.etaMinutes))
  );
  return routeInfo;
}

function createPopupRouteLine(label, value) {
  const row = document.createElement("div");
  const labelElement = document.createElement("span");
  labelElement.textContent = label;
  const valueElement = document.createElement("strong");
  valueElement.textContent = value;
  row.append(labelElement, valueElement);
  return row;
}

async function fetchJson(url, options) {
  const response = await fetch(url, options);
  const data = await response.json().catch(() => ({}));

  if (!response.ok) {
    throw new Error(data.error || `HTTP ${response.status}`);
  }

  return data;
}

function adminHeaders() {
  return state.adminToken ? { "X-Admin-Token": state.adminToken } : {};
}

function withAdminToken(url) {
  if (!state.adminToken) return url;

  const parsedUrl = new URL(url, window.location.origin);
  parsedUrl.searchParams.set("adminToken", state.adminToken);
  return `${parsedUrl.pathname}${parsedUrl.search}`;
}

function requestAdminToken() {
  const token = window.prompt("관리자 토큰");
  if (!token) {
    setConnectionStatus("토큰 필요", "error");
    return;
  }

  state.adminToken = token.trim();
  refreshDrivers();
  refreshLatestLocations();
  connectEvents();
}

function setConnectionStatus(text, mode) {
  elements.connectionStatus.textContent = text;
  elements.connectionStatus.classList.toggle("is-online", mode === "online");
  elements.connectionStatus.classList.toggle("is-error", mode === "error");
}

function showMapMessage(text) {
  elements.mapMessage.textContent = text;
  elements.mapMessage.classList.remove("is-hidden");
}

function hideMapMessage() {
  elements.mapMessage.classList.add("is-hidden");
}

function getVehicleType(source) {
  const key = String(source.vehicleType || "cargo-truck").trim();
  return VEHICLE_TYPES[key] || VEHICLE_TYPES["cargo-truck"];
}

function getLocationStatus(location) {
  const receivedAt = new Date(location.receivedAt).getTime();
  const ageMs = Date.now() - receivedAt;

  if (ageMs <= 2 * 60 * 1000) return { key: "active", label: "수신 중" };
  if (ageMs <= 10 * 60 * 1000) return { key: "stale", label: "주의" };
  return { key: "offline", label: "미수신" };
}

function formatDateTime(value) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "-";
  return new Intl.DateTimeFormat("ko-KR", {
    month: "2-digit",
    day: "2-digit",
    hour: "2-digit",
    minute: "2-digit"
  }).format(date);
}

function formatTime(value) {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "-";
  return new Intl.DateTimeFormat("ko-KR", {
    hour: "2-digit",
    minute: "2-digit",
    second: "2-digit"
  }).format(date);
}

function formatSpeed(value) {
  if (value === null || value === undefined || value === "") return "-";
  const speed = Number(value);
  if (!Number.isFinite(speed)) return "-";
  return `${Math.round(speed)} km/h`;
}

function formatCoordinate(value) {
  return Number(value).toFixed(5);
}

function getCargoSummary(location) {
  return [getCargoName(location), getCargoQuantity(location), getCargoWeight(location)]
    .filter(Boolean)
    .join(" · ") || "미등록";
}

function getCargoName(location) {
  return firstString(location.cargoName, location.cargoItem, location.cargo, location.itemName);
}

function getCargoQuantity(location) {
  return firstString(location.cargoQuantity, location.quantity, location.qty, location.count);
}

function getCargoWeight(location) {
  return firstString(location.cargoWeight, location.weight);
}

function getRouteSummary(location) {
  const origin = firstString(location.origin, location.departure);
  const destination = firstString(location.destination, location.arrival);

  if (origin && destination) return `${origin} → ${destination}`;
  if (destination) return `도착: ${destination}`;
  if (origin) return `상차: ${origin}`;
  return "도착 미등록";
}

function getRouteProgressSummary(location) {
  const metrics = getRouteMetrics(location);
  if (!metrics) return "";
  return `${formatDistance(metrics.remainingDistanceKm)} · ${formatClockTime(metrics.estimatedArrivalAt)} 도착`;
}

function getRouteProgressPercent(location) {
  const metrics = getRouteMetrics(location);
  if (!metrics) return 14;

  const urgency = 100 - Math.min(96, Math.max(0, metrics.etaMinutes / 180 * 100));
  return Math.max(12, Math.min(94, Math.round(urgency)));
}

function getRouteMetrics(location) {
  const routePath = getRoutePath(location);
  if (routePath.length < 2) return null;

  const providedDistance = parseFiniteNumber(location.remainingDistanceKm || location.distanceKm || location.routeDistanceKm);
  const remainingDistanceKm = providedDistance || calculatePathDistanceKm(routePath);
  const providedEta = parseFiniteNumber(location.etaMinutes || location.estimatedMinutes || location.remainingMinutes);
  const speed = Math.max(24, parseFiniteNumber(location.speed) || 52);
  const etaMinutes = Math.max(2, Math.round(providedEta || (remainingDistanceKm / speed) * 60));
  const providedArrival = parseOptionalDate(location.estimatedArrivalAt || location.etaAt || location.arrivalAt);
  const estimatedArrivalAt = providedArrival || new Date(Date.now() + etaMinutes * 60 * 1000);

  return {
    remainingDistanceKm,
    etaMinutes,
    estimatedArrivalAt
  };
}

function getRoutePath(location) {
  const current = normalizeDisplayRoutePoint({
    latitude: location.latitude,
    longitude: location.longitude
  });
  if (!current) return [];

  const providedPath = normalizeDisplayRoutePath(location.routePath || location.path || location.polyline);
  const destination = getDestinationCoordinate(location) ||
    (providedPath.length ? providedPath[providedPath.length - 1] : null);

  if (providedPath.length >= 2) {
    return ensureRouteEndpoints(providedPath, current, destination);
  }

  if (!destination) return [];
  return createFallbackRoutePath(current, destination, location.driverId || location.vehicleNo || "");
}

function ensureRouteEndpoints(routePath, current, destination) {
  const normalizedPath = [...routePath];

  if (calculateDistanceKm(current, normalizedPath[0]) > 0.1) {
    normalizedPath.unshift(current);
  }

  if (destination && calculateDistanceKm(destination, normalizedPath[normalizedPath.length - 1]) > 0.1) {
    normalizedPath.push(destination);
  }

  return normalizedPath;
}

function getDestinationCoordinate(location) {
  const latitude = parseFiniteNumber(
    location.destinationLatitude || location.destinationLat || location.arrivalLatitude || location.arrivalLat
  );
  const longitude = parseFiniteNumber(
    location.destinationLongitude || location.destinationLng || location.destinationLon ||
      location.arrivalLongitude || location.arrivalLng || location.arrivalLon
  );

  if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
  return normalizeDisplayRoutePoint({ latitude, longitude });
}

function getDestinationName(location) {
  return firstString(location.destination, location.arrival);
}

function normalizeDisplayRoutePath(value) {
  if (!Array.isArray(value)) return [];
  return value.map(normalizeDisplayRoutePoint).filter(Boolean);
}

function normalizeDisplayRoutePoint(value) {
  if (!value) return null;
  const latitude = Array.isArray(value)
    ? parseFiniteNumber(value[0])
    : parseFiniteNumber(value.latitude ?? value.lat);
  const longitude = Array.isArray(value)
    ? parseFiniteNumber(value[1])
    : parseFiniteNumber(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, longitude };
}

function createFallbackRoutePath(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 = hashText(seed) % 2 === 0 ? 1 : -1;
  const offsetSize = Math.min(0.28, Math.max(0.02, length * 0.12));
  const perpendicularLatitude = (-deltaLongitude / length) * offsetSize * offsetDirection;
  const perpendicularLongitude = (deltaLatitude / length) * offsetSize * offsetDirection;

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

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 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 interpolateNumber(from, to, progress) {
  return from + (to - from) * progress;
}

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

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

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

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

function formatDistance(value) {
  const distance = Number(value);
  if (!Number.isFinite(distance)) return "-";
  if (distance < 1) return `${Math.round(distance * 1000)} m`;
  if (distance < 10) return `${distance.toFixed(1)} km`;
  return `${Math.round(distance)} km`;
}

function formatDuration(minutes) {
  const duration = Math.max(0, Math.round(Number(minutes) || 0));
  if (duration < 60) return `약 ${duration}분`;
  const hours = Math.floor(duration / 60);
  const remainingMinutes = duration % 60;
  return remainingMinutes ? `약 ${hours}시간 ${remainingMinutes}분` : `약 ${hours}시간`;
}

function formatEtaTime(value) {
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return "-";
  return new Intl.DateTimeFormat("ko-KR", {
    hour: "2-digit",
    minute: "2-digit"
  }).format(date);
}

function formatClockTime(value) {
  const date = value instanceof Date ? value : new Date(value);
  if (Number.isNaN(date.getTime())) return "-";
  return new Intl.DateTimeFormat("ko-KR", {
    hour: "2-digit",
    minute: "2-digit",
    hour12: false
  }).format(date);
}

function firstString(...values) {
  for (const value of values) {
    if (value === undefined || value === null) continue;
    const text = String(value).trim();
    if (text) return text;
  }
  return "";
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}
