import type { LocationStrategy, ScoreRule, StorageLoc } from '../types'
import { evalExpr, locToWcs } from './expr'

export interface ScoredCandidate {
  loc: StorageLoc
  total: number
  scores: Record<string, number>
}

export interface StrategyRunResult {
  candidates: ScoredCandidate[]
  eliminated: Array<{ storageId: string; groupId?: string; reason: string; attempt: number }>
  degrade: Array<{ attempt: number; requireGroups: string[]; candidateCount: number }>
  pick?: StorageLoc
}

function scoreOne(loc: StorageLoc, rule: ScoreRule, all: StorageLoc[]): number {
  const p = rule.params
  switch (rule.function) {
    case 'constant':
      return Number(p.value ?? 0)
    case 'field_match_bonus': {
      // not fully wired in proto scores of seed; keep simple
      return 0
    }
    case 'fifo_age': {
      const field = String(p.timeField ?? 'inboundAt')
      const t = (loc as unknown as Record<string, unknown>)[field]
      if (!t || typeof t !== 'string') return 0
      const times = all
        .map((x) => (x as unknown as Record<string, unknown>)[field])
        .filter((x): x is string => typeof x === 'string')
        .map((x) => new Date(x).getTime())
      if (!times.length) return 0
      const oldest = Math.min(...times)
      const newest = Math.max(...times)
      const cur = new Date(t).getTime()
      if (newest === oldest) return 100
      return ((newest - cur) / (newest - oldest)) * 100
    }
    case 'nearer_to_ref': {
      const refX = Number(p.refX ?? 0)
      const refY = Number(p.refY ?? 0)
      const dist = Math.hypot(loc.x - refX, loc.y - refY)
      const dists = all.map((x) => Math.hypot(x.x - refX, x.y - refY))
      const min = Math.min(...dists)
      const max = Math.max(...dists)
      if (max === min) return 100
      return (1 - (dist - min) / (max - min)) * 100
    }
    default:
      return 0
  }
}

function passGroups(
  loc: StorageLoc,
  strategy: LocationStrategy,
  groupIds: string[],
  context: Record<string, unknown>
): { ok: boolean; groupId?: string; reason?: string } {
  const wcs = locToWcs(loc)
  for (const gid of groupIds) {
    const g = strategy.filterGroups.find((x) => x.id === gid)
    if (!g) continue
    const r = evalExpr(g.expr, wcs, context)
    if (!r.ok) return { ok: false, groupId: gid, reason: r.failReason ?? 'filter_fail' }
  }
  return { ok: true }
}

export function runLocationStrategy(
  strategy: LocationStrategy,
  universe: StorageLoc[],
  context: Record<string, unknown>,
  reservedIds: Set<string>
): StrategyRunResult {
  const eliminated: StrategyRunResult['eliminated'] = []
  const degrade: StrategyRunResult['degrade'] = []
  const chain = strategy.degradeChain.length
    ? [...strategy.degradeChain].sort((a, b) => a.attempt - b.attempt)
    : [{ attempt: 0, requireGroups: strategy.filterGroups.map((g) => g.id) }]

  let lastCandidates: ScoredCandidate[] = []

  for (const step of chain) {
    // hard safety: never drop hard groups even if misconfigured
    const hardIds = strategy.filterGroups.filter((g) => g.severity === 'hard').map((g) => g.id)
    const require = Array.from(new Set([...hardIds, ...step.requireGroups]))

    const passed: StorageLoc[] = []
    for (const loc of universe) {
      if (reservedIds.has(loc.storageId) || loc.forceReserved) {
        eliminated.push({ storageId: loc.storageId, reason: 'already_reserved', attempt: step.attempt })
        continue
      }
      const r = passGroups(loc, strategy, require, context)
      if (!r.ok) {
        eliminated.push({
          storageId: loc.storageId,
          groupId: r.groupId,
          reason: r.reason ?? 'fail',
          attempt: step.attempt
        })
        continue
      }
      passed.push(loc)
    }

    const scored = passed.map((loc) => {
      const scores: Record<string, number> = {}
      let total = 0
      for (const rule of strategy.scores) {
        const s = scoreOne(loc, rule, passed)
        scores[rule.id] = Math.round(s * 100) / 100
        total += s * rule.weight
      }
      return { loc, total, scores }
    })
    scored.sort((a, b) => b.total - a.total || a.loc.storageId.localeCompare(b.loc.storageId))
    const topN = strategy.allocate.topN ?? 5
    lastCandidates = scored.slice(0, topN)
    degrade.push({ attempt: step.attempt, requireGroups: require, candidateCount: lastCandidates.length })
    if (lastCandidates.length) {
      return {
        candidates: lastCandidates,
        eliminated,
        degrade,
        pick: lastCandidates[0].loc
      }
    }
  }

  return { candidates: lastCandidates, eliminated, degrade }
}
