/**
 * Server-side initialization — called once on first API request per worker.
 * When FORGE_EXTERNAL_SERVICES=1 (set by forge-server), telegram/terminal/tunnel
 * are managed externally — only task runner starts here.
 */

import { ensureRunnerStarted } from './task-manager';
import { startTelegramBot, stopTelegramBot } from './telegram-bot';
import { startWatcherLoop } from './session-watcher';
import { getAdminPassword } from './password';
import { loadSettings, saveSettings } from './settings';
import { startTunnel } from './cloudflared';
import { isEncrypted, SECRET_FIELDS } from './crypto';
import { assessProcess, reclaimProcess, ownedKill } from './safe-reclaim';
import { runAllowFail } from './safe-exec';
import { spawn } from 'node:child_process';
import { join } from 'node:path';

const initKey = Symbol.for('mw-initialized');
const gInit = globalThis as any;

/** Migrate plaintext secrets to encrypted on first run */
function migrateSecrets() {
  try {
    const { existsSync, readFileSync } = require('node:fs');
    const YAML = require('yaml');
    const { getDataDir: _gdd } = require('./dirs');
    const dataDir = _gdd();
    const file = join(dataDir, 'settings.yaml');
    if (!existsSync(file)) return;
    const raw = YAML.parse(readFileSync(file, 'utf-8')) || {};
    let needsSave = false;
    for (const field of SECRET_FIELDS) {
      if (raw[field] && typeof raw[field] === 'string' && !isEncrypted(raw[field])) {
        needsSave = true;
        break;
      }
    }
    if (needsSave) {
      // loadSettings returns decrypted, saveSettings encrypts
      const settings = loadSettings();
      saveSettings(settings);
      console.log('[init] Migrated plaintext secrets to encrypted storage');
    }
  } catch (e) {
    console.error('[init] Secret migration error:', e);
  }
}

