const sessionKey = "mh-dashboard-session";

const loginPanel = document.getElementById("login-panel");
const dashboardPanel = document.getElementById("dashboard-panel");
const loginForm = document.getElementById("login-form");
const loginMessage = document.getElementById("login-message");
const logoutBtn = document.getElementById("logout-btn");
const userBadge = document.getElementById("user-badge");
const userPopover = document.getElementById("user-popover");
const currentViewTitle = document.getElementById("current-view-title");
const globalDateControls = document.getElementById("global-date-controls");
const globalStartDateInput = document.getElementById("global-start-date");
const globalEndDateInput = document.getElementById("global-end-date");
const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]"));
const organizationFrame = document.getElementById("organization-frame");
const organizationStage = document.getElementById("organization-stage");
const projectFrame = document.getElementById("project-frame");
const projectStage = document.getElementById("project-stage");
const teamFrame = document.getElementById("team-frame");
const teamStage = document.getElementById("team-stage");
const seatMapAdminStage = document.getElementById("seatmap-admin-stage");
const seatMapReadonlyStage = document.getElementById("seatmap-readonly-stage");
const emptyStage = document.getElementById("empty-stage");

const seatMapDom = {
  admin: {
    stage: seatMapAdminStage,
    name: document.getElementById("seatmap-admin-name"),
    status: document.getElementById("seatmap-admin-status"),
    saveBtn: document.getElementById("seatmap-admin-save-btn"),
    cancelBtn: null,
    exitBtn: document.getElementById("seatmap-admin-exit-btn"),
    actions: document.getElementById("seatmap-admin-actions"),
    boardWrap: document.getElementById("seatmap-admin-board-wrap"),
    board: document.getElementById("seatmap-admin-board"),
    empty: document.getElementById("seatmap-admin-empty"),
    settingsPanel: document.getElementById("seatmap-admin-settings-panel"),
    settingsForm: document.getElementById("seatmap-admin-settings-form"),
    formName: document.getElementById("seatmap-admin-form-name"),
    fileName: document.getElementById("seatmap-admin-file-name"),
    formRows: null,
    formCols: null,
    formGap: null,
    formImage: document.getElementById("seatmap-admin-form-image"),
    search: document.getElementById("seatmap-admin-search"),
    unassigned: document.getElementById("seatmap-admin-unassigned"),
    officeTabs: document.getElementById("seatmap-admin-office-tabs"),
    sidebarTitle: document.getElementById("seatmap-admin-sidebar-title"),
    sidebarDesc: document.getElementById("seatmap-admin-sidebar-desc"),
  },
  readonly: {
    stage: seatMapReadonlyStage,
    name: document.getElementById("seatmap-readonly-name"),
    status: document.getElementById("seatmap-readonly-status"),
    saveBtn: null,
    cancelBtn: null,
    exitBtn: document.getElementById("seatmap-readonly-exit-btn"),
    actions: document.getElementById("seatmap-readonly-actions"),
    boardWrap: document.getElementById("seatmap-readonly-board-wrap"),
    board: document.getElementById("seatmap-readonly-board"),
    empty: document.getElementById("seatmap-readonly-empty"),
    settingsPanel: null,
    settingsForm: null,
    formName: null,
    fileName: null,
    formRows: null,
    formCols: null,
    formGap: null,
    formImage: null,
    search: document.getElementById("seatmap-readonly-search"),
    unassigned: document.getElementById("seatmap-readonly-unassigned"),
    officeTabs: document.getElementById("seatmap-readonly-office-tabs"),
    sidebarTitle: document.getElementById("seatmap-readonly-sidebar-title"),
    sidebarDesc: document.getElementById("seatmap-readonly-sidebar-desc"),
  },
};

let seatMapName = seatMapDom.admin.name;
let seatMapStatus = seatMapDom.admin.status;
let seatMapSaveBtn = seatMapDom.admin.saveBtn;
let seatMapCancelBtn = seatMapDom.admin.cancelBtn;
let seatMapExitBtn = seatMapDom.admin.exitBtn;
let seatMapActions = seatMapDom.admin.actions;
let seatMapBoardWrap = seatMapDom.admin.boardWrap;
let seatMapBoard = seatMapDom.admin.board;
let seatMapEmpty = seatMapDom.admin.empty;
let seatMapSettingsPanel = seatMapDom.admin.settingsPanel;
let seatMapSettingsForm = seatMapDom.admin.settingsForm;
let seatMapFormName = seatMapDom.admin.formName;
let seatMapFileName = seatMapDom.admin.fileName;
let seatMapFormRows = seatMapDom.admin.formRows;
let seatMapFormCols = seatMapDom.admin.formCols;
let seatMapFormGap = seatMapDom.admin.formGap;
let seatMapFormImage = seatMapDom.admin.formImage;
let seatMapSearch = seatMapDom.admin.search;
let seatMapUnassigned = seatMapDom.admin.unassigned;
let seatMapOfficeTabs = seatMapDom.admin.officeTabs;
let seatMapSidebarTitle = seatMapDom.admin.sidebarTitle;
let seatMapSidebarDesc = seatMapDom.admin.sidebarDesc;
const APP_BASE_URL = String(window.__MH_BASE_URL || "").replace(/\/$/, "");

const seatMapOffices = [
  { key: "technical-development-center", label: "기술개발센터", ready: true },
  { key: "hanmac-building-6f", label: "한맥빌딩 6층", ready: true },
  { key: "hanmac-building-7f", label: "한맥빌딩 7층", ready: true },
];

const viewLabels = {
  ledger: "사업관리대장",
  project: "프로젝트별 분석",
  team: "팀/개인별 분석",
  organization: "조직 현황",
  "seatmap-admin": "자리배치도",
  "seatmap-readonly": "자리배치도",
};

const seatMapState = {
  loaded: false,
  loading: false,
  seatMap: null,
  members: [],
  slots: [],
  placements: [],
  draftPlacements: [],
  editMode: false,
  dirty: false,
  search: "",
  status: "",
  statusTone: "info",
  draggingMemberId: null,
  zoom: 1,
  panning: false,
  panStartX: 0,
  panStartY: 0,
  panScrollLeft: 0,
  panScrollTop: 0,
  hoveredSlotId: null,
  viewerOffsetX: 0,
  viewerOffsetY: 0,
  viewerPointerX: 0,
  viewerPointerY: 0,
  viewerDragging: false,
  viewerDragStartX: 0,
  viewerDragStartY: 0,
  viewerDragOffsetX: 0,
  viewerDragOffsetY: 0,
  officeKey: "technical-development-center",
  forceReadOnly: false,
};

let currentView = "project";
const globalDateState = {
  loaded: true,
  startDate: "2026-01-01",
  endDate: "2026-01-31",
};

function getGlobalAsOfDate() {
  return globalDateState.endDate || "";
}

function getSession() {
  try {
    return JSON.parse(sessionStorage.getItem(sessionKey) || "null");
  } catch {
    return null;
  }
}

function setSession(session) {
  sessionStorage.setItem(sessionKey, JSON.stringify(session));
}

function clearSession() {
  sessionStorage.removeItem(sessionKey);
}

function buildAuthHeaders(headers) {
  const nextHeaders = new Headers(headers || {});
  const token = getSession()?.token;
  if (token && !nextHeaders.has("Authorization")) {
    nextHeaders.set("Authorization", `Bearer ${token}`);
  }
  return nextHeaders;
}

function shouldShowGlobalDateControls() {
  return currentView === "ledger"
    || currentView === "project"
    || currentView === "team"
    || currentView === "organization"
    || currentView === "seatmap-admin"
    || currentView === "seatmap-readonly";
}

function syncGlobalDateControlVisibility() {
  if (!globalDateControls) return;
  globalDateControls.classList.toggle("hidden", !shouldShowGlobalDateControls());
}

function syncGlobalDateControlInputs() {
  if (globalStartDateInput) globalStartDateInput.value = globalDateState.startDate || "";
  if (globalEndDateInput) globalEndDateInput.value = globalDateState.endDate || "";
}

