import { reactive } from 'vue'
import type { StorageLoc, TaskInstance, TaskTemplateDsl, TrialResult, TriggerEvent } from '../types'
import { createPresetTemplates, createSeedStorages, DEMO_EVENT, SEED_AREAS } from '../mock/seed'
import { ReservationStore } from '../engine/reservation'
import { cancelInstance, handleTrigger, mockDispatchAck, trialRun } from '../engine/orchestrator'
import { validateTemplate } from '../engine/validate'
import { clearLookupCache } from '../engine/context'

export interface DemoScriptResult {
  id: string
  name: string
  ok: boolean
  detail: string
}

function cloneTemplates(): TaskTemplateDsl[] {
  return JSON.parse(JSON.stringify(createPresetTemplates())) as TaskTemplateDsl[]
}

export function createWorld() {
  const state = reactive({
    areas: SEED_AREAS,
    storages: createSeedStorages() as StorageLoc[],
    templates: cloneTemplates(),
    instances: [] as TaskInstance[],
    audits: [] as Array<{ at: string; type: string; detail: unknown }>,
    lookupBroken: false,
    lastTrial: null as TrialResult | null,
    lastTriggerEvent: { ...DEMO_EVENT } as TriggerEvent,
    demoResults: [] as DemoScriptResult[]
  })

  const reservations = new ReservationStore()

  function deps() {
    return {
      templates: state.templates,
      storages: state.storages,
      reservations,
      instances: state.instances,
      lookupBroken: state.lookupBroken,
      audits: state.audits
    }
  }

  return {
    state,
    reservations,
    reset() {
      state.storages = createSeedStorages()
      state.templates = cloneTemplates()
      state.instances = []
      state.audits = []
      state.lookupBroken = false
      state.lastTrial = null
      state.demoResults = []
      reservations.clear()
      clearLookupCache()
    },
    saveTemplate(tpl: TaskTemplateDsl) {
      const issues = validateTemplate(tpl, 'save').filter((i) => i.level === 'error')
      if (issues.length) return { ok: false as const, issues }
      const idx = state.templates.findIndex((t) => t.id === tpl.id)
      const copy = JSON.parse(JSON.stringify(tpl)) as TaskTemplateDsl
      if (idx >= 0) state.templates[idx] = copy
      else state.templates.push(copy)
      return { ok: true as const, issues: validateTemplate(copy, 'save') }
    },
    publishTemplate(id: string) {
      const tpl = state.templates.find((t) => t.id === id)
      if (!tpl) return { ok: false as const, issues: [{ code: 'X', level: 'error' as const, message: '模板不存在' }] }
      const issues = validateTemplate(tpl, 'publish')
      if (issues.some((i) => i.level === 'error')) return { ok: false as const, issues }
      tpl.published = true
      return { ok: true as const, issues }
    },
    trial(templateId: string, payload: Record<string, unknown>) {
      const tpl = state.templates.find((t) => t.id === templateId)
      if (!tpl) throw new Error('模板不存在')
      state.lastTrial = trialRun(tpl, payload, state.storages, reservations.reservedIds(), state.lookupBroken)
      return state.lastTrial
    },
    trigger(event?: TriggerEvent) {
      const ev = event ?? (JSON.parse(JSON.stringify(state.lastTriggerEvent)) as TriggerEvent)
      state.lastTriggerEvent = ev
      return handleTrigger(deps(), ev)
    },
    dispatch(instanceId: string, action: 'start' | 'complete' | 'reject') {
      const inst = state.instances.find((i) => i.id === instanceId)
      if (!inst) return
      mockDispatchAck(inst, action, deps())
    },
    cancel(instanceId: string) {
      const inst = state.instances.find((i) => i.id === instanceId)
      if (!inst) return
      cancelInstance(inst, deps())
    },
    forceReserve(storageId: string) {
      const loc = state.storages.find((s) => s.storageId === storageId)
      if (loc) loc.forceReserved = true
      reservations.forceHold(storageId)
    },
    clearForce(storageId: string) {
      const loc = state.storages.find((s) => s.storageId === storageId)
      if (loc) loc.forceReserved = false
      reservations.releaseForce(storageId)
    },
    runDemoScripts(): DemoScriptResult[] {
      const results: DemoScriptResult[] = []
      const run = (id: string, name: string, fn: () => string | void) => {
        this.reset()
        try {
          const detail = fn() ?? 'ok'
          results.push({ id, name, ok: true, detail: String(detail) })
        } catch (e) {
          results.push({ id, name, ok: false, detail: String(e) })
        }
      }

      run('happy', '快乐路径', () => {
        const inst = this.trigger({ ...DEMO_EVENT, eventId: 'evt-happy' })!
        if (inst.status !== 'Dispatched' && inst.status !== 'Reserved') throw new Error(`状态=${inst.status}`)
        if (!inst.sourceId || !inst.targetId) throw new Error('未选中库位')
        this.dispatch(inst.id, 'start')
        this.dispatch(inst.id, 'complete')
        // 状态会被 dispatch 就地改写；避免 TS 把前面的联合收窄带到此处
        if (String(inst.status) !== 'Completed') throw new Error(`完成失败 ${inst.status}`)
        return `${inst.sourceId}→${inst.targetId}`
      })

      run('hard', '硬性不放宽', () => {
        for (const s of state.storages) {
          s.materialAffinity = s.materialAffinity.filter((m) => m !== 'M001')
        }
        const inst = this.trigger({ ...DEMO_EVENT, eventId: 'evt-hard' })!
        if (inst.status !== 'Failed') throw new Error(`期望 Failed，实际 ${inst.status}`)
        return '硬性无候选 → Failed'
      })

      run('degrade', '弹性降级', () => {
        // 让 soft x<25 失败：近处存储位去掉 M001，保留远处 S-06
        for (const s of state.storages) {
          if (['S-01', 'S-02', 'S-04'].includes(s.storageId)) s.materialAffinity = ['M999']
        }
        const trial = this.trial('tpl-line-replenish-A', DEMO_EVENT.payload)
        if (!trial.ok || !trial.pick) throw new Error('降级后应仍能选到位')
        if (trial.pick.sourceId !== 'S-06') throw new Error(`期望源 S-06，实际 ${trial.pick.sourceId}`)
        const deg = trial.explain.degrade as { source: Array<{ attempt: number }> }
        if (!deg?.source?.some((d) => d.attempt >= 1)) throw new Error('未见降级 attempt>=1')
        return `选中 ${trial.pick.sourceId} → ${trial.pick.targetId}（经降级）`
      })

      run('pair_rollback', '成对回滚', () => {
        this.forceReserve('T-L1-01')
        this.forceReserve('T-L1-02')
        const before = reservations.list().length
        const inst = this.trigger({ ...DEMO_EVENT, eventId: 'evt-pair' })!
        if (inst.status !== 'Failed') throw new Error(`期望 Failed，实际 ${inst.status}`)
        const alloc = inst.explain.allocate as { result?: string }
        // 可能 no_candidate（过滤阶段已无终点）或 pair_partial_rollback
        if (!alloc || (alloc.result !== 'no_candidate' && alloc.result !== 'pair_partial_rollback')) {
          throw new Error(`allocate=${JSON.stringify(alloc)}`)
        }
        const leaked = reservations.list().filter((r) => r.instanceId === inst.id)
        if (leaked.length) throw new Error('存在实例残留预占')
        return `无残留预占（手动占位仍 ${before} 条）`
      })

      run('dedupe', '去重', () => {
        const a = this.trigger({ ...DEMO_EVENT, eventId: 'evt-dup' })!
        const b = this.trigger({ ...DEMO_EVENT, eventId: 'evt-dup' })!
        if (b.status !== 'IgnoredDuplicate') throw new Error(`第二次应为 IgnoredDuplicate，实际 ${b.status}`)
        const active = state.instances.filter((i) => i.eventId === 'evt-dup' && i.status !== 'IgnoredDuplicate')
        if (active.length !== 1) throw new Error('活跃实例应只有 1 条')
        return `原单 ${a.id}，重复 ${b.id}`
      })

      run('arbitrate', '多模板仲裁', () => {
        const inst = this.trigger({ ...DEMO_EVENT, eventId: 'evt-arb' })!
        if (inst.templateId !== 'tpl-line-replenish-A') throw new Error(`应选 A，实际 ${inst.templateId}`)
        const lost = state.audits.some(
          (a) => a.type === 'template_not_selected' && (a.detail as { templateId?: string }).templateId === 'tpl-line-replenish-B'
        )
        if (!lost) throw new Error('缺少 B 落选审计')
        return `胜出 ${inst.templateId}`
      })

      run('cancel', '取消善后', () => {
        const inst = this.trigger({ ...DEMO_EVENT, eventId: 'evt-cancel' })!
        if (inst.status !== 'Reserved' && inst.status !== 'Dispatched') throw new Error(inst.status)
        this.cancel(inst.id)
        if (String(inst.status) !== 'Cancelled') throw new Error(inst.status)
        if (reservations.list().some((r) => r.instanceId === inst.id)) throw new Error('预占未释放')
        return '已取消并释放预占'
      })

      state.demoResults = results
      return results
    }
  }
}

export type WcsProtoWorld = ReturnType<typeof createWorld>
