/**
 * Scheduler — Main scheduling engine
 *
 * Implements contracts §9; DD §7.1 + state machine §20.2.
 * INV-1: status only via emitted events for projection
 * INV-5: rebuild queues from SQLite, not EventBus replay
 *
 * @module packages/runtime/src/scheduler/Scheduler
 */

import type { TaskID, SessionID, ProjectID } from '@aircoding/contracts'
import { TaskGraph } from './TaskGraph.js'
import { WavePlanner } from './WavePlanner.js'
import { RetryPlanner } from './RetryPlanner.js'
import { WorkspaceManager } from './WorkspaceManager.js'
import { AgentMonitor } from './AgentMonitor.js'
import { eventIngestor } from '../events/EventIngestor.js'
import type { WorkerManager } from '../workers/WorkerManager.js'

export type SchedulerState =
  | 'IDLE'
  | 'LOADING_GRAPH'
  | 'PLANNING_WAVE'
  | 'DISPATCHING'
  | 'MONITORING'
  | 'COLLECTING_RESULTS'
  | 'MERGING'
  | 'REVIEWING_WAVE'
  | 'REPAIRING_OR_CONTINUING'
  | 'COMPLETED'
  | 'TERMINATED'
  | 'BLOCKED'
  | 'CANCELLED'

export interface SchedulerContext {
  session_id: SessionID
  project_id: ProjectID
  project_root: string
}

export class Scheduler {
  private state: SchedulerState = 'IDLE'
  private graph: TaskGraph
  private wave_planner: WavePlanner
  private retry_planner: RetryPlanner
  private workspace_manager: WorkspaceManager
  private agent_monitor: AgentMonitor
  private context: SchedulerContext
  private worker_manager?: WorkerManager

  constructor(context: SchedulerContext, worker_manager?: WorkerManager) {
    this.context = context
    this.graph = new TaskGraph()
    this.wave_planner = new WavePlanner()
    this.retry_planner = new RetryPlanner()
    this.workspace_manager = new WorkspaceManager(context.project_root)
    this.agent_monitor = new AgentMonitor()
    this.worker_manager = worker_manager
  }

  /**
   * Create tasks from specifications.
   */
  create_tasks(tasks: Array<{ id: TaskID; type: string; title: string; depends_on?: string[] }>): void {
    for (const task of tasks) {
      this.graph.add_task({
        id: task.id,
        status: 'pending',
        dependencies: task.depends_on?.map(d => ({ task_id: d, type: 'hard' as const })) || []
      })
    }

    // Emit task.created events (INV-1: via projection, not direct status write)
    this.state = 'PLANNING_WAVE'
  }

  /**
   * Run until idle — drives state machine to terminal state.
   */
  async run_until_idle(): Promise<SchedulerState> {
    while (
      this.state !== 'COMPLETED' &&
      this.state !== 'TERMINATED' &&
      this.state !== 'BLOCKED' &&
      this.state !== 'CANCELLED'
    ) {
      await this.step()
    }
    return this.state
  }

