/**
 * Warehouse Builder — LayoutJSON schema v1 (CUST_WarehouseLayout.LayoutJSON).
 *
 * One layout = one floor/hall of a warehouse drawn from above. Units are metres, origin
 * top-left, y grows downwards (SVG). Positions snap to 0.5 m, sizes may be finer (0.1 m).
 * `x,y,w,d` are ALWAYS the world axis-aligned footprint; `rotation` only tells where the
 * object's local "front" (access side) points, so rotating never leaves the 0.5 grid.
 *
 * Nothing per bay/level is persisted — bays, locator codes, access points and the pick
 * sequence are DERIVED (geometry.ts / wiring.ts / pick.ts). The persisted bay↔locator
 * mapping is `M_Locator.LayoutObjectKey` (+ LayoutPosX/Y/Side/PickSeqNo/LayoutZone), written
 * by the Apply step. DOM-free: imported by the server routes too.
 */

export type WbSide = 'N' | 'E' | 'S' | 'W'
export type WbRotation = 0 | 90 | 180 | 270
export type WbAccess = 'front' | 'back'
export type WbObjectType = 'shelf' | 'pallet' | 'door' | 'table' | 'obstacle' | 'walkway' | 'label' | 'camera'

export interface WbPlaced {
  key: string
  x: number
  y: number
  w: number
  d: number
  rotation: WbRotation
  label?: string
  locked?: boolean
}

export interface WbBaySpec {
  count: number
  start: string
  step: number
  pad: number
  direction: 'asc' | 'desc'
  codes?: string[]
}

export interface WbShelf extends WbPlaced {
  type: 'shelf'
  rackCode: string
  bays: WbBaySpec
  levels: string[]
  accessSide: WbAccess
  /** 'shelf' = Fachbodenregal (steel-blue shelving), 'rack' = Palettenregal (blue frames RAL 5010, orange beams RAL 2004) */
  style?: 'shelf' | 'rack'
  /** metres per level (default 0.45 shelf / 1.5 rack) */
  levelHeight?: number
  zoneOverride?: string | null
  pick?: { aisleNo?: number }
  perBay?: Record<number, { levels?: string[]; skip?: boolean }>
}

export interface WbPallet extends WbPlaced {
  type: 'pallet'
  locatorId?: number | null
  locatorCode?: string
  accessSide: WbAccess
  zoneOverride?: string | null
}

/** `hinge`: which end carries the hinge (a = left/top end, b = right/bottom end); `swing`: side the leaf opens to (a = −y/−x, b = +y/+x). */
export interface WbDoor extends WbPlaced { type: 'door'; doorType: 'in' | 'out' | 'both'; kind?: 'gate' | 'door'; hinge?: 'a' | 'b'; swing?: 'a' | 'b' }
export interface WbTable extends WbPlaced { type: 'table'; commissionTableId: number | null; tableNumber?: number | null }
export interface WbObstacle extends WbPlaced { type: 'obstacle'; kind: 'pillar' | 'machine' | 'office' | 'stairs' | 'other' }
export interface WbWalkway extends WbPlaced { type: 'walkway' }
export interface WbLabel extends WbPlaced { type: 'label'; text: string; fontSize: number }
/** Surveillance camera: `heading` in degrees (0 = north/up, clockwise), `fov` opening angle, `range` in m, `height` mount height (3D). */
export interface WbCamera extends WbPlaced { type: 'camera'; heading: number; fov: number; range: number; height?: number }

export type WbObject = WbShelf | WbPallet | WbDoor | WbTable | WbObstacle | WbWalkway | WbLabel | WbCamera

export interface WbZone {
  key: string
  code: string
  name: string
  color: string
  x: number
  y: number
  w: number
  d: number
  priority?: number
}

/** Orthogonal wall segment on the 0.5 m grid lines, normalised (x1<=x2, y1<=y2). */
export type WbWall = [number, number, number, number]

/** Walking lane drawn as a polyline (metres); `width` is the lane width. Non-blocking, a hint for the route engine. */
export interface WbWalkwayPath { key: string; points: Array<[number, number]>; width: number; label?: string; locked?: boolean }