function getGlobalDateRangePayload() {
  return {
    source: "total-control",
    type: "date-range",
    startDate: globalDateState.startDate || "",
    endDate: globalDateState.endDate || "",
  };
}

function postGlobalDateRangeToFrame(frame) {
  if (!frame?.contentWindow || !globalDateState.loaded) return;
  frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin);
}

function buildAsOfQuery() {
  const asOf = getGlobalAsOfDate();
  if (!asOf) return "";
  return `?as_of=${encodeURIComponent(asOf)}`;
}

function notifyEmbeddedTabActivated() {
  if (currentView === "project" && projectFrame?.contentWindow) {
    projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
  }
  if (currentView === "team" && teamFrame?.contentWindow) {
    teamFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "mh" }, window.location.origin);
  }
}

async function ensureGlobalDateRangeLoaded() {
  if (globalDateState.loaded) return;
  try {
    const payload = await fetchJson("/api/integration/summary");
    const work = payload?.date_ranges?.work || {};
    const voucher = payload?.date_ranges?.voucher || {};
    const starts = [work.min_work_date, voucher.min_voucher_date].filter(Boolean).sort();
    const ends = [work.max_work_date, voucher.max_voucher_date].filter(Boolean).sort();
    globalDateState.startDate = starts[0] ? String(starts[0]).slice(0, 10) : "";
    globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : "";
    globalDateState.loaded = true;
    syncGlobalDateControlInputs();
    postGlobalDateRangeToFrame(organizationFrame);
    postGlobalDateRangeToFrame(projectFrame);
    postGlobalDateRangeToFrame(teamFrame);
  } catch (error) {
    console.error("공통 기간을 불러오지 못했습니다.", error);
  }
}

function hideUserPopover() {
  userPopover?.classList.add("hidden");
}

function toggleUserPopover() {
  userPopover?.classList.toggle("hidden");
}

function isAdmin() {
  return getSession()?.user?.role === "admin";
}

function isSlotBasedSeatMap() {
  return seatMapState.seatMap?.source_type === "dxf" || seatMapState.seatMap?.source_type === "fixed_html";
}

function canEditSeatMap() {
  return isAdmin() && !seatMapState.forceReadOnly;
}

function getSeatMapScreenMode() {
  return canEditSeatMap() ? "admin" : "readonly";
}

function isSeatMapAdminMode() {
  return getSeatMapScreenMode() === "admin";
}

function syncSeatMapDomRefs() {
  const dom = currentView === "seatmap-readonly" ? seatMapDom.readonly : seatMapDom.admin;
  seatMapName = dom.name;
  seatMapStatus = dom.status;
  seatMapSaveBtn = dom.saveBtn;
  seatMapCancelBtn = dom.cancelBtn;
  seatMapExitBtn = dom.exitBtn;
  seatMapActions = dom.actions;
  seatMapBoardWrap = dom.boardWrap;
  seatMapBoard = dom.board;
  seatMapEmpty = dom.empty;
  seatMapSettingsPanel = dom.settingsPanel;
  seatMapSettingsForm = dom.settingsForm;
  seatMapFormName = dom.formName;
  seatMapFileName = dom.fileName;
  seatMapFormRows = dom.formRows;
  seatMapFormCols = dom.formCols;
  seatMapFormGap = dom.formGap;
  seatMapFormImage = dom.formImage;
  seatMapSearch = dom.search;
  seatMapUnassigned = dom.unassigned;
  seatMapOfficeTabs = dom.officeTabs;
  seatMapSidebarTitle = dom.sidebarTitle;
  seatMapSidebarDesc = dom.sidebarDesc;
}

function getCurrentSeatMapOffice() {
  return seatMapOffices.find((item) => item.key === seatMapState.officeKey) || seatMapOffices[0];
}

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

function resolveAppUrl(path) {
  if (!path) return path;
  if (/^https?:\/\//i.test(path)) return path;
  if (!APP_BASE_URL) return path;
  if (path.startsWith("/")) return `${APP_BASE_URL}${path}`;
  return path;
}

function clonePlacements(items) {
  return items.map((item) => ({
    member_id: Number(item.member_id),
    seat_slot_id: item.seat_slot_id == null ? null : Number(item.seat_slot_id),
    row_index: Number(item.row_index),
    col_index: Number(item.col_index),
    seat_label: item.seat_label || "",
  }));
}

function computeSeatLabel(rowIndex, colIndex) {
  let quotient = rowIndex;
  let rowLabel = "";
  while (true) {
    const remainder = quotient % 26;
    rowLabel = String.fromCharCode(65 + remainder) + rowLabel;
    quotient = Math.floor(quotient / 26);
    if (quotient === 0) break;
    quotient -= 1;
  }
  return `${rowLabel}-${String(colIndex + 1).padStart(2, "0")}`;
}

function getInitials(name) {
  const trimmed = String(name || "").trim();
  if (!trimmed) return "?";
  return trimmed.slice(0, 2).toUpperCase();
}

function getPlacementSource() {
  return seatMapState.editMode ? seatMapState.draftPlacements : seatMapState.placements;
}

function setSeatMapStatus(message, tone = "info") {
  seatMapState.status = message || "";
  seatMapState.statusTone = tone;
  if (seatMapStatus) {
    seatMapStatus.textContent = seatMapState.status;
    seatMapStatus.dataset.tone = seatMapState.statusTone;
  }
}

function resetSeatMapDraft() {
  seatMapState.draftPlacements = clonePlacements(seatMapState.placements);
  seatMapState.dirty = false;
}

function clampSeatMapZoom(nextZoom) {
  return Math.min(4, Math.max(0.35, Number(nextZoom.toFixed(2))));
}

function setSeatMapZoom(nextZoom) {
  seatMapState.zoom = clampSeatMapZoom(nextZoom);
  renderSeatMap();
}

function getDxfCanvasSize() {
  return {
    width: Math.max(960, seatMapBoardWrap?.clientWidth || seatMapBoard?.clientWidth || 960),
    height: Math.max(680, seatMapBoardWrap?.clientHeight || seatMapBoard?.clientHeight || 680),
  };
}

function centerSeatMapBoard() {
  fitDxfSeatMapBoard();
}

function fitDxfSeatMapBoard() {
  const viewerData = seatMapState.seatMap?.viewer_data;
  if (!viewerData) return;
  const world = viewerData.meta?.world;
  const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
  if (!world || !canvas) return;
  const rect = canvas.getBoundingClientRect();
  const pad = 36;
  const scaleX = (rect.width - pad * 2) / Math.max(Number(world.width || 1), 1);
  const scaleY = (rect.height - pad * 2) / Math.max(Number(world.height || 1), 1);
  seatMapState.zoom = clampSeatMapZoom(Math.min(scaleX, scaleY));
  seatMapState.viewerOffsetX =
    pad - Number(world.min_x) * seatMapState.zoom + (rect.width - pad * 2 - Number(world.width) * seatMapState.zoom) / 2;
  seatMapState.viewerOffsetY =
    pad - Number(world.min_y) * seatMapState.zoom + (rect.height - pad * 2 - Number(world.height) * seatMapState.zoom) / 2;
  drawDxfCanvasViewer();
}

function zoomDxfSeatMapAtPoint(clientX, clientY, factor) {
  const viewerData = seatMapState.seatMap?.viewer_data;
  const world = viewerData?.meta?.world;
  const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
  if (!viewerData || !world || !canvas) return;

  const rect = canvas.getBoundingClientRect();
  const mx = clientX - rect.left;
  const my = clientY - rect.top;
  const before = screenToWorld(mx, my, world);
  const nextZoom = clampSeatMapZoom(seatMapState.zoom * factor);
  if (nextZoom === seatMapState.zoom) return;
  seatMapState.zoom = nextZoom;
  const after = worldToScreen(before.x, before.y, world);
  seatMapState.viewerOffsetX += mx - after.x;
  seatMapState.viewerOffsetY += my - after.y;
  drawDxfCanvasViewer();
}

function getHoveredSeatMapSlotMeta() {
  if (seatMapState.hoveredSlotId == null) return null;
  const slot = getSeatSlotMap().get(Number(seatMapState.hoveredSlotId));
  if (!slot) return null;
  const placement = getSlotPlacementMap().get(Number(slot.id));
  const member = placement ? getMemberMap().get(Number(placement.member_id)) : null;
  return {
    label: slot.label || `SLOT-${slot.id}`,
    memberName: member?.name || "",
  };
}

function updateSeatMapViewerHoverChip() {
  const chip = seatMapBoard?.querySelector("[data-seatmap-chip='hover']");
  if (!chip) return;
  const hoveredMeta = getHoveredSeatMapSlotMeta();
  chip.textContent = hoveredMeta
    ? `hover ${hoveredMeta.label}${hoveredMeta.memberName ? ` · ${hoveredMeta.memberName}` : ""}`
    : "hover none";
}

function worldToScreen(x, y, world) {
  return {
    x: x * seatMapState.zoom + seatMapState.viewerOffsetX,
    y: (Number(world.max_y) - y + Number(world.min_y)) * seatMapState.zoom + seatMapState.viewerOffsetY,
  };
}

function screenToWorld(x, y, world) {
  return {
    x: (x - seatMapState.viewerOffsetX) / seatMapState.zoom,
    y: Number(world.max_y) + Number(world.min_y) - (y - seatMapState.viewerOffsetY) / seatMapState.zoom,
  };
}

function pickViewerChair(screenX, screenY, viewerData) {
  const world = viewerData.meta.world;
  const threshold = 12;
  let best = null;

  for (const chair of viewerData.chairs || []) {
    const min = worldToScreen(Number(chair.min_x), Number(chair.max_y), world);
    const max = worldToScreen(Number(chair.max_x), Number(chair.min_y), world);
    const left = Math.min(min.x, max.x) - threshold;
    const right = Math.max(min.x, max.x) + threshold;
    const top = Math.min(min.y, max.y) - threshold;
    const bottom = Math.max(min.y, max.y) + threshold;
    if (screenX < left || screenX > right || screenY < top || screenY > bottom) continue;

    let dist = Infinity;
    for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
      const segment = viewerData.chair_segments[index];
      if (!segment) continue;
      const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
      const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
      const dx = b.x - a.x;
      const dy = b.y - a.y;
      const len2 = dx * dx + dy * dy;
      let segDist;
      if (len2 === 0) {
        segDist = Math.hypot(screenX - a.x, screenY - a.y);
      } else {
        let t = ((screenX - a.x) * dx + (screenY - a.y) * dy) / len2;
        t = Math.max(0, Math.min(1, t));
        const px = a.x + t * dx;
        const py = a.y + t * dy;
        segDist = Math.hypot(screenX - px, screenY - py);
      }
      if (segDist < dist) dist = segDist;
      if (dist <= threshold) break;
    }

    if (dist > threshold) continue;
    if (!best || dist < best.dist) {
      best = { chair, dist };
    }
  }

  return best?.chair || null;
}