  /**
   * Execute one scheduler step.
   */
  async step(): Promise<void> {
    switch (this.state) {
      case 'IDLE':
        this.state = 'LOADING_GRAPH'
        break

      case 'LOADING_GRAPH':
        // Validate graph references
        const validation = this.graph.validate_refs()
        if (!validation.valid) {
          console.error('Graph validation failed:', validation.errors)
          this.state = 'TERMINATED'
          return
        }
        this.state = 'PLANNING_WAVE'
        break

      case 'PLANNING_WAVE': {
        // Check if all tasks done
        const counts = this.graph.count_by_status()
        const remaining = (counts.pending || 0) + (counts.running || 0)

        if (remaining === 0) {
          this.state = 'COMPLETED'
          return
        }

        // Plan next wave
        const plan = this.wave_planner.plan(this.graph)
        if (plan.length === 0) {
          // Check for blocked tasks
          const pending = this.graph.count_by_status().pending || 0
          if (pending > 0) {
            this.state = 'REPAIRING_OR_CONTINUING'
            return
          }
          this.state = 'COMPLETED'
          return
        }

        this.state = 'DISPATCHING'
        break
      }

      case 'DISPATCHING': {
        const runnable = this.graph.get_runnable_tasks()
        for (const task of runnable) {
          const agent_id = `agent_${task.id}`

          // INV-1: Emit task.started event (durable) for projection
          const now = new Date().toISOString()
          await eventIngestor.ingest({
            id: `evt_${task.id}_started`,
            type: 'task.started',
            version: 1,
            session_id: this.context.session_id,
            project_id: this.context.project_id,
            timestamp: now,
            source: { kind: 'scheduler' },
            route: ['scheduler', 'dispatch'],
            payload: { task_id: task.id, agent_id, attempt_id: `${task.id}_1`, attempt_index: 0, workspace_id: `ws_${task.id}` }
          })

          if (this.worker_manager) {
            try {
              await this.worker_manager.spawn({
                entrypoint: 'packages/workers/src/main.ts',
                agent_id,
                session_id: this.context.session_id,
                project_root: this.context.project_root,
              })
              this.agent_monitor.record_heartbeat(agent_id, task.id)
            } catch {
              // INV-1: emit task.failed event for projection
              await eventIngestor.ingest({
                id: `evt_${task.id}_failed`,
                type: 'task.failed',
                version: 1,
                session_id: this.context.session_id,
                project_id: this.context.project_id,
                timestamp: new Date().toISOString(),
                source: { kind: 'scheduler' },
                route: ['scheduler', 'dispatch'],
                payload: { task_id: task.id, agent_id, attempt_id: `${task.id}_1`, error: { message: 'Worker spawn failed' }, evidence_refs: [], metadata: {} }
              })
            }
          } else {
            this.agent_monitor.record_heartbeat(agent_id, task.id)
          }
        }
        this.state = 'MONITORING'
        break
      }

      case 'MONITORING':
        // Check agent health
        const lost = this.agent_monitor.detect_lost_agents()
        for (const l of lost) {
          // Emit agent.lost + task.failed events for projection (INV-1/INV-5)
          const hb = this.agent_monitor.get(l.agent_id)
          if (hb) {
            const now = new Date().toISOString()
            await eventIngestor.ingest({
              id: `evt_${hb.task_id}_lost`,
              type: 'agent.lost',
              version: 1,
              session_id: this.context.session_id,
              project_id: this.context.project_id,
              timestamp: now,
              source: { kind: 'scheduler' },
              route: ['scheduler', 'monitoring'],
              payload: { agent_id: l.agent_id, task_id: hb.task_id, last_heartbeat_at: l.last_heartbeat, detection_reason: l.state }
            })
            await eventIngestor.ingest({
              id: `evt_${hb.task_id}_failed`,
              type: 'task.failed',
              version: 1,
              session_id: this.context.session_id,
              project_id: this.context.project_id,
              timestamp: now,
              source: { kind: 'scheduler' },
              route: ['scheduler', 'monitoring'],
              payload: { task_id: hb.task_id, agent_id: l.agent_id, attempt_id: '', error: { message: `Agent ${l.state}` }, evidence_refs: [], metadata: {} }
            })
            this.agent_monitor.remove(l.agent_id)
          }
        }

        const timeouts = this.agent_monitor.enforce_timeouts()
        for (const t of timeouts) {
          const hb = this.agent_monitor.get(t.agent_id)
          const task_id = hb?.task_id
          switch (t.action) {
            case 'hard_cancel':
            case 'soft_cancel':
              if (task_id) {
                await eventIngestor.ingest({
                  id: `evt_${task_id}_cancelled`,
                  type: 'agent.cancelled',
                  version: 1,
                  session_id: this.context.session_id,
                  project_id: this.context.project_id,
                  timestamp: new Date().toISOString(),
                  source: { kind: 'scheduler' },
                  route: ['scheduler', 'monitoring'],
                  payload: { agent_id: t.agent_id, task_id, reason: t.action }
                })
              }
              this.agent_monitor.remove(t.agent_id)
              break
            case 'ping':
              break
          }
        }

        // Check if any running tasks remain
        const running = (this.graph.count_by_status().running || 0)
        if (running === 0) {
          this.state = 'COLLECTING_RESULTS'
        }
        break

      case 'COLLECTING_RESULTS':
        // Results arrive via events, projection updates task status
        this.state = 'MERGING'
        break

      case 'MERGING': {
        const active_ws = this.workspace_manager.get_active()
        for (const ws of active_ws) {
          await this.workspace_manager.merge_workspace(ws.id)
        }
        this.state = 'REVIEWING_WAVE'
        break
      }

      case 'REVIEWING_WAVE':
        this.state = 'REPAIRING_OR_CONTINUING'
        break

      case 'REPAIRING_OR_CONTINUING': {
        const counts = this.graph.count_by_status()
        const failed = counts.failed || 0

        if (failed > 0) {
          // Retry logic handled by RetryPlanner
        }

        this.state = 'PLANNING_WAVE'
        break
      }

      case 'BLOCKED':
      case 'CANCELLED':
      case 'COMPLETED':
      case 'TERMINATED':
        break
    }
  }

  /**
   * Rebuild scheduler state from SQLite (INV-5: from EventStore, not EventBus).
   */
  async rebuild_from_db(): Promise<void> {
    this.state = 'LOADING_GRAPH'
    // Would load all tasks from SQLite, reconstruct graph
    // Load pending/running tasks, agent status, workspaces
  }

  /**
   * Get current state.
   */
  get_state(): SchedulerState {
    return this.state
  }

  /**
   * Get the task graph (for inspection).
   */
  get_graph(): TaskGraph {
    return this.graph
  }
}