export interface WbLayout {
  schemaVersion: 1
  grid: { width: number; height: number }
  units: { unit: 'm'; snap: 0.5 }
  walls: WbWall[]
  zones: WbZone[]
  walkways: WbWalkwayPath[]
  objects: WbObject[]
  pick: { startDoorKey?: string; aisleGap: number; aisleAxis: 'auto' | 'x' | 'y' }
  /** One physical hall mirrored per org: Apply + stock lookups treat equal locator codes in ALL warehouses as the same bin. */
  share?: { allWarehouses: boolean }
  print?: { paper: 'A4' | 'A3'; title?: string; showBayLabels: boolean }
  meta: { version: number; savedAt: string; savedBy: string; northAngle?: number }
}

export const WB_SNAP = 0.5
export const WB_MAX_GRID = 500
export const WB_MIN_SIZE = 0.5

export const ZONE_COLORS = ['#2563eb', '#16a34a', '#f59e0b', '#e11d48', '#7c3aed', '#0d9488', '#65a30d', '#64748b']

/** Flat tile-map palette — theme independent (print is always light). */
export const WB_COLORS = {
  floor: '#f3f1ec',
  floorDark: '#22262b',
  grid: 'rgba(0,0,0,0.07)',
  gridBold: 'rgba(0,0,0,0.14)',
  wall: '#2b2f36',
  shelf: '#6b8cae',
  shelfDivider: '#3f5f80',
  rackFrame: '#0e4c92',   // RAL 5010 gentian blue
  rackBeam: '#e75b12',    // RAL 2004 pure orange
  rackBody: '#e3e9f0',
  shelfText: '#ffffff',
  accessStripe: '#fbbf24',
  pallet: '#a9743f',
  palletSlat: '#7e5328',
  doorIn: '#22a06b',
  doorOut: '#e8871e',
  doorBoth: '#0e9aa7',
  table: '#5b5bd6',
  walkway: '#d9d2c0',
  obstacle: '#9aa0a6',
  camera: '#ea580c',
  obstacleHatch: '#6b7280',
  label: '#374151',
  selection: '#2563eb',
  collision: '#dc2626'
}

export const OBJECT_META: Record<WbObjectType, { de: string; en: string; icon: string; color: string; blocking: boolean }> = {
  shelf: { de: 'Regal', en: 'Shelf', icon: 'i-mdi-bookshelf', color: WB_COLORS.shelf, blocking: true },
  pallet: { de: 'Palettenplatz', en: 'Pallet place', icon: 'i-mdi-shipping-pallet', color: WB_COLORS.pallet, blocking: true },
  door: { de: 'Tor / Tür', en: 'Door', icon: 'i-mdi-door-open', color: WB_COLORS.doorBoth, blocking: false },
  table: { de: 'Kommissioniertisch', en: 'Commission table', icon: 'i-mdi-table-furniture', color: WB_COLORS.table, blocking: true },
  obstacle: { de: 'Hindernis', en: 'Obstacle', icon: 'i-mdi-cube-outline', color: WB_COLORS.obstacle, blocking: true },
  walkway: { de: 'Laufweg', en: 'Walkway', icon: 'i-mdi-walk', color: WB_COLORS.walkway, blocking: false },
  label: { de: 'Beschriftung', en: 'Label', icon: 'i-mdi-format-text', color: WB_COLORS.label, blocking: false },
  camera: { de: 'Überwachungskamera', en: 'Surveillance camera', icon: 'i-mdi-cctv', color: WB_COLORS.camera, blocking: false }
}

export const isBlockingType = (type: string): boolean => !!OBJECT_META[type as WbObjectType]?.blocking

export const uid = (): string => {
  try {
    const c = (globalThis as any)?.crypto
    if (c?.randomUUID) return c.randomUUID()
  } catch { /* noop */ }
  return 'wb_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36)
}

export const round2 = (n: number): number => Math.round((Number(n) || 0) * 100) / 100
export const snap = (v: number, step: number = WB_SNAP): number => round2(Math.round((Number(v) || 0) / step) * step)
export const clamp = (n: number, min: number, max: number): number => Math.min(Math.max(n, min), max)

export function createLayout (partial: Partial<WbLayout> = {}): WbLayout {
  return {
    schemaVersion: 1,
    grid: { width: partial.grid?.width ?? 40, height: partial.grid?.height ?? 25 },
    units: { unit: 'm', snap: 0.5 },
    walls: Array.isArray(partial.walls) ? partial.walls : [],
    zones: Array.isArray(partial.zones) ? partial.zones : [],
    walkways: Array.isArray(partial.walkways) ? partial.walkways : [],
    objects: Array.isArray(partial.objects) ? partial.objects : [],
    pick: { aisleGap: 3, aisleAxis: 'auto', ...(partial.pick || {}) },
    ...(partial.share ? { share: { allWarehouses: !!partial.share.allWarehouses } } : {}),
    print: { paper: 'A3', showBayLabels: true, ...(partial.print || {}) },
    meta: { version: 0, savedAt: '', savedBy: '', ...(partial.meta || {}) }
  }
}

