import type { World } from '../world/types'
import type { GameState, ParsedCommand, DispatchResult, ItemInstance, TranscriptLine } from './types'
import { SCHEMA_VERSION, TRANSCRIPT_CAP } from './types'
import { applyVerbToEncounter, maybeTriggerEncounter } from './encounters'

const HALFSTREET_ASCII = String.raw`
 _   _       _  __     ____  _                 _
| | | | __ _| |/ _|   / ___|| |_ _ __ ___  ___| |_
| |_| |/ _\` | | |_    \___ \| __| '__/ _ \/ _ \ __|
|  _  | (_| | |  _|    ___) | |_| | |  __/  __/ |_
|_| |_|\__,_|_|_|     |____/ \__|_|  \___|\___|\__|
`.trim()

export function initialStateFor(world: World): GameState {
  const startingRoom = world.rooms[world.startingRoom]
  if (!startingRoom) throw new Error(`World has invalid startingRoom: ${world.startingRoom}`)

  const inventory: ItemInstance[] = world.startingInventory.map((id) => {
    const item = world.items[id]
    if (!item) throw new Error(`Starting inventory references unknown item: ${id}`)
    return { id, state: { ...item.initialState } }
  })

  const opening: TranscriptLine[] = [
    { kind: 'system', text: HALFSTREET_ASCII },
    { kind: 'system', text: startingRoom.title },
    { kind: 'narration', text: startingRoom.descriptions.firstVisit },
  ]

  return {
    schemaVersion: SCHEMA_VERSION,
    location: world.startingRoom,
    inventory,
    roomState: { [world.startingRoom]: { visited: true } },
    flags: {},
    resolveLevel: 'steady',
    encounterState: {},
    lastNoun: null,
    pendingDisambiguation: null,
    transcript: opening,
    endedWith: null,
  }
}

function append(state: GameState, lines: TranscriptLine[]): GameState {
  const transcript = [...state.transcript, ...lines]
  return { ...state, transcript: transcript.slice(-TRANSCRIPT_CAP) }
}

export function getItemsInRoom(state: GameState, world: World, roomId: string): string[] {
  const baseItems = world.rooms[roomId]?.items ?? []
  const dropped = (state.roomState[roomId]?.['droppedItems'] ?? []) as string[]
  const taken = (state.roomState[roomId]?.['takenItems'] ?? []) as string[]
  return [...baseItems.filter((i) => !taken.includes(i)), ...dropped]
}

function setRoomFlag(state: GameState, roomId: string, key: string, value: string | boolean | number | string[]): GameState {
  return {
    ...state,
    roomState: {
      ...state.roomState,
      [roomId]: { ...(state.roomState[roomId] ?? {}), [key]: value },
    },
  }
}

const ENDING_PRIORITY: ('true' | 'wrong' | 'bad')[] = ['true', 'wrong', 'bad']

function evaluateEndings(state: GameState, world: World): GameState | null {
  if (state.endedWith) return null
  for (const id of ENDING_PRIORITY) {
    const ending = world.endings[id]
    const flags = ending.whenFlags
    let allMatch = true
    for (const [k, v] of Object.entries(flags)) {
      if (state.flags[k] !== v) { allMatch = false; break }
    }
    if (!allMatch) continue
    return {
      ...state,
      endedWith: id,
      transcript: [...state.transcript, { kind: 'ending', text: ending.narration }],
    }
  }
  return null
}

function withEndingCheck(result: DispatchResult, world: World): DispatchResult {
  const updated = evaluateEndings(result.state, world)
  if (!updated) return result
  const endingLine: TranscriptLine = updated.transcript[updated.transcript.length - 1]!
  return { state: updated, appended: [...result.appended, endingLine] }
}

