/**
 * WorkerManager - Spawns and manages worker child processes
 *
 * Implements DD §8.1. spawn (Bun child process + handshake), cancel.
 * INV-1: WorkerManager never writes agents.status directly.
 *
 * @module packages/runtime/src/workers/WorkerManager
 */

import { spawn, execSync } from 'child_process'
import type { ChildProcess } from 'child_process'
import { WorkerProcess, type WorkerExitCode } from './WorkerProcess.js'
import { WorkerProtocol, type WorkerMessage, type WorkerMessageType } from './WorkerProtocol.js'
import type { WorkerResult, WorkerStatus, AgentType } from '@aircoding/contracts'

export interface WorkerConfig {
  entrypoint: string       // Path to worker main.ts
  agent_id: string
  session_id: string
  project_root: string
  timeout_ms?: number
  env?: Record<string, string>
}

export interface WorkerHandle {
  worker_id: string
  process: WorkerProcess
  config: WorkerConfig
  state: 'starting' | 'ready' | 'running' | 'completed' | 'error' | 'cancelled'
  started_at: string
  completed_at?: string
  result?: WorkerResult<unknown>
}

export class WorkerManager {
  private protocol: WorkerProtocol
  private workers: Map<string, WorkerHandle> = new Map()

  constructor() {
    this.protocol = new WorkerProtocol()
  }

  /**
   * Spawn a worker child process and perform handshake.
   * INV-1: worker.ready handshake is a live signal, not a status write.
   */
  async spawn(config: WorkerConfig): Promise<WorkerHandle> {
    const proc = new WorkerProcess()
    const handle: WorkerHandle = {
      worker_id: config.agent_id,
      process: proc,
      config,
      state: 'starting',
      started_at: new Date().toISOString()
    }

    // Spawn worker process using Bun
    const bun_path = this.find_bun()
    const child = spawn(bun_path, ['run', config.entrypoint], {
      stdio: ['pipe', 'pipe', 'pipe'],
      env: {
        ...process.env,
        ...config.env,
        AIRCODING_AGENT_ID: config.agent_id,
        AIRCODING_SESSION_ID: config.session_id,
        AIRCODING_PROJECT_ROOT: config.project_root
      },
      cwd: config.project_root
    })

    proc.set_process(child)

    // Wait for handshake: worker.ready
    await this.wait_for_handshake(proc, config)

    // Validate protocol version
    const ready_msg = this.send_and_wait(proc, 'agent.start', {
      protocol_version: this.protocol.get_version(),
      agent_id: config.agent_id,
      session_id: config.session_id,
      project_root: config.project_root
    })

    handle.state = 'ready'
    this.workers.set(config.agent_id, handle)

    // Set up timeout
    if (config.timeout_ms) {
      setTimeout(() => this.cancel(config.agent_id, 'timeout'), config.timeout_ms)
    }

    return handle
  }

  /**
   * Cancel a worker.
   */
  async cancel(agent_id: string, reason: string): Promise<void> {
    const handle = this.workers.get(agent_id)
    if (!handle) return

    const msg = this.protocol.create_message('agent.cancel', { reason }, 'parent_to_worker')
    handle.process.send(msg)
    handle.state = 'cancelled'

    // Wait briefly then force kill
    setTimeout(() => {
      if (handle.process.is_alive()) {
        handle.process.kill('SIGKILL')
      }
    }, 5000)
  }

  /**
   * Send a message to a worker.
   */
  send(agent_id: string, type: WorkerMessageType, payload: Record<string, unknown>): void {
    const handle = this.workers.get(agent_id)
    if (!handle) throw new Error(`Worker not found: ${agent_id}`)

    const msg = this.protocol.create_message(type, payload, 'parent_to_worker')
    handle.process.send(msg)
  }

  /**
   * Get a worker handle.
   */
  get(agent_id: string): WorkerHandle | undefined {
    return this.workers.get(agent_id)
  }

  /**
   * List all workers.
   */
  list(): WorkerHandle[] {
    return Array.from(this.workers.values())
  }

  /**
   * List workers by state.
   */
  list_by_state(state: WorkerHandle['state']): WorkerHandle[] {
    return this.list().filter(w => w.state === state)
  }

  /**
   * Check if any workers are running.
   */
  has_running(): boolean {
    return this.list().some(w => w.state === 'running' || w.state === 'ready' || w.state === 'starting')
  }

  /**
   * Get the stored WorkerResult for an agent.
   */
  get_result(agent_id: string): WorkerResult<unknown> | undefined {
    const handle = this.workers.get(agent_id)
    if (!handle) return undefined
    return handle.result
  }

  // ============================================================================
  // Private
  // ============================================================================

  /**
   * Wrap a raw worker payload into a properly typed WorkerResult envelope.
   * Provides safe defaults for any missing fields.
   */
  private wrap_worker_result(payload: Record<string, unknown>, handle: WorkerHandle): WorkerResult<unknown> {
    return {
      task_id: (payload.task_id as string) || '' as any,
      agent_id: (payload.agent_id as string) || handle.config.agent_id as any,
      agent_type: (payload.agent_type as AgentType) || 'executor',
      status: (payload.status as WorkerStatus) || 'completed',
      summary: (payload.summary as string) || '',
      changed_files: (payload.changed_files as string[]) || [],
      diff_ref: (payload.diff_ref as string | undefined) || undefined,
      artifacts: (payload.artifacts as any[]) || [],
      verification: (payload.verification as any[]) || [],
      risks: (payload.risks as any[]) || [],
      follow_up_tasks: (payload.follow_up_tasks as any[]) || [],
      evidence_refs: (payload.evidence_refs as any[]) || [],
      result: (payload.result as unknown) || null,
    }
  }

  private async wait_for_handshake(proc: WorkerProcess, config: WorkerConfig): Promise<void> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error(`Worker handshake timeout: ${config.agent_id}`))
      }, 30000)

      proc.on_message('worker.ready', (msg) => {
        clearTimeout(timeout)
        const version = msg.payload.protocol_version as number
        const check = this.protocol.check_version(version)
        if (!check.compatible) {
          reject(new Error(check.error))
          return
        }
        resolve()
      })

      // Also handle worker.error
      proc.on_message('worker.error', (msg) => {
        clearTimeout(timeout)
        reject(new Error(`Worker error during handshake: ${msg.payload.message}`))
      })
    })
  }

  private async send_and_wait(
    proc: WorkerProcess,
    type: WorkerMessageType,
    payload: Record<string, unknown>
  ): Promise<WorkerMessage> {
    const msg = this.protocol.create_message(type, payload, 'parent_to_worker')

    return new Promise((resolve, reject) => {
      // Wait for worker.send callback (worker acknowledges agent.start)
      // For now just send and resolve after a short delay
      proc.send(msg)
      setTimeout(() => resolve(msg), 100)
    })
  }

  private find_bun(): string {
    try {
      return execSync('which bun', { encoding: 'utf-8' }).trim()
    } catch {
      // Try common paths
      const common = ['/home/airlongdian/.bun/bin/bun', '/usr/local/bin/bun', '/usr/bin/bun']
      for (const path of common) {
        try {
          execSync(`test -x ${path}`)
          return path
        } catch { /* */ }
      }
      return 'bun'
    }
  }
}
