import { spawn } from "node:child_process";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { DiscordBot, validateDiscordConfig } from "./discord-bot.js";
import { EventBridge } from "./event-bridge.js";
import type { Logger } from "./logger.js";
import { Orchestrator } from "./orchestrator.js";
import { scanForProjects } from "./project-scanner.js";
import { SessionManager } from "./session-manager.js";
import { syncSwarmRegistryFromProjects } from "./swarm-registry.js";
import type { DaemonConfig, ProjectInfo } from "./types.js";

/** Cadence for the model-catalog + benchmark-coverage maintenance timer.
 * Matches the catalog cache TTL in model-catalog-cache.js (6 hours), so the
 * spawned `sf --maintain` call is a no-op when the cache is still fresh. */
const MAINTENANCE_INTERVAL_MS = 6 * 60 * 60 * 1000;
const DEFAULT_SWARM_REFRESH_MS = 60_000;

function resolveSwarmConfig(
	config: DaemonConfig,
): NonNullable<DaemonConfig["swarms"]> {
	return (
		config.swarms ?? {
			registry_path: resolve(homedir(), ".sf", "swarms.json"),
			auto_sync: true,
			auto_start: false,
			refresh_ms: DEFAULT_SWARM_REFRESH_MS,
		}
	);
}

/**
 * Core daemon class — ties config + logger together with lifecycle management.
 * Registers SIGTERM/SIGINT handlers for clean shutdown.
 */
export class Daemon {
	private shuttingDown = false;
	private keepaliveTimer: ReturnType<typeof setInterval> | undefined;
	private healthTimer: ReturnType<typeof setInterval> | undefined;
	private maintenanceTimer: ReturnType<typeof setInterval> | undefined;
	private swarmSyncTimer: ReturnType<typeof setInterval> | undefined;
	private readonly onSigterm: () => void;
	private readonly onSigint: () => void;
	private sessionManager: SessionManager | undefined;
	private discordBot: DiscordBot | undefined;
	private eventBridge: EventBridge | undefined;
	private orchestrator: Orchestrator | undefined;

	constructor(
		private readonly config: DaemonConfig,
		private readonly logger: Logger,
		private readonly healthIntervalMs: number = 300_000,
		private readonly maintenanceIntervalMs: number = MAINTENANCE_INTERVAL_MS,
	) {
		this.onSigterm = () => void this.shutdown();
		this.onSigint = () => void this.shutdown();
	}