export function dispatch(state: GameState, command: ParsedCommand, world: World): DispatchResult {
  // Disambiguation reply: re-issue the original verb with the chosen target.
  if (command.kind === 'disambiguation') {
    const pending = state.pendingDisambiguation
    if (!pending) {
      return narrate(state, [{ kind: 'narration', text: 'Nothing to choose between.' }])
    }
    const cleared: GameState = { ...state, pendingDisambiguation: null }
    return dispatch(
      cleared,
      { kind: 'verb-target', verb: pending.verb, target: { canonical: command.chosen, raw: command.chosen } },
      world,
    )
  }

  // Once the game has ended, only restart/undo (handled by the UI) can clear state.
  if (state.endedWith) {
    return narrate(state, [{ kind: 'narration', text: 'The story has ended. Type `restart` or `undo`.' }])
  }

  if (command.kind === 'unknown') {
    const text =
      command.reason === 'unknown-verb' ? 'You consider the words, but they don\'t fit this place.'
      : command.reason === 'unknown-noun' ? 'You don\'t see anything like that here.'
      : 'You hesitate.'
    return narrate(state, [{ kind: 'narration', text }])
  }

  if (command.kind === 'meta') {
    return handleMeta(state, command.verb)
  }

  if (command.kind === 'go') {
    return withEndingCheck(handleGo(state, command.direction, world), world)
  }

  if (command.kind === 'ambiguous') {
    const candidateShorts = command.candidates.map((id) => world.items[id]?.short ?? id)
    const list =
      candidateShorts.length === 2
        ? `${candidateShorts[0]}, or ${candidateShorts[1]}`
        : candidateShorts.slice(0, -1).join(', ') + ', or ' + candidateShorts[candidateShorts.length - 1]
    const prompt = `Which ${command.rawNoun} — ${list}?`
    const next: GameState = {
      ...state,
      pendingDisambiguation: { verb: command.verb, candidates: command.candidates, prompt },
    }
    return narrate(next, [{ kind: 'narration', text: prompt }])
  }

  if (command.kind === 'verb-only') {
    const encResult = applyVerbToEncounter(state, command, world)
    if (encResult?.consumed) {
      return withEndingCheck({ state: encResult.state, appended: encResult.lines }, world)
    }
    if (command.verb === 'look') return withEndingCheck(handleLook(state, world), world)
    if (command.verb === 'inventory') return withEndingCheck(handleInventory(state, world), world)
    if (command.verb === 'wait') return withEndingCheck(narrate(state, [{ kind: 'narration', text: 'Time passes.' }]), world)
  }

  if (command.kind === 'verb-target') {
    const stateWithNoun: GameState = { ...state, lastNoun: command.target }
    // Try the active encounter first — it may consume verbs like 'attack', 'hold'.
    const encResult = applyVerbToEncounter(stateWithNoun, command, world)
    if (encResult?.consumed) {
      return withEndingCheck({ state: encResult.state, appended: encResult.lines }, world)
    }
    if (command.verb === 'take') return withEndingCheck(handleTake(stateWithNoun, command.target.canonical, world), world)
    if (command.verb === 'drop') return withEndingCheck(handleDrop(stateWithNoun, command.target.canonical, world), world)
    if (command.verb === 'examine' || command.verb === 'look') return withEndingCheck(handleExamine(stateWithNoun, command.target.canonical, world), world)
    if (command.verb === 'read') return withEndingCheck(handleRead(stateWithNoun, command.target.canonical, world), world)
    if (command.verb === 'light') return withEndingCheck(handleLight(stateWithNoun, command.target.canonical, null, world), world)
    if (command.verb === 'extinguish') return withEndingCheck(handleExtinguish(stateWithNoun, command.target.canonical, world), world)
    if (command.verb === 'use') return withEndingCheck(narrate(stateWithNoun, [{ kind: 'narration', text: "You can't think how to use that here." }]), world)
    return withEndingCheck(narrate(stateWithNoun, [{ kind: 'narration', text: `You're not sure how to ${command.verb} that.` }]), world)
  }

  if (command.kind === 'verb-target-prep') {
    const stateWithNoun: GameState = { ...state, lastNoun: command.target }
    // Try the encounter first — it may consume verbs like 'cut vines with shears'.
    const encResult = applyVerbToEncounter(stateWithNoun, command, world)
    if (encResult?.consumed) {
      return withEndingCheck({ state: encResult.state, appended: encResult.lines }, world)
    }
    if (command.verb === 'light' && command.preposition === 'with') {
      return withEndingCheck(handleLight(stateWithNoun, command.target.canonical, command.indirect.canonical, world), world)
    }
    if (command.verb === 'use') {
      return withEndingCheck(narrate(stateWithNoun, [{ kind: 'narration', text: "You can't think how to use that here." }]), world)
    }
    return withEndingCheck(narrate(stateWithNoun, [{ kind: 'narration', text: `You're not sure how to ${command.verb} that.` }]), world)
  }

  return narrate(state, [{ kind: 'narration', text: 'Nothing happens.' }])
}

