/**
 * Warehouse Builder — pick preparation: serpentine walking order + the Apply payload.
 *
 * This pass only PREPARES pick routing: every mapped locator gets its access point
 * (LayoutPosX/Y + LayoutSide), its zone and a deterministic `PickSeqNo` so any picker can
 * `orderby PickSeqNo`. The real shortest-path engine (walkable grid from walls/objects) comes
 * later and only has to rewrite the same columns.
 *
 * Serpentine heuristic: racks are grouped into aisles by the coordinate of their access
 * faces (greedy clustering within `pick.aisleGap`, only faces that look at each other), aisles
 * are walked in order of position starting at the door, alternating direction per aisle;
 * levels bottom→top. `PickSeqNo = floorNo·100000 + bayOrdinal·10 + levelIdx`.
 */
import type { WbDoor, WbLayout, WbShelf, WbSide } from './schema'
import { lengthAxis } from './geometry'
import type { WbBay } from './geometry'
import type { CodeIndex } from './wiring'

export interface WbAssignment {
  locatorId: number
  posX: number
  posY: number
  side: WbSide
  pickSeqNo: number
  objectKey: string
  zone: string | null
  code: string
  status: string
}

interface Face { bay: WbBay; coord: number; along: number; cy: number; faces: 1 | -1; aisleNo?: number }

const majorityAxis = (layout: WbLayout): 'x' | 'y' => {
  let x = 0, y = 0
  for (const o of layout.objects) if (o.type === 'shelf') { if (lengthAxis(o.rotation) === 'x') x += o.w; else y += o.d }
  return y > x ? 'y' : 'x'
}

/** bayKey → 1-based ordinal in walking order. */
export function computePickOrder (layout: WbLayout, bays: WbBay[]): { order: Map<string, number>; aisles: Array<{ no: number; coord: number; bayKeys: string[] }> } {
  const axis = layout.pick.aisleAxis === 'auto' ? majorityAxis(layout) : layout.pick.aisleAxis
  const gap = layout.pick.aisleGap || 3
  const shelves = new Map<string, WbShelf>(layout.objects.filter(o => o.type === 'shelf').map(o => [o.key, o as WbShelf]))
  // aisle coordinate = access point across the aisle axis; along = position along the aisle
  const faces: Face[] = bays.filter(b => !b.skip).map(b => {
    const coord = axis === 'x' ? b.access.y : b.access.x
    const along = axis === 'x' ? b.access.x : b.access.y
    const centre = axis === 'x' ? b.cy : b.cx
    // +1 = the rack lies BEFORE the aisle (faces towards +coord), -1 = after it
    const facesDir: 1 | -1 = coord >= centre ? 1 : -1
    return { bay: b, coord, along, cy: centre, faces: facesDir, aisleNo: shelves.get(b.objectKey)?.pick?.aisleNo }
  })
  faces.sort((a, b) => a.coord - b.coord || a.along - b.along)

  type Cluster = { faces: Face[]; min: number; max: number; forced?: number }
  const clusters: Cluster[] = []
  const forced = new Map<number, Cluster>()
  const compatible = (c: Cluster, f: Face) => {
    if (f.coord - c.min > gap) return false
    // two racks looking away from each other must not share an aisle
    for (const m of c.faces) {
      if (m.faces === 1 && f.faces === -1 && m.cy > f.cy) return false
      if (m.faces === -1 && f.faces === 1 && m.cy < f.cy) return false
    }
    return true
  }
  for (const f of faces) {
    if (f.aisleNo != null) {
      let c = forced.get(f.aisleNo)
      if (!c) { c = { faces: [], min: f.coord, max: f.coord, forced: f.aisleNo }; forced.set(f.aisleNo, c); clusters.push(c) }
      c.faces.push(f); c.min = Math.min(c.min, f.coord); c.max = Math.max(c.max, f.coord)
      continue
    }
    let target: Cluster | null = null
    for (let i = clusters.length - 1; i >= 0; i--) {
      const c = clusters[i]
      if (c.forced != null) continue
      if (compatible(c, f)) { target = c; break }
      if (f.coord - c.max > gap) break
    }
    if (!target) { target = { faces: [], min: f.coord, max: f.coord }; clusters.push(target) }
    target.faces.push(f); target.min = Math.min(target.min, f.coord); target.max = Math.max(target.max, f.coord)
  }
  const mean = (c: Cluster) => c.faces.reduce((s, f) => s + f.coord, 0) / (c.faces.length || 1)
  clusters.sort((a, b) => (a.forced != null && b.forced != null) ? a.forced - b.forced : mean(a) - mean(b))

  // start near the inbound door when there is one
  const door = (layout.pick.startDoorKey && layout.objects.find(o => o.key === layout.pick.startDoorKey)) ||
    layout.objects.find(o => o.type === 'door' && (o as WbDoor).doorType !== 'out') ||
    layout.objects.find(o => o.type === 'door')
  let reverseAisles = false, startDesc = false
  if (door && clusters.length) {
    const dCoord = axis === 'x' ? door.y + door.d / 2 : door.x + door.w / 2
    const dAlong = axis === 'x' ? door.x + door.w / 2 : door.y + door.d / 2
    const first = mean(clusters[0]), last = mean(clusters[clusters.length - 1])
    reverseAisles = Math.abs(dCoord - last) < Math.abs(dCoord - first)
    const alongs = faces.map(f => f.along)
    const lo = Math.min(...alongs), hi = Math.max(...alongs)
    startDesc = Math.abs(dAlong - hi) < Math.abs(dAlong - lo)
  }
  if (reverseAisles) clusters.reverse()

  const order = new Map<string, number>()
  const aisles: Array<{ no: number; coord: number; bayKeys: string[] }> = []
  let ordinal = 0
  clusters.forEach((c, k) => {
    const desc = (k % 2 === 1) !== startDesc
    const sorted = [...c.faces].sort((a, b) => (desc ? b.along - a.along : a.along - b.along) || a.faces - b.faces)
    const keys: string[] = []
    for (const f of sorted) { ordinal++; order.set(f.bay.key, ordinal); keys.push(f.bay.key) }
    aisles.push({ no: k + 1, coord: Math.round(mean(c) * 100) / 100, bayKeys: keys })
  })
  return { order, aisles }
}

export const pickSeqNo = (floorNo: number, ordinal: number, levelIdx: number): number =>
  (Math.max(0, floorNo) * 100000) + Math.min(ordinal, 9999) * 10 + Math.min(levelIdx, 9)

/** The Apply payload: one row per resolved locator (conflicts excluded). */
export function buildAssignments (
  layout: WbLayout, bays: WbBay[], codeIndex: CodeIndex, zones: Map<string, string | null>, floorNo: number
): { assignments: WbAssignment[]; order: Map<string, number> } {
  const { order } = computePickOrder(layout, bays)
  const bayByKey = new Map(bays.map(b => [b.key, b]))
  const assignments: WbAssignment[] = []
  for (const e of codeIndex.entries) {
    if (!e.locatorId || e.status === 'conflict' || e.status === 'missing') continue
    const b = bayByKey.get(e.bayKey)
    if (!b) continue
    assignments.push({
      locatorId: e.locatorId,
      posX: b.access.x,
      posY: b.access.y,
      side: b.side,
      pickSeqNo: pickSeqNo(floorNo, order.get(b.key) || 0, e.levelIdx),
      objectKey: e.objectKey,
      zone: zones.get(b.key) ?? null,
      code: e.code,
      status: e.status
    })
  }
  return { assignments, order }
}
