/**
 * worker-bootstrap.ts — entry point for the default "worker" mode of AAO.
 *
 * Phase 1 of the AAO Gateway split (2026-05-18) extracted the original
 * `src/index.ts` startup logic into this function so `src/main.ts` can
 * dispatch on `AAO_MODE` without two parallel entry points. The previous
 * `src/index.ts` is kept as a thin compatibility shim that just calls
 * `start()` here so `node dist/index.js` continues to work for anyone
 * who has wired that path into their service manager.
 *
 * Worker mode behaviour is unchanged: load config → init DB → start
 * WorkerManager + Scheduler → mount Express API. Anything that used to
 * live in `index.ts main()` now lives in `start()`.
 */
import { Repository, BrowserSessionRepo } from './db/repository.js';
import { startCoreServer } from './bridge/server.js';
import { runMigrations } from './db/migrate.js';
import { logger } from './logger.js';
import { accessSync, existsSync, mkdirSync, constants } from 'fs';
import { dirname, resolve, join } from 'path';
import { OpenAICompatClient } from './llm/openai-compat.js';
import { ConfigManager } from './config-manager.js';
import { WorkerManager } from './worker-manager.js';
import { classifyPiece } from './engine/piece-classifier.js';
import { PieceCatalog } from './engine/piece-catalog.js';
import { Scheduler } from './scheduler.js';
import { buildTitleFallback, buildTitlePrompt } from './title-generation.js';
import { initMcpKeyFromFile } from './mcp/crypto.js';
import { registerShutdownHook, installSignalHandlers } from './bridge/shutdown.js';
import { acquireWorkerLock } from './instance-lock.js';
import { checkBwrapAvailable } from './engine/tools/sandbox.js';
import { SkillCatalog } from './engine/skills.js';
import { VapidKeyStore } from './vapid-store.js';
import { PushService } from './push-service.js';

function runPreflight(configPath: string, dbPath: string): void {
  const errors: string[] = [];

  if (!existsSync(configPath)) {
    logger.info(`[preflight] config file not found: ${configPath}; continuing with defaults`);
  }

  logger.info('[preflight] running in local-only mode');

  const dbDir = dirname(dbPath);
  try {
    mkdirSync(dbDir, { recursive: true });
    accessSync(dbDir, constants.W_OK);
  } catch (e) {
    errors.push(`DB directory is not writable: ${dbDir} (${String(e)})`);
  }

  const schemaCandidates = [
    resolve(process.cwd(), 'dist/db/schema.sql'),
    resolve(process.cwd(), 'src/db/schema.sql'),
  ];
  const hasSchema = schemaCandidates.some(p => existsSync(p));
  if (!hasSchema) {
    errors.push(`schema.sql not found. expected one of: ${schemaCandidates.join(', ')}`);
  }

  if (errors.length > 0) {
    throw new Error(`Preflight failed:\n- ${errors.join('\n- ')}`);
  }
}

export interface StartWorkerOptions {
  /** Override config.yaml path. Defaults to 'config.yaml' (cwd-relative). */
  configPath?: string;
}