function drawViewerSegments(ctx, viewerData) {
  const world = viewerData.meta.world;
  const bgSegments = viewerData.background_segments || [];
  const chairSegments = viewerData.chair_segments || [];
  const placementMap = getSlotPlacementMap();
  const slotByKey = new Map((seatMapState.slots || []).map((slot) => [String(slot.slot_key), slot]));

  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.fillStyle = "#ffffff";
  ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

  ctx.save();
  ctx.strokeStyle = "rgba(21,35,48,0.10)";
  ctx.lineWidth = 1;
  for (let index = 0; index < bgSegments.length; index += 1) {
    const segment = bgSegments[index];
    const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
    const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
    ctx.beginPath();
    ctx.moveTo(a.x, a.y);
    ctx.lineTo(b.x, b.y);
    ctx.stroke();
  }
  ctx.restore();

  for (const chair of viewerData.chairs || []) {
    const slot = slotByKey.get(String(chair.key));
    const occupied = slot ? placementMap.has(Number(slot.id)) : false;
    const hovered = slot && Number(slot.id) === seatMapState.hoveredSlotId;
    ctx.save();
    ctx.strokeStyle = occupied
      ? "rgba(220, 38, 38, 0.98)"
      : hovered
        ? "rgba(15, 118, 110, 0.98)"
        : "rgba(15, 118, 110, 0.82)";
    ctx.lineWidth = occupied ? 2.8 : hovered ? 2.2 : 1.5;
    ctx.lineCap = "round";
    ctx.lineJoin = "round";
    ctx.beginPath();
    for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
      const segment = chairSegments[index];
      if (!segment) continue;
      const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
      const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
      ctx.moveTo(a.x, a.y);
      ctx.lineTo(b.x, b.y);
    }
    ctx.stroke();
    ctx.restore();
  }
}

function drawDxfCanvasViewer() {
  const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
  const viewerData = seatMapState.seatMap?.viewer_data;
  if (!canvas || !viewerData) return;
  const ctx = canvas.getContext("2d");
  const pixelRatio = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();
  canvas.width = Math.round(rect.width * pixelRatio);
  canvas.height = Math.round(rect.height * pixelRatio);
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
  drawViewerSegments(ctx, viewerData);
  updateSeatMapViewerHoverChip();
}

function setupDxfCanvasViewer() {
  const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
  const viewerData = seatMapState.seatMap?.viewer_data;
  if (!canvas || !viewerData) return;

  canvas.addEventListener("pointerdown", (event) => {
    seatMapState.viewerDragging = true;
    seatMapState.viewerDragStartX = event.clientX;
    seatMapState.viewerDragStartY = event.clientY;
    seatMapState.viewerDragOffsetX = seatMapState.viewerOffsetX;
    seatMapState.viewerDragOffsetY = seatMapState.viewerOffsetY;
    canvas.classList.add("dragging");
  });

  canvas.addEventListener("pointermove", (event) => {
    const rect = canvas.getBoundingClientRect();
    seatMapState.viewerPointerX = event.clientX - rect.left;
    seatMapState.viewerPointerY = event.clientY - rect.top;
    if (seatMapState.viewerDragging) {
      seatMapState.viewerOffsetX = seatMapState.viewerDragOffsetX + (event.clientX - seatMapState.viewerDragStartX);
      seatMapState.viewerOffsetY = seatMapState.viewerDragOffsetY + (event.clientY - seatMapState.viewerDragStartY);
      drawDxfCanvasViewer();
      return;
    }
    const chair = pickViewerChair(seatMapState.viewerPointerX, seatMapState.viewerPointerY, viewerData);
    const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
    const nextSlotId = slot ? Number(slot.id) : null;
    if (nextSlotId !== seatMapState.hoveredSlotId) {
      seatMapState.hoveredSlotId = nextSlotId;
      drawDxfCanvasViewer();
    }
  });

  canvas.addEventListener("pointerleave", () => {
    seatMapState.hoveredSlotId = null;
    drawDxfCanvasViewer();
  });

  canvas.addEventListener("pointerup", () => {
    if (!seatMapState.viewerDragging) return;
    seatMapState.viewerDragging = false;
    canvas.classList.remove("dragging");
  });

  canvas.addEventListener("click", (event) => {
    const rect = canvas.getBoundingClientRect();
    const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
    const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
    seatMapState.hoveredSlotId = slot ? Number(slot.id) : null;
    drawDxfCanvasViewer();
  });

  canvas.addEventListener("wheel", (event) => {
    event.preventDefault();
    zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
  }, { passive: false });

  fitDxfSeatMapBoard();
}

function getSeatSlotMap() {
  return new Map((seatMapState.slots || []).map((slot) => [Number(slot.id), slot]));
}

function getMemberMap() {
  return new Map(seatMapState.members.map((member) => [Number(member.id), member]));
}

function getPlacementForMember(memberId) {
  return getPlacementSource().find((item) => Number(item.member_id) === Number(memberId)) || null;
}

function memberMatchesSeatMapSearch(member) {
  const keyword = seatMapState.search.trim().toLowerCase();
  if (!keyword) return true;
  const haystack = `${member.name || ""} ${member.department || ""} ${member.team || ""} ${member.rank || ""}`.toLowerCase();
  return haystack.includes(keyword);
}

