/**
 * Warehouse Builder — wiring of map objects to M_Locator rows.
 *
 * Shelf bay × level → locator code `${X}-${Y}-${Z}` (X = rack code, Y = bay code, Z = level) —
 * the warehouse-wide convention (locatorCatalog.parseXyz). Pallet place → one locator id.
 * Statuses tell the Inspector / wiring modal what Apply would do. Also hosts the import
 * wizard's auto-layout (existing locators → facing rack pairs). DOM-free.
 */
import type { WbLayout, WbObject, WbPallet, WbShelf } from './schema'
import { createLayout, createObject, snap, uid } from './schema'
import type { WbBay } from './geometry'

export interface WbLocatorRow {
  id: number
  value: string
  x: string
  y: string
  z: string
  priorityNo: number
  isDefault: boolean
  isActive: boolean
  locatorTypeId: number | null
  locatorType?: string
  layoutPosX?: number | null
  layoutPosY?: number | null
  layoutSide?: string | null
  pickSeqNo?: number | null
  layoutObjectKey?: string | null
  layoutZone?: string | null
  layoutId?: number | null
}

export const norm = (v: any): string => String(v ?? '').trim().toLowerCase().replace(/\s+/g, '')
export const xyzKey = (x: any, y: any, z: any): string => `${norm(x)}|${norm(y)}|${norm(z)}`
export const locatorCode = (x: string, y: string, z: string): string => `${x}-${y}-${z}`

/** Tolerant mapper from an iDempiere m_locator record (any casing). */
export function mapLocatorRow (r: any): WbLocatorRow {
  const idOf = (v: any) => (v && typeof v === 'object' ? Number(v.id) : Number(v)) || null
  const numOrNull = (v: any) => (v === undefined || v === null || v === '' ? null : Number(v))
  const isY = (v: any) => v === true || v === 'Y'
  return {
    id: Number(r.id),
    value: String(r.Value ?? r.value ?? ''),
    x: String(r.X ?? r.x ?? ''),
    y: String(r.Y ?? r.y ?? ''),
    z: String(r.Z ?? r.z ?? ''),
    priorityNo: Number(r.PriorityNo ?? r.priorityNo ?? 50) || 50,
    isDefault: isY(r.IsDefault ?? r.isDefault),
    isActive: (r.IsActive ?? r.isActive) === undefined ? true : isY(r.IsActive ?? r.isActive),
    locatorTypeId: idOf(r.M_LocatorType_ID),
    locatorType: r.M_LocatorType_ID && typeof r.M_LocatorType_ID === 'object' ? String(r.M_LocatorType_ID.identifier || '') : '',
    layoutPosX: numOrNull(r.LayoutPosX ?? r.layoutPosX),
    layoutPosY: numOrNull(r.LayoutPosY ?? r.layoutPosY),
    layoutSide: (r.LayoutSide ?? r.layoutSide) || null,
    pickSeqNo: numOrNull(r.PickSeqNo ?? r.pickSeqNo),
    layoutObjectKey: (r.LayoutObjectKey ?? r.layoutObjectKey) || null,
    layoutZone: (r.LayoutZone ?? r.layoutZone) || null,
    layoutId: idOf(r.CUST_WarehouseLayout_ID)
  }
}

export interface LocatorIndex {
  byXyz: Map<string, WbLocatorRow>
  byValue: Map<string, WbLocatorRow>
  byId: Map<number, WbLocatorRow>
}

export function buildLocatorIndex (rows: WbLocatorRow[]): LocatorIndex {
  const byXyz = new Map<string, WbLocatorRow>(), byValue = new Map<string, WbLocatorRow>(), byId = new Map<number, WbLocatorRow>()
  for (const r of rows) {
    byId.set(r.id, r)
    if (r.value && !byValue.has(norm(r.value))) byValue.set(norm(r.value), r)
    if (r.x || r.y || r.z) { const k = xyzKey(r.x, r.y, r.z); if (!byXyz.has(k)) byXyz.set(k, r) }
  }
  return { byXyz, byValue, byId }
}

export const findLocator = (index: LocatorIndex, x: string, y: string, z: string): WbLocatorRow | null =>
  index.byXyz.get(xyzKey(x, y, z)) || index.byValue.get(norm(locatorCode(x, y, z))) || null

