import './style.css';
import { Camera } from '@game/camera';
import { GameRenderer, screenToWorld, worldToScreen } from '@game/renderer';
import { buildWorldTerrainContext, townsApiToInfluences } from '@game/procedural';
import type { BuildingData, NPCData, TownData, TownObjectData } from '@game/types';

type AdminTown = {
  id: number;
  name: string;
  nameKey?: string;
  biome: string;
  worldX: number;
  worldY: number;
  radius: number;
  levelMin: number;
  levelMax: number;
};

type AdminNPC = {
  id: number;
  townId: number;
  name: string;
  nameKey?: string;
  type: NPCData['type'];
  offsetX: number;
  offsetY: number;
  buildingId?: number | null;
};

type AdminBuilding = {
  id: number;
  townId: number;
  buildingType: string;
  offsetX: number;
  offsetY: number;
  facing: BuildingData['facing'];
  footprintW: number;
  footprintH: number;
};

type AdminTownObject = {
  id: number;
  townId: number;
  objectType: string;
  variant: number;
  offsetX: number;
  offsetY: number;
};

type TownLayoutResponse = {
  town: AdminTown;
  npcs: AdminNPC[];
  buildings: AdminBuilding[];
  objects: AdminTownObject[];
};

type DraftNPC = AdminNPC;
type DraftBuilding = AdminBuilding;
type DraftObject = AdminTownObject;

type Selection =
  | { type: 'npc'; id: number }
  | { type: 'building'; id: number }
  | { type: 'object'; id: number }
  | { type: 'none' };

type DragState =
  | {
      type: 'npc';
      id: number;
      startOffsetX: number;
      startOffsetY: number;
    }
  | {
      type: 'building';
      id: number;
      startOffsetX: number;
      startOffsetY: number;
      linkedNpcOffsets: Map<number, { offsetX: number; offsetY: number }>;
    }
  | {
      type: 'object';
      id: number;
      startOffsetX: number;
      startOffsetY: number;
    }
  | null;

type PanState = {
  startClientX: number;
  startClientY: number;
  startCameraX: number;
  startCameraY: number;
} | null;

const state = {
  auth: {
    username: sessionStorage.getItem('admin_user') || '',
    password: sessionStorage.getItem('admin_pass') || '',
  },
  towns: [] as AdminTown[],
  selectedTownId: null as number | null,
  townData: null as TownData | null,
  npcData: [] as NPCData[],
  draftTown: null as AdminTown | null,
  draftNPCs: [] as DraftNPC[],
  draftBuildings: [] as DraftBuilding[],
  draftObjects: [] as DraftObject[],
  deletedNPCIds: [] as number[],
  deletedBuildingIds: [] as number[],
  deletedObjectIds: [] as number[],
  dirty: false,
  tempBuildingId: -1,
  tempNpcId: -1,
  tempObjectId: -1,
  selection: { type: 'none' } as Selection,
  drag: null as DragState,
  pan: null as PanState,
  contextWorld: null as { x: number; y: number } | null,
};

const renderer = new GameRenderer();
const camera = new Camera();

const authUserInput = document.getElementById('auth-user') as HTMLInputElement;
const authPassInput = document.getElementById('auth-pass') as HTMLInputElement;
const authSaveBtn = document.getElementById('auth-save') as HTMLButtonElement;
const authStatus = document.getElementById('auth-status') as HTMLDivElement;

const townsReloadBtn = document.getElementById('towns-reload') as HTMLButtonElement;
const townSelect = document.getElementById('town-select') as HTMLSelectElement;
const layoutReloadBtn = document.getElementById('layout-reload') as HTMLButtonElement;
const layoutSaveBtn = document.getElementById('layout-save') as HTMLButtonElement;
const selectionInfo = document.getElementById('selection-info') as HTMLDivElement;
const layoutStatus = document.getElementById('layout-status') as HTMLDivElement;
const canvasRoot = document.getElementById('canvas-root') as HTMLDivElement;
const contextMenu = document.getElementById('context-menu') as HTMLDivElement;

authUserInput.value = state.auth.username;
authPassInput.value = state.auth.password;
selectionInfo.textContent = 'No selection.';

function setStatus(el: HTMLElement, message: string, isError = false): void {
  el.textContent = message;
  el.style.color = isError ? '#ff8f8f' : '';
}