function getSidebarMembers() {
  const members = isSeatMapAdminMode()
    ? getUnassignedMembers()
    : seatMapState.members.filter((member) => Boolean(getPlacementForMember(Number(member.id))));
  return members.filter(memberMatchesSeatMapSearch);
}

function focusSeatMapMember(memberId) {
  const placement = getPlacementForMember(memberId);
  const member = getMemberMap().get(Number(memberId));
  if (!placement) {
    setSeatMapStatus("해당 인원은 아직 배치되지 않았습니다.", "info");
    return;
  }
  const slot = getSeatSlotMap().get(Number(placement.seat_slot_id));
  if (!slot) {
    setSeatMapStatus("좌석 정보를 찾지 못했습니다.", "error");
    return;
  }
  const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
  if (!frame?.contentWindow) {
    setSeatMapStatus("현재 선택한 사무실 도면에서는 좌석을 표시할 수 없습니다.", "info");
    return;
  }
  frame.contentWindow.postMessage(
    { type: "seatmap-focus-chair", key: String(slot.slot_key), padding: 2600 },
    window.location.origin,
  );
  setSeatMapStatus(`${member?.name || "구성원"} 좌석으로 이동했습니다.`, "info");
}

function renderSeatMapOfficeTabs() {
  if (!seatMapOfficeTabs) return;
  seatMapOfficeTabs.innerHTML = seatMapOffices.map((office) => `
    <button
      class="seatmap-office-tab${seatMapState.officeKey === office.key ? " active" : ""}"
      type="button"
      data-seatmap-office="${office.key}"
    >${escapeHtml(office.label)}</button>
  `).join("");
}

function getUnassignedMembers() {
  const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
  return seatMapState.members.filter((member) => {
    if (placedIds.has(Number(member.id))) return false;
    return memberMatchesSeatMapSearch(member);
  });
}

function getPlacedMembers() {
  const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
  return seatMapState.members.filter((member) => {
    if (!placedIds.has(Number(member.id))) return false;
    return memberMatchesSeatMapSearch(member);
  });
}

function getCellPlacementMap() {
  const cellMap = new Map();
  getPlacementSource().forEach((item) => {
    cellMap.set(`${item.row_index}:${item.col_index}`, item);
  });
  return cellMap;
}

function getSlotPlacementMap() {
  const slotMap = new Map();
  getPlacementSource().forEach((item) => {
    if (item.seat_slot_id != null) {
      slotMap.set(Number(item.seat_slot_id), item);
    }
  });
  return slotMap;
}

function upsertDraftPlacement(memberId, rowIndex, colIndex) {
  const cellMap = getCellPlacementMap();
  const existing = cellMap.get(`${rowIndex}:${colIndex}`);
  if (existing && Number(existing.member_id) !== Number(memberId)) {
    setSeatMapStatus("이미 사용 중인 칸입니다. 빈 칸으로 이동해주세요.", "error");
    return;
  }

  const nextPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId));
  nextPlacements.push({
    member_id: Number(memberId),
    row_index: Number(rowIndex),
    col_index: Number(colIndex),
    seat_label: computeSeatLabel(rowIndex, colIndex),
  });
  seatMapState.draftPlacements = nextPlacements.sort((left, right) => {
    if (left.row_index !== right.row_index) return left.row_index - right.row_index;
    return left.col_index - right.col_index;
  });
  seatMapState.dirty = true;
  setSeatMapStatus("배치를 수정했습니다. 저장 버튼으로 반영하세요.", "info");
}

function upsertDraftPlacementForSlot(memberId, seatSlotId) {
  const placementMap = getSlotPlacementMap();
  const existing = placementMap.get(Number(seatSlotId));
  if (existing && Number(existing.member_id) !== Number(memberId)) {
    setSeatMapStatus("이미 사용 중인 좌석입니다. 빈 좌석으로 이동해주세요.", "error");
    return;
  }

  const seatSlot = getSeatSlotMap().get(Number(seatSlotId));
  const nextPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId));
  nextPlacements.push({
    member_id: Number(memberId),
    seat_slot_id: Number(seatSlotId),
    row_index: 0,
    col_index: 0,
    seat_label: seatSlot?.label || `SLOT-${seatSlotId}`,
  });
  seatMapState.draftPlacements = nextPlacements;
  seatMapState.dirty = true;
  setSeatMapStatus("배치를 수정했습니다. 저장 버튼으로 반영하세요.", "info");
}

function removeDraftPlacement(memberId) {
  const before = seatMapState.draftPlacements.length;
  seatMapState.draftPlacements = seatMapState.draftPlacements.filter((item) => Number(item.member_id) !== Number(memberId));
  if (seatMapState.draftPlacements.length !== before) {
    seatMapState.dirty = true;
    setSeatMapStatus("구성원을 미배치 목록으로 이동했습니다. 저장 버튼으로 반영하세요.", "info");
  }
}

function renderMemberCard(member, draggable) {
  const photoUrl = member.photo_url ? escapeHtml(member.photo_url) : "";
  const avatar = photoUrl
    ? `<span class="seatmap-member-avatar"><img src="${photoUrl}" alt="${escapeHtml(member.name)}"></span>`
    : `<span class="seatmap-member-avatar seatmap-member-avatar-fallback">${escapeHtml(getInitials(member.name))}</span>`;
  return `
    <div class="seatmap-member-card${draggable ? " draggable" : ""}" draggable="${draggable}" data-member-id="${Number(member.id)}">
      ${avatar}
      <span class="seatmap-member-text">
        <strong>${escapeHtml(member.name || "-")}</strong>
        <em>${escapeHtml(member.department || member.team || member.rank || "-")}</em>
      </span>
    </div>
  `;
}

function renderUnassignedMemberCard(member, draggable) {
  return `
    <div class="seatmap-member-card seatmap-member-card-compact${draggable ? " draggable" : ""}" draggable="${draggable}" data-member-id="${Number(member.id)}">
      <span class="seatmap-member-text seatmap-member-text-inline">
        <strong>${escapeHtml(member.name || "-")}</strong>
        <em>${escapeHtml(member.rank || "-")}</em>
      </span>
    </div>
  `;
}

function renderSeatMapSearchCard(member) {
  const placement = getPlacementForMember(Number(member.id));
  if (!placement) return "";
  const badge = `<span class="seatmap-member-badge occupied">${escapeHtml(placement.seat_label || "배치완료")}</span>`;
  return `
    <button class="seatmap-member-search-card" type="button" data-member-id="${Number(member.id)}">
      <span class="seatmap-member-text seatmap-member-text-inline">
        <strong>${escapeHtml(member.name || "-")}</strong>
        <em>${escapeHtml(member.rank || member.department || "-")}</em>
      </span>
      ${badge}
    </button>
  `;
}

function renderSeatMapBoard() {
  if (!seatMapBoard || !seatMapState.seatMap) return;

  if (seatMapState.seatMap.source_type === "dxf" || seatMapState.seatMap.source_type === "fixed_html") {
    renderDxfSeatMapBoard();
    return;
  }

  const memberMap = getMemberMap();
  const placementMap = getCellPlacementMap();
  const rows = Number(seatMapState.seatMap.grid_rows || 0);
  const cols = Number(seatMapState.seatMap.grid_cols || 0);
  const gap = Number(seatMapState.seatMap.cell_gap || 0);
  const editable = seatMapState.editMode && isAdmin();
  const cells = [];

  for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
    for (let colIndex = 0; colIndex < cols; colIndex += 1) {
      const key = `${rowIndex}:${colIndex}`;
      const placement = placementMap.get(key);
      const member = placement ? memberMap.get(Number(placement.member_id)) : null;
      cells.push(`
        <div class="seatmap-cell${placement ? " occupied" : ""}${editable ? " editable" : ""}" data-row="${rowIndex}" data-col="${colIndex}">
          <span class="seatmap-cell-label">${escapeHtml(computeSeatLabel(rowIndex, colIndex))}</span>
          ${member ? renderMemberCard(member, editable) : ""}
        </div>
      `);
    }
  }

  seatMapBoard.innerHTML = `
    <div class="seatmap-canvas" style="--seatmap-rows:${rows}; --seatmap-cols:${cols}; --seatmap-gap:${gap}px;">
      <img class="seatmap-image" src="${escapeHtml(seatMapState.seatMap.image_url)}" alt="${escapeHtml(seatMapState.seatMap.name)}">
      <div class="seatmap-grid">${cells.join("")}</div>
    </div>
  `;
}