export async function start(opts: StartWorkerOptions = {}): Promise<void> {
  logger.info('maestro starting (mode=worker)...');

  const configPath = opts.configPath ?? 'config.yaml';
  const configManager = new ConfigManager(configPath);
  const config = configManager.getConfig();

  const dbPath = process.env['DB_PATH'] ?? './data/maestro.db';
  runPreflight(configPath, dbPath);

  // Single-writer guard: refuse to start if another worker process on this
  // host already owns this DB. Prevents two schedulers from double-claiming
  // and double-executing jobs (see instance-lock.ts).
  const workerLock = acquireWorkerLock(dbPath);
  registerShutdownHook('worker-instance-lock', async () => workerLock.release());

  const requiresBwrap =
    config.safety?.bashUnrestricted || config.safety?.bashSandbox === 'always';
  if (requiresBwrap) {
    const bwrapCheck = await checkBwrapAvailable();
    if (!bwrapCheck.ok) {
      throw new Error(
        `Bash sandboxing requires bwrap but it is not available: ${bwrapCheck.reason}\n` +
        `Enable user namespaces in the container/host, or set safety.bash_sandbox: auto.`
      );
    }
    logger.info('[startup] bash sandbox enabled — bwrap verified');
  } else if ((config.safety?.bashSandbox ?? 'auto') === 'auto') {
    const bwrapCheck = await checkBwrapAvailable();
    if (!bwrapCheck.ok) {
      logger.warn(
        '[startup] bwrap unavailable — Bash falls back to hardened whitelist ' +
        '(env-scrubbed, no FS/net namespace). Set bash_sandbox: always for prod isolation.'
      );
    }
  }

  const repo = new Repository(dbPath);
  runMigrations(repo.getDb());

  // 起動時に孤立ジョブを回復
  await repo.recoverOrphanedJobs();

  // Worker 起動（WorkerManager に委譲）
  const workerManager = new WorkerManager(repo, configManager);
  workerManager.start();

  const port = parseInt(process.env['PORT'] ?? '9876', 10);

  // タイトル自動生成関数を作成（roles に 'title' を持つ worker を優先、なければ最初の worker）
  const titleWorker =
    config.provider.workers.find(w => w.enabled !== false && w.roles?.includes('title')) ??
    config.provider.workers[0];
  let titleClient: OpenAICompatClient | null = null;
  let generateTitle: ((body: string) => Promise<string>) | undefined;

  if (titleWorker) {
    const titleModel = titleWorker.model ?? config.provider.model;
    logger.info(`Config: title generation worker=${titleWorker.id} model=${titleModel ?? '<none>'}`);
    titleClient = new OpenAICompatClient(
      titleWorker.endpoint,
      titleModel,
      undefined,
      config.provider.retry,
      (config.provider.timeoutMinutes ?? 10) * 60 * 1000,
    );

    generateTitle = async (body: string): Promise<string> => {
      const fallback = buildTitleFallback(body);
      const prompt = buildTitlePrompt(body);
      if (!prompt) return fallback;
      let title = '';
      try {
        for await (const event of titleClient!.chat([{ role: 'user', content: prompt }])) {
          if (event.type === 'text') title += event.text;
          if (event.type === 'error') return fallback;
          if (event.type === 'done') break;
        }
        title = title.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
        title = title.split('\n')[0]?.trim() ?? '';
        return title.slice(0, 60) || fallback;
      } catch {
        return fallback;
      }
    };
  }

  const piecesDir = resolve(process.cwd(), 'pieces');
  const customPiecesDir = config.customPiecesDir ? resolve(config.customPiecesDir) : undefined;
  logger.info(`[startup] piecesDir=${piecesDir} customPiecesDir=${customPiecesDir ?? 'none'}`);

  // カスタムディレクトリが設定されていれば作成
  if (customPiecesDir) {
    mkdirSync(customPiecesDir, { recursive: true });
  }

  // Per-user piece catalog: built-ins loaded once, per-user overrides with 60s TTL cache.
  // The fallback userId 'local' is used when no authenticated user is present (local mode).
  const userFolderRoot = config.userFolderRoot ?? './data/users';
  const pieceCatalog = new PieceCatalog(piecesDir, userFolderRoot);
  logger.info(`[startup] PieceCatalog initialised builtinDir=${piecesDir} dataDir=${userFolderRoot}`);

  const skillsDir = resolve(process.cwd(), 'data', 'skills');
  mkdirSync(skillsDir, { recursive: true });
  const skillCatalog = new SkillCatalog(skillsDir, userFolderRoot);
  workerManager.setSkillCatalog(skillCatalog);
  logger.info(`[startup] SkillCatalog initialised systemDir=${skillsDir} userRoot=${userFolderRoot}`);

  // V2 Web Push notifications.
  // Spec: docs/superpowers/specs/2026-05-28-browser-notifications-v2-webpush.md.
  // When `notifications.push.enabled` is false (the default), we still mount
  // the API routes so the UI can see a clear 503 / "管理者により無効化"
  // signal, but workers do not fire any pushes.
  let pushService: PushService | null = null;
  let vapidStore: VapidKeyStore | null = null;
  const pushCfg = config.notifications?.push;
  if (pushCfg?.enabled) {
    const currentPath =
      pushCfg.vapidCurrentPath ?? join(process.cwd(), 'data/secrets/vapid.json');
    const historyDir =
      pushCfg.vapidHistoryDir ?? join(process.cwd(), 'data/secrets/vapid-history');
    const subject = pushCfg.vapidSubject ?? 'https://maestro.example.com/';
    vapidStore = new VapidKeyStore(currentPath, historyDir);
    vapidStore.loadOrGenerate(subject);
    pushService = new PushService(repo, vapidStore, {
      queueConcurrency: pushCfg.queueConcurrency,
      perSendTimeoutMs: pushCfg.perSendTimeoutMs,
      payloadMaxBytes: pushCfg.payloadMaxBytes,
    });
    workerManager.setPushService(pushService);
    logger.info(`[startup] Web Push V2 enabled keyId=${vapidStore.getCurrent().keyId}`);
  } else {
    logger.info('[startup] Web Push V2 disabled (notifications.push.enabled=false)');
  }

  const selectPiece = titleClient
    ? async (body: string, fileNames: string[], userId?: string): Promise<string> => {
        const pieces = pieceCatalog.getForUser(userId ?? 'local');
        const result = await classifyPiece(titleClient!, body, pieces, fileNames);
        return result ?? 'chat';
      }
    : undefined;

  // スケジューラ起動 (selectPiece 注入で 'auto' を実 piece に解決)
  // task_kind='script' をサポートするため、sessRepo / masterKeyPath / userFolderRoot も渡す。
  const sessRepoForScheduler = new BrowserSessionRepo(repo.getDb());
  const scheduler = new Scheduler(repo, config.worktreeDir, {
    selectPiece,
    sessRepo: sessRepoForScheduler,
    masterKeyPath: config.secrets?.masterKeyPath ?? './data/secrets/master.key',
    userFolderRoot: config.userFolderRoot ?? './data/users',
  });
  scheduler.start();

  // Auto-init MCP encryption key from file (generates on first boot if missing).
  // Must run before startCoreServer so isKeyConfigured() is truthy when routes register.
  try {
    initMcpKeyFromFile(config.secrets?.mcpKeyPath ?? './data/secrets/mcp.key');
    logger.info('[startup] MCP encryption key loaded from file');
  } catch (e) {
    logger.warn(`[startup] MCP encryption key init failed: ${e} — MCP client features will be disabled`);
  }

  startCoreServer({
    repo,
    worktreeDir: config.worktreeDir,
    configuredRepos: [],
    generateTitle,
    selectPiece,
    configManager,
    piecesDir,
    customPiecesDir,
    scheduler,
    authConfig: config.auth,
    brandingDir: join(dirname(dbPath), 'branding'),
    workerManager,
    skillCatalog,
    pushService,
    vapidStore,
  }, port);

  // Graceful shutdown — see installWorkerShutdownHooks() docstring.
  installWorkerShutdownHooks({
    scheduler,
    workerManager,
    repo,
  });

  logger.info('maestro ready (mode=worker)');
}