export type WiringStatus = 'exists-free' | 'exists-mapped-here' | 'exists-mapped-elsewhere' | 'missing' | 'conflict'

export interface WbCodeEntry {
  code: string
  x: string; y: string; z: string
  objectKey: string
  bayKey: string
  bayIndex: number
  levelIdx: number
  locatorId: number | null
  status: WiringStatus
  otherKey?: string | null
}

export interface CodeIndex {
  entries: WbCodeEntry[]
  byCode: Map<string, WbCodeEntry>
  byBay: Map<string, WbCodeEntry[]>
  byObject: Map<string, WbCodeEntry[]>
  duplicates: string[]
  mappedLocatorIds: Set<number>
  counts: Record<WiringStatus, number>
}

/** Every (bay, level) of the layout resolved against the warehouse locators. */
export function computeCodeIndex (layout: WbLayout, bays: WbBay[], index: LocatorIndex): CodeIndex {
  const objects = new Map<string, WbObject>(layout.objects.map(o => [o.key, o]))
  const entries: WbCodeEntry[] = []
  const seen = new Map<string, WbCodeEntry>()
  const dup = new Set<string>()
  for (const b of bays) {
    const o = objects.get(b.objectKey)
    if (!o) continue
    if (o.type === 'shelf') {
      if (b.skip) continue
      const s = o as WbShelf
      b.levels.forEach((z, levelIdx) => {
        const code = locatorCode(s.rackCode, b.code, z)
        const e: WbCodeEntry = { code, x: s.rackCode, y: b.code, z, objectKey: s.key, bayKey: b.key, bayIndex: b.index, levelIdx, locatorId: null, status: 'missing' }
        const k = norm(code)
        if (seen.has(k)) { dup.add(code); e.status = 'conflict'; const first = seen.get(k)!; first.status = 'conflict' } else seen.set(k, e)
        entries.push(e)
      })
    } else if (o.type === 'pallet') {
      const p = o as WbPallet
      const row = p.locatorId ? index.byId.get(Number(p.locatorId)) : null
      const code = row?.value || p.locatorCode || ''
      const e: WbCodeEntry = { code, x: row?.x || '', y: row?.y || '', z: row?.z || '', objectKey: p.key, bayKey: b.key, bayIndex: 0, levelIdx: 0, locatorId: row?.id ?? null, status: row ? 'exists-free' : 'missing' }
      if (row) { const k = 'id:' + row.id; if (seen.has(k)) { e.status = 'conflict'; seen.get(k)!.status = 'conflict'; dup.add(code) } else seen.set(k, e) }
      entries.push(e)
    }
  }
  const mapped = new Set<number>()
  for (const e of entries) {
    if (e.status === 'conflict') continue
    const row = e.locatorId ? index.byId.get(e.locatorId) : findLocator(index, e.x, e.y, e.z)
    if (!row) { e.status = 'missing'; continue }
    e.locatorId = row.id
    if (mapped.has(row.id)) { e.status = 'conflict'; dup.add(e.code); continue }
    mapped.add(row.id)
    const ok = row.layoutObjectKey
    if (!ok || ok === e.objectKey) e.status = ok ? 'exists-mapped-here' : 'exists-free'
    else { e.status = 'exists-mapped-elsewhere'; e.otherKey = ok }
  }
  const byCode = new Map<string, WbCodeEntry>(), byBay = new Map<string, WbCodeEntry[]>(), byObject = new Map<string, WbCodeEntry[]>()
  const counts: Record<WiringStatus, number> = { 'exists-free': 0, 'exists-mapped-here': 0, 'exists-mapped-elsewhere': 0, missing: 0, conflict: 0 }
  for (const e of entries) {
    counts[e.status]++
    if (e.code) byCode.set(norm(e.code), e)
    if (!byBay.has(e.bayKey)) byBay.set(e.bayKey, [])
    byBay.get(e.bayKey)!.push(e)
    if (!byObject.has(e.objectKey)) byObject.set(e.objectKey, [])
    byObject.get(e.objectKey)!.push(e)
  }
  return { entries, byCode, byBay, byObject, duplicates: [...dup], mappedLocatorIds: mapped, counts }
}

/** Warehouse locators that no object of this layout produces. */
export const unmappedLocators = (rows: WbLocatorRow[], ci: CodeIndex): WbLocatorRow[] =>
  rows.filter(r => !ci.mappedLocatorIds.has(r.id))

