import type {
  ExplainSection,
  InstanceStatus,
  TaskInstance,
  TaskTemplateDsl,
  TrialResult,
  TriggerEvent
} from '../types'
import { assembleContext } from './context'
import { runLocationStrategy } from './match'
import type { ReservationStore } from './reservation'
import type { StorageLoc } from '../types'

function now() {
  return new Date().toISOString()
}

function pushTimeline(inst: TaskInstance, status: InstanceStatus, note?: string) {
  inst.status = status
  inst.updatedAt = now()
  inst.timeline.push({ at: inst.updatedAt, status, note })
}

export function trialRun(
  tpl: TaskTemplateDsl,
  payload: Record<string, unknown>,
  storages: StorageLoc[],
  reserved: Set<string>,
  lookupBroken?: boolean
): TrialResult {
  const assembled = assembleContext(tpl.bindings, payload, { lookupBroken })
  const explain: ExplainSection = { bindings: assembled.explain }
  if (!assembled.ok) {
    return {
      ok: false,
      context: assembled.context,
      sourceCandidates: [],
      targetCandidates: [],
      eliminated: [],
      degrade: [],
      error: assembled.error,
      explain
    }
  }
  const src = runLocationStrategy(tpl.locationStrategies.source, storages, assembled.context, reserved)
  const tgt = runLocationStrategy(tpl.locationStrategies.target, storages, assembled.context, reserved)
  explain.filter = { source: src.eliminated.slice(0, 40), target: tgt.eliminated.slice(0, 40) }
  explain.degrade = { source: src.degrade, target: tgt.degrade }
  explain.score = {
    source: src.candidates.map((c) => ({ id: c.loc.storageId, total: c.total, scores: c.scores })),
    target: tgt.candidates.map((c) => ({ id: c.loc.storageId, total: c.total, scores: c.scores }))
  }
  const eliminated = [
    ...src.eliminated.map((e) => ({ ...e, strategy: 'source' })),
    ...tgt.eliminated.map((e) => ({ ...e, strategy: 'target' }))
  ]
  if (!src.pick || !tgt.pick) {
    explain.allocate = { result: 'no_candidate' }
    return {
      ok: false,
      context: assembled.context,
      sourceCandidates: src.candidates.map((c) => ({ storageId: c.loc.storageId, total: c.total, scores: c.scores })),
      targetCandidates: tgt.candidates.map((c) => ({ storageId: c.loc.storageId, total: c.total, scores: c.scores })),
      eliminated,
      degrade: [src.degrade, tgt.degrade],
      error: '无可用库位候选',
      explain
    }
  }
  explain.allocate = { result: 'ok', sourceId: src.pick.storageId, targetId: tgt.pick.storageId }
  return {
    ok: true,
    context: assembled.context,
    sourceCandidates: src.candidates.map((c) => ({ storageId: c.loc.storageId, total: c.total, scores: c.scores })),
    targetCandidates: tgt.candidates.map((c) => ({ storageId: c.loc.storageId, total: c.total, scores: c.scores })),
    eliminated,
    degrade: [src.degrade, tgt.degrade],
    pick: { sourceId: src.pick.storageId, targetId: tgt.pick.storageId },
    explain
  }
}

export interface RuntimeDeps {
  templates: TaskTemplateDsl[]
  storages: StorageLoc[]
  reservations: ReservationStore
  instances: TaskInstance[]
  lookupBroken: boolean
  audits: Array<{ at: string; type: string; detail: unknown }>
}

let seq = 1