	/** Start the daemon: log startup info, register signal handlers, start keepalive. */
	async start(): Promise<void> {
		this.sessionManager = new SessionManager(this.logger);

		this.logger.info("daemon started", {
			log_level: this.logger.effectiveLevel,
			scan_roots: this.config.projects.scan_roots.length,
			discord_configured: !!this.config.discord,
		});

		process.on("SIGTERM", this.onSigterm);
		process.on("SIGINT", this.onSigint);

		// Keep the event loop alive. The write stream alone doesn't hold a ref
		// when there's no pending I/O, so we need an explicit timer.
		this.keepaliveTimer = setInterval(() => {}, 60_000);

		// Conditionally start Discord bot if config is present and valid
		if (this.config.discord?.token) {
			try {
				validateDiscordConfig(this.config.discord);
				this.discordBot = new DiscordBot({
					config: this.config.discord,
					logger: this.logger,
					sessionManager: this.sessionManager,
					scanProjects: () => this.scanProjects(),
				});
				await this.discordBot.login();

				// Wire up EventBridge after bot is ready
				const channelManager = this.discordBot.getChannelManager();
				const client = this.discordBot.getClient();
				if (channelManager && client) {
					this.eventBridge = new EventBridge({
						sessionManager: this.sessionManager,
						channelManager,
						client,
						config: this.config,
						logger: this.logger,
						ownerId: this.config.discord.owner_id,
					});
					this.discordBot.setEventBridge(this.eventBridge);
					this.eventBridge.start();
					this.logger.info("event bridge wired");

					// Wire up Orchestrator if control_channel_id is configured
					if (this.config.discord.control_channel_id) {
						this.orchestrator = new Orchestrator({
							sessionManager: this.sessionManager,
							channelManager,
							scanProjects: () => this.scanProjects(),
							config: {
								model:
									this.config.discord.orchestrator?.model ??
									"claude-haiku-4-5-20251001",
								max_tokens:
									this.config.discord.orchestrator?.max_tokens ?? 1024,
								control_channel_id: this.config.discord.control_channel_id,
							},
							logger: this.logger,
							ownerId: this.config.discord.owner_id,
						});
						client.on("messageCreate", (message) => {
							void this.orchestrator!.handleMessage(message);
						});
						this.logger.info("orchestrator wired", {
							control_channel_id: this.config.discord.control_channel_id,
						});
					}
				} else {
					this.logger.warn(
						"event bridge skipped — channel manager or client not available",
					);
				}
			} catch (err) {
				// Log error but don't abort daemon startup — bot is optional
				this.logger.error("discord bot login failed", {
					error: err instanceof Error ? err.message : String(err),
				});
				this.discordBot = undefined;
			}
		}

		// Health heartbeat — logs uptime, session count, Discord status, memory
		const startTime = Date.now();
		this.healthTimer = setInterval(() => {
			const sessions = this.sessionManager?.getAllSessions() ?? [];
			const activeSessions = sessions.filter(
				(s) => s.status === "running" || s.status === "blocked",
			).length;
			this.logger.info("health", {
				uptime_s: Math.floor((Date.now() - startTime) / 1000),
				active_sessions: activeSessions,
				discord_connected: !!this.discordBot?.getClient()?.isReady(),
				memory_rss_mb: Math.round(process.memoryUsage().rss / 1024 / 1024),
			});
		}, this.healthIntervalMs);

		// Model-catalog + benchmark coverage maintenance — spawn `sf --maintain`
		// every MAINTENANCE_INTERVAL_MS. Subprocess keeps the daemon decoupled
		// from sf extension internals; the maintenance command is TTL-checked
		// and idempotent, so back-to-back fires are no-ops. The first session
		// launched after the daemon comes up still triggers register-hooks'
		// session_start refresh path, so we don't lose the first 6h window by
		// not firing on startup here.
		this.maintenanceTimer = setInterval(
			() => this.runMaintenance(),
			this.maintenanceIntervalMs,
		);

		const swarmConfig = resolveSwarmConfig(this.config);
		if (swarmConfig.auto_sync) {
			await this.syncSwarms();
			this.swarmSyncTimer = setInterval(
				() => void this.syncSwarms(),
				swarmConfig.refresh_ms,
			);
		}
	}

	/**
	 * Spawn `sf --maintain` and log the outcome. Failures are logged but never
	 * thrown — the daemon's main work must never be blocked by maintenance.
	 */
	private runMaintenance(): void {
		const child = spawn("sf", ["--maintain"], {
			stdio: ["ignore", "pipe", "pipe"],
			detached: false,
		});
		let stdout = "";
		let stderr = "";
		child.stdout?.on("data", (chunk) => {
			stdout += chunk.toString();
		});
		child.stderr?.on("data", (chunk) => {
			stderr += chunk.toString();
		});
		child.on("error", (err) => {
			this.logger.warn("maintenance spawn failed", {
				error: err instanceof Error ? err.message : String(err),
			});
		});
		child.on("exit", (code) => {
			if (code === 0) {
				this.logger.info("maintenance complete", {
					stdout: stdout.trim().slice(0, 500),
				});
			} else {
				this.logger.warn("maintenance exited non-zero", {
					code,
					stderr: stderr.trim().slice(0, 500),
				});
			}
		});
	}

	/** Scan configured project roots for project directories. */
	async scanProjects(): Promise<ProjectInfo[]> {
		return scanForProjects(this.config.projects.scan_roots);
	}