/** Overall bay status for colouring (worst wins). */
export function bayStatus (entries: WbCodeEntry[] | undefined): WiringStatus | 'empty' {
  if (!entries || !entries.length) return 'empty'
  const order: WiringStatus[] = ['conflict', 'missing', 'exists-mapped-elsewhere', 'exists-free', 'exists-mapped-here']
  for (const s of order) if (entries.some(e => e.status === s)) return s
  return 'exists-free'
}

export const naturalCompare = (a: string, b: string): number => a.localeCompare(b, 'de', { numeric: true, sensitivity: 'base' })

// ---- import wizard ----------------------------------------------------------------------------
export interface AutoLayoutOptions {
  aisleGap?: number     // metres between facing racks (default 3)
  backGap?: number      // metres between back-to-back racks (default 0.5)
  bayWidth?: number     // default 1
  depth?: number        // default 1
  margin?: number       // default 2
  existingRackCodes?: Set<string>
}

/** Racks (X) → shelves laid out as facing pairs, bays = distinct Y, levels = distinct Z. */
export function autoLayoutFromLocators (rows: WbLocatorRow[], opts: AutoLayoutOptions = {}): { objects: WbShelf[]; grid: { width: number; height: number }; skipped: WbLocatorRow[] } {
  const aisleGap = opts.aisleGap ?? 3, backGap = opts.backGap ?? 0.5, bayW = opts.bayWidth ?? 1, depth = opts.depth ?? 1, margin = opts.margin ?? 2
  const racks = new Map<string, { ys: Set<string>; zs: Set<string> }>()
  const skipped: WbLocatorRow[] = []
  for (const r of rows) {
    if (!r.x || !r.y || !r.z) { skipped.push(r); continue }
    if (opts.existingRackCodes?.has(r.x.toUpperCase())) continue
    if (!racks.has(r.x)) racks.set(r.x, { ys: new Set(), zs: new Set() })
    racks.get(r.x)!.ys.add(r.y); racks.get(r.x)!.zs.add(r.z)
  }
  const codes = [...racks.keys()].sort(naturalCompare)
  const objects: WbShelf[] = []
  let y = margin, maxLen = 0
  codes.forEach((code, i) => {
    const rack = racks.get(code)!
    const ys = [...rack.ys].sort(naturalCompare)
    const zs = [...rack.zs].sort(naturalCompare)
    const w = snap(Math.max(bayW, ys.length * bayW))
    maxLen = Math.max(maxLen, w)
    const pairIdx = Math.floor(i / 2), second = i % 2 === 1
    // pair k: rack A faces down at yA, rack B faces up directly below the aisle
    const yA = margin + pairIdx * (2 * depth + aisleGap + backGap)
    const yy = second ? yA + depth + aisleGap : yA
    const contiguous = ys.every((v, k) => k === 0 || (/^\d+$/.test(v) && /^\d+$/.test(ys[k - 1]) && parseInt(v, 10) === parseInt(ys[k - 1], 10) + 1))
    const numeric = ys.every(v => /^\d+$/.test(v))
    objects.push({
      key: uid(), type: 'shelf', x: margin, y: snap(yy), w, d: depth, rotation: 0,
      rackCode: code,
      bays: { count: ys.length, start: ys[0], step: 1, pad: numeric ? ys[0].length : 2, direction: 'asc', ...(contiguous && numeric ? {} : { codes: ys }) },
      levels: zs,
      accessSide: second ? 'back' : 'front'
    })
    y = yy + depth
  })
  const width = Math.max(10, Math.ceil(maxLen + 2 * margin))
  const height = Math.max(10, Math.ceil(y + margin))
  return { objects, grid: { width, height }, skipped }
}

/** Convenience: new layout from existing locators (empty 40×25 when there are none). */
export function layoutFromLocators (rows: WbLocatorRow[]): WbLayout {
  if (!rows.length) return createLayout()
  const r = autoLayoutFromLocators(rows)
  const layout = createLayout({ grid: r.grid, objects: r.objects })
  // outer walls around the hall
  layout.walls = [[0, 0, r.grid.width, 0], [r.grid.width, 0, r.grid.width, r.grid.height], [0, r.grid.height, r.grid.width, r.grid.height], [0, 0, 0, r.grid.height]]
  return layout
}

export { createObject }
