/**
 * Apply-changes, v0 — the write path's planning half. Estimates and priority
 * live as exclusive label axes (`est/*`, `p/*`); a change swaps the axis label.
 * This computes the resulting label set + a human-readable diff *purely*, so the
 * UI can show a propose-approve consequence before the write and tests can pin
 * the semantics. The actual PUT (name→id resolution + network) is the bridge's
 * job — additive here, never assumed.
 */

import {
  type EstimateLabel,
  ESTIMATE_LABELS,
  type PriorityLabel,
  PRIORITY_LABELS,
} from '../labels/label-schema.js'

export type IssueChange =
  | { kind: 'reestimate'; issue: number; estimate: EstimateLabel | null }
  | { kind: 'reprioritize'; issue: number; priority: PriorityLabel | null }

export interface LabelPlan {
  /** The full resulting label-name set (order: kept labels, then the new axis label). */
  labels: string[]
  /** Axis labels being added (0 or 1). */
  added: string[]
  /** Axis labels being removed (includes clearing a duplicated axis). */
  removed: string[]
  /** true when the change would leave the labels unchanged. */
  noop: boolean
}

function axisFor(change: IssueChange): { labels: readonly string[]; target: string | null } {
  return change.kind === 'reestimate'
    ? { labels: ESTIMATE_LABELS, target: change.estimate }
    : { labels: PRIORITY_LABELS, target: change.priority }
}

/**
 * Plan the label mutation for a single change. Removes every label on the
 * change's axis except the target, adds the target if absent. Setting the axis
 * to null clears it. Cleans up a duplicated axis (two `est/*`) as a side effect.
 */
export function planIssueChange(current: string[], change: IssueChange): LabelPlan {
  const { labels: axis, target } = axisFor(change)
  const onAxis = current.filter((l) => axis.includes(l))
  const removed = onAxis.filter((l) => l !== target)
  const added = target && !current.includes(target) ? [target] : []
  const kept = current.filter((l) => !axis.includes(l))
  const labels = target ? [...kept, target] : kept
  return { labels, added, removed, noop: added.length === 0 && removed.length === 0 }
}

/** A short "est/2d → est/3d" (or "+p/1" / "−est/5d") summary for the confirm UI. */
export function describeChange(plan: LabelPlan): string {
  if (plan.noop) return 'no change'
  const from = plan.removed.length ? plan.removed.join(', ') : '∅'
  const to = plan.added.length ? plan.added.join(', ') : '∅'
  return `${from} → ${to}`
}

/** A change the agent proposes: the concrete op + its diff, ready for approve-then-apply. */
export interface ChangeProposal {
  change: IssueChange
  plan: LabelPlan
  issueTitle?: string
}

/** What the `propose_change` tool accepts — a target issue and the axes to set. */
export interface ProposeChangeArgs {
  issue: number
  estimate?: EstimateLabel
  priority?: PriorityLabel
}

function isEstimate(v: unknown): v is EstimateLabel {
  return typeof v === 'string' && (ESTIMATE_LABELS as readonly string[]).includes(v)
}
function isPriority(v: unknown): v is PriorityLabel {
  return typeof v === 'string' && (PRIORITY_LABELS as readonly string[]).includes(v)
}

/**
 * Build the concrete, non-noop proposals for a `propose_change` request against
 * an issue's current labels. Invalid or unchanged axes are dropped — the agent
 * proposes only real changes, and never a label outside the est/* · p/* axes.
 */
export function proposalsFor(
  args: ProposeChangeArgs,
  currentLabels: string[],
  issueTitle?: string,
): ChangeProposal[] {
  const out: ChangeProposal[] = []
  if (isEstimate(args.estimate)) {
    const change: IssueChange = { kind: 'reestimate', issue: args.issue, estimate: args.estimate }
    const plan = planIssueChange(currentLabels, change)
    if (!plan.noop) out.push({ change, plan, issueTitle })
  }
  if (isPriority(args.priority)) {
    const change: IssueChange = { kind: 'reprioritize', issue: args.issue, priority: args.priority }
    const plan = planIssueChange(currentLabels, change)
    if (!plan.noop) out.push({ change, plan, issueTitle })
  }
  return out
}