export function handleTrigger(deps: RuntimeDeps, event: TriggerEvent): TaskInstance | null {
  const published = deps.templates.filter((t) => t.published && t.trigger.source === event.source)
  const sorted = [...published].sort((a, b) => b.meta.priority - a.meta.priority || a.id.localeCompare(b.id))

  // mutex / single winner
  const winners: TaskTemplateDsl[] = []
  const seenGroup = new Set<string>()
  for (const t of sorted) {
    const mode = t.meta.routeMode ?? 'single_winner'
    const g = t.meta.mutexGroup ?? `__solo__${t.id}`
    if (mode === 'single_winner') {
      if (seenGroup.has(g)) {
        deps.audits.push({
          at: now(),
          type: 'template_not_selected',
          detail: { templateId: t.id, reason: 'mutex_lost', eventId: event.eventId }
        })
        continue
      }
      seenGroup.add(g)
      winners.push(t)
      // 同组只取一个；无组时每个模板自己的 solo 组
      continue
    }
    winners.push(t)
  }

  // 全局 single_winner：设计默认同一事件只跑优先级最高的一套（跨组也只取第一个 winner）
  const chosen = winners[0]
  for (const t of winners.slice(1)) {
    deps.audits.push({
      at: now(),
      type: 'template_not_selected',
      detail: { templateId: t.id, reason: 'single_winner', eventId: event.eventId, winner: chosen?.id }
    })
  }

  if (!chosen) {
    deps.audits.push({ at: now(), type: 'no_template', detail: { eventId: event.eventId } })
    return null
  }

  // idempotency
  const keyMode = chosen.trigger.idempotency?.keyMode ?? 'eventId'
  const idemKey = keyMode === 'eventId'
    ? event.eventId
    : `${String(event.payload.businessKey ?? event.eventId)}:${chosen.id}`

  const existing = deps.instances.find((i) => i.eventId === idemKey || (i.eventId === event.eventId && i.templateId === chosen.id))
  if (existing && !['Completed', 'Failed', 'Cancelled', 'IgnoredDuplicate'].includes(existing.status)) {
    const dup: TaskInstance = {
      id: `inst-${seq++}`,
      templateId: chosen.id,
      templateName: chosen.name,
      eventId: event.eventId,
      status: 'IgnoredDuplicate',
      context: {},
      explain: { arbitration: { winner: chosen.id, duplicateOf: existing.id } },
      createdAt: now(),
      updatedAt: now(),
      timeline: [{ at: now(), status: 'IgnoredDuplicate', note: `重复事件，沿用 ${existing.id}` }]
    }
    deps.instances.unshift(dup)
    return dup
  }
  if (existing && ['Completed', 'Failed', 'Cancelled'].includes(existing.status)) {
    const hit = chosen.trigger.idempotency?.onTerminalHit ?? 'reject'
    if (hit === 'reject') {
      const dup: TaskInstance = {
        id: `inst-${seq++}`,
        templateId: chosen.id,
        templateName: chosen.name,
        eventId: event.eventId,
        status: 'IgnoredDuplicate',
        context: {},
        explain: { arbitration: { winner: chosen.id, rejectedTerminal: existing.id } },
        createdAt: now(),
        updatedAt: now(),
        timeline: [{ at: now(), status: 'IgnoredDuplicate', note: '终态后拒绝重复 eventId' }]
      }
      deps.instances.unshift(dup)
      return dup
    }
  }

  const inst: TaskInstance = {
    id: `inst-${seq++}`,
    templateId: chosen.id,
    templateName: chosen.name,
    eventId: event.eventId,
    status: 'Pending',
    context: {},
    explain: { arbitration: { winner: chosen.id, priority: chosen.meta.priority, candidates: published.map((p) => p.id) } },
    createdAt: now(),
    updatedAt: now(),
    timeline: []
  }
  pushTimeline(inst, 'Pending', '事件入站')
  deps.instances.unshift(inst)

  // Assembling
  pushTimeline(inst, 'Assembling')
  const assembled = assembleContext(chosen.bindings, event.payload, { lookupBroken: deps.lookupBroken })
  inst.explain.bindings = assembled.explain
  if (assembled.suspend) {
    pushTimeline(inst, 'Suspended', assembled.error)
    return inst
  }
  if (!assembled.ok) {
    inst.error = assembled.error
    pushTimeline(inst, 'Failed', assembled.error)
    return inst
  }
  inst.context = assembled.context

  // Allocating with retries
  pushTimeline(inst, 'Allocating')
  const max = chosen.policy.allocateMaxAttempts ?? 5
  let lastAllocate: unknown
  for (let attempt = 0; attempt < max; attempt++) {
    const reserved = deps.reservations.reservedIds()
    const src = runLocationStrategy(chosen.locationStrategies.source, deps.storages, inst.context, reserved)
    const tgt = runLocationStrategy(chosen.locationStrategies.target, deps.storages, inst.context, reserved)
    inst.explain.filter = { sourceElim: src.eliminated.length, targetElim: tgt.eliminated.length }
    inst.explain.degrade = { source: src.degrade, target: tgt.degrade }
    inst.explain.score = {
      sourceTop: src.candidates.map((c) => c.loc.storageId),
      targetTop: tgt.candidates.map((c) => c.loc.storageId)
    }

    if (!src.pick || !tgt.pick) {
      lastAllocate = { result: 'no_candidate', attempt }
      continue
    }

    const pair = deps.reservations.tryReservePair(inst.id, src.pick.storageId, tgt.pick.storageId)
    if (!pair.ok) {
      lastAllocate = {
        result: 'pair_partial_rollback',
        attempt,
        reason: pair.reason,
        tried: { sourceId: src.pick.storageId, targetId: tgt.pick.storageId },
        heldThenReleased: pair.heldThenReleased
      }
      inst.explain.allocate = lastAllocate
      continue
    }

    inst.sourceId = src.pick.storageId
    inst.targetId = tgt.pick.storageId
    inst.explain.allocate = { result: 'ok', sourceId: inst.sourceId, targetId: inst.targetId, attempt }
    pushTimeline(inst, 'Reserved', `${inst.sourceId} → ${inst.targetId}`)

    if (chosen.blueprint.options.autoDispatch) {
      pushTimeline(inst, 'Dispatched', 'Mock 执行器已接单（待确认完成）')
      inst.explain.dispatch = { adapter: 'MockDispatcher', state: 'accepted' }
    }
    return inst
  }

  inst.explain.allocate = lastAllocate ?? { result: 'no_candidate' }
  inst.error = '选位失败：无候选或预占冲突'
  pushTimeline(inst, 'Failed', inst.error)
  return inst
}

export function mockDispatchAck(inst: TaskInstance, action: 'start' | 'complete' | 'reject', deps: RuntimeDeps) {
  if (action === 'start' && inst.status === 'Dispatched') {
    pushTimeline(inst, 'InTransit', '执行中')
    return
  }
  if (action === 'complete' && (inst.status === 'Dispatched' || inst.status === 'InTransit')) {
    deps.reservations.releaseByInstance(inst.id)
    pushTimeline(inst, 'Completed', '搬运完成，预占已释放')
    return
  }
  if (action === 'reject') {
    pushTimeline(inst, 'Compensating', '派车拒绝，善后中')
    const released = deps.reservations.releaseByInstance(inst.id)
    pushTimeline(inst, 'Failed', `已释放预占: ${released.join(',') || '无'}`)
  }
}

export function cancelInstance(inst: TaskInstance, deps: RuntimeDeps) {
  if (['Completed', 'Failed', 'Cancelled', 'IgnoredDuplicate'].includes(inst.status)) return
  pushTimeline(inst, 'Compensating', '取消善后')
  const released = deps.reservations.releaseByInstance(inst.id)
  pushTimeline(inst, 'Cancelled', `已释放预占: ${released.join(',') || '无'}`)
}
