// src/worker-manager.ts
import { Worker } from './worker.js';
import { Repository } from './db/repository.js';
import { ConfigManager } from './config-manager.js';
import { type AppConfig } from './config.js';
import { logger } from './logger.js';
import type { McpTokenManager } from './mcp/token-manager.js';
import type { WorkerMetrics } from './metrics/worker-metrics.js';
import type { SkillCatalog } from './engine/skills.js';
import type { PushService } from './push-service.js';

export class WorkerManager {
  private workers: Worker[] = [];
  private repo: Repository;
  private configManager: ConfigManager;
  private rebuildPromise: Promise<any> | null = null;
  private mcpTokenManager: McpTokenManager | null = null;
  /**
   * Phase 3b: Prometheus metrics handle. Wired post-construction by
   * worker-bootstrap once createCoreServer has built the registry.
   * Fanned out to every Worker on construct + on rebuild.
   */
  private workerMetrics: WorkerMetrics | null = null;
  private skillCatalog: SkillCatalog | null = null;
  private pushService: PushService | null = null;

  constructor(repo: Repository, configManager: ConfigManager) {
    this.repo = repo;
    this.configManager = configManager;
    this.configManager.onConfigChanged((config) => {
      this.rebuild(config).catch(err =>
        logger.error(`WorkerManager rebuild failed: ${err}`)
      );
    });
  }

  start(): void {
    const config = this.configManager.getConfig();
    this.workers = this.createWorkers(config);
    for (const w of this.workers) w.start();
    logger.info(`WorkerManager: started ${this.workers.length} workers`);
  }

  async stop(timeoutMs = 30000): Promise<{ drained: string[]; requeued: string[] }> {
    const drained: string[] = [];
    const requeued: string[] = [];

    // Stop polling for all workers
    for (const w of this.workers) w.stop();

    // Wait for completion with timeout
    const results = await Promise.all(
      this.workers.map(w => w.waitForCompletion(timeoutMs))
    );

    // Track drained (completed normally) and requeue (timed out)
    for (let i = 0; i < this.workers.length; i++) {
      const w = this.workers[i]!;
      if (results[i]) {
        drained.push(w.id);
      } else {
        // Timed out — requeue running jobs for this worker
        // Note: Repository doesn't have a workerId filter for running jobs,
        // so we requeue all running jobs. This is acceptable because we just
        // stopped all workers.
        requeued.push(w.id);
      }
    }

    // If any workers timed out, requeue all running jobs
    if (requeued.length > 0) {
      this.repo.requeueRunningJobs();
      logger.info(`WorkerManager: requeued running jobs for timed-out workers: ${requeued.join(', ')}`);
    }

    this.workers = [];
    return { drained, requeued };
  }

  async rebuild(newConfig: AppConfig): Promise<{ drained: string[]; requeued: string[] }> {
    // Serialize rebuilds to prevent re-entrancy issues
    if (this.rebuildPromise) {
      await this.rebuildPromise;
    }
    this.rebuildPromise = this.doRebuild(newConfig);
    try {
      return await this.rebuildPromise;
    } finally {
      this.rebuildPromise = null;
    }
  }

  private async doRebuild(newConfig: AppConfig): Promise<{ drained: string[]; requeued: string[] }> {
    logger.info('WorkerManager: rebuilding workers for new config');
    const result = await this.stop();
    this.workers = this.createWorkers(newConfig);
    for (const w of this.workers) w.start();
    logger.info(`WorkerManager: rebuilt ${this.workers.length} workers`);
    return result;
  }

  getWorkers(): Worker[] {
    return this.workers;
  }

  setMcpDeps(deps: { tokenManager: McpTokenManager }): void {
    this.mcpTokenManager = deps.tokenManager;
    for (const w of this.workers) {
      w.setMcpTokenManager(deps.tokenManager);
    }
  }

  /**
   * Phase 3b: install (or remove) the Prometheus metrics handle on
   * every Worker. Called by worker-bootstrap after createCoreServer
   * builds the registry. Setting null disables emission.
   */
  setWorkerMetrics(metrics: WorkerMetrics | null): void {
    this.workerMetrics = metrics;
    for (const w of this.workers) {
      w.setWorkerMetrics(metrics);
    }
  }

  setSkillCatalog(catalog: SkillCatalog): void {
    this.skillCatalog = catalog;
    for (const w of this.workers) {
      w.setSkillCatalog(catalog);
    }
  }

  /**
   * V2 Web Push notification service. Null disables push hooks across
   * all workers (rebuild safe). Fanned out to every Worker on construct
   * + on rebuild.
   * Spec: docs/superpowers/specs/2026-05-28-browser-notifications-v2-webpush.md.
   */
  setPushService(service: PushService | null): void {
    this.pushService = service;
    for (const w of this.workers) {
      w.setPushService(service);
    }
  }

  private createWorkers(config: AppConfig): Worker[] {
    const workers = config.provider.workers
      .filter(def => def.enabled !== false)
      .map(def => new Worker(
        def.id,
        def.endpoint,
        def.model,
        this.repo,
        config,
      ));
    if (this.mcpTokenManager) {
      for (const w of workers) {
        w.setMcpTokenManager(this.mcpTokenManager);
      }
    }
    if (this.workerMetrics) {
      for (const w of workers) {
        w.setWorkerMetrics(this.workerMetrics);
      }
    }
    if (this.skillCatalog) {
      for (const w of workers) {
        w.setSkillCatalog(this.skillCatalog);
      }
    }
    if (this.pushService) {
      for (const w of workers) {
        w.setPushService(this.pushService);
      }
    }
    return workers;
  }
}