/** Default object of `type` at (x, y) (already snapped by the caller). */
export function createObject (type: WbObjectType, x: number, y: number, patch: Record<string, any> = {}): WbObject {
  const base = { key: uid(), x: snap(x), y: snap(y), rotation: 0 as WbRotation }
  switch (type) {
    case 'shelf':
      return { ...base, type, w: 3, d: 1, rackCode: 'A', bays: { count: 3, start: '01', step: 1, pad: 2, direction: 'asc' }, levels: ['01', '02', '03'], accessSide: 'front', ...patch } as WbShelf
    case 'pallet':
      return { ...base, type, w: 1, d: 1, locatorId: null, accessSide: 'front', ...patch } as WbPallet
    case 'door':
      return { ...base, type, w: 3, d: 0.5, doorType: 'both', kind: 'gate', ...patch } as WbDoor
    case 'table':
      return { ...base, type, w: 2, d: 1, commissionTableId: null, ...patch } as WbTable
    case 'obstacle':
      return { ...base, type, w: 1, d: 1, kind: 'pillar', ...patch } as WbObstacle
    case 'walkway':
      return { ...base, type, w: 4, d: 2, ...patch } as WbWalkway
    case 'label':
      return { ...base, type, w: 4, d: 1, text: 'Text', fontSize: 0.6, ...patch } as WbLabel
    case 'camera':
      return { ...base, type, w: 0.5, d: 0.5, heading: 0, fov: 90, range: 15, height: 3, ...patch } as WbCamera
  }
}

export function createZone (x: number, y: number, w: number, d: number, index: number, patch: Partial<WbZone> = {}): WbZone {
  const letter = String.fromCharCode(65 + (index % 26))
  return { key: uid(), code: 'Z' + letter, name: 'Zone ' + letter, color: ZONE_COLORS[index % ZONE_COLORS.length], x: snap(x), y: snap(y), w: snap(w), d: snap(d), priority: 0, ...patch }
}

const num = (v: any, def = 0): number => (Number.isFinite(Number(v)) ? Number(v) : def)
const ROTS: WbRotation[] = [0, 90, 180, 270]

const normPlaced = (o: any) => ({
  key: String(o.key || uid()),
  x: round2(num(o.x)),
  y: round2(num(o.y)),
  w: Math.max(0.1, round2(num(o.w, 1))),
  d: Math.max(0.1, round2(num(o.d, 1))),
  rotation: (ROTS.includes(num(o.rotation) as WbRotation) ? num(o.rotation) : 0) as WbRotation,
  ...(o.label ? { label: String(o.label) } : {}),
  ...(o.locked ? { locked: true } : {})
})