function setDirty(value: boolean): void {
  state.dirty = value;
  layoutSaveBtn.disabled = !value;
}

function setSelection(next: Selection): void {
  state.selection = next;
  if (next.type === 'none') {
    selectionInfo.textContent = 'No selection.';
    return;
  }
  selectionInfo.textContent = `${next.type.toUpperCase()} #${next.id}`;
}

function saveAuth(): void {
  state.auth.username = authUserInput.value.trim();
  state.auth.password = authPassInput.value.trim();
  sessionStorage.setItem('admin_user', state.auth.username);
  sessionStorage.setItem('admin_pass', state.auth.password);
  setStatus(authStatus, 'Credentials saved.');
}

function authHeader(): string {
  return `Basic ${btoa(`${state.auth.username}:${state.auth.password}`)}`;
}

async function api(path: string, opts: RequestInit = {}): Promise<Response> {
  const headers = Object.assign(
    { Authorization: authHeader(), 'Content-Type': 'application/json' },
    opts.headers || {},
  );
  return fetch(`/admin-api/${path}`, { cache: 'no-store', ...opts, headers });
}

function townSize(radius: number): string {
  if (radius > 40) return 'XL';
  if (radius > 25) return 'M';
  if (radius > 15) return 'S';
  return 'XS';
}

function buildTownData(town: AdminTown, npcs: AdminNPC[], buildings: AdminBuilding[]): TownData {
  const npcData: NPCData[] = npcs.map((n) => ({
    id: n.id,
    name: n.nameKey || n.name,
    nameKey: n.nameKey,
    type: n.type,
    worldX: town.worldX + n.offsetX,
    worldY: town.worldY + n.offsetY,
    buildingId: n.buildingId ?? undefined,
    townId: town.id,
    townLevelMin: town.levelMin,
    townLevelMax: town.levelMax,
  }));

  const buildingData: BuildingData[] = buildings.map((b) => ({
    id: b.id,
    buildingType: b.buildingType,
    worldX: town.worldX + b.offsetX,
    worldY: town.worldY + b.offsetY,
    facing: b.facing,
    footprintW: b.footprintW,
    footprintH: b.footprintH,
  }));

  const objectData: TownObjectData[] = state.draftObjects.map((o) => ({
    id: o.id,
    objectType: o.objectType,
    variant: o.variant,
    worldX: town.worldX + o.offsetX,
    worldY: town.worldY + o.offsetY,
  }));

  return {
    id: town.id,
    name: town.nameKey || town.name,
    nameKey: town.nameKey,
    centerX: town.worldX,
    centerY: town.worldY,
    radius: town.radius,
    biome: town.biome,
    levelMin: town.levelMin,
    size: townSize(town.radius),
    npcs: npcData,
    buildings: buildingData,
    objects: objectData,
  };
}

function refreshTownData(): void {
  if (!state.draftTown) return;
  state.townData = buildTownData(state.draftTown, state.draftNPCs, state.draftBuildings);
  state.npcData = state.townData.npcs || [];
}

function applyLayout(data: TownLayoutResponse): void {
  state.draftTown = data.town;
  state.draftNPCs = data.npcs.map((n) => ({ ...n }));
  state.draftBuildings = data.buildings.map((b) => ({ ...b }));
  state.draftObjects = (data.objects || []).map((o) => ({ ...o }));
  state.deletedNPCIds = [];
  state.deletedBuildingIds = [];
  state.deletedObjectIds = [];
  refreshTownData();
  if (state.townData) {
    const center = worldToScreen(state.townData.centerX, state.townData.centerY);
    camera.setTarget(center.x, center.y);
    camera.snapToTarget();
  }
  setSelection({ type: 'none' });
  setDirty(false);
}