/** Auto-detect agent binaries */
function autoDetectAgents() {
  try {
    const settings = loadSettings();
    // Backward compat: detect claude if not configured
    if (!settings.claudePath) {
      const { execSync } = require('node:child_process');
      try {
        const path = execSync('which claude', { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
        if (path) {
          settings.claudePath = path;
          saveSettings(settings);
          console.log(`[init] Auto-detected claude: ${path}`);
        }
      } catch {}
    }
    // Detect all agents
    const { autoDetectAgents: detect } = require('./agents');
    detect();
  } catch {}
}

export function ensureInitialized() {
  if (gInit[initKey]) return;
  gInit[initKey] = true;

  // Per-step timing — earlier startup hangs were invisible because all
  // these steps log heterogeneously (or not at all). \`time(label, fn)\`
  // prints \`[init] <label> took 1234ms\` for anything taking >250ms so
  // we can spot the slow one without instrumenting elsewhere.
  const time = <T>(label: string, fn: () => T): T => {
    const t = Date.now();
    try { return fn(); }
    finally {
      const ms = Date.now() - t;
      if (ms >= 250) console.log(`[init] ${label} took ${ms}ms`);
    }
  };

  time('logger', () => { try { const { initLogger } = require('./logger'); initLogger(); } catch {} });
  time('migrateDataDir', () => { try { const { migrateDataDir } = require('./dirs'); migrateDataDir(); } catch {} });
  time('ensureScratchProject', () => {
    // Synthetic 'scratch' project under <dataDir>/scratch — default
    // workspace for prompt schedules / chat-launched temp tasks that
    // don't care about a real project.
    try { const { ensureScratchProject } = require('./projects'); ensureScratchProject(); }
    catch (e) { console.warn('[init] ensureScratchProject failed:', (e as Error).message); }
  });
  time('migrateSecrets', migrateSecrets);
  time('migrateAgentsFlatten', () => {
    try {
      const { migrateAgentsFlatten } = require('./agents/migrate');
      const settings = loadSettings();
      if (migrateAgentsFlatten(settings)) {
        saveSettings(settings);
      }
    } catch (e) { console.warn('[init] migrateAgentsFlatten failed:', (e as Error).message); }
  });
  time('migratePluginSecrets', () => {
    try {
      const { migratePluginSecrets } = require('./plugins/registry');
      migratePluginSecrets();
    } catch (e) { console.warn('[init] migratePluginSecrets failed:', (e as Error).message); }
  });
  time('cleanupNotifications', () => { try { const { cleanupNotifications } = require('./notifications'); cleanupNotifications(); } catch {} });
  time('startScratchCleanup', () => {
    try { const { startScratchCleanup } = require('./scratch-cleanup'); startScratchCleanup(); }
    catch (e) { console.warn('[init] startScratchCleanup failed:', (e as Error).message); }
  });
  time('autoDetectAgents', autoDetectAgents);
  time('ensureGlabHttps', () => {
    // Default glab CLI to HTTPS so `glab repo clone` uses the connector PAT
    // instead of SSH (which would hit ssh 2fa_verify). Best-effort; absent
    // glab or read-only config dir just skip.
    try {
      const { execSync } = require('node:child_process');
      const cur = execSync('glab config get git_protocol 2>/dev/null', { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
      if (cur && cur !== 'https') {
        execSync('glab config set -g git_protocol https', { timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
        console.log(`[init] glab git_protocol: ${cur} → https`);
      }
    } catch {}
  });
  time('logToolStatus', () => {
    try { const { logToolStatus } = require('./health'); logToolStatus(); }
    catch (e) { console.warn('[tools] health check failed:', (e as Error).message); }
  });
  time('installForgeSkills', () => {
    try {
      const { installForgeSkills } = require('./workspace/skill-installer');
      installForgeSkills('', '', '', Number(process.env.PORT) || 8403);
      console.log('[init] Forge skills installed/updated');
    } catch {}
  });
  // One-shot: relocate Forge-installed global skills from ~/.claude/skills →
  // <dataDir>/skills (Forge-owned). Only moves skills Forge installed (DB
  // installed_global=1), never the user's own Claude Code skills. Idempotent:
  // skips any already present in the new location.
  time('migrateGlobalSkillsToDataDir', () => {
    try {
      const { existsSync, cpSync, rmSync } = require('node:fs');
      const { join: joinPath } = require('node:path');
      const { homedir } = require('node:os');
      const { getDataDir } = require('./dirs');
      const { getDb } = require('../src/core/db/database');
      const { getDbPath } = require('../src/config');
      const oldRoot = joinPath(homedir(), '.claude', 'skills');
      const newRoot = joinPath(getDataDir(), 'skills');
      if (!existsSync(oldRoot)) return;
      const rows = getDb(getDbPath()).prepare('SELECT name FROM skills WHERE installed_global = 1').all() as Array<{ name: string }>;
      let moved = 0, removed = 0;
      for (const { name } of rows) {
        const src = joinPath(oldRoot, name);
        const dst = joinPath(newRoot, name);
        if (!existsSync(src)) continue;
        if (!existsSync(dst)) { try { cpSync(src, dst, { recursive: true }); moved++; } catch {} }
        // Remove the old ~/.claude/skills copy so the Claude Code CLI (which
        // still reads ~/.claude/skills) can't load a STALE shadow of a
        // Forge-managed global skill — the per-task project install is the
        // single source of truth. Only Forge's own installed_global skills.
        try { rmSync(src, { recursive: true, force: true }); removed++; } catch {}
      }
      if (moved || removed) console.log(`[init] migrated ${moved} + removed ${removed} stale global skill copy(ies) from ~/.claude/skills`);
    } catch {}
  });

  // Sync help docs + CLAUDE.md to data dir on startup
  time('syncHelpDocs', () => {
    try {
      const { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync } = require('node:fs');
      const { join: joinPath } = require('node:path');
      const { getConfigDir, getDataDir } = require('./dirs');
      const helpDir = joinPath(getConfigDir(), 'help');
      const sourceDir = joinPath(process.cwd(), 'lib', 'help-docs');
      if (existsSync(sourceDir)) {
        if (!existsSync(helpDir)) mkdirSync(helpDir, { recursive: true });
        for (const f of readdirSync(sourceDir)) {
          if (f.endsWith('.md')) writeFileSync(joinPath(helpDir, f), readFileSync(joinPath(sourceDir, f)));
        }
        const claudeMd = joinPath(helpDir, 'CLAUDE.md');
        if (existsSync(claudeMd)) writeFileSync(joinPath(getDataDir(), 'CLAUDE.md'), readFileSync(claudeMd));
      }
    } catch {}
  });

  // Sync skills registry (async, non-blocking) — on startup + every 30 min
  try {
    const { syncSkills } = require('./skills');
    syncSkills().catch(() => {});
    setInterval(() => { syncSkills().catch(() => {}); }, 60 * 60 * 1000);
  } catch {}

  // Daily config snapshot (settings.yaml + .encrypt-key + connector configs).
  // Runs on first cold start of each calendar day; the 6h interval is a
  // safety net so a process that stays up across midnight still gets one.
  // Cheap stat-only fast-path when today's dir already exists. See
  // lib/config-backup.ts for the full file list + retention.
  try {
    const { maybeRunDailyBackup } = require('./config-backup');
    maybeRunDailyBackup();
    setInterval(() => { try { maybeRunDailyBackup(); } catch {} }, 6 * 60 * 60 * 1000);
  } catch (e) {
    console.warn('[init] config-backup setup failed:', (e as Error).message);
  }

  // Foundry auto-push — mirror of the daily backup, but pushes the snapshot to
  // the external Enterprise Center. Skips silently when disabled/unconfigured;
  // gated by foundryBackupIntervalHours since the last successful push. Wrapped
  // so a Foundry outage never crashes init.
  const maybePushFoundry = async () => {
    try {
      const { loadSettings, saveSettings } = require('./settings');
      const s = loadSettings();
      if (!s.foundryEnabled || !s.foundryAutoBackup || !s.foundryUrl || !s.foundryKey) return;
      const intervalMs = Math.max(1, s.foundryBackupIntervalHours || 24) * 60 * 60 * 1000;
      const last = s.foundryLastBackupAt ? new Date(s.foundryLastBackupAt).getTime() : 0;
      if (Date.now() - last < intervalMs) return;
      const { pushBackup } = require('./foundry');
      const r = await pushBackup({ label: `auto ${new Date().toISOString()}` });
      if (r.ok) {
        const cur = loadSettings();
        cur.foundryLastBackupAt = new Date().toISOString();
        saveSettings(cur);
        console.log(`[foundry] pushed backup ${r.id ?? ''} (${r.bytes ?? 0} bytes)`);
      } else if (!r.disabled) {
        console.warn('[foundry] auto-push failed:', r.error);
      }
    } catch (e) {
      console.warn('[foundry] auto-push error:', (e as Error).message);
    }
  };
  try {
    maybePushFoundry();
    setInterval(() => { maybePushFoundry(); }, 6 * 60 * 60 * 1000);
  } catch (e) {
    console.warn('[init] foundry setup failed:', (e as Error).message);
  }

  // One-shot migration: move pre-existing connector rows out of the
  // plugin registry and into <dataDir>/connectors/. Idempotent — safe
  // to call on every boot. Must run BEFORE syncRegistry so the
  // migrated rows are visible to the sync layer (which uses
  // installed_version to decide what to refresh).
  try {
    time('migrateConnectorConfigs', () => {
      const { migrateConnectorConfigs, migrateConnectorSettingKeys } = require('./connectors/migration');
      migrateConnectorConfigs();
      // Setting-key renames (e.g. mantis default_project → default_project_name).
      // Run after the file-level migration so we touch the canonical
      // connector-configs.json, not the legacy plugin-configs.json.
      migrateConnectorSettingKeys();
    });
  } catch (err) {
    console.warn('[connectors] migration failed:', err);
  }

  // Sync connectors registry (async, non-blocking). Refresh installed
  // manifests in case the user has older versions on disk. Same cadence
  // as skills.
  try {
    const { syncRegistry } = require('./connectors/sync');
    syncRegistry({ refreshInstalled: true }).catch(() => {});
    setInterval(() => {
      syncRegistry({ refreshInstalled: true }).catch(() => {});
    }, 60 * 60 * 1000);
  } catch {}

  // Reconcile orphaned tasks — any DB row at status='running' or 'queued'
  // at startup is by definition stuck (its parent next-server process is
  // gone; we're booting the new one). Without this, the Activity panel
  // shows zombie tasks indefinitely and dispatch_task can collide with
  // stale project locks. Idempotent — second boot finds zero.
  time('reconcileOrphanedTasks', () => {
    try {
      const { reconcileOrphanedTasks } = require('./task-manager');
      reconcileOrphanedTasks();
    } catch (e) {
      console.warn('[init] reconcileOrphanedTasks failed:', (e as Error).message);
    }
  });

  // Usage scanner — defer to next tick so it doesn't block ensureInitialized().
  // On a host with hundreds of project dirs in ~/.claude/projects/, the
  // synchronous readdirSync + statSync loop can take 5-10s; running it on
  // the critical path of the first API request made startup feel hung.
  // setImmediate keeps it in the same process but yields the event loop first.
  setImmediate(() => {
    time('scanUsage (initial)', () => {
      try { const { scanUsage } = require('./usage-scanner'); scanUsage(); } catch {}
    });
  });
  time('require usage-scanner + hourly', () => {
    try {
      const { scanUsage } = require('./usage-scanner');
      setInterval(() => { try { scanUsage(); } catch {} }, 60 * 60 * 1000);
    } catch {}
  });

  // Task runner is safe in every worker (DB-level coordination)
  time('ensureRunnerStarted', ensureRunnerStarted);

  // Pipeline tmp-dir GC — scan project worktrees/pipeline-<id>/ + delete
  // expired (failed/cancelled past retention). Interval from settings,
  // clamped to >= 1h to avoid runaway IO. Runs in background only.
  try {
    const { gcPipelineTmp, gcClonedProjects } = require('./pipeline-gc');
    const { loadSettings: ls } = require('./settings');
    const hours = Math.max(1, Number(ls().pipelineTmpGcIntervalHours) || 6);
    setInterval(() => {
      try {
        const r = gcPipelineTmp();
        if (r.removed.length) console.log(`[pipeline-gc] swept ${r.removed.length}/${r.scanned} dir(s)`);
      } catch (e) { console.warn('[pipeline-gc] sweep failed:', (e as Error).message); }
      // Also sweep the auto-clone cache (long retention; conservative).
      try {
        const c = gcClonedProjects();
        if (c.removed.length) console.log(`[pipeline-gc] swept ${c.removed.length}/${c.scanned} cloned repo(s)`);
      } catch (e) { console.warn('[pipeline-gc] cloned-projects sweep failed:', (e as Error).message); }
    }, hours * 60 * 60 * 1000);
  } catch (e) { console.warn('[pipeline-gc] setup failed:', (e as Error).message); }

  // Session watcher is safe (file-based, idempotent)
  time('startWatcherLoop', startWatcherLoop);

  // Pipeline scheduler — periodic execution + issue scanning for project-bound workflows
  time('pipeline-scheduler', () => {
    try {
      const { startScheduler } = require('./pipeline-scheduler');
      startScheduler();
    } catch {}
  });

  // Jobs scheduler — DEPRECATED, no longer started. Backend code remains
  // (lib/jobs/, app/api/jobs/) for reversion / data inspection only.
  // Any rows in the `jobs` table will NOT tick. Use Schedules instead.

  // Schedules scheduler — newer pure-pipeline triggers. Runs parallel to Jobs.
  time('schedules-scheduler', () => {
    import('./schedules/scheduler').then(({ startSchedulesScheduler }) => {
      try { startSchedulesScheduler(); }
      catch (e) { console.error('[schedules-scheduler] start failed', e); }
    }).catch((e) => console.error('[schedules-scheduler] import failed', e));
  });

  // If services are managed externally (forge-server), skip
  if (process.env.FORGE_EXTERNAL_SERVICES === '1') {
    // Password display
    const admin = getAdminPassword();
    if (admin) {
      console.log(`[init] Admin password: configured`);
    } else {
      console.log('[init] No admin password set — configure in Settings');
    };
    return;
  }

  // Standalone mode (pnpm dev without forge-server) — start everything here
  const admin2 = getAdminPassword();
  if (admin2) {
    console.log(`[init] Admin password: configured`);
  } else {
    console.log('[init] No admin password set — configure in Settings');
  }

  startTelegramBot(); // registers task event listener only
  startTerminalProcess();
  startTelegramProcess(); // spawns telegram-standalone
  startWorkspaceProcess(); // spawns workspace-standalone
  startBrowserBridgeProcess(); // spawns browser-bridge-standalone
  startChatProcess(); // spawns chat-standalone
  startMemoryProcess(); // spawns memory-standalone

  const settings = loadSettings();
  if (settings.tunnelAutoStart) {
    startTunnel().then(result => {
      if (result.url) console.log(`[init] Tunnel started: ${result.url}`);
      else if (result.error) console.log(`[init] Tunnel failed: ${result.error}`);
    });
  }

  console.log('[init] Background services started');
}

/** Restart Telegram bot (e.g. after settings change) */
export function restartTelegramBot() {
  stopTelegramBot();
  startTelegramBot();
  // Kill existing telegram process and restart if configured
  if (telegramChild) {
    try { ownedKill(telegramChild, 'SIGTERM'); } catch {}
    telegramChild = null;
  }
  startTelegramProcess();
}

let telegramChild: ReturnType<typeof spawn> | null = null;

/** Instance tag every standalone spawn carries — lets /api/monitor filter
 *  by the running web port so dev-test (port 4000) doesn't surface the
 *  user's production Forge (port 8403) processes and vice versa. */
function instanceTag(): string {
  return `--forge-port=${process.env.PORT || 8403}`;
}

function instanceTagRegex(): RegExp {
  return new RegExp(`${instanceTag().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`);
}

function findPortPids(port: number): number[] {
  const pids = new Set<number>();
  const lsof = runAllowFail('lsof', [`-ti:${port}`], { timeout: 3000 });
  for (const line of lsof.stdout.split('\n')) {
    const pid = Number(line.trim());
    if (Number.isInteger(pid) && pid > 0) pids.add(pid);
  }
  if (pids.size) return [...pids];

  const fuser = runAllowFail('fuser', [`${port}/tcp`], { timeout: 3000 });
  for (const token of `${fuser.stdout} ${fuser.stderr}`.split(/\s+/)) {
    const pid = Number(token);
    if (Number.isInteger(pid) && pid > 0) pids.add(pid);
  }
  return [...pids];
}

function isOwnWorkspaceStandalone(pid: number): boolean {
  const cmd = runAllowFail('ps', ['-p', String(pid), '-o', 'command='], { timeout: 2000 }).stdout;
  return /workspace-standalone/.test(cmd) && instanceTagRegex().test(cmd);
}

function reapStaleWorkspacePort(port: number): void {
  for (const pid of findPortPids(port)) {
    const proof = assessProcess(pid, new Set([pid]), isOwnWorkspaceStandalone);
    if (proof.safe) reclaimProcess(proof, 'SIGTERM');
    else console.warn(`[workspace] Not killing pid ${pid} on port ${port}: ${proof.reason}`);
  }
}

function startTelegramProcess() {
  if (telegramChild) return;
  const settings = loadSettings();
  if (!settings.telegramBotToken || !settings.telegramChatId) return;

  const script = join(process.cwd(), 'lib', 'telegram-standalone.ts');
  telegramChild = spawn('npx', ['tsx', script, instanceTag()], {
    stdio: ['ignore', 'inherit', 'inherit'],
    env: { ...process.env, PORT: String(process.env.PORT || 8403) },
    detached: false,
  });
  telegramChild.on('exit', () => { telegramChild = null; });
  console.log('[telegram] Started standalone (pid:', telegramChild.pid, ')');
}

let terminalChild: ReturnType<typeof spawn> | null = null;

function startTerminalProcess() {
  if (terminalChild) return;

  const termPort = Number(process.env.TERMINAL_PORT) || 8404;

  const net = require('node:net');
  const tester = net.createServer();
  tester.once('error', () => {
    console.log(`[terminal] Port ${termPort} already in use, reusing existing`);
  });
  tester.once('listening', () => {
    tester.close();
    const script = join(process.cwd(), 'lib', 'terminal-standalone.ts');
    terminalChild = spawn('npx', ['tsx', script, instanceTag()], {
      stdio: ['ignore', 'inherit', 'inherit'],
      env: { ...Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== 'CLAUDECODE')) } as NodeJS.ProcessEnv,
      detached: false,
    });
    terminalChild.on('exit', () => { terminalChild = null; });
    console.log('[terminal] Started standalone server (pid:', terminalChild.pid, ')');
  });
  tester.listen(termPort);
}

let workspaceChild: ReturnType<typeof spawn> | null = null;

function startWorkspaceProcess() {
  if (workspaceChild) return;

  const wsPort = Number(process.env.WORKSPACE_PORT) || 8405;

  const net = require('node:net');
  const tester = net.createServer();
  tester.once('error', () => {
    // Port in use — only reap a workspace standalone from THIS Forge instance.
    console.log(`[workspace] Port ${wsPort} in use, checking for stale workspace daemon...`);
    reapStaleWorkspacePort(wsPort);
    setTimeout(() => {
      if (!workspaceChild) launchWorkspaceDaemon();
    }, 2000);
  });
  tester.once('listening', () => {
    tester.close();
    launchWorkspaceDaemon();
  });
  tester.listen(wsPort);
}

function launchWorkspaceDaemon() {
  const script = join(process.cwd(), 'lib', 'workspace-standalone.ts');
  workspaceChild = spawn('npx', ['tsx', script, instanceTag()], {
    stdio: ['ignore', 'inherit', 'inherit'],
    env: { ...process.env },
    detached: false,
  });
  workspaceChild.on('exit', () => { workspaceChild = null; });
  console.log('[workspace] Started daemon (pid:', workspaceChild.pid, ')');
}

let chatChild: ReturnType<typeof spawn> | null = null;

function startChatProcess() {
  if (chatChild) return;

  const chatPort = Number(process.env.CHAT_PORT) || 8408;

  const net = require('node:net');
  const tester = net.createServer();
  tester.once('error', () => {
    console.log(`[chat] Port ${chatPort} already in use, reusing existing`);
  });
  tester.once('listening', () => {
    tester.close();
    const script = join(process.cwd(), 'lib', 'chat-standalone.ts');
    chatChild = spawn('npx', ['tsx', script, instanceTag()], {
      stdio: ['ignore', 'inherit', 'inherit'],
      env: { ...process.env },
      detached: false,
    });
    chatChild.on('exit', () => { chatChild = null; });
    console.log('[chat] Started standalone (pid:', chatChild.pid, ')');
  });
  tester.listen(chatPort);
}

let bridgeChild: ReturnType<typeof spawn> | null = null;

function startBrowserBridgeProcess() {
  if (bridgeChild) return;

  const bridgePort = Number(process.env.BRIDGE_PORT) || 8407;

  const net = require('node:net');
  const tester = net.createServer();
  tester.once('error', () => {
    console.log(`[bridge] Port ${bridgePort} already in use, reusing existing`);
  });
  tester.once('listening', () => {
    tester.close();
    const script = join(process.cwd(), 'lib', 'browser-bridge-standalone.ts');
    bridgeChild = spawn('npx', ['tsx', script, instanceTag()], {
      stdio: ['ignore', 'inherit', 'inherit'],
      env: { ...process.env },
      detached: false,
    });
    bridgeChild.on('exit', () => { bridgeChild = null; });
    console.log('[bridge] Started standalone (pid:', bridgeChild.pid, ')');
  });
  tester.listen(bridgePort);
}

let memoryChild: ReturnType<typeof spawn> | null = null;

function startMemoryProcess() {
  if (memoryChild) return;
  // No HTTP port — pure background poller. Just spawn-if-not-running.
  const script = join(process.cwd(), 'lib', 'memory-standalone.ts');
  memoryChild = spawn('npx', ['tsx', script, instanceTag()], {
    stdio: ['ignore', 'inherit', 'inherit'],
    env: { ...process.env },
    detached: false,
  });
  memoryChild.on('exit', () => { memoryChild = null; });
  console.log('[memory] Started standalone (pid:', memoryChild.pid, ')');
}