function narrate(state: GameState, lines: TranscriptLine[]): DispatchResult {
  return { state: append(state, lines), appended: lines }
}

function handleMeta(state: GameState, verb: 'restart' | 'undo' | 'hint' | 'save' | 'quit' | 'theme'): DispatchResult {
  if (verb === 'save') return narrate(state, [{ kind: 'system', text: '(your progress is saved automatically)' }])
  // 'theme' is a UI preference: the terminal intercepts it before dispatch and
  // dispatches a 'halfstreet-toggle-theme' DOM event. The engine no-ops here so
  // typing the verb still produces transcript output if the UI ever misses it.
  if (verb === 'theme') return narrate(state, [{ kind: 'system', text: '(theme)' }])
  // restart / undo / hint / quit are handled by the UI layer (state mutations
  // require coordination with the save layer and route navigation). The
  // engine acknowledges them with a no-op narration; the UI intercepts before
  // calling dispatch for these.
  return narrate(state, [{ kind: 'system', text: `(${verb})` }])
}

function handleGo(state: GameState, direction: 'n' | 's' | 'e' | 'w' | 'u' | 'd', world: World): DispatchResult {
  const room = world.rooms[state.location]
  if (!room) return narrate(state, [{ kind: 'narration', text: 'You are nowhere.' }])

  const dest = room.exits[direction]
  if (!dest) {
    return narrate(state, [{ kind: 'narration', text: 'You can\'t go that way.' }])
  }

  const lock = room.lockedExits?.[direction]
  if (lock) {
    const hasKey = state.inventory.some((i) => i.id === lock.requires) || !!state.flags[lock.requires]
    if (!hasKey) {
      return narrate(state, [{ kind: 'narration', text: lock.lockedNarration }])
    }
  }

  const destRoom = world.rooms[dest]
  if (!destRoom) return narrate(state, [{ kind: 'narration', text: 'The way ahead is unfinished.' }])

  const visited = !!state.roomState[dest]?.['visited']
  const description = visited ? destRoom.descriptions.revisit : destRoom.descriptions.firstVisit

  let next: GameState = { ...state, location: dest }
  next = setRoomFlag(next, dest, 'visited', true)

  if (destRoom.safe) {
    const ladder = ['steady', 'shaken', 'reeling', 'returning'] as const
    const idx = ladder.indexOf(state.resolveLevel)
    if (idx > 0) next = { ...next, resolveLevel: ladder[idx - 1]! }
  }

  const arrivalLines: TranscriptLine[] = [
    { kind: 'system', text: destRoom.title },
    { kind: 'narration', text: description },
  ]
  const result = narrate(next, arrivalLines)

  // Trigger any encounter waiting in this room.
  const triggered = maybeTriggerEncounter(result.state, world)
  if (triggered) {
    return { state: triggered.state, appended: [...arrivalLines, ...triggered.appended] }
  }
  return result
}

function handleLook(state: GameState, world: World): DispatchResult {
  const room = world.rooms[state.location]
  if (!room) return narrate(state, [{ kind: 'narration', text: 'You see nothing.' }])
  const items = getItemsInRoom(state, world, state.location)
  const itemNarration = describeRoomItems(items.map((id) => world.items[id]?.short ?? id))
  return narrate(state, [
    { kind: 'system', text: room.title },
    { kind: 'narration', text: room.descriptions.examined },
    ...(itemNarration ? [{ kind: 'narration' as const, text: itemNarration }] : []),
  ])
}