async function loadTowns(): Promise<void> {
  if (!state.auth.username || !state.auth.password) {
    setStatus(authStatus, 'Set admin credentials first.', true);
    return;
  }
  setStatus(layoutStatus, 'Loading towns...');
  const res = await api('quests/towns');
  if (!res.ok) {
    setStatus(layoutStatus, `Failed to load towns (${res.status}).`, true);
    return;
  }
  const data = await res.json();
  const towns = (data.towns || []) as AdminTown[];
  state.towns = towns;
  townSelect.innerHTML = '';
  for (const t of towns) {
    const opt = document.createElement('option');
    opt.value = String(t.id);
    opt.textContent = `${t.name} (Lv ${t.levelMin}-${t.levelMax})`;
    townSelect.appendChild(opt);
  }
  if (!state.selectedTownId && towns.length > 0) {
    state.selectedTownId = towns[0].id;
  }
  if (state.selectedTownId) {
    townSelect.value = String(state.selectedTownId);
  }
  const influences = townsApiToInfluences(towns);
  renderer.setWorldTerrainContext(buildWorldTerrainContext(influences, null));
  setStatus(layoutStatus, 'Towns loaded.');
  if (state.selectedTownId) {
    await loadLayout(state.selectedTownId);
  }
}

async function loadLayout(townId: number): Promise<void> {
  setStatus(layoutStatus, 'Loading town layout...');
  const res = await api(`quests/towns/${townId}/layout`);
  if (!res.ok) {
    setStatus(layoutStatus, `Failed to load layout (${res.status}).`, true);
    return;
  }
  const data = (await res.json()) as TownLayoutResponse;
  applyLayout(data);
  setStatus(layoutStatus, 'Layout loaded.');
}

async function saveLayout(): Promise<void> {
  if (!state.draftTown) return;
  setStatus(layoutStatus, 'Saving layout...');
  layoutSaveBtn.disabled = true;
  const payload = {
    npcs: state.draftNPCs.map((n) => ({
      ...(n.id > 0 ? { id: n.id } : {}),
      name: n.name,
      nameKey: n.nameKey || '',
      type: n.type,
      offsetX: n.offsetX,
      offsetY: n.offsetY,
      ...(n.buildingId ? { buildingId: n.buildingId } : {}),
    })),
    buildings: state.draftBuildings.map((b) => ({
      ...(b.id > 0 ? { id: b.id } : {}),
      buildingType: b.buildingType,
      offsetX: b.offsetX,
      offsetY: b.offsetY,
      facing: b.facing,
      footprintW: b.footprintW,
      footprintH: b.footprintH,
    })),
    objects: state.draftObjects.map((o) => ({
      ...(o.id > 0 ? { id: o.id } : {}),
      objectType: o.objectType,
      variant: o.variant,
      offsetX: o.offsetX,
      offsetY: o.offsetY,
    })),
    deleteNpcIds: state.deletedNPCIds,
    deleteBuildingIds: state.deletedBuildingIds,
    deleteObjectIds: state.deletedObjectIds,
  };
  const res = await api(`quests/towns/${state.draftTown.id}/layout`, {
    method: 'PUT',
    body: JSON.stringify(payload),
  });
  if (!res.ok) {
    setStatus(layoutStatus, `Failed to save layout (${res.status}).`, true);
    layoutSaveBtn.disabled = false;
    return;
  }
  const data = (await res.json()) as TownLayoutResponse;
  applyLayout(data);
  setStatus(layoutStatus, 'Layout saved.');
}

const buildingMenuItems = [
  { label: 'House: Quest Giver', type: 'house.quest_giver', w: 2.5, h: 2.0 },
  { label: 'House: Merchant', type: 'house.merchant', w: 2.5, h: 2.0 },
  { label: 'House: Armorer', type: 'house.armorer', w: 2.5, h: 2.0 },
  { label: 'House: Weapon Smith', type: 'house.weapon_smith', w: 2.5, h: 2.0 },
  { label: 'House: Jeweler', type: 'house.jeweler', w: 2.5, h: 2.0 },
  { label: 'House: Bounty Hunter', type: 'house.bounty_hunter', w: 2.5, h: 2.0 },
  { label: 'House: Elder', type: 'house.elder', w: 2.5, h: 2.0 },
  { label: 'House: Healer', type: 'house.healer', w: 2.5, h: 2.0 },
  { label: 'Decoration: Well', type: 'decoration.well', w: 1.5, h: 1.5 },
  { label: 'Decoration: Stall', type: 'decoration.stall', w: 1.5, h: 1.5 },
  { label: 'Decoration: Signpost', type: 'decoration.signpost', w: 0.5, h: 0.5 },
];

