/**
 * Warehouse Builder — page-local editor controller (NOT useState; one per page mount,
 * provided as `wbLayout` to Stage / Toolbar / Palette / Inspector / Dock / modals).
 *
 * Owns: the layout model + undo/redo, selection, active tool, viewport, the warehouse's
 * locators (+ derived wiring/zone/pick data from app/utils/warehouseLayout/*), persistence
 * (floors of a warehouse, save with optimistic lock, apply onto M_Locator) and the
 * localStorage draft. SSR-safe: browser APIs only inside handlers / onMounted.
 */
import type { WbCamera, WbLayout, WbObject, WbObjectType, WbPallet, WbShelf, WbWall, WbWalkwayPath, WbZone } from '~/utils/warehouseLayout/schema'
import { WB_SNAP, createLayout, createObject, createWalkway, createZone, parseLayout, snap, uid, round2, isBlockingType, walkwayBox, walkwayLength } from '~/utils/warehouseLayout/schema'
import { cameraCoverage, coverageUnionArea, dockDoorToWall, doorOnWall, findCollision, insideGrid, layoutBays, mergeWall, nextRackCode, orthoWall, rectCrossesWall, roomWalls, rotatedPatch, wallIndexAt, zoneMembership } from '~/utils/warehouseLayout/geometry'
import type { CoveragePoly } from '~/utils/warehouseLayout/geometry'
import type { Rect, WbBay } from '~/utils/warehouseLayout/geometry'
import { buildLocatorIndex, computeCodeIndex, mapLocatorRow, unmappedLocators } from '~/utils/warehouseLayout/wiring'
import type { WbLocatorRow } from '~/utils/warehouseLayout/wiring'
import { buildAssignments, computePickOrder } from '~/utils/warehouseLayout/pick'
import type { WbAssignment } from '~/utils/warehouseLayout/pick'

export type WbTool = 'select' | 'pan' | 'wall' | 'room' | 'obstacle' | 'zone' | 'walkway' | 'erase' | 'measure' | 'place' | 'draw'

export interface WbFloor {
  id: number; warehouseId: number | null; warehouseName: string; name: string; floorNo: number; isDefault: boolean
  gridWidth: number; gridHeight: number; layoutVersion: number; appliedVersion: number; description: string; updated: string
}
export interface WbCommissionTable { id: number; name: string; number: number | null; warehouseId: number | null; organizationId: number | null }
export interface WbCapabilities { layoutTable: boolean; locatorColumns: boolean; missingColumns: string[]; commissionWarehouseColumn: boolean; checkedAt: string; messages: Record<string, string> }

