// `openthomas run` — launch one agent instance with its own wire identity.
//
// Wiring an agent type edits a shared config file, so every `claude` process
// on the machine shares one routing decision and one cost bucket. `run`
// gives a single launch its own instance id: it points that process at
// `…/wire/<agent>@<instance>` (via Claude Code's per-session `--settings`
// override, which the global settings.json can't clobber) and registers a
// per-instance routing policy. Five `claude` windows become five fleets of
// one — each separately costed, each independently routable to a model,
// monitor-only, or left raw.
//
// Scope: launch-per-task agents (Claude Code today). Daemon agents (OpenClaw)
// don't launch per task — their per-instance control is config-driven; see
// `openthomas route`.

import { spawn } from 'node:child_process'
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
import process from 'node:process'
import { Command } from 'commander'
import type { AgentId } from '../../core/agent.ts'
import { logger } from '../../core/logger.ts'
import { DAEMON_BASE_URL, paths } from '../../core/paths.ts'
import { policyKeyFor, type RoutingTarget } from '../../core/routing-policy.ts'
import { parseJsonc } from '../rewrite/jsonc.ts'
import { thomasUrlForInstance } from '../wire/url.ts'

// Per-agent launch wiring. Each entry knows how to point one launch at a
// custom base URL without touching the agent's shared config. Claude Code
// exposes a per-session `--settings` override (an inline JSON string whose
// keys win over every settings file); that is the only clobber-proof seam.
type LaunchWiring = {
  agent: AgentId
  /** The env var the agent reads its API base URL from. */
  baseUrlEnv: string
  /** The real upstream a `--raw` launch points back at. */
  rawUpstream: string
  /** Path to the agent's own settings file, read to preserve its other env. */
  settingsPath: string
  /** Build the argv prefix that applies a per-session env override. */
  settingsArgs: (settingsJson: string) => string[]
}

const CLAUDE_CODE: LaunchWiring = {
  agent: 'claude-code',
  baseUrlEnv: 'ANTHROPIC_BASE_URL',
  rawUpstream: 'https://api.anthropic.com',
  settingsPath: paths.agent.claudeCode.settings,
  settingsArgs: (json) => ['--settings', json],
}

/** Map a launched binary to its launch wiring. The binary's basename is the
 *  signal (`claude` → claude-code); `--agent` overrides it. Returns undefined
 *  for agents `run` can't wrap (daemon agents, unknown bins). */
function wiringFor(bin: string, override?: string): LaunchWiring | undefined {
  const name = override ?? basename(bin)
  if (name === 'claude' || name === 'claude-code') return CLAUDE_CODE
  if (override === 'claude-code') return CLAUDE_CODE
  return undefined
}

/** A launch instance id from a label (or a short fallback). Slugged so it
 *  rides cleanly in a URL path segment and a routing-policy key — no `@`,
 *  `/`, or whitespace. A stable label means spend accrues across relaunches
 *  of "the worker window." */
function instanceIdFrom(label: string | undefined): string {
  if (label && label.trim()) {
    const slug = label
      .trim()
      .toLowerCase()
      .replace(/[^a-z0-9-]+/g, '-')
      .replace(/^-+|-+$/g, '')
    if (slug) return slug
  }
  // No label: a short, launch-unique id. PID is enough to disambiguate
  // concurrent launches and avoids Math.random.
  return `pid-${process.pid}`
}

async function readAgentEnv(settingsPath: string): Promise<Record<string, string>> {
  try {
    const text = await readFile(settingsPath, 'utf8')
    const parsed = parseJsonc<{ env?: Record<string, string> }>(text)
    return parsed?.env && typeof parsed.env === 'object' ? { ...parsed.env } : {}
  } catch {
    return {}
  }
}

/** Register (or clear) the instance's routing policy through the daemon so a
 *  running daemon picks it up live — same surface as `openthomas route set`. */
async function setInstancePolicy(
  routeKey: string,
  target: RoutingTarget | null,
): Promise<void> {
  const url = `${DAEMON_BASE_URL}/api/routing/agent`
  let res: Response
  try {
    res =
      target === null
        ? await fetch(`${url}?routeKey=${encodeURIComponent(routeKey)}`, {
            method: 'DELETE',
            signal: AbortSignal.timeout(5000),
          })
        : await fetch(url, {
            method: 'POST',
            headers: { 'content-type': 'application/json' },
            body: JSON.stringify({ routeKey, target }),
            signal: AbortSignal.timeout(5000),
          })
  } catch {
    throw new Error(
      'cannot reach the OpenThomas daemon — start it with `openthomas daemon` or `openthomas wire`',
    )
  }
  if (!res.ok) {
    let msg = `HTTP ${res.status}`
    try {
      const j = (await res.json()) as { error?: unknown }
      if (j?.error) msg = String(j.error)
    } catch {
      // keep the status-code message
    }
    throw new Error(msg)
  }
}