const npcTypeItems = [
  { label: 'Quest Giver', type: 'quest_giver' as NPCData['type'] },
  { label: 'Merchant', type: 'merchant' as NPCData['type'] },
  { label: 'Armorer', type: 'armorer' as NPCData['type'] },
  { label: 'Weapon', type: 'weapon' as NPCData['type'] },
  { label: 'Jeweler', type: 'jeweler' as NPCData['type'] },
  { label: 'Bounty Hunter', type: 'bounty_hunter' as NPCData['type'] },
  { label: 'Elder', type: 'elder' as NPCData['type'] },
  { label: 'Healer', type: 'healer' as NPCData['type'] },
];

const objectTypeItems = [
  { label: 'Tree', type: 'tree' },
  { label: 'Rock', type: 'rock' },
  { label: 'Cart', type: 'cart' },
  { label: 'Barrel', type: 'barrel' },
  { label: 'Bush', type: 'bush' },
  { label: 'Mushroom', type: 'mushroom' },
  { label: 'Leaves', type: 'leaves' },
  { label: 'Stump', type: 'stump' },
  { label: 'Bones', type: 'bones' },
  { label: 'Ruin', type: 'ruin' },
];

function hideContextMenu(): void {
  contextMenu.classList.add('hidden');
}

function addContextHeader(label: string): void {
  const header = document.createElement('div');
  header.textContent = label;
  header.style.padding = '6px 12px 4px 12px';
  header.style.fontSize = '11px';
  header.style.color = '#9eb0d6';
  contextMenu.appendChild(header);
}

function addContextButton(label: string, onClick: () => void): void {
  const btn = document.createElement('button');
  btn.textContent = label;
  btn.addEventListener('click', onClick);
  contextMenu.appendChild(btn);
}

function addContextSubmenu(label: string, build: (submenu: HTMLDivElement) => void): void {
  const wrapper = document.createElement('div');
  wrapper.className = 'context-menu-item';
  const button = document.createElement('button');
  button.textContent = `${label} ▶`;
  const submenu = document.createElement('div');
  submenu.className = 'submenu';
  build(submenu);
  wrapper.appendChild(button);
  wrapper.appendChild(submenu);
  contextMenu.appendChild(wrapper);
}

function showContextMenu(clientX: number, clientY: number, render: () => void): void {
  contextMenu.innerHTML = '';
  render();
  const area = canvasRoot.parentElement?.getBoundingClientRect();
  if (!area) return;
  contextMenu.style.left = `${clientX - area.left}px`;
  contextMenu.style.top = `${clientY - area.top}px`;
  contextMenu.classList.remove('hidden');
}

function removeNpcById(id: number): void {
  state.draftNPCs = state.draftNPCs.filter((n) => n.id !== id);
  if (id > 0) state.deletedNPCIds.push(id);
  setSelection({ type: 'none' });
  refreshTownData();
  setDirty(true);
}

function removeBuildingById(id: number): void {
  state.draftBuildings = state.draftBuildings.filter((b) => b.id !== id);
  if (id > 0) state.deletedBuildingIds.push(id);
  setSelection({ type: 'none' });
  refreshTownData();
  setDirty(true);
}

function removeObjectById(id: number): void {
  state.draftObjects = state.draftObjects.filter((o) => o.id !== id);
  if (id > 0) state.deletedObjectIds.push(id);
  setSelection({ type: 'none' });
  refreshTownData();
  setDirty(true);
}