/**
 * Shutdown-hook wiring for worker mode. Extracted so unit tests can
 * verify the registry calls without spinning up the entire boot path.
 *
 * Registers hooks with the shared `shutdown` registry instead of
 * stacking per-subsystem signal listeners. The gateway path uses the
 * same registry, so SIGTERM behaviour is now consistent across modes
 * and future subsystems can `registerShutdownHook` without hitting
 * Node's default MaxListeners cap.
 *
 * The registry runs hooks concurrently (Promise.allSettled), so any
 * hook with internal ordering requirements collapses its sequence
 * into a single closure. Worker drain → repo close is one such case:
 * closing the DB before WorkerManager.stop() finishes risks
 * SQLITE_BUSY on the in-flight job write, so we serialise them inside
 * one hook.
 */
export interface WorkerShutdownDeps {
  scheduler: { stop(): void };
  /**
   * WorkerManager.stop() returns { drained, requeued } as of the AAO Gateway
   * refactor, but this shutdown hook discards the result. Widen to
   * `Promise<unknown>` so the structural type accepts any stop() return shape
   * without recreating a coupling to WorkerManager's concrete type here.
   */
  workerManager: { stop(): Promise<unknown> };
  repo: { close(): void };
}

export function installWorkerShutdownHooks(deps: WorkerShutdownDeps): void {
  registerShutdownHook('worker-scheduler', async () => {
    deps.scheduler.stop();
  });
  registerShutdownHook('worker-manager+repo', async () => {
    await deps.workerManager.stop();
    deps.repo.close();
  });
  installSignalHandlers();
}