function renderDxfSeatMapBoard() {
  if (!seatMapBoard || !seatMapState.seatMap) return;
  const viewerData = seatMapState.seatMap.viewer_data;
  if (!viewerData) {
    seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
    return;
  }
  const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildAsOfQuery()}`);
  seatMapBoard.innerHTML = `
    <div class="seatmap-dxf-frame-shell">
      <div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div>
      <iframe
        id="seatmap-dxf-frame"
        class="seatmap-dxf-frame"
        src="${escapeHtml(viewerUrl)}"
        title="${escapeHtml(seatMapState.seatMap.name || "DXF Viewer")}"
        loading="eager"
        referrerpolicy="same-origin"
      ></iframe>
    </div>
  `;
  setupSeatMapViewerFrame();
}

function setSeatMapDropOverlayActive(active) {
  const overlay = seatMapBoard?.querySelector("[data-seatmap-drop-overlay]");
  if (!overlay) return;
  overlay.classList.toggle("is-active", Boolean(active && seatMapState.editMode));
}

function getDraftPlacedSlotKeys() {
  const slotMap = getSeatSlotMap();
  return (seatMapState.draftPlacements || [])
    .map((placement) => slotMap.get(Number(placement.seat_slot_id))?.slot_key)
    .filter(Boolean)
    .map((value) => String(value));
}

function getSeatAssignmentPayload() {
  const slotMap = getSeatSlotMap();
  const memberMap = getMemberMap();
  return getPlacementSource()
    .map((placement) => {
      const slot = slotMap.get(Number(placement.seat_slot_id));
      const member = memberMap.get(Number(placement.member_id));
      if (!slot || !member) return null;
      return {
        key: String(slot.slot_key),
        member_id: Number(member.id),
        name: member.name || "-",
        rank: member.rank || "-",
      };
    })
    .filter(Boolean);
}

function syncSeatMapViewerFrame() {
  const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
  if (!frame?.contentWindow) return;
  frame.contentWindow.postMessage(
    { type: "seatmap-set-mode", mode: isSeatMapAdminMode() ? "default" : "readonly" },
    window.location.origin,
  );
  frame.contentWindow.postMessage(
    { type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
    window.location.origin,
  );
  frame.contentWindow.postMessage(
    { type: "seatmap-set-assignments", items: getSeatAssignmentPayload() },
    window.location.origin,
  );
}

function scheduleSeatMapViewerSync() {
  syncSeatMapViewerFrame();
  window.setTimeout(() => {
    syncSeatMapViewerFrame();
  }, 80);
}

function renderSeatMapActions() {
  const hasSeatMap = Boolean(seatMapState.seatMap);
  const adminMode = isSeatMapAdminMode();
  if (seatMapSaveBtn) {
    seatMapSaveBtn.hidden = !adminMode || !hasSeatMap;
    seatMapSaveBtn.disabled = !seatMapState.dirty;
  }
  if (seatMapExitBtn) {
    seatMapExitBtn.hidden = !hasSeatMap;
  }
  if (seatMapActions) {
    seatMapActions.hidden = !hasSeatMap;
  }
}

function updateSeatMapDraftUi() {
  renderSeatMapActions();
  renderUnassignedMembers();
  syncSeatMapViewerFrame();
}

function setupSeatMapViewerFrame() {
  const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
  const overlay = seatMapBoard?.querySelector("[data-seatmap-drop-overlay]");
  if (!frame) return;

  frame.addEventListener("load", () => {
    syncSeatMapViewerFrame();
    if (!seatMapState.editMode) return;
    const frameWindow = frame.contentWindow;
    const frameDocument = frame.contentDocument;
    const canvas = frameDocument?.getElementById("canvas");
    if (!frameWindow || !frameDocument || !canvas || !frameWindow.__mhSeatmap) return;

    const handleDrop = (event) => {
      event.preventDefault();
      const memberId = getDraggedMemberId(event);
      if (!memberId) {
        setSeatMapStatus("드롭 감지됨: memberId를 읽지 못했습니다.", "error");
        return;
      }
      const frameRect = frame.getBoundingClientRect();
      const canvasRect = canvas.getBoundingClientRect();
      const picked = frameWindow.__mhSeatmap.pickChairAt(
        event.clientX - frameRect.left - canvasRect.left,
        event.clientY - frameRect.top - canvasRect.top,
      );
      if (!picked?.key) {
        setSeatMapStatus(`드롭 감지됨: 좌석 인식 실패 (memberId=${memberId})`, "error");
        return;
      }
      const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(picked.key));
      if (!matchedSlot) {
        setSeatMapStatus(`드롭 감지됨: slot 매칭 실패 (${picked.key})`, "error");
        return;
      }
      upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
      setSeatMapStatus(`드롭 성공: memberId=${memberId}, slot=${picked.key}, slotId=${matchedSlot.id}`, "info");
      updateSeatMapDraftUi();
    };

    const handleDragOver = (event) => {
      event.preventDefault();
      event.dataTransfer.dropEffect = "move";
    };

    canvas.addEventListener("dragover", handleDragOver);
    canvas.addEventListener("drop", handleDrop);
    overlay?.addEventListener("dragover", handleDragOver);
    overlay?.addEventListener("drop", handleDrop);
  }, { once: true });
}

function renderAdminSeatMapSidebar() {
  const unassignedMembers = getUnassignedMembers();
  const placedMembers = getPlacedMembers();
  if (seatMapSidebarTitle) {
    seatMapSidebarTitle.textContent = "전체 인원";
  }
  if (seatMapSidebarDesc) {
    seatMapSidebarDesc.textContent = "미배치 인원은 상단, 배치 완료 인원은 하단에 표시됩니다.";
  }
  if (!unassignedMembers.length && !placedMembers.length) {
    seatMapUnassigned.innerHTML = `
      <div class="seatmap-list-empty">
        ${seatMapState.search ? "검색 결과가 없습니다." : "표시할 인원이 없습니다."}
      </div>
    `;
    return;
  }
  seatMapUnassigned.innerHTML = `
    <section class="seatmap-member-section">
      <div class="seatmap-member-section-head">
        <strong>미배치 인원</strong>
        <span>${unassignedMembers.length}명</span>
      </div>
      <div class="seatmap-member-section-list">
        ${unassignedMembers.length
          ? unassignedMembers.map((member) => renderUnassignedMemberCard(member, true)).join("")
          : '<div class="seatmap-list-empty seatmap-list-empty-inline">미배치 인원이 없습니다.</div>'}
      </div>
    </section>
    <section class="seatmap-member-section">
      <div class="seatmap-member-section-head">
        <strong>배치 완료</strong>
        <span>${placedMembers.length}명</span>
      </div>
      <div class="seatmap-member-section-list">
        ${placedMembers.length
          ? placedMembers.map((member) => renderSeatMapSearchCard(member)).join("")
          : '<div class="seatmap-list-empty seatmap-list-empty-inline">배치된 인원이 없습니다.</div>'}
      </div>
    </section>
  `;
}

function renderReadonlySeatMapSidebar() {
  const members = getSidebarMembers();
  if (seatMapSidebarTitle) {
    seatMapSidebarTitle.textContent = "배치 인원 검색";
  }
  if (seatMapSidebarDesc) {
    seatMapSidebarDesc.textContent = "이름이나 부서를 검색하고 클릭하면 해당 좌석으로 바로 확대 이동합니다.";
  }
  if (!members.length) {
    seatMapUnassigned.innerHTML = `
      <div class="seatmap-list-empty">
        ${seatMapState.search ? "검색 결과가 없습니다." : "배치된 인원이 없습니다."}
      </div>
    `;
    return;
  }
  seatMapUnassigned.innerHTML = members.map((member) => renderSeatMapSearchCard(member)).join("");
}

function renderUnassignedMembers() {
  if (!seatMapUnassigned) return;
  if (isSeatMapAdminMode()) {
    renderAdminSeatMapSidebar();
    return;
  }
  renderReadonlySeatMapSidebar();
}

function renderSeatMapEmpty() {
  if (!seatMapEmpty) return;
  const office = getCurrentSeatMapOffice();
  if (seatMapState.seatMap) {
    seatMapEmpty.classList.add("hidden");
    seatMapEmpty.innerHTML = "";
    return;
  }

  seatMapEmpty.classList.remove("hidden");
  seatMapEmpty.innerHTML = `
    <div class="seatmap-empty-card">
      <strong>${escapeHtml(office.label)} 도면이 아직 준비되지 않았습니다.</strong>
      <p>${office.ready ? (canEditSeatMap() ? "오른쪽 설정 패널에서 이미지와 그리드를 등록하세요." : "관리자에게 자리배치도 등록을 요청하세요.") : "도면 파일을 추후 연결하면 여기서 바로 전환해 볼 수 있습니다."}</p>
    </div>
  `;
}

function syncSeatMapSettingsForm() {
  if (!seatMapSettingsForm) return;
  if (seatMapFormName) {
    seatMapFormName.value = seatMapState.seatMap?.name || "";
  }
  if (seatMapFormRows) {
    seatMapFormRows.value = seatMapState.seatMap?.grid_rows || 12;
  }
  if (seatMapFormCols) {
    seatMapFormCols.value = seatMapState.seatMap?.grid_cols || 24;
  }
  if (seatMapFormGap) {
    seatMapFormGap.value = seatMapState.seatMap?.cell_gap ?? 2;
  }
  if (seatMapFormImage) {
    seatMapFormImage.value = "";
  }
  if (seatMapFileName) {
    seatMapFileName.textContent = "선택된 파일 없음";
  }
}

function renderSeatMap() {
  const hasSeatMap = Boolean(seatMapState.seatMap);
  const admin = isSeatMapAdminMode();
  const fixedViewerMap = seatMapState.seatMap?.source_type === "fixed_html";
  const office = getCurrentSeatMapOffice();

  if (seatMapName) {
    seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : office.label;
  }
  if (seatMapStatus) {
    seatMapStatus.textContent = seatMapState.status;
    seatMapStatus.dataset.tone = seatMapState.statusTone;
  }
  if (seatMapSettingsPanel) {
    seatMapSettingsPanel.classList.toggle("hidden", !admin || fixedViewerMap);
  }
  renderSeatMapActions();
  if (seatMapSettingsForm) {
    seatMapSettingsForm.querySelector("button[type='submit']").textContent = hasSeatMap ? "배치도 저장" : "배치도 생성";
  }
  renderSeatMapOfficeTabs();

  renderSeatMapEmpty();
  if (seatMapBoardWrap) {
    seatMapBoardWrap.classList.toggle("hidden", !hasSeatMap);
  }
  if (hasSeatMap) {
    renderSeatMapBoard();
  } else if (seatMapBoard) {
    seatMapBoard.innerHTML = "";
  }
  renderUnassignedMembers();
}

function handleEmbeddedNavigationMessage(event) {
  const data = event.data;
  if (!data || typeof data !== "object") return;
  if (data.type === "open-seatmap") {
    hideUserPopover();
    seatMapState.forceReadOnly = Boolean(data.readOnly);
    setActiveView(data.readOnly ? "seatmap-readonly" : "seatmap-admin");
  }
  if (data.type === "open-organization") {
    hideUserPopover();
    setActiveView("organization");
  }
  if (data.type === "seatmap-clear-slot" && canEditSeatMap()) {
    const cleared = clearDraftPlacementBySlotKey(String(data.key || ""));
    if (cleared) {
      setSeatMapStatus("구성원을 공석으로 이동했습니다. 저장 버튼으로 반영하세요.", "info");
      updateSeatMapDraftUi();
    }
  }
}

async function fetchJson(url, options) {
  const requestOptions = {
    ...options,
    headers: buildAuthHeaders(options?.headers),
  };
  const response = await fetch(resolveAppUrl(url), requestOptions);
  let payload = null;
  try {
    payload = await response.json();
  } catch {
    payload = null;
  }
  if (!response.ok) {
    const message = payload?.detail || "요청 처리에 실패했습니다.";
    const error = new Error(message);
    error.status = response.status;
    throw error;
  }
  return payload;
}

async function loadSeatMapData(force = false) {
  if (seatMapState.loading || (seatMapState.loaded && !force)) return;
  seatMapState.loading = true;
  setSeatMapStatus("자리배치도를 불러오는 중입니다.", "info");
  renderSeatMap();

  try {
    const office = getCurrentSeatMapOffice();
    const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`);
    const activeSeatMap = activePayload.item;
    const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout${buildAsOfQuery()}`);
    seatMapState.seatMap = {
      ...(layoutPayload.seat_map || {}),
      viewer_data: layoutPayload.viewer_data || null,
    };
    seatMapState.members = Array.isArray(layoutPayload.members) ? layoutPayload.members : [];
    seatMapState.slots = Array.isArray(layoutPayload.slots) ? layoutPayload.slots : [];
    seatMapState.placements = clonePlacements(layoutPayload.placements || []);
    seatMapState.zoom = 1;
    seatMapState.hoveredSlotId = null;
    seatMapState.editMode = canEditSeatMap();
    resetSeatMapDraft();
    seatMapState.loaded = true;
    setSeatMapStatus(canEditSeatMap() ? "구성원을 바로 드래그해서 배치한 뒤 저장하세요." : "자리배치도를 불러왔습니다.", "success");
    syncSeatMapSettingsForm();
  } catch (error) {
    if (error.status === 404) {
      seatMapState.seatMap = null;
      seatMapState.members = [];
      seatMapState.slots = [];
      seatMapState.placements = [];
      seatMapState.zoom = 1;
      seatMapState.hoveredSlotId = null;
      seatMapState.editMode = canEditSeatMap();
      resetSeatMapDraft();
      seatMapState.loaded = true;
      setSeatMapStatus("활성화된 자리배치도가 없습니다.", "info");
      syncSeatMapSettingsForm();
    } else {
      setSeatMapStatus(error.message || "자리배치도 조회에 실패했습니다.", "error");
    }
  } finally {
    seatMapState.loading = false;
    renderSeatMap();
  }
}

async function getImageDimensions(file) {
  return new Promise((resolve) => {
    const image = new Image();
    const objectUrl = URL.createObjectURL(file);
    image.onload = () => {
      resolve({ width: image.naturalWidth || null, height: image.naturalHeight || null });
      URL.revokeObjectURL(objectUrl);
    };
    image.onerror = () => {
      resolve({ width: null, height: null });
      URL.revokeObjectURL(objectUrl);
    };
    image.src = objectUrl;
  });
}

function clearDraftPlacementBySlotKey(slotKey) {
  const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(slotKey));
  if (!matchedSlot) return false;
  const placement = (seatMapState.draftPlacements || []).find((item) => Number(item.seat_slot_id) === Number(matchedSlot.id));
  if (!placement) return false;
  removeDraftPlacement(Number(placement.member_id));
  return true;
}

async function uploadSeatMapImage(file, name) {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("name", name);
  return fetchJson("/api/seat-maps/dxf", {
    method: "POST",
    body: formData,
  });
}

async function submitSeatMapSettings(event) {
  event.preventDefault();
  if (!canEditSeatMap()) return;

  const name = seatMapFormName?.value?.trim() || "";
  const imageFile = seatMapFormImage?.files?.[0] || null;

  if (!name) {
    setSeatMapStatus("배치도 이름을 입력하세요.", "error");
    return;
  }
  if (!imageFile) {
    setSeatMapStatus("DXF 파일을 선택하세요.", "error");
    return;
  }
  if (!imageFile.name.toLowerCase().endsWith(".dxf")) {
    setSeatMapStatus("DXF 파일만 업로드할 수 있습니다.", "error");
    return;
  }

  try {
    setSeatMapStatus("DXF 자리배치도를 업로드하고 분석하는 중입니다.", "info");
    await uploadSeatMapImage(imageFile, name);
    if (seatMapFormImage) seatMapFormImage.value = "";
    if (seatMapFileName) seatMapFileName.textContent = "선택된 파일 없음";
    await loadSeatMapData(true);
    setSeatMapStatus("DXF 자리배치도를 저장했습니다.", "success");
  } catch (error) {
    setSeatMapStatus(error.message || "DXF 자리배치도 저장에 실패했습니다.", "error");
  } finally {
    renderSeatMap();
  }
}

async function saveSeatLayout() {
  if (!seatMapState.seatMap || !seatMapState.editMode || !seatMapState.dirty) return;
  try {
    setSeatMapStatus("자리배치를 저장하는 중입니다.", "info");
    await fetchJson(`/api/seat-maps/${seatMapState.seatMap.id}/layout`, {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ placements: seatMapState.draftPlacements }),
    });
    await loadSeatMapData(true);
    if (organizationFrame?.contentWindow) {
      organizationFrame.contentWindow.postMessage({ type: "seatmap-layout-updated" }, window.location.origin);
    }
    setSeatMapStatus("자리배치를 저장했습니다.", "success");
  } catch (error) {
    setSeatMapStatus(error.message || "자리배치도 저장에 실패했습니다.", "error");
    renderSeatMap();
  }
}

function cancelSeatMapEdit() {
  resetSeatMapDraft();
  setSeatMapStatus("현재까지 수정한 배치를 취소했습니다.", "info");
  renderSeatMap();
}

function getDraggedMemberId(event) {
  const raw = event.dataTransfer?.getData("text/plain") || seatMapState.draggingMemberId;
  const memberId = Number(raw);
  if (!Number.isInteger(memberId) || memberId <= 0) return null;
  return memberId;
}

function handleSeatMapCellDrop(event) {
  if (!seatMapState.editMode) return;
  event.preventDefault();
  const memberId = getDraggedMemberId(event);
  if (!memberId) return;
  if (isSlotBasedSeatMap()) {
    const slot = event.target.closest(".seatmap-slot");
    if (slot) {
      upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
      renderSeatMap();
      return;
    }
    const viewerData = seatMapState.seatMap.viewer_data;
    const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
    if (!viewerData || !canvas) return;
    const rect = canvas.getBoundingClientRect();
    const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
    if (!chair) return;
    const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key));
    if (!matchedSlot) return;
    upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
  } else {
    const cell = event.target.closest(".seatmap-cell");
    if (!cell) return;
    upsertDraftPlacement(memberId, Number(cell.dataset.row), Number(cell.dataset.col));
  }
  renderSeatMap();
}

function handleSeatMapListDrop(event) {
  if (!seatMapState.editMode) return;
  event.preventDefault();
  const memberId = getDraggedMemberId(event);
  if (!memberId) return;
  removeDraftPlacement(memberId);
  updateSeatMapDraftUi();
}

function setActiveView(view) {
  const previousView = currentView;
  currentView = view in viewLabels ? view : "organization";
  syncSeatMapDomRefs();
  if (currentViewTitle) {
    currentViewTitle.textContent = viewLabels[currentView];
  }
  syncGlobalDateControlVisibility();
  if (shouldShowGlobalDateControls()) {
    ensureGlobalDateRangeLoaded();
  }

  navButtons.forEach((button) => {
    const active = button.dataset.view === currentView;
    button.classList.toggle("active", active);
    button.classList.toggle("muted", !active);
  });

  const isOrganization = currentView === "organization";
  const isProject = currentView === "project";
  const isTeam = currentView === "team";
  const isSeatMapAdmin = currentView === "seatmap-admin";
  const isSeatMapReadonly = currentView === "seatmap-readonly";
  if (organizationStage) {
    organizationStage.hidden = !isOrganization;
    organizationStage.style.display = isOrganization ? "flex" : "none";
  }
  if (projectStage) {
    projectStage.hidden = !isProject;
    projectStage.style.display = isProject ? "flex" : "none";
  }
  if (teamStage) {
    teamStage.hidden = !isTeam;
    teamStage.style.display = isTeam ? "flex" : "none";
  }
  if (seatMapAdminStage) {
    seatMapAdminStage.hidden = !isSeatMapAdmin;
    seatMapAdminStage.style.display = isSeatMapAdmin ? "flex" : "none";
  }
  if (seatMapReadonlyStage) {
    seatMapReadonlyStage.hidden = !isSeatMapReadonly;
    seatMapReadonlyStage.style.display = isSeatMapReadonly ? "flex" : "none";
  }
  if (emptyStage) {
    const showEmpty = !isOrganization && !isProject && !isTeam && !isSeatMapAdmin && !isSeatMapReadonly;
    emptyStage.hidden = !showEmpty;
    emptyStage.style.display = showEmpty ? "flex" : "none";
  }

  if (isOrganization && previousView !== "organization" && organizationFrame) {
    const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
    organizationFrame.src = resolveAppUrl(frameSrc);
  }
  if (isProject && previousView !== "project" && projectFrame) {
    const frameSrc = projectFrame.dataset.src || projectFrame.src;
    projectFrame.src = resolveAppUrl(frameSrc);
  } else if (isProject) {
    postGlobalDateRangeToFrame(projectFrame);
  }
  if (isTeam && previousView !== "team" && teamFrame) {
    const frameSrc = teamFrame.dataset.src || teamFrame.src;
    teamFrame.src = resolveAppUrl(frameSrc);
  } else if (isTeam) {
    postGlobalDateRangeToFrame(teamFrame);
  }
  if (isSeatMapAdmin || isSeatMapReadonly) {
    loadSeatMapData();
  }
  notifyEmbeddedTabActivated();
}

function renderAuth() {
  const session = getSession();
  const authenticated = Boolean(session?.user?.display_name);
  loginPanel.classList.toggle("hidden", authenticated);
  dashboardPanel.classList.toggle("hidden", !authenticated);
  if (authenticated) {
    const displayName = session.user.display_name || "접속자";
    const rank = session.user.rank || "-";
    const employeeId = session.user.username || "-";
    userBadge.innerHTML = `<span class="user-chip-icon">◎</span><span class="user-chip-text"><strong>${escapeHtml(displayName)}</strong><em>${escapeHtml(rank)}</em></span><span class="user-chip-caret" aria-hidden="true">▾</span>`;
    userBadge.title = `${displayName} / -`;
    if (userPopover) {
      userPopover.innerHTML = `
        <div class="user-popover-row">
          <span class="user-popover-label">이름</span>
          <strong>${escapeHtml(displayName)}</strong>
        </div>
        <div class="user-popover-row">
          <span class="user-popover-label">직급</span>
          <span>${escapeHtml(rank)}</span>
        </div>
        <div class="user-popover-row">
          <span class="user-popover-label">권한</span>
          <span>${escapeHtml(session.user.role || "-")}</span>
        </div>
        <div class="user-popover-row">
          <span class="user-popover-label">사번</span>
          <span>${escapeHtml(employeeId)}</span>
        </div>
      `;
    }
  }
  renderSeatMap();
}

if (loginForm) {
  loginForm.addEventListener("submit", async (event) => {
    event.preventDefault();
    loginMessage.textContent = "로그인 처리 중입니다.";
    const formData = new FormData(loginForm);
    try {
      const payload = await fetchJson("/api/auth/login", {
        method: "POST",
        body: formData,
      });
      setSession(payload);
      setActiveView("project");
      loginForm.reset();
      loginMessage.textContent = "";
      renderAuth();
    } catch (error) {
      loginMessage.textContent = error.message || "로그인에 실패했습니다.";
    }
  });
}

if (userBadge) {
  userBadge.addEventListener("click", (event) => {
    event.stopPropagation();
    toggleUserPopover();
  });
}

if (logoutBtn) {
  logoutBtn.addEventListener("click", async (event) => {
    event.stopPropagation();
    try {
      await fetchJson("/api/auth/logout", {
        method: "POST",
      });
    } catch {
      // Ignore logout API errors and clear the local session regardless.
    }
    clearSession();
    hideUserPopover();
    renderAuth();
  });
}

if (globalStartDateInput) {
  globalStartDateInput.addEventListener("change", () => {
    globalDateState.startDate = globalStartDateInput.value || "";
    postGlobalDateRangeToFrame(organizationFrame);
    postGlobalDateRangeToFrame(projectFrame);
    postGlobalDateRangeToFrame(teamFrame);
  });
}

if (globalEndDateInput) {
  globalEndDateInput.addEventListener("change", () => {
    globalDateState.endDate = globalEndDateInput.value || "";
    postGlobalDateRangeToFrame(organizationFrame);
    postGlobalDateRangeToFrame(projectFrame);
    postGlobalDateRangeToFrame(teamFrame);
    if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") {
      seatMapState.loaded = false;
      loadSeatMapData(true);
    }
  });
}

organizationFrame?.addEventListener("load", () => {
  postGlobalDateRangeToFrame(organizationFrame);
});

projectFrame?.addEventListener("load", () => {
  postGlobalDateRangeToFrame(projectFrame);
  if (currentView === "project") {
    notifyEmbeddedTabActivated();
  }
});

teamFrame?.addEventListener("load", () => {
  postGlobalDateRangeToFrame(teamFrame);
  if (currentView === "team") {
    notifyEmbeddedTabActivated();
  }
});

navButtons.forEach((button) => {
  button.addEventListener("click", () => {
    hideUserPopover();
    setActiveView(button.dataset.view || "organization");
  });
});

Object.values(seatMapDom).forEach((dom) => {
  dom.officeTabs?.addEventListener("click", (event) => {
    const button = event.target.closest("[data-seatmap-office]");
    if (!button) return;
    const officeKey = button.dataset.seatmapOffice || "";
    if (!officeKey || officeKey === seatMapState.officeKey) return;
    seatMapState.officeKey = officeKey;
    seatMapState.loaded = false;
    seatMapState.search = "";
    Object.values(seatMapDom).forEach((item) => {
      if (item.search) item.search.value = "";
    });
    loadSeatMapData(true);
  });

  dom.settingsForm?.addEventListener("submit", submitSeatMapSettings);
  dom.saveBtn?.addEventListener("click", saveSeatLayout);
  dom.exitBtn?.addEventListener("click", () => {
    if (dom === seatMapDom.admin && seatMapState.dirty) {
      const confirmed = window.confirm("저장하지 않고 나가면 저장하지 않은 내용은 사라집니다. 나가시겠습니까?");
      if (!confirmed) return;
      resetSeatMapDraft();
    }
    hideUserPopover();
    setActiveView("organization");
  });
  dom.search?.addEventListener("input", () => {
    seatMapState.search = dom.search.value || "";
    const otherMode = dom === seatMapDom.admin ? seatMapDom.readonly : seatMapDom.admin;
    if (otherMode.search) otherMode.search.value = seatMapState.search;
    renderUnassignedMembers();
  });

  dom.unassigned?.addEventListener("click", (event) => {
    const button = event.target.closest("[data-member-id]");
    if (!button) return;
    if (button.classList.contains("seatmap-member-search-card")) {
      focusSeatMapMember(Number(button.dataset.memberId));
      return;
    }
    if (canEditSeatMap()) return;
    focusSeatMapMember(Number(button.dataset.memberId));
  });
  dom.unassigned?.addEventListener("dragover", (event) => {
    if (!seatMapState.editMode) return;
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  });
  dom.unassigned?.addEventListener("drop", handleSeatMapListDrop);
});

Object.values(seatMapDom).forEach((dom) => {
  dom.formImage?.addEventListener("change", () => {
    if (dom.fileName) {
      dom.fileName.textContent = dom.formImage.files?.[0]?.name || "선택된 파일 없음";
    }
  });
});

Object.values(seatMapDom).forEach((dom) => {
  dom.board?.addEventListener("wheel", (event) => {
    if (!isSlotBasedSeatMap()) return;
    event.preventDefault();
    zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
  }, { passive: false });
  dom.board?.addEventListener("click", (event) => {
    const fitButton = event.target.closest("[data-seatmap-action='fit']");
    if (!fitButton) return;
    fitDxfSeatMapBoard();
  });
  dom.board?.addEventListener("dragover", (event) => {
    if (!seatMapState.editMode) return;
    const target = isSlotBasedSeatMap()
      ? (event.target.closest(".seatmap-slot") || event.target.closest("#seatmap-dxf-canvas"))
      : event.target.closest(".seatmap-cell");
    if (!target) return;
    event.preventDefault();
    event.dataTransfer.dropEffect = "move";
  });
  dom.board?.addEventListener("drop", handleSeatMapCellDrop);

  dom.boardWrap?.addEventListener("mousedown", (event) => {
    if (!isSlotBasedSeatMap()) return;
    if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
    if (event.button !== 0) return;
    if (event.target.closest(".seatmap-member-card, button, input, label")) return;
    event.preventDefault();
    seatMapState.panning = true;
    seatMapState.panStartX = event.clientX;
    seatMapState.panStartY = event.clientY;
    seatMapState.panScrollLeft = seatMapBoardWrap.scrollLeft;
    seatMapState.panScrollTop = seatMapBoardWrap.scrollTop;
    seatMapBoardWrap.classList.add("is-panning");
  });
  dom.boardWrap?.addEventListener("mouseleave", () => {
    if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
    if (seatMapState.hoveredSlotId == null) return;
    seatMapState.hoveredSlotId = null;
    updateSeatMapViewerHoverChip();
  });
  dom.boardWrap?.addEventListener("mousemove", (event) => {
    if (!isSlotBasedSeatMap()) return;
    if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
    const slot = event.target.closest(".seatmap-slot");
    const nextSlotId = slot ? Number(slot.dataset.slotId) : null;
    if (nextSlotId === seatMapState.hoveredSlotId) return;
    seatMapState.hoveredSlotId = nextSlotId;
    updateSeatMapViewerHoverChip();
  });
});

document.addEventListener("mousemove", (event) => {
  if (!seatMapState.panning || !seatMapBoardWrap) return;
  const deltaX = event.clientX - seatMapState.panStartX;
  const deltaY = event.clientY - seatMapState.panStartY;
  seatMapBoardWrap.scrollLeft = seatMapState.panScrollLeft - deltaX;
  seatMapBoardWrap.scrollTop = seatMapState.panScrollTop - deltaY;
});

document.addEventListener("mouseup", () => {
  if (!seatMapState.panning || !seatMapBoardWrap) return;
  seatMapState.panning = false;
  seatMapBoardWrap.classList.remove("is-panning");
});


document.addEventListener("dragstart", (event) => {
  const card = event.target.closest(".seatmap-member-card");
  if (!seatMapState.editMode || !card) return;
  const memberId = Number(card.dataset.memberId);
  if (!memberId) return;
  seatMapState.draggingMemberId = memberId;
  setSeatMapDropOverlayActive(true);
  event.dataTransfer.effectAllowed = "move";
  event.dataTransfer.setData("text/plain", String(memberId));
});

document.addEventListener("dragend", () => {
  seatMapState.draggingMemberId = null;
  setSeatMapDropOverlayActive(false);
});

document.addEventListener("click", () => {
  hideUserPopover();
});

window.addEventListener("message", handleEmbeddedNavigationMessage);

syncGlobalDateControlInputs();
setActiveView(currentView);
renderAuth();

window.addEventListener("resize", () => {
  if (!isSlotBasedSeatMap() || (currentView !== "seatmap-admin" && currentView !== "seatmap-readonly")) return;
  requestAnimationFrame(() => {
    if (seatMapState.zoom === 1) {
      centerSeatMapBoard();
    }
  });
});