function showContextMenuForSelection(clientX: number, clientY: number, selection: Selection): void {
  showContextMenu(clientX, clientY, () => {
    if (!state.draftTown) return;
    if (selection.type === 'npc') {
      addContextHeader('NPC');
      addContextSubmenu('Change type', (submenu) => {
        for (const item of npcTypeItems) {
          const btn = document.createElement('button');
          btn.textContent = item.label;
          btn.addEventListener('click', () => {
            const npc = state.draftNPCs.find((n) => n.id === selection.id);
            if (!npc) return;
            npc.type = item.type;
            if (!npc.name) npc.name = `New ${item.label}`;
            setDirty(true);
            refreshTownData();
            hideContextMenu();
          });
          submenu.appendChild(btn);
        }
      });
      addContextHeader('Actions');
      addContextButton('Delete NPC', () => {
        removeNpcById(selection.id);
        hideContextMenu();
      });
      return;
    }
    if (selection.type === 'building') {
      addContextHeader('Building');
      addContextSubmenu('Change type', (submenu) => {
        for (const item of buildingMenuItems) {
          const btn = document.createElement('button');
          btn.textContent = item.label;
          btn.addEventListener('click', () => {
            const building = state.draftBuildings.find((b) => b.id === selection.id);
            if (!building) return;
            building.buildingType = item.type;
            building.footprintW = item.w;
            building.footprintH = item.h;
            setDirty(true);
            refreshTownData();
            hideContextMenu();
          });
          submenu.appendChild(btn);
        }
      });
      addContextHeader('Actions');
      addContextButton('Delete building', () => {
        removeBuildingById(selection.id);
        hideContextMenu();
      });
      return;
    }
    if (selection.type === 'object') {
      addContextHeader('Object');
      addContextSubmenu('Change type', (submenu) => {
        for (const item of objectTypeItems) {
          const btn = document.createElement('button');
          btn.textContent = item.label;
          btn.addEventListener('click', () => {
            const obj = state.draftObjects.find((o) => o.id === selection.id);
            if (!obj) return;
            obj.objectType = item.type;
            setDirty(true);
            refreshTownData();
            hideContextMenu();
          });
          submenu.appendChild(btn);
        }
      });
      addContextSubmenu('Variant', (submenu) => {
        const btn0 = document.createElement('button');
        btn0.textContent = 'Variant v0';
        btn0.addEventListener('click', () => {
          const obj = state.draftObjects.find((o) => o.id === selection.id);
          if (!obj) return;
          obj.variant = 0;
          setDirty(true);
          refreshTownData();
          hideContextMenu();
        });
        const btn1 = document.createElement('button');
        btn1.textContent = 'Variant v1';
        btn1.addEventListener('click', () => {
          const obj = state.draftObjects.find((o) => o.id === selection.id);
          if (!obj) return;
          obj.variant = 1;
          setDirty(true);
          refreshTownData();
          hideContextMenu();
        });
        submenu.appendChild(btn0);
        submenu.appendChild(btn1);
      });
      addContextHeader('Actions');
      addContextButton('Delete object', () => {
        removeObjectById(selection.id);
        hideContextMenu();
      });
      return;
    }
  });
}

function showContextMenuForEmpty(clientX: number, clientY: number): void {
  showContextMenu(clientX, clientY, () => {
    if (!state.draftTown || !state.contextWorld) return;
    const world = state.contextWorld;
    addContextSubmenu('Add NPC', (submenu) => {
      for (const item of npcTypeItems) {
        const btn = document.createElement('button');
        btn.textContent = item.label;
        btn.addEventListener('click', () => {
          const offsetX = world.x - state.draftTown!.worldX;
          const offsetY = world.y - state.draftTown!.worldY;
          const newId = state.tempNpcId--;
          state.draftNPCs.push({
            id: newId,
            townId: state.draftTown!.id,
            name: `New ${item.label}`,
            nameKey: '',
            type: item.type,
            offsetX,
            offsetY,
            buildingId: null,
          });
          refreshTownData();
          setSelection({ type: 'npc', id: newId });
          setDirty(true);
          hideContextMenu();
        });
        submenu.appendChild(btn);
      }
    });
    addContextSubmenu('Add Building', (submenu) => {
      for (const item of buildingMenuItems) {
        const btn = document.createElement('button');
        btn.textContent = item.label;
        btn.addEventListener('click', () => {
          const offsetX = world.x - state.draftTown!.worldX;
          const offsetY = world.y - state.draftTown!.worldY;
          const newId = state.tempBuildingId--;
          state.draftBuildings.push({
            id: newId,
            townId: state.draftTown!.id,
            buildingType: item.type,
            offsetX,
            offsetY,
            facing: 'south',
            footprintW: item.w,
            footprintH: item.h,
          });
          refreshTownData();
          setSelection({ type: 'building', id: newId });
          setDirty(true);
          hideContextMenu();
        });
        submenu.appendChild(btn);
      }
    });
    addContextSubmenu('Add Object', (submenu) => {
      for (const item of objectTypeItems) {
        const btn = document.createElement('button');
        btn.textContent = item.label;
        btn.addEventListener('click', () => {
          const offsetX = world.x - state.draftTown!.worldX;
          const offsetY = world.y - state.draftTown!.worldY;
          const newId = state.tempObjectId--;
          state.draftObjects.push({
            id: newId,
            townId: state.draftTown!.id,
            objectType: item.type,
            variant: 0,
            offsetX,
            offsetY,
          });
          refreshTownData();
          setSelection({ type: 'object', id: newId });
          setDirty(true);
          hideContextMenu();
        });
        submenu.appendChild(btn);
      }
    });
  });
}
function eventToWorld(evt: MouseEvent): { x: number; y: number } | null {
  if (!renderer.initialized) return null;
  const canvas = renderer.app.canvas as HTMLCanvasElement;
  const rect = canvas.getBoundingClientRect();
  if (evt.clientX < rect.left || evt.clientX > rect.right || evt.clientY < rect.top || evt.clientY > rect.bottom) {
    return null;
  }
  const localX = evt.clientX - rect.left;
  const localY = evt.clientY - rect.top;
  const screenW = renderer.app.renderer.width;
  const screenH = renderer.app.renderer.height;
  const worldScreenX = localX - screenW / 2 + camera.finalX;
  const worldScreenY = localY - screenH / 2 + camera.finalY;
  return screenToWorld(worldScreenX, worldScreenY);
}