	/**
	 * Refresh the SF server swarm registry from configured project scan roots.
	 *
	 * Purpose: keep the single SF server's swarm read model current without
	 * requiring one-off enrollment or watchdog scripts when repos are added,
	 * removed, or renamed.
	 *
	 * Consumer: Daemon.start() and the periodic swarm sync timer.
	 */
	async syncSwarms(): Promise<void> {
		try {
			const projects = await this.scanProjects();
			const swarmConfig = resolveSwarmConfig(this.config);
			const result = syncSwarmRegistryFromProjects(
				projects,
				swarmConfig.registry_path,
			);
			if (swarmConfig.auto_start) {
				await this.ensureSwarmClients(projects);
			}
			this.logger.debug("swarm registry synced", {
				registry_path: result.registryPath,
				discovered: result.discovered,
				written: result.written,
				auto_start: swarmConfig.auto_start,
			});
		} catch (error) {
			this.logger.warn("swarm registry sync failed", {
				error: error instanceof Error ? error.message : String(error),
			});
		}
	}

	/**
	 * Start missing daemon-managed autonomous clients for discovered repos.
	 *
	 * Purpose: let sf-server reconcile both the SF server swarm registry and the
	 * corresponding per-repo autonomous clients from one configured scan root.
	 *
	 * Consumer: syncSwarms() when `swarms.auto_start` is enabled.
	 */
	private async ensureSwarmClients(projects: ProjectInfo[]): Promise<void> {
		const manager = this.getSessionManager();
		for (const project of projects) {
			const existing = manager.getSessionByDir(project.path);
			if (
				existing &&
				["starting", "running", "blocked"].includes(existing.status)
			) {
				continue;
			}
			if (existing) {
				this.logger.warn("swarm client not restarted from terminal state", {
					projectDir: project.path,
					status: existing.status,
				});
				continue;
			}
			try {
				const sessionId = await manager.startSession({
					projectDir: project.path,
				});
				this.logger.info("swarm client started", {
					projectDir: project.path,
					sessionId,
				});
			} catch (error) {
				this.logger.warn("swarm client start failed", {
					projectDir: project.path,
					error: error instanceof Error ? error.message : String(error),
				});
			}
		}
	}

	/** Accessor for the session manager (available after start()). */
	getSessionManager(): SessionManager {
		if (!this.sessionManager) {
			throw new Error(
				"Daemon not started — call start() before accessing the session manager",
			);
		}
		return this.sessionManager;
	}

	/** Accessor for the event bridge (available after start() with Discord configured). */
	getEventBridge(): EventBridge | undefined {
		return this.eventBridge;
	}

	/** Accessor for the orchestrator (available after start() with control_channel_id configured). */
	getOrchestrator(): Orchestrator | undefined {
		return this.orchestrator;
	}

	/** Idempotent shutdown: log, cleanup sessions, close logger, exit. */
	async shutdown(): Promise<void> {
		if (this.shuttingDown) return;
		this.shuttingDown = true;

		this.logger.info("daemon shutting down");

		// Remove signal handlers to avoid double-fire
		process.removeListener("SIGTERM", this.onSigterm);
		process.removeListener("SIGINT", this.onSigint);

		// Clear health heartbeat timer
		if (this.healthTimer) {
			clearInterval(this.healthTimer);
			this.healthTimer = undefined;
		}

		// Clear maintenance timer
		if (this.maintenanceTimer) {
			clearInterval(this.maintenanceTimer);
			this.maintenanceTimer = undefined;
		}

		// Clear swarm registry sync timer
		if (this.swarmSyncTimer) {
			clearInterval(this.swarmSyncTimer);
			this.swarmSyncTimer = undefined;
		}

		// Clear keepalive so the event loop can drain
		if (this.keepaliveTimer) {
			clearInterval(this.keepaliveTimer);
			this.keepaliveTimer = undefined;
		}

		// Stop Orchestrator first
		if (this.orchestrator) {
			this.orchestrator.stop();
			this.orchestrator = undefined;
		}

		// Stop EventBridge before Discord bot destroy
		if (this.eventBridge) {
			await this.eventBridge.stop();
			this.eventBridge = undefined;
		}

		// Destroy Discord bot before session cleanup
		if (this.discordBot) {
			await this.discordBot.destroy();
			this.discordBot = undefined;
		}

		// Clean up active sessions before closing logger
		if (this.sessionManager) {
			await this.sessionManager.cleanup();
		}

		await this.logger.close();
		process.exit(0);
	}
}
