/**
 * Deterministic scheduler, v0. Orders the open backlog by dependency topology,
 * then priority, then estimate; lays it out on a single serial capacity and
 * marks the critical path. The LLM never does this — it's plain code.
 *
 * v0 simplifications (each is a later slice, not a hack):
 *  - single serial worker; per-person capacity is #8.
 *  - point durations from estimate labels; Monte Carlo cones are #10.
 *  - a null estimate defaults to DEFAULT_ESTIMATE_DAYS.
 * Dependencies on out-of-scope issues (closed/done, or not passed in) are
 * treated as satisfied and dropped — you schedule what's left to do.
 */

export const DEFAULT_ESTIMATE_DAYS = 2

export interface SchedulableIssue {
  number: number
  title: string
  labels: string[]
  /** From label facts; null when unestimated. */
  estimateDays: number | null
  /** 1 (most urgent) … 4; null when unset. */
  priority: number | null
}

export interface DependencyEdge {
  issue: number
  dependsOn: number
}

export interface ScheduledItem {
  number: number
  title: string
  labels: string[]
  /** 0-based position in the plan. */
  order: number
  /** Working-day offsets from now. */
  startDay: number
  endDay: number
  durationDays: number
  /** In-scope dependencies (all still open, so all still blocking). */
  blockedBy: number[]
  /** Issues that depend on this one. */
  blocks: number[]
  /** On a longest-duration dependency chain. */
  critical: boolean
  rationale: string
}

export interface SchedulePlan {
  items: ScheduledItem[]
  /** A dependency cycle (issue numbers) if one was found — the plan is then empty. */
  cycle: number[] | null
}

function durationOf(issue: SchedulableIssue): number {
  return issue.estimateDays ?? DEFAULT_ESTIMATE_DAYS
}

/** Best-first comparison among ready nodes: priority (1 first), then larger estimate, then number. */
function readyRank(a: SchedulableIssue, b: SchedulableIssue): number {
  const pa = a.priority ?? 99
  const pb = b.priority ?? 99
  if (pa !== pb) return pa - pb
  const da = durationOf(a)
  const db = durationOf(b)
  if (da !== db) return db - da
  return a.number - b.number
}

function rationaleFor(item: {
  critical: boolean
  blockedBy: number[]
  blocks: number[]
  priority: number | null
}): string {
  const blocksNote = item.blocks.length ? `unblocks ${item.blocks.map((n) => `#${n}`).join(', ')}` : ''
  if (item.critical) return ['on the critical path', blocksNote].filter(Boolean).join(' · ')
  if (item.blockedBy.length) return `waits on ${item.blockedBy.map((n) => `#${n}`).join(', ')}`
  if (blocksNote) return blocksNote
  if (item.priority != null) return `p/${item.priority} · ready`
  return 'ready'
}

export function schedule(issues: SchedulableIssue[], edges: DependencyEdge[]): SchedulePlan {
  const byNumber = new Map(issues.map((i) => [i.number, i]))

  // keep only edges whose endpoints are both in scope
  const scoped = edges.filter((e) => byNumber.has(e.issue) && byNumber.has(e.dependsOn))
  const deps = new Map<number, number[]>() // issue → its dependencies
  const dependents = new Map<number, number[]>() // issue → who depends on it
  for (const i of issues) {
    deps.set(i.number, [])
    dependents.set(i.number, [])
  }
  for (const e of scoped) {
    deps.get(e.issue)!.push(e.dependsOn)
    dependents.get(e.dependsOn)!.push(e.issue)
  }

  // Kahn topo with priority tie-break
  const indegree = new Map(issues.map((i) => [i.number, deps.get(i.number)!.length]))
  const order: number[] = []
  const ready = issues.filter((i) => indegree.get(i.number) === 0)
  while (ready.length) {
    ready.sort(readyRank)
    const next = ready.shift()!
    order.push(next.number)
    for (const dep of dependents.get(next.number)!) {
      const d = indegree.get(dep)! - 1
      indegree.set(dep, d)
      if (d === 0) ready.push(byNumber.get(dep)!)
    }
  }

  if (order.length < issues.length) {
    // cycle: report the nodes that never became ready
    const stuck = issues.filter((i) => indegree.get(i.number)! > 0).map((i) => i.number)
    return { items: [], cycle: stuck }
  }

  // longest-path (by duration) for critical-path marking
  const longestThrough = new Map<number, number>() // node → longest chain duration passing through it
  const before = new Map<number, number>() // longest duration of chain ending at node's start
  for (const n of order) {
    const depMax = Math.max(0, ...deps.get(n)!.map((d) => before.get(d)! + durationOf(byNumber.get(d)!)))
    before.set(n, depMax)
  }
  const after = new Map<number, number>()
  for (const n of [...order].reverse()) {
    const depMax = Math.max(0, ...dependents.get(n)!.map((d) => after.get(d)! + durationOf(byNumber.get(d)!)))
    after.set(n, depMax)
  }
  let globalMax = 0
  for (const n of order) {
    const total = before.get(n)! + durationOf(byNumber.get(n)!) + after.get(n)!
    longestThrough.set(n, total)
    globalMax = Math.max(globalMax, total)
  }

  // serial layout
  let cursor = 0
  const items: ScheduledItem[] = order.map((n, idx) => {
    const issue = byNumber.get(n)!
    const dur = durationOf(issue)
    const start = cursor
    cursor += dur
    const blockedBy = deps.get(n)!
    const blocks = dependents.get(n)!
    const critical = globalMax > 0 && longestThrough.get(n) === globalMax
    return {
      number: n,
      title: issue.title,
      labels: issue.labels,
      order: idx,
      startDay: start,
      endDay: cursor,
      durationDays: dur,
      blockedBy,
      blocks,
      critical,
      rationale: rationaleFor({ critical, blockedBy, blocks, priority: issue.priority }),
    }
  })

  return { items, cycle: null }
}

export interface Focus {
  now: ScheduledItem | null
  next: ScheduledItem | null
  later: ScheduledItem | null
}

/** Now/Next/Later = the first three of the plan (already dependency- + priority-ordered). */
export function selectFocus(plan: SchedulePlan): Focus {
  return {
    now: plan.items[0] ?? null,
    next: plan.items[1] ?? null,
    later: plan.items[2] ?? null,
  }
}