function findHit(world: { x: number; y: number }): Selection {
  if (!state.draftTown) return { type: 'none' };
  const npcHitRadius = 1.8;
  const objectHitRadius = 1.8;
  const buildingPadding = 1.0;
  for (const npc of state.draftNPCs) {
    const nx = state.draftTown.worldX + npc.offsetX;
    const ny = state.draftTown.worldY + npc.offsetY;
    const dx = world.x - nx;
    const dy = world.y - ny;
    if (Math.hypot(dx, dy) <= npcHitRadius) {
      return { type: 'npc', id: npc.id };
    }
  }
  for (const obj of state.draftObjects) {
    const ox = state.draftTown.worldX + obj.offsetX;
    const oy = state.draftTown.worldY + obj.offsetY;
    if (Math.hypot(world.x - ox, world.y - oy) <= objectHitRadius) {
      return { type: 'object', id: obj.id };
    }
  }
  for (const building of state.draftBuildings) {
    const bx = state.draftTown.worldX + building.offsetX;
    const by = state.draftTown.worldY + building.offsetY;
    const dx = Math.abs(world.x - bx);
    const dy = Math.abs(world.y - by);
    if (dx <= building.footprintW / 2 + buildingPadding && dy <= building.footprintH / 2 + buildingPadding) {
      return { type: 'building', id: building.id };
    }
  }
  return { type: 'none' };
}

function startDrag(selection: Selection): void {
  if (selection.type === 'npc') {
    const npc = state.draftNPCs.find((n) => n.id === selection.id);
    if (!npc) return;
    state.drag = {
      type: 'npc',
      id: npc.id,
      startOffsetX: npc.offsetX,
      startOffsetY: npc.offsetY,
    };
    return;
  }
  if (selection.type === 'building') {
    const building = state.draftBuildings.find((b) => b.id === selection.id);
    if (!building) return;
    const linked = new Map<number, { offsetX: number; offsetY: number }>();
    for (const npc of state.draftNPCs) {
      if (npc.buildingId === building.id) {
        linked.set(npc.id, { offsetX: npc.offsetX, offsetY: npc.offsetY });
      }
    }
    state.drag = {
      type: 'building',
      id: building.id,
      startOffsetX: building.offsetX,
      startOffsetY: building.offsetY,
      linkedNpcOffsets: linked,
    };
    return;
  }
  if (selection.type === 'object') {
    const obj = state.draftObjects.find((o) => o.id === selection.id);
    if (!obj) return;
    state.drag = {
      type: 'object',
      id: obj.id,
      startOffsetX: obj.offsetX,
      startOffsetY: obj.offsetY,
    };
  }
}