const normObject = (o: any): WbObject | null => {
  if (!o || typeof o !== 'object') return null
  const p = normPlaced(o)
  switch (o.type) {
    case 'shelf': {
      const b = o.bays || {}
      const codes = Array.isArray(b.codes) ? b.codes.map((c: any) => String(c)) : undefined
      return {
        ...p, type: 'shelf', rackCode: String(o.rackCode ?? 'A'),
        bays: { count: clamp(Math.round(num(b.count, 1)), 1, 200), start: String(b.start ?? '01'), step: clamp(Math.round(num(b.step, 1)), 1, 10), pad: clamp(Math.round(num(b.pad, 2)), 0, 4), direction: b.direction === 'desc' ? 'desc' : 'asc', ...(codes ? { codes } : {}) },
        levels: Array.isArray(o.levels) && o.levels.length ? o.levels.map((l: any) => String(l)) : ['01'],
        accessSide: o.accessSide === 'back' ? 'back' : 'front',
        ...(o.style === 'rack' ? { style: 'rack' as const } : {}),
        ...(Number.isFinite(Number(o.levelHeight)) && Number(o.levelHeight) > 0 ? { levelHeight: clamp(num(o.levelHeight, 0.45), 0.2, 3) } : {}),
        ...(o.zoneOverride !== undefined ? { zoneOverride: o.zoneOverride === null ? null : String(o.zoneOverride) } : {}),
        ...(o.pick && Number.isFinite(Number(o.pick.aisleNo)) ? { pick: { aisleNo: Number(o.pick.aisleNo) } } : {}),
        ...(o.perBay && typeof o.perBay === 'object' ? { perBay: o.perBay } : {})
      }
    }
    case 'pallet':
      return { ...p, type: 'pallet', locatorId: o.locatorId ? Number(o.locatorId) : null, ...(o.locatorCode ? { locatorCode: String(o.locatorCode) } : {}), accessSide: o.accessSide === 'back' ? 'back' : 'front', ...(o.zoneOverride !== undefined ? { zoneOverride: o.zoneOverride === null ? null : String(o.zoneOverride) } : {}) }
    case 'door':
      return { ...p, type: 'door', doorType: ['in', 'out', 'both'].includes(o.doorType) ? o.doorType : 'both', kind: o.kind === 'door' ? 'door' : 'gate', hinge: o.hinge === 'b' ? 'b' : 'a', swing: o.swing === 'a' ? 'a' : 'b' }
    case 'table':
      return { ...p, type: 'table', commissionTableId: o.commissionTableId ? Number(o.commissionTableId) : null, ...(o.tableNumber != null ? { tableNumber: Number(o.tableNumber) } : {}) }
    case 'obstacle':
      return { ...p, type: 'obstacle', kind: ['pillar', 'machine', 'office', 'stairs', 'other'].includes(o.kind) ? o.kind : 'other' }
    case 'walkway':
      return { ...p, type: 'walkway' }
    case 'label':
      return { ...p, type: 'label', text: String(o.text ?? ''), fontSize: clamp(num(o.fontSize, 0.6), 0.2, 5) }
    case 'camera':
      return { ...p, type: 'camera', heading: ((num(o.heading, 0) % 360) + 360) % 360, fov: clamp(num(o.fov, 90), 10, 360), range: clamp(num(o.range, 15), 1, 200), height: clamp(num(o.height, 3), 0.5, 20) }
    default:
      return null
  }
}

/**
 * Parse + normalise a stored layout (string or object). Never throws: returns `layout: null`
 * with an `error` when the input is not a v1 layout. Unknown object types are dropped.
 */
export function parseLayout (input: any): { layout: WbLayout | null; error?: string } {
  let raw: any = input
  if (typeof input === 'string') {
    if (!input.trim()) return { layout: null, error: 'empty' }
    try { raw = JSON.parse(input) } catch (e: any) { return { layout: null, error: 'invalid JSON: ' + (e?.message || e) } }
  }
  if (!raw || typeof raw !== 'object') return { layout: null, error: 'not an object' }
  if (Number(raw.schemaVersion) !== 1) return { layout: null, error: 'unsupported schemaVersion ' + raw.schemaVersion }
  const width = clamp(Math.round(num(raw.grid?.width, 40)), 2, WB_MAX_GRID)
  const height = clamp(Math.round(num(raw.grid?.height, 25)), 2, WB_MAX_GRID)
  const walls: WbWall[] = (Array.isArray(raw.walls) ? raw.walls : [])
    .filter((w: any) => Array.isArray(w) && w.length === 4 && w.every((n: any) => Number.isFinite(Number(n))))
    .map((w: any) => [round2(num(w[0])), round2(num(w[1])), round2(num(w[2])), round2(num(w[3]))] as WbWall)
    .filter((w: WbWall) => (w[0] === w[2]) !== (w[1] === w[3]))
  const seenZone = new Set<string>()
  const zones: WbZone[] = (Array.isArray(raw.zones) ? raw.zones : []).map((z: any, i: number) => {
    let code = String(z?.code || '').trim().toUpperCase().slice(0, 40) || ('Z' + (i + 1))
    while (seenZone.has(code)) code = code.slice(0, 38) + '_' + (i + 1)
    seenZone.add(code)
    return { key: String(z?.key || uid()), code, name: String(z?.name || code), color: String(z?.color || ZONE_COLORS[i % ZONE_COLORS.length]), x: round2(num(z?.x)), y: round2(num(z?.y)), w: Math.max(0.5, round2(num(z?.w, 1))), d: Math.max(0.5, round2(num(z?.d, 1))), priority: Math.round(num(z?.priority, 0)) }
  })
  const walkways: WbWalkwayPath[] = (Array.isArray(raw.walkways) ? raw.walkways : []).map((w: any) => ({
    key: String(w?.key || uid()),
    points: (Array.isArray(w?.points) ? w.points : []).filter((pt: any) => Array.isArray(pt) && pt.length === 2 && pt.every((n: any) => Number.isFinite(Number(n)))).map((pt: any) => [round2(num(pt[0])), round2(num(pt[1]))] as [number, number]),
    width: clamp(num(w?.width, 1.5), 0.5, 10),
    ...(w?.label ? { label: String(w.label) } : {}), ...(w?.locked ? { locked: true } : {})
  })).filter((w: WbWalkwayPath) => w.points.length >= 2)
  const seenKey = new Set<string>()
  const objects: WbObject[] = []
  for (const o of (Array.isArray(raw.objects) ? raw.objects : [])) {
    const n = normObject(o)
    if (!n) continue
    if (seenKey.has(n.key)) n.key = uid()
    seenKey.add(n.key)
    objects.push(n)
  }
  const layout = createLayout({
    grid: { width, height }, walls, zones, walkways, objects,
    pick: { aisleGap: clamp(num(raw.pick?.aisleGap, 3), 1, 20), aisleAxis: ['x', 'y'].includes(raw.pick?.aisleAxis) ? raw.pick.aisleAxis : 'auto', ...(raw.pick?.startDoorKey ? { startDoorKey: String(raw.pick.startDoorKey) } : {}) },
    ...(raw.share && typeof raw.share === 'object' ? { share: { allWarehouses: !!raw.share.allWarehouses } } : {}),
    print: { paper: raw.print?.paper === 'A4' ? 'A4' : 'A3', showBayLabels: raw.print?.showBayLabels !== false, ...(raw.print?.title ? { title: String(raw.print.title) } : {}) },
    meta: { version: Math.round(num(raw.meta?.version, 0)), savedAt: String(raw.meta?.savedAt || ''), savedBy: String(raw.meta?.savedBy || ''), ...(raw.meta?.northAngle != null ? { northAngle: num(raw.meta.northAngle) } : {}) }
  })
  return { layout }
}