const HISTORY_CAP = 50
const DRAFT_PREFIX = 'wb:draft:'
export function useWarehouseLayout () {
  // captured once in setup — the composable's async functions run long after setup (no Nuxt instance there)
  const cookieHeaders = useRequestHeaders(['cookie'])
  const instanceShared = !!(useRuntimeConfig().public as any).warehouseLayoutShared
  const freshLayout = (partial: Partial<WbLayout> = {}) => createLayout({ share: { allWarehouses: instanceShared }, ...partial })
  /** effective scope: explicit plan setting, else the instance flag */
  const sharedScope = computed(() => layout.value.share?.allWarehouses ?? instanceShared)
  const hdr = () => ({ headers: cookieHeaders })
  // ---- model -----------------------------------------------------------------------------------
  const layout = ref<WbLayout>(freshLayout())
  const layoutId = ref<number | null>(null)
  const layoutVersion = ref(0)
  const appliedVersion = ref(0)
  const layoutName = ref('')
  const floorNo = ref(0)
  const warehouseId = ref<number | null>(null)
  const warehouseName = ref('')
  const organizationId = ref<number | null>(null)
  const floors = ref<WbFloor[]>([])
  const layoutTableMissing = ref(false)
  const loading = ref(false)
  const dirty = ref(false)
  const saving = ref(false)
  const conflict = ref<{ serverVersion: number } | null>(null)
  const message = ref<{ type: 'info' | 'error' | 'success'; text: string } | null>(null)
  const isNew = computed(() => !layoutId.value)

  // ---- selection / tool / view ----------------------------------------------------------------
  const selection = ref<Set<string>>(new Set())
  const primaryKey = ref<string | null>(null)
  const selectedWall = ref<number | null>(null)
  const tool = ref<WbTool>('select')
  const placingType = ref<WbObjectType | null>(null)
  const placingPatch = ref<Record<string, any>>({})
  const hoverKey = ref<string | null>(null)
  const view = ref({ vx: -1, vy: -1, vw: 42, pxPerM: 20 })
  const showGrid = ref(true)
  const showLabels = ref(true)
  const showZones = ref(true)
  const showWiring = ref(true)
  const showRulers = ref(true)
  const snapOn = ref(true)
  const fitRequest = ref(0)
  const flashKey = ref<string | null>(null)

  // ---- history ----------------------------------------------------------------------------------
  const past = ref<string[]>([])
  const future = ref<string[]>([])
  const snapshot = () => JSON.stringify(layout.value)
  const record = () => {
    past.value.push(snapshot())
    if (past.value.length > HISTORY_CAP) past.value.shift()
    future.value = []
    dirty.value = true
  }
  const canUndo = computed(() => past.value.length > 0)
  const canRedo = computed(() => future.value.length > 0)
  const pruneSelection = () => {
    const keys = new Set([...layout.value.objects.map(o => o.key), ...layout.value.zones.map(z => z.key), ...layout.value.walkways.map(w => w.key)])
    selection.value = new Set([...selection.value].filter(k => keys.has(k)))
    if (primaryKey.value && !keys.has(primaryKey.value)) primaryKey.value = [...selection.value][0] || null
    if (selectedWall.value != null && selectedWall.value >= layout.value.walls.length) selectedWall.value = null
  }
  /** Drop the newest history entry (a drag that was reverted). */
  const discardLastRecord = () => { past.value.pop() }
  const undo = () => { if (!past.value.length) return; future.value.push(snapshot()); layout.value = JSON.parse(past.value.pop()!); dirty.value = true; pruneSelection() }
  const redo = () => { if (!future.value.length) return; past.value.push(snapshot()); layout.value = JSON.parse(future.value.pop()!); dirty.value = true; pruneSelection() }

  // ---- lookups -----------------------------------------------------------------------------------
  const objectByKey = computed(() => new Map<string, WbObject>(layout.value.objects.map(o => [o.key, o])))
  const zoneByKey = computed(() => new Map<string, WbZone>(layout.value.zones.map(z => [z.key, z])))
  const walkwayByKey = computed(() => new Map<string, WbWalkwayPath>(layout.value.walkways.map(w => [w.key, w])))
  const findAny = (key: string): { kind: 'object'; item: WbObject } | { kind: 'zone'; item: WbZone } | { kind: 'walkway'; item: WbWalkwayPath } | null => {
    const o = objectByKey.value.get(key); if (o) return { kind: 'object', item: o }
    const z = zoneByKey.value.get(key); if (z) return { kind: 'zone', item: z }
    const w = walkwayByKey.value.get(key); if (w) return { kind: 'walkway', item: w }
    return null
  }
  /** World bounding box of any selectable thing (object, zone, walkway). */
  const rectOfKey = (key: string): Rect | null => {
    const h = findAny(key)
    if (!h) return null
    if (h.kind === 'walkway') return walkwayBox(h.item)
    return { x: h.item.x, y: h.item.y, w: h.item.w, d: h.item.d }
  }
  const selectedItems = computed(() => [...selection.value].map(findAny).filter(Boolean) as Array<{ kind: 'object' | 'zone' | 'walkway'; item: any }>)
  const selectedObjects = computed(() => selectedItems.value.filter(s => s.kind === 'object').map(s => s.item as WbObject))
  const primary = computed(() => (primaryKey.value ? findAny(primaryKey.value) : null))

  // ---- selection actions ------------------------------------------------------------------------
  const select = (key: string | null, additive = false) => {
    selectedWall.value = null
    if (!key) { selection.value = new Set(); primaryKey.value = null; return }
    if (additive) {
      const s = new Set(selection.value)
      if (s.has(key)) { s.delete(key); if (primaryKey.value === key) primaryKey.value = [...s][0] || null } else { s.add(key); primaryKey.value = key }
      selection.value = s
    } else { selection.value = new Set([key]); primaryKey.value = key }
  }
  const selectMany = (keys: string[]) => { selectedWall.value = null; selection.value = new Set(keys); primaryKey.value = keys[0] || null }
  const selectWall = (index: number | null) => { selection.value = new Set(); primaryKey.value = null; selectedWall.value = index }
  const selectAll = () => selectMany(layout.value.objects.map(o => o.key))

  // ---- placement validation ----------------------------------------------------------------------
  const placementProblem = (rect: Rect, selfKeys: string[] = [], blocking = true, type?: string): string | null => {
    if (!insideGrid(rect, layout.value.grid)) return 'outside'
    if (type === 'door' && !doorOnWall(rect, layout.value.walls)) return 'nowall' // doors and gates never stand free
    if (!blocking) return null
    if (findCollision(rect, layout.value.objects, selfKeys)) return 'collision'
    if (rectCrossesWall(rect, layout.value.walls)) return 'wall'
    return null
  }

  // ---- object CRUD ------------------------------------------------------------------------------------
  const addObject = (type: WbObjectType, x: number, y: number, patch: Record<string, any> = {}): WbObject | null => {
    const obj = createObject(type, x, y, patch)
    if (type === 'door') { // snap into the nearest wall (turned along it); without a wall nearby nothing is placed
      const dock = dockDoorToWall(obj, layout.value.walls)
      if (!dock) return null
      Object.assign(obj, dock.rect, { rotation: dock.horizontal ? 0 : 90 })
    }
    if (type === 'shelf' && !patch.rackCode) (obj as WbShelf).rackCode = nextRackCode(layout.value)
    if (type === 'table' && !patch.commissionTableId) {
      const free = tablesForWarehouse.value.find(t => !layout.value.objects.some(o => o.type === 'table' && (o as any).commissionTableId === t.id))
      if (free) { (obj as any).commissionTableId = free.id; (obj as any).tableNumber = free.number; obj.label = free.name }
    }
    const problem = placementProblem(obj, [], isBlockingType(type), type)
    if (problem && problem !== 'wall') return null
    record()
    layout.value.objects.push(obj)
    select(obj.key)
    return obj
  }
  const updateObject = (key: string, patch: Record<string, any>, commit = true) => {
    const hit = findAny(key)
    if (!hit) return
    if (commit) record()
    if (hit.kind === 'walkway') {
      // a walkway has no x/y of its own: x/y patches move the whole polyline by the bbox delta
      const { x, y, ...rest } = patch
      if (x !== undefined || y !== undefined) {
        const b = walkwayBox(hit.item)
        const dx = x !== undefined ? round2(x - b.x) : 0, dy = y !== undefined ? round2(y - b.y) : 0
        if (dx || dy) hit.item.points = hit.item.points.map(([px, py]) => [round2(px + dx), round2(py + dy)] as [number, number])
      }
      Object.assign(hit.item, rest)
    } else Object.assign(hit.item, patch)
    if (!commit) dirty.value = true
  }
  const removeKeys = (keys: string[]) => {
    const set = new Set(keys)
    if (![...set].some(k => findAny(k))) return
    record()
    layout.value.objects = layout.value.objects.filter(o => !set.has(o.key))
    layout.value.zones = layout.value.zones.filter(z => !set.has(z.key))
    layout.value.walkways = layout.value.walkways.filter(w => !set.has(w.key))
    if (layout.value.pick.startDoorKey && set.has(layout.value.pick.startDoorKey)) delete layout.value.pick.startDoorKey
    pruneSelection()
  }
  const deleteSelection = () => {
    if (selectedWall.value != null) { record(); layout.value.walls.splice(selectedWall.value, 1); selectedWall.value = null; return }
    removeKeys([...selection.value])
  }
  const duplicateSelection = () => {
    const items = selectedItems.value.filter(s => !(s.item as any).locked)
    if (!items.length) return
    record()
    const newKeys: string[] = []
    for (const s of items) {
      const copy = JSON.parse(JSON.stringify(s.item))
      copy.key = uid(); copy.x = snap(copy.x + 1); copy.y = snap(copy.y + 1)
      if (s.kind === 'object') {
        if (copy.type === 'shelf') copy.rackCode = nextRackCode({ ...layout.value, objects: [...layout.value.objects, ...newKeys.map(k => objectByKey.value.get(k)).filter(Boolean) as WbObject[]] })
        if (copy.type === 'pallet') { copy.locatorId = null; delete copy.locatorCode }
        if (copy.type === 'table') { copy.commissionTableId = null; delete copy.tableNumber; delete copy.label }
        layout.value.objects.push(copy)
      } else if (s.kind === 'walkway') {
        copy.points = copy.points.map(([px, py]: [number, number]) => [snap(px + 1), snap(py + 1)])
        layout.value.walkways.push(copy)
      } else {
        copy.code = copy.code + '2'; copy.name = copy.name + ' 2'
        layout.value.zones.push(copy)
      }
      newKeys.push(copy.key)
    }
    selectMany(newKeys)
  }
  const translateKeys = (keys: string[], dx: number, dy: number, commit = true) => {
    if (!keys.length || (!dx && !dy)) return
    if (commit) record()
    for (const k of keys) {
      const h = findAny(k)
      if (!h || (h.item as any).locked) continue
      if (h.kind === 'walkway') h.item.points = h.item.points.map(([px, py]) => [round2(px + dx), round2(py + dy)] as [number, number])
      else { h.item.x = round2(h.item.x + dx); h.item.y = round2(h.item.y + dy) }
    }
    dirty.value = true
  }
  const nudge = (dx: number, dy: number) => {
    const keys = [...selection.value]
    if (!keys.length) return
    const rects = keys.map(k => rectOfKey(k)!).map(r => ({ x: r.x + dx, y: r.y + dy, w: r.w, d: r.d }))
    if (rects.some(r => !insideGrid(r, layout.value.grid))) return
    translateKeys(keys, dx, dy)
  }
  const rotateSelection = (dir: 1 | -1 = 1) => {
    const objs = selectedObjects.value.filter(o => !o.locked)
    if (!objs.length) return
    record()
    for (const o of objs) {
      if (o.type === 'camera') { (o as WbCamera).heading = (((o as WbCamera).heading + 45 * dir) % 360 + 360) % 360; continue } // R turns the view direction
      Object.assign(o, rotatedPatch(o, dir))
    }
  }
  /** Force an object's long side along X ('h') or Y ('v'), keeping its centre (gates/doors in walls). */
  const setOrientation = (key: string, o: 'h' | 'v') => {
    const h = findAny(key)
    if (!h || h.kind !== 'object') return
    const it = h.item as WbObject
    const long = Math.max(it.w, it.d), short = Math.min(it.w, it.d)
    const w = o === 'h' ? long : short, d = o === 'h' ? short : long
    if (w === it.w && d === it.d) return
    const cx = it.x + it.w / 2, cy = it.y + it.d / 2
    record()
    Object.assign(it, { w, d, x: snap(cx - w / 2), y: snap(cy - d / 2), rotation: (o === 'h' ? 0 : 90) })
    if (it.type === 'door') { const dock = dockDoorToWall(it, layout.value.walls); if (dock) Object.assign(it, dock.rect) }
  }
  /** Pull a door/gate onto the nearest wall (used after drags / resizes); false when no wall is near. */
  const dockDoor = (key: string): boolean => {
    const h = findAny(key)
    if (!h || h.kind !== 'object' || (h.item as WbObject).type !== 'door') return true
    const dock = dockDoorToWall(h.item as WbObject, layout.value.walls)
    if (!dock) return false
    Object.assign(h.item, dock.rect, { rotation: dock.horizontal ? 0 : 90 })
    return true
  }
  const setLocked = (keys: string[], locked: boolean) => { record(); for (const k of keys) { const h = findAny(k); if (h) (h.item as any).locked = locked } }
  const bringToFront = (key: string) => { const i = layout.value.objects.findIndex(o => o.key === key); if (i < 0) return; record(); const [o] = layout.value.objects.splice(i, 1); layout.value.objects.push(o) }
  const sendToBack = (key: string) => { const i = layout.value.objects.findIndex(o => o.key === key); if (i < 0) return; record(); const [o] = layout.value.objects.splice(i, 1); layout.value.objects.unshift(o) }

  // ---- walls / room / zones / obstacles -----------------------------------------------------------
  const addWall = (x1: number, y1: number, x2: number, y2: number): boolean => {
    const w = orthoWall(x1, y1, x2, y2)
    if (!w) return false
    record()
    layout.value.walls = mergeWall(layout.value.walls, w)
    return true
  }
  const addRoom = (r: Rect) => {
    const walls = roomWalls(r)
    if (!walls.length) return
    record()
    let all = layout.value.walls
    for (const w of walls) all = mergeWall(all, w)
    layout.value.walls = all
  }
  const removeWallAt = (px: number, py: number): boolean => {
    const i = wallIndexAt(layout.value.walls, px, py)
    if (i < 0) return false
    record(); layout.value.walls.splice(i, 1); return true
  }
  const addWalkway = (points: Array<[number, number]>, width = 1.5): WbWalkwayPath | null => {
    const pts = points.map(([x, y]) => [snap(x), snap(y)] as [number, number]).filter((pt, i, a) => i === 0 || pt[0] !== a[i - 1][0] || pt[1] !== a[i - 1][1])
    if (pts.length < 2) return null
    const w = createWalkway(pts, width)
    record(); layout.value.walkways.push(w); select(w.key); return w
  }
  /** Drag-to-size placement (doors, office areas, …): the drawn rectangle becomes the object's footprint. */
  // the stage already snapped (grid or magnet to a wall / neighbour) — only round, never re-snap to the 0.5 grid
  const addObjectRect = (type: WbObjectType, r: Rect, patch: Record<string, any> = {}): WbObject | null => {
    let rect = { x: round2(r.x), y: round2(r.y), w: Math.max(WB_SNAP, round2(r.w)), d: Math.max(WB_SNAP, round2(r.d)) }
    if (type === 'door') { // the drawn stroke gives the length, the brick the thickness; addObject docks it into the wall
      const tmp = createObject(type, 0, 0, patch), thick = Math.min(tmp.w, tmp.d), len = Math.max(rect.w, rect.d)
      rect = rect.w >= rect.d ? { x: rect.x, y: round2(rect.y + rect.d / 2 - thick / 2), w: len, d: thick } : { x: round2(rect.x + rect.w / 2 - thick / 2), y: rect.y, w: thick, d: len }
    }
    return addObject(type, rect.x, rect.y, { ...patch, w: rect.w, d: rect.d })
  }
  const addZoneRect = (r: Rect): WbZone | null => {
    const z = createZone(r.x, r.y, Math.max(1, r.w), Math.max(1, r.d), layout.value.zones.length)
    while (layout.value.zones.some(o => o.code === z.code)) { z.code = z.code + 'X'; z.name = z.name + ' X' }
    record(); layout.value.zones.push(z); select(z.key); return z
  }
  const addObstacleRect = (r: Rect, kind: string = 'other'): WbObject | null => {
    const rect = { x: round2(r.x), y: round2(r.y), w: Math.max(WB_SNAP, round2(r.w)), d: Math.max(WB_SNAP, round2(r.d)) }
    if (placementProblem(rect, [], true) === 'collision') return null
    return addObject('obstacle', rect.x, rect.y, { w: rect.w, d: rect.d, kind })
  }
  const eraseAt = (key: string | null, px: number, py: number) => {
    if (key) { removeKeys([key]); return true }
    return removeWallAt(px, py)
  }
  /** Largest x / y any wall, zone, walkway or object reaches — the grid may not shrink below it. */
  const contentExtent = computed(() => {
    const l = layout.value
    let x = 0, y = 0
    for (const o of l.objects) { x = Math.max(x, o.x + o.w); y = Math.max(y, o.y + o.d) }
    for (const z of l.zones) { x = Math.max(x, z.x + z.w); y = Math.max(y, z.y + z.d) }
    for (const w of l.walls) { x = Math.max(x, w[2]); y = Math.max(y, w[3]) }
    for (const w of l.walkways) for (const [px, py] of w.points) { x = Math.max(x, px + w.width / 2); y = Math.max(y, py + w.width / 2) }
    return { x: Math.ceil(x), y: Math.ceil(y) }
  })
  const setGrid = (width: number, height: number) => {
    const w = Math.max(2, Math.round(width), contentExtent.value.x), h = Math.max(2, Math.round(height), contentExtent.value.y)
    if (w === layout.value.grid.width && h === layout.value.grid.height) return
    record()
    layout.value.grid = { width: Math.min(500, w), height: Math.min(500, h) }
  }
  const growGrid = (dx: number, dy: number) => setGrid(layout.value.grid.width + dx, layout.value.grid.height + dy)
  const clearLayout = () => { record(); layout.value.walls = []; layout.value.zones = []; layout.value.walkways = []; layout.value.objects = []; select(null) }

  // ---- locators / capabilities / commission tables ---------------------------------------------
  const locators = shallowRef<WbLocatorRow[]>([])
  const locatorsLoading = ref(false)
  const columnsMissing = ref(false)
  const capabilities = ref<WbCapabilities | null>(null)
  const commissionTables = ref<WbCommissionTable[]>([])

  const loadLocators = async (fresh = false) => {
    if (!warehouseId.value) { locators.value = []; return }
    locatorsLoading.value = true
    try {
      const res: any = await $fetch(`/api/materials/warehouse-layouts/by-warehouse/${warehouseId.value}/locators${fresh ? '?fresh=1' : ''}`, hdr())
      locators.value = (res?.records || []) as WbLocatorRow[]
      columnsMissing.value = !!res?.columnsMissing
    } catch (e: any) { message.value = { type: 'error', text: e?.data?.message || e?.message || 'locators failed' } }
    locatorsLoading.value = false
  }
  const loadCapabilities = async (fresh = false) => {
    try { capabilities.value = await $fetch(`/api/materials/warehouse-layouts/meta/capabilities${fresh ? '?fresh=1' : ''}`, hdr()) as any } catch { /* banner stays hidden */ }
  }
  const loadCommissionTables = async () => {
    try {
      const res: any = await $fetch('/api/settings/commission-tables', hdr())
      const idOf = (v: any) => (v && typeof v === 'object' ? Number(v.id) : Number(v)) || null
      commissionTables.value = (res?.records || []).filter((r: any) => (r.IsActive ?? r.isActive) !== false && (r.IsActive ?? r.isActive) !== 'N').map((r: any) => ({
        id: Number(r.id), name: String(r.Name ?? r.name ?? ''), number: Number(r.CommissionTableNumber ?? r.commissionTableNumber) || null,
        warehouseId: idOf(r.M_Warehouse_ID), organizationId: idOf(r.AD_Org_ID)
      }))
    } catch { commissionTables.value = [] }
  }
  /** Write M_Warehouse_ID on a commission table (Settings → Kommissioniertische row). */
  const assignTableWarehouse = async (tableId: number) => {
    if (!warehouseId.value) return false
    const res: any = await $fetch('/api/settings/commission-tables/update', { method: 'PUT', ...hdr(), body: { id: tableId, warehouseId: warehouseId.value } }).catch((e: any) => e?.data || { status: 500 })
    if (Number(res?.status) !== 200) { message.value = { type: 'error', text: res?.message || 'update failed' }; return false }
    commissionTables.value = commissionTables.value.map(t => t.id === tableId ? { ...t, warehouseId: warehouseId.value } : t)
    return true
  }
  /** Tables of this warehouse plus tables without a warehouse yet. */
  const tablesForWarehouse = computed(() => commissionTables.value.filter(t => !t.warehouseId || t.warehouseId === warehouseId.value))

  /** Merge freshly created locators into the list without a reload. */
  const addLocatorRows = (rows: any[]) => {
    const mapped = rows.map(r => (typeof r.id === 'number' && 'value' in r && 'x' in r) ? ({ ...mapLocatorRow({}), ...r, isActive: true, priorityNo: r.priorityNo || 50, locatorTypeId: r.locatorTypeId ?? null } as WbLocatorRow) : mapLocatorRow(r))
    const have = new Set(locators.value.map(l => l.id))
    locators.value = [...locators.value, ...mapped.filter(m => !have.has(m.id))]
  }

  // ---- derived ---------------------------------------------------------------------------------------
  const activeLocators = computed(() => locators.value.filter(l => l.isActive))
  const locatorIndex = computed(() => buildLocatorIndex(activeLocators.value))
  const bays = computed<WbBay[]>(() => layoutBays(layout.value))
  const baysByObject = computed(() => { const m = new Map<string, WbBay[]>(); for (const b of bays.value) { if (!m.has(b.objectKey)) m.set(b.objectKey, []); m.get(b.objectKey)!.push(b) } return m })
  const zoneMap = computed(() => zoneMembership(layout.value, bays.value))
  const codeIndex = computed(() => computeCodeIndex(layout.value, bays.value, locatorIndex.value))
  const unmapped = computed(() => unmappedLocators(activeLocators.value, codeIndex.value))
  const pickOrder = computed(() => computePickOrder(layout.value, bays.value))
  const assignments = computed<WbAssignment[]>(() => buildAssignments(layout.value, bays.value, codeIndex.value, zoneMap.value, floorNo.value).assignments)
  const zoneCounts = computed(() => { const m: Record<string, number> = {}; for (const [, code] of zoneMap.value) if (code) m[code] = (m[code] || 0) + 1; return m })
  const stats = computed(() => {
    const o = layout.value.objects
    const by = (t: string) => o.filter(x => x.type === t).length
    const c = codeIndex.value.counts
    return {
      area: layout.value.grid.width * layout.value.grid.height,
      shelves: by('shelf'), pallets: by('pallet'), doors: by('door'), tables: by('table'), obstacles: by('obstacle'), cameras: by('camera'), walkways: by('walkway') + layout.value.walkways.length,
      bays: bays.value.filter(b => b.objectType === 'shelf').length,
      codes: codeIndex.value.entries.length,
      wired: c['exists-free'] + c['exists-mapped-here'] + c['exists-mapped-elsewhere'],
      missing: c.missing, conflicts: c.conflict, stolen: c['exists-mapped-elsewhere'],
      locators: activeLocators.value.length, unmapped: unmapped.value.length,
      zones: layout.value.zones.length, walls: layout.value.walls.length,
      applyPending: layoutVersion.value > appliedVersion.value || dirty.value
    }
  })

  /** Live floor metrics for the metrics card (all m / m²). */
  /** View cones of all cameras (ray-cast against walls / shelves / obstacles), by object key. */
  const coverage = computed(() => {
    const m = new Map<string, CoveragePoly>()
    for (const o of layout.value.objects) if (o.type === 'camera') m.set(o.key, cameraCoverage(o as WbCamera, layout.value))
    return m
  })
  const metrics = computed(() => {
    const l = layout.value
    const r2 = (n: number) => Math.round(n * 100) / 100
    const area = (o: { w: number; d: number }) => o.w * o.d
    const sum = (t: string) => l.objects.filter(o => o.type === t).reduce((s, o) => s + area(o), 0)
    const gridArea = l.grid.width * l.grid.height
    const wallLength = l.walls.reduce((s, [x1, y1, x2, y2]) => s + Math.hypot(x2 - x1, y2 - y1), 0)
    // enclosed floor = bounding box of the walls (outer shell); falls back to the grid
    let hull = gridArea
    let hullRect: Rect = { x: 0, y: 0, w: l.grid.width, d: l.grid.height }
    if (l.walls.length) {
      const xs = l.walls.flatMap(w => [w[0], w[2]]), ys = l.walls.flatMap(w => [w[1], w[3]])
      hullRect = { x: Math.min(...xs), y: Math.min(...ys), w: Math.max(...xs) - Math.min(...xs), d: Math.max(...ys) - Math.min(...ys) }
      hull = Math.max(0, hullRect.w * hullRect.d) || gridArea
    }
    // office areas are not warehouse: they are taken out of the hull before any utilisation figure
    const officeArea = l.objects.filter(o => o.type === 'obstacle' && (o as any).kind === 'office').reduce((s, o) => s + area(o), 0)
    const warehouseArea = Math.max(0, hull - officeArea)
    const shelfArea = sum('shelf'), palletArea = sum('pallet'), tableArea = sum('table'), obstacleArea = sum('obstacle') - officeArea
    const cams = [...coverage.value.values()]
    const cameraCover = cams.length ? coverageUnionArea(cams, hullRect) : 0
    const walkwayLen = l.walkways.reduce((s, w) => s + walkwayLength(w), 0)
    const walkwayArea = sum('walkway') + l.walkways.reduce((s, w) => s + walkwayLength(w) * w.width, 0)
    const shelfLevelArea = l.objects.filter(o => o.type === 'shelf').reduce((s, o) => s + area(o) * ((o as WbShelf).levels.length || 1), 0)
    const blocked = shelfArea + palletArea + tableArea + obstacleArea
    const free = Math.max(0, warehouseArea - blocked)
    const zoneAreas = l.zones.map(z => ({ code: z.code, name: z.name, color: z.color, area: r2(area(z)), bays: zoneCounts.value[z.code] || 0 }))
    // estimated serpentine walking distance: consecutive access points in pick order
    const ord = pickOrder.value.order
    const seq = bays.value.filter(b => ord.has(b.key)).sort((a, b) => ord.get(a.key)! - ord.get(b.key)!)
    let path = 0
    for (let i = 1; i < seq.length; i++) path += Math.abs(seq[i].access.x - seq[i - 1].access.x) + Math.abs(seq[i].access.y - seq[i - 1].access.y)
    const shelves = l.objects.filter(o => o.type === 'shelf') as WbShelf[]
    const levels = shelves.reduce((s, o) => s + o.levels.length, 0)
    return {
      gridArea: r2(gridArea), hull: r2(hull), officeArea: r2(officeArea), warehouseArea: r2(warehouseArea), wallLength: r2(wallLength),
      shelfArea: r2(shelfArea), shelfLevelArea: r2(shelfLevelArea), palletArea: r2(palletArea), tableArea: r2(tableArea), obstacleArea: r2(obstacleArea), walkwayArea: r2(walkwayArea), walkwayLen: r2(walkwayLen),
      blocked: r2(blocked), free: r2(free), utilisation: warehouseArea ? Math.round((blocked / warehouseArea) * 100) : 0, storageShare: warehouseArea ? Math.round(((shelfArea + palletArea) / warehouseArea) * 100) : 0,
      cameras: cams.length, cameraCoverage: r2(cameraCover), cameraCoveragePct: warehouseArea ? Math.round((cameraCover / warehouseArea) * 100) : 0,
      shelfRunLength: r2(shelves.reduce((s, o) => s + Math.max(o.w, o.d), 0)), avgLevels: shelves.length ? r2(levels / shelves.length) : 0,
      zoneAreas, zonedArea: r2(l.zones.reduce((s, z) => s + area(z), 0)),
      aisles: pickOrder.value.aisles.length, pickPath: r2(path), codes: codeIndex.value.entries.length,
      codesPerM2: warehouseArea ? r2(codeIndex.value.entries.length / warehouseArea) : 0
    }
  })

  // ---- persistence ------------------------------------------------------------------------------------
  const draftKey = () => DRAFT_PREFIX + (layoutId.value ? String(layoutId.value) : 'new:' + (warehouseId.value || 0))
  const draftAvailable = ref<{ savedAt: string; layout: WbLayout } | null>(null)
  let draftTimer: any = null
  const writeDraft = () => {
    if (!import.meta.client || !dirty.value) return
    try { localStorage.setItem(draftKey(), JSON.stringify({ baseVersion: layoutVersion.value, savedAt: new Date().toISOString(), layout: layout.value })) } catch { /* quota */ }
  }
  const clearDraft = () => { if (import.meta.client) try { localStorage.removeItem(draftKey()) } catch { /* noop */ } draftAvailable.value = null }
  const checkDraft = () => {
    if (!import.meta.client) return
    try {
      const raw = localStorage.getItem(draftKey())
      if (!raw) { draftAvailable.value = null; return }
      const d = JSON.parse(raw)
      const parsed = parseLayout(d?.layout)
      if (!parsed.layout || Number(d?.baseVersion) !== layoutVersion.value) { localStorage.removeItem(draftKey()); draftAvailable.value = null; return }
      if (JSON.stringify(parsed.layout) === JSON.stringify(layout.value)) { localStorage.removeItem(draftKey()); draftAvailable.value = null; return }
      draftAvailable.value = { savedAt: String(d.savedAt || ''), layout: parsed.layout }
    } catch { draftAvailable.value = null }
  }
  const restoreDraft = () => { if (!draftAvailable.value) return; record(); layout.value = draftAvailable.value.layout; draftAvailable.value = null; dirty.value = true }
  watch(layout, () => { if (draftTimer) clearTimeout(draftTimer); draftTimer = setTimeout(writeDraft, 1500) }, { deep: true })

  const resetEditor = () => { past.value = []; future.value = []; select(null); dirty.value = false; conflict.value = null; tool.value = 'select'; placingType.value = null }

  const applyFloor = (rec: any) => {
    layoutId.value = Number(rec.id) || null
    layoutVersion.value = Number(rec.layoutVersion) || 1
    appliedVersion.value = Number(rec.appliedVersion) || 0
    layoutName.value = rec.name || ''
    floorNo.value = Number(rec.floorNo) || 0
    if (rec.warehouseName) warehouseName.value = rec.warehouseName
    if (rec.organizationId) organizationId.value = rec.organizationId
    layout.value = rec.layout || freshLayout({ grid: { width: rec.gridWidth || 40, height: rec.gridHeight || 25 } })
  }

  /** Floors of a warehouse; opens the default (or given) floor, or a fresh unsaved layout. */
  const openWarehouse = async (wid: number, floorId?: number | null, whName = '', orgId: number | null = null) => {
    warehouseId.value = wid
    warehouseName.value = whName
    organizationId.value = orgId
    loading.value = true
    message.value = null
    resetEditor()
    layoutId.value = null; layoutVersion.value = 0; appliedVersion.value = 0; layoutName.value = whName; floorNo.value = 0
    layout.value = freshLayout()
    try {
      const res: any = await $fetch(`/api/materials/warehouse-layouts/by-warehouse/${wid}`, hdr())
      layoutTableMissing.value = !!res?.layoutTableMissing
      floors.value = res?.records || []
      const pick = floors.value.find(f => f.id === floorId) || floors.value.find(f => f.isDefault) || floors.value[0]
      if (pick) await openFloor(pick.id)
      else checkDraft()
    } catch (e: any) { message.value = { type: 'error', text: e?.data?.message || e?.message || 'load failed' } }
    loading.value = false
    fitRequest.value++
    await Promise.all([loadLocators(), loadCommissionTables()])
  }
  const openFloor = async (id: number) => {
    loading.value = true
    try {
      const res: any = await $fetch(`/api/materials/warehouse-layouts/${id}`, hdr())
      if (res?.record) { resetEditor(); applyFloor(res.record); if (res.record.parseError) message.value = { type: 'error', text: 'Layout JSON: ' + res.record.parseError }; checkDraft() }
      else message.value = { type: 'error', text: res?.message || 'floor not found' }
    } catch (e: any) { message.value = { type: 'error', text: e?.data?.message || e?.message || 'load failed' } }
    loading.value = false
    fitRequest.value++
  }
  const newFloor = (name: string, no: number, width: number, height: number) => {
    resetEditor()
    layoutId.value = null; layoutVersion.value = 0; appliedVersion.value = 0; layoutName.value = name; floorNo.value = no
    layout.value = freshLayout({ grid: { width, height } })
    dirty.value = true
    fitRequest.value++
  }
  const replaceLayout = (next: WbLayout) => { record(); layout.value = next; select(null); fitRequest.value++ }

  const save = async (opts: { name?: string; floorNo?: number; isDefault?: boolean } = {}): Promise<boolean> => {
    if (!warehouseId.value) return false
    saving.value = true
    conflict.value = null
    message.value = null
    try {
      if (!layoutId.value) {
        const res: any = await $fetch('/api/materials/warehouse-layouts', { method: 'POST', ...hdr(), body: { warehouseId: warehouseId.value, name: opts.name ?? layoutName.value, floorNo: opts.floorNo ?? floorNo.value, isDefault: opts.isDefault ?? floors.value.length === 0, gridWidth: layout.value.grid.width, gridHeight: layout.value.grid.height, layout: layout.value } })
        if (Number(res?.status) !== 200) { layoutTableMissing.value = !!res?.layoutTableMissing; message.value = { type: 'error', text: res?.message || 'save failed' }; saving.value = false; return false }
        applyFloor(res.record)
        floors.value = [...floors.value, { ...res.record, layout: undefined }]
      } else {
        const res: any = await $fetch(`/api/materials/warehouse-layouts/${layoutId.value}`, { method: 'PUT', ...hdr(), body: { layout: layout.value, expectedVersion: layoutVersion.value, ...(opts.name !== undefined ? { name: opts.name } : {}), ...(opts.floorNo !== undefined ? { floorNo: opts.floorNo } : {}), ...(opts.isDefault !== undefined ? { isDefault: opts.isDefault } : {}) } })
        if (Number(res?.status) === 409) { conflict.value = { serverVersion: Number(res.serverVersion) }; saving.value = false; return false }
        if (Number(res?.status) !== 200) { message.value = { type: 'error', text: res?.message || 'save failed' }; saving.value = false; return false }
        layoutVersion.value = Number(res.layoutVersion) || layoutVersion.value + 1
        if (opts.name !== undefined) layoutName.value = opts.name
        if (opts.floorNo !== undefined) floorNo.value = opts.floorNo
        floors.value = floors.value.map(f => f.id === layoutId.value ? { ...f, name: layoutName.value, floorNo: floorNo.value, layoutVersion: layoutVersion.value, gridWidth: layout.value.grid.width, gridHeight: layout.value.grid.height } : f)
      }
      layout.value.meta.version = layoutVersion.value
      dirty.value = false
      clearDraft()
      message.value = { type: 'success', text: 'saved' }
      saving.value = false
      return true
    } catch (e: any) {
      message.value = { type: 'error', text: e?.data?.message || e?.message || 'save failed' }
      saving.value = false
      return false
    }
  }
  const reloadFromServer = async () => { if (layoutId.value) { clearDraft(); await openFloor(layoutId.value) } }

  /** Apply onto M_Locator (chunks of 500). Resolves with the aggregated result. */
  const apply = async (dryRun: boolean, onProgress?: (done: number, total: number) => void) => {
    if (!layoutId.value) return { status: 400, message: 'save first' }
    const all = assignments.value
    const keys = layout.value.objects.filter(o => o.type === 'shelf' || o.type === 'pallet').map(o => o.key)
    const allIds = all.map(a => a.locatorId)
    const agg: any = { status: 200, toWrite: 0, toClear: 0, skipped: 0, written: 0, cleared: 0, mirrored: 0, failed: [], columnsMissing: false, sample: [], allWarehouses: sharedScope.value }
    const chunks: WbAssignment[][] = []
    for (let i = 0; i < all.length; i += 500) chunks.push(all.slice(i, i + 500))
    if (!chunks.length) chunks.push([])
    for (let i = 0; i < chunks.length; i++) {
      const res: any = await $fetch(`/api/materials/warehouse-layouts/${layoutId.value}/apply`, { method: 'POST', ...hdr(), body: { assignments: chunks[i], layoutObjectKeys: keys, allLocatorIds: allIds, dryRun, final: i === chunks.length - 1, appliedVersion: layoutVersion.value, allWarehouses: sharedScope.value } })
      if (Number(res?.status) !== 200) { agg.status = res?.status; agg.message = res?.message; break }
      for (const k of ['toWrite', 'toClear', 'skipped', 'written', 'cleared', 'mirrored']) agg[k] += Number(res[k] || 0)
      agg.failed.push(...(res.failed || []))
      agg.sample.push(...(res.sample || []))
      if (res.columnsMissing) { agg.columnsMissing = true; agg.missingColumns = res.missingColumns; break }
      onProgress?.(i + 1, chunks.length)
    }
    if (!dryRun && agg.status === 200 && !agg.columnsMissing && !agg.failed.length) { appliedVersion.value = layoutVersion.value; floors.value = floors.value.map(f => f.id === layoutId.value ? { ...f, appliedVersion: layoutVersion.value } : f) }
    if (!dryRun) await loadLocators(true)
    return agg
  }

  return {
    // model
    layout, layoutId, layoutVersion, appliedVersion, layoutName, floorNo, warehouseId, warehouseName, organizationId, floors, layoutTableMissing, loading, dirty, saving, conflict, message, isNew,
    // selection / tool / view
    selection, primaryKey, selectedWall, tool, placingType, placingPatch, hoverKey, view, showGrid, showLabels, showZones, showWiring, showRulers, snapOn, fitRequest, flashKey,
    selectedItems, selectedObjects, primary, objectByKey, zoneByKey, walkwayByKey, findAny, rectOfKey,
    select, selectMany, selectWall, selectAll,
    // history
    canUndo, canRedo, record, discardLastRecord, undo, redo,
    // editing
    placementProblem, dockDoor, addObject, updateObject, removeKeys, deleteSelection, duplicateSelection, translateKeys, nudge, rotateSelection, setOrientation, setLocked, bringToFront, sendToBack,
    addWall, addRoom, removeWallAt, addZoneRect, addWalkway, addObstacleRect, addObjectRect, eraseAt, setGrid, clearLayout, replaceLayout,
    // locators & co
    locators, activeLocators, locatorsLoading, columnsMissing, capabilities, commissionTables, tablesForWarehouse, assignTableWarehouse, loadLocators, loadCapabilities, loadCommissionTables, addLocatorRows,
    locatorIndex, bays, baysByObject, zoneMap, codeIndex, unmapped, pickOrder, assignments, zoneCounts, stats, metrics, coverage, contentExtent, growGrid,
    instanceShared, sharedScope,
    // persistence
    draftAvailable, restoreDraft, clearDraft, openWarehouse, openFloor, newFloor, save, reloadFromServer, apply
  }
}

export type WarehouseLayoutController = ReturnType<typeof useWarehouseLayout>