function describeRoomItems(shorts: string[]): string {
  if (shorts.length === 0) return ''
  const names = [sentenceCase(shorts[0]!), ...shorts.slice(1)]
  const verb = names.length === 1 ? 'is' : 'are'
  return `${joinList(names)} ${verb} here.`
}

function sentenceCase(value: string): string {
  return value.length === 0 ? value : value[0]!.toUpperCase() + value.slice(1)
}

function joinList(values: string[]): string {
  if (values.length === 1) return values[0]!
  if (values.length === 2) return `${values[0]} and ${values[1]}`
  return `${values.slice(0, -1).join(', ')}, and ${values[values.length - 1]}`
}

function handleInventory(state: GameState, world: World): DispatchResult {
  if (state.inventory.length === 0) {
    return narrate(state, [{ kind: 'narration', text: 'You are empty-handed.' }])
  }
  const lines = state.inventory.map((inst) => {
    const item = world.items[inst.id]
    const litSuffix = inst.state['lit'] === true ? ' (lit)' : ''
    return `  ${item?.short ?? inst.id}${litSuffix}`
  })
  return narrate(state, [
    { kind: 'narration', text: 'You are carrying:' },
    { kind: 'narration', text: lines.join('\n') },
  ])
}

function handleTake(state: GameState, itemId: string, world: World): DispatchResult {
  const item = world.items[itemId]
  if (!item) return narrate(state, [{ kind: 'narration', text: 'You don\'t see that here.' }])
  if (!item.takeable) return narrate(state, [{ kind: 'narration', text: 'You can\'t take that.' }])

  const itemsHere = getItemsInRoom(state, world, state.location)
  if (!itemsHere.includes(itemId)) {
    return narrate(state, [{ kind: 'narration', text: 'You don\'t see that here.' }])
  }
  if (state.inventory.find((i) => i.id === itemId)) {
    return narrate(state, [{ kind: 'narration', text: 'You already have it.' }])
  }

  const wasInRoomBase = (world.rooms[state.location]?.items ?? []).includes(itemId)
  let next: GameState = {
    ...state,
    inventory: [...state.inventory, { id: itemId, state: { ...item.initialState } }],
  }
  if (wasInRoomBase) {
    const taken = (next.roomState[state.location]?.['takenItems'] ?? []) as string[]
    next = setRoomFlag(next, state.location, 'takenItems', [...taken, itemId])
  } else {
    const dropped = (next.roomState[state.location]?.['droppedItems'] ?? []) as string[]
    next = setRoomFlag(next, state.location, 'droppedItems', dropped.filter((id) => id !== itemId))
  }
  return narrate(next, [{ kind: 'narration', text: 'Taken.' }])
}

function handleDrop(state: GameState, itemId: string, _world: World): DispatchResult {
  if (!state.inventory.find((i) => i.id === itemId)) {
    return narrate(state, [{ kind: 'narration', text: 'You don\'t have that.' }])
  }
  let next: GameState = {
    ...state,
    inventory: state.inventory.filter((i) => i.id !== itemId),
  }
  const dropped = (next.roomState[state.location]?.['droppedItems'] ?? []) as string[]
  next = setRoomFlag(next, state.location, 'droppedItems', [...dropped, itemId])
  return narrate(next, [{ kind: 'narration', text: 'Dropped.' }])
}

function handleExamine(state: GameState, itemId: string, world: World): DispatchResult {
  const item = world.items[itemId]
  if (!item) return narrate(state, [{ kind: 'narration', text: 'You don\'t see anything like that.' }])
  const visible =
    state.inventory.find((i) => i.id === itemId) ||
    getItemsInRoom(state, world, state.location).includes(itemId)
  if (!visible) return narrate(state, [{ kind: 'narration', text: 'You don\'t see anything like that.' }])
  return narrate(state, [{ kind: 'narration', text: item.long }])
}

function handleRead(state: GameState, itemId: string, world: World): DispatchResult {
  const item = world.items[itemId]
  if (!item) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
  const visible =
    state.inventory.find((i) => i.id === itemId) ||
    getItemsInRoom(state, world, state.location).includes(itemId)
  if (!visible) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
  if (!item.readable || !item.readableText) {
    return narrate(state, [{ kind: 'narration', text: "There's nothing to read on it." }])
  }
  return narrate(state, [{ kind: 'narration', text: item.readableText }])
}