export function createWalkway (points: Array<[number, number]>, width = 1.5, patch: Partial<WbWalkwayPath> = {}): WbWalkwayPath {
  return { key: uid(), points: points.map(([x, y]) => [snap(x), snap(y)] as [number, number]), width: clamp(width, 0.5, 10), ...patch }
}
export const walkwayLength = (w: { points: Array<[number, number]> }): number => {
  let l = 0
  for (let i = 1; i < w.points.length; i++) l += Math.hypot(w.points[i][0] - w.points[i - 1][0], w.points[i][1] - w.points[i - 1][1])
  return round2(l)
}
export const walkwayBox = (w: { points: Array<[number, number]>; width: number }): { x: number; y: number; w: number; d: number } => {
  const xs = w.points.map(p => p[0]), ys = w.points.map(p => p[1])
  const h = w.width / 2
  const x = Math.min(...xs) - h, y = Math.min(...ys) - h
  return { x: round2(x), y: round2(y), w: round2(Math.max(...xs) + h - x), d: round2(Math.max(...ys) + h - y) }
}

/** Total height and level base heights of a shelf/rack (metres). */
export function rackMetrics (s: { levels: string[]; style?: 'shelf' | 'rack'; levelHeight?: number }): { H: number; lh: number; levelY: (k: number) => number; levels: number } {
  const levels = Math.max(1, s.levels.length)
  const lh = s.levelHeight && s.levelHeight > 0 ? s.levelHeight : (s.style === 'rack' ? 1.5 : 0.45)
  const H = Math.min(12, s.style === 'rack' ? levels * lh + 0.2 : 0.3 + levels * lh)
  const levelY = (k: number) => (s.style === 'rack' ? k * lh : 0.08 + k * ((H - 0.1) / levels))
  return { H, lh, levelY, levels }
}

/** Dev guard: every placed thing sits on the 0.5 m grid (positions) — sizes may be finer. */
export function offGridItems (layout: WbLayout): string[] {
  const bad: string[] = []
  const on = (v: number) => Math.abs(v / WB_SNAP - Math.round(v / WB_SNAP)) < 1e-6
  for (const o of layout.objects) if (!on(o.x) || !on(o.y)) bad.push(o.key)
  for (const z of layout.zones) if (!on(z.x) || !on(z.y)) bad.push(z.key)
  for (const w of layout.walls) if (!w.every(on)) bad.push(w.join(','))
  return bad
}