type RunOpts = {
  as?: string
  model?: string
  monitor?: boolean
  raw?: boolean
  agent?: string
  ephemeral?: boolean
}

export const runCommand = new Command('run')
  .description(
    'Launch one agent instance with its own wire identity — per-instance cost ' +
      'tracking and routing, without touching the agent\'s shared config.\n' +
      '  Examples:\n' +
      '    openthomas run --as planner --model claude-opus-4-8 -- claude   # keep Opus\n' +
      '    openthomas run --as worker --model claude-sonnet-4-6 -- claude  # downgrade\n' +
      '    openthomas run --as audit --monitor -- claude                   # track, never reroute\n' +
      '    openthomas run --as solo --raw -- claude                        # bypass OpenThomas',
  )
  .argument('<command...>', 'the agent command to launch, e.g. `-- claude` (pass it after `--`)')
  .option('--as <label>', 'instance label — stable across relaunches; spend accrues to it')
  .option('--model <id>', 'route this instance to a single model')
  .option('--monitor', 'capture this instance but never reroute (passthrough)')
  .option('--raw', 'bypass OpenThomas entirely for this instance (no capture, no routing)')
  .option('--agent <id>', 'force the agent type instead of inferring it from the command')
  .option('--ephemeral', 'remove the instance routing policy when the process exits')
  .action(async (command: string[], opts: RunOpts) => {
    const fail = (m: string): never => {
      logger.print(`error: ${m}`)
      process.exit(1)
    }

    if (command.length === 0) fail('no command to launch — pass it after `--`, e.g. `openthomas run -- claude`')
    const chosen = [opts.model, opts.monitor, opts.raw].filter(Boolean)
    if (chosen.length > 1) fail('pass at most one of --model, --monitor, or --raw')

    const [bin, ...rest] = command
    const wiring = wiringFor(bin!, opts.agent)
    if (!wiring) {
      fail(
        `openthomas run can't wrap "${opts.agent ?? basename(bin!)}" yet — it supports ` +
          'launch-per-task agents (Claude Code). For daemon agents like OpenClaw, ' +
          'set per-instance routing with `openthomas route set <agent>/<wrapmodel>`.',
      )
    }
    const w = wiring!

    const instanceId = instanceIdFrom(opts.as)
    const routeKey = policyKeyFor(w.agent, '*', instanceId)

    // Decide the base URL and the routing policy for this instance.
    let baseUrl: string
    if (opts.raw) {
      baseUrl = w.rawUpstream
    } else {
      baseUrl = thomasUrlForInstance(w.agent, instanceId)
      const target: RoutingTarget | null = opts.monitor
        ? { kind: 'passthrough' }
        : opts.model
          ? { kind: 'chain', members: [{ modelId: opts.model }] }
          : null // inherit the type-level default; still captured per-instance
      if (target !== null) {
        try {
          await setInstancePolicy(routeKey, target)
        } catch (e) {
          fail((e as Error).message)
        }
      }
    }

    // Apply the base URL via the agent's clobber-proof per-session override,
    // preserving whatever else lives in its settings `env`.
    const env = await readAgentEnv(w.settingsPath)
    env[w.baseUrlEnv] = baseUrl
    const settingsArgs = w.settingsArgs(JSON.stringify({ env }))
    const argv = [...settingsArgs, ...rest]

    const mode = opts.raw
      ? 'raw (bypassing OpenThomas)'
      : opts.monitor
        ? 'monitor-only'
        : opts.model
          ? `→ ${opts.model}`
          : 'type default'
    logger.print(`▶ ${w.agent}@${instanceId}  ${mode}`)

    const child = spawn(bin!, argv, { stdio: 'inherit', env: process.env })

    const cleanup = async (): Promise<void> => {
      if (opts.ephemeral && !opts.raw) {
        try {
          await setInstancePolicy(routeKey, null)
        } catch {
          // best-effort; a stale instance policy is harmless and reusable
        }
      }
    }

    child.on('exit', async (code, signal) => {
      await cleanup()
      if (signal) process.kill(process.pid, signal)
      else process.exit(code ?? 0)
    })
    child.on('error', (err) => {
      logger.print(`error: failed to launch ${bin}: ${err.message}`)
      process.exit(127)
    })
  })