function handleLight(state: GameState, targetId: string, instrumentId: string | null, world: World): DispatchResult {
  const target = world.items[targetId]
  if (!target) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
  if (!target.lightable) return narrate(state, [{ kind: 'narration', text: "You can't light that." }])
  const targetInst = state.inventory.find((i) => i.id === targetId) ?? null
  const visibleInRoom = getItemsInRoom(state, world, state.location).includes(targetId)
  if (!targetInst && !visibleInRoom) {
    return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
  }
  // The 'lit' state lives on the inventory instance for inventory items, or
  // (eventually) on roomState for items left in a room. For now we only
  // support lighting items the player is carrying.
  if (!targetInst) {
    return narrate(state, [{ kind: 'narration', text: "You'd have to be carrying it." }])
  }
  if (targetInst.state['lit'] === true) {
    return narrate(state, [{ kind: 'narration', text: "It's already lit." }])
  }

  // Pick an instrument. If explicit, validate it; if implicit, find any.
  let lighterInst = null as typeof state.inventory[number] | null
  if (instrumentId) {
    lighterInst = state.inventory.find((i) => i.id === instrumentId) ?? null
    if (!lighterInst) return narrate(state, [{ kind: 'narration', text: "You don't have that." }])
    const lighterDef = world.items[instrumentId]
    if (!lighterDef?.lighter) return narrate(state, [{ kind: 'narration', text: "That isn't going to help." }])
    if (typeof lighterInst.state['uses'] === 'number' && lighterInst.state['uses'] <= 0) {
      return narrate(state, [{ kind: 'narration', text: "It is spent." }])
    }
  } else {
    for (const inst of state.inventory) {
      const def = world.items[inst.id]
      if (!def?.lighter) continue
      if (typeof inst.state['uses'] === 'number' && inst.state['uses'] <= 0) continue
      lighterInst = inst
      break
    }
    if (!lighterInst) {
      return narrate(state, [{ kind: 'narration', text: 'You have nothing to light it with.' }])
    }
  }

  // Apply state changes immutably.
  const lighterDef = world.items[lighterInst.id]!
  const lighterUsesField = typeof lighterInst.state['uses'] === 'number' ? lighterInst.state['uses'] : null
  const newLighterUses = lighterUsesField === null ? null : lighterUsesField - 1
  const newInventory = state.inventory.map((i) => {
    if (i.id === targetInst.id) return { ...i, state: { ...i.state, lit: true } }
    if (i.id === lighterInst!.id && newLighterUses !== null) return { ...i, state: { ...i.state, uses: newLighterUses } }
    return i
  })
  const lines: TranscriptLine[] = [{ kind: 'narration', text: target.litText ?? 'It catches.' }]
  if (newLighterUses === 0) {
    lines.push({ kind: 'narration', text: lighterDef.lighterEmptyText ?? 'It is spent.' })
  }
  return narrate({ ...state, inventory: newInventory }, lines)
}

function handleExtinguish(state: GameState, targetId: string, world: World): DispatchResult {
  const target = world.items[targetId]
  if (!target) return narrate(state, [{ kind: 'narration', text: "You don't see anything like that." }])
  if (!target.lightable) return narrate(state, [{ kind: 'narration', text: "You can't extinguish that." }])
  const targetInst = state.inventory.find((i) => i.id === targetId)
  if (!targetInst) return narrate(state, [{ kind: 'narration', text: "You'd have to be carrying it." }])
  if (targetInst.state['lit'] !== true) {
    return narrate(state, [{ kind: 'narration', text: "It isn't lit." }])
  }
  const newInventory = state.inventory.map((i) =>
    i.id === targetId ? { ...i, state: { ...i.state, lit: false } } : i,
  )
  return narrate({ ...state, inventory: newInventory }, [{ kind: 'narration', text: target.extinguishedText ?? 'The flame dies.' }])
}