function updateDrag(world: { x: number; y: number }): void {
  if (!state.draftTown || !state.drag) return;
  const offsetX = world.x - state.draftTown.worldX;
  const offsetY = world.y - state.draftTown.worldY;
  if (state.drag.type === 'npc') {
    const npc = state.draftNPCs.find((n) => n.id === state.drag?.id);
    if (!npc) return;
    npc.offsetX = offsetX;
    npc.offsetY = offsetY;
  }
  if (state.drag.type === 'building') {
    const building = state.draftBuildings.find((b) => b.id === state.drag?.id);
    if (!building) return;
    const deltaX = offsetX - state.drag.startOffsetX;
    const deltaY = offsetY - state.drag.startOffsetY;
    building.offsetX = offsetX;
    building.offsetY = offsetY;
    for (const npc of state.draftNPCs) {
      const start = state.drag.linkedNpcOffsets.get(npc.id);
      if (!start) continue;
      npc.offsetX = start.offsetX + deltaX;
      npc.offsetY = start.offsetY + deltaY;
    }
  }
  if (state.drag.type === 'object') {
    const obj = state.draftObjects.find((o) => o.id === state.drag?.id);
    if (!obj) return;
    obj.offsetX = offsetX;
    obj.offsetY = offsetY;
  }
  refreshTownData();
  setDirty(true);
}

function startPan(evt: MouseEvent): void {
  state.pan = {
    startClientX: evt.clientX,
    startClientY: evt.clientY,
    startCameraX: camera.x,
    startCameraY: camera.y,
  };
}

function updatePan(evt: MouseEvent): void {
  if (!state.pan) return;
  const dx = evt.clientX - state.pan.startClientX;
  const dy = evt.clientY - state.pan.startClientY;
  const nextX = state.pan.startCameraX - dx;
  const nextY = state.pan.startCameraY - dy;
  camera.setTarget(nextX, nextY);
  camera.snapToTarget();
}

function renderLoop(): void {
  requestAnimationFrame(renderLoop);
  if (!renderer.initialized) return;
  if (!state.townData) return;

  const screenW = renderer.app.renderer.width;
  const screenH = renderer.app.renderer.height;
  camera.applyTo(renderer.worldContainer, screenW, screenH);
  renderer.drawGround(camera, screenW, screenH);
  renderer.drawTowns([state.townData], camera, screenW, screenH);
  renderer.drawTownObjects(state.townData.objects ?? [], camera, screenW, screenH);
  if (state.npcData.length > 0) {
    renderer.drawNPCs(state.npcData, camera, screenW, screenH, performance.now());
  }
}

async function bootstrap(): Promise<void> {
  await renderer.init(canvasRoot);
  window.addEventListener('resize', () => renderer.resize());
  renderLoop();
  const canvas = renderer.app.canvas as HTMLCanvasElement;
  canvas.addEventListener('contextmenu', (evt) => {
    evt.preventDefault();
    const world = eventToWorld(evt);
    if (!world) return;
    state.contextWorld = world;
    const hit = findHit(world);
    if (hit.type === 'none') {
      showContextMenuForEmpty(evt.clientX, evt.clientY);
      return;
    }
    setSelection(hit);
    showContextMenuForSelection(evt.clientX, evt.clientY, hit);
  });
  canvas.addEventListener('mousedown', (evt) => {
    hideContextMenu();
    const world = eventToWorld(evt);
    if (!world) return;
    if (evt.button === 1) {
      startPan(evt);
      return;
    }
    if (evt.button !== 0) return;
    const hit = findHit(world);
    if (hit.type === 'none') {
      setSelection(hit);
      startPan(evt);
      return;
    }
    setSelection(hit);
    startDrag(hit);
  });
  window.addEventListener('mousemove', (evt) => {
    if (state.drag) {
      const world = eventToWorld(evt);
      if (!world) return;
      updateDrag(world);
      return;
    }
    if (state.pan) {
      updatePan(evt);
    }
  });
  window.addEventListener('mouseup', () => {
    state.drag = null;
    state.pan = null;
  });
  window.addEventListener('click', () => hideContextMenu());
  if (state.auth.username && state.auth.password) {
    void loadTowns();
  }
}

authSaveBtn.addEventListener('click', saveAuth);
townsReloadBtn.addEventListener('click', () => void loadTowns());
layoutReloadBtn.addEventListener('click', () => {
  if (!state.selectedTownId) return;
  void loadLayout(state.selectedTownId);
});
layoutSaveBtn.addEventListener('click', () => void saveLayout());
townSelect.addEventListener('change', () => {
  const id = Number(townSelect.value);
  if (!id) return;
  state.selectedTownId = id;
  void loadLayout(id);
});

void bootstrap();
