import { spawn, type ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { LspClient } from './client.js';
import { getServerConfig } from './config.js';

const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
const SWEEP_INTERVAL_MS = 30_000;
const INIT_TIMEOUT_MS = 10_000;

interface LspInstance {
  client: LspClient;
  proc: ChildProcess;
  lastUsed: number;
  timer: ReturnType<typeof setTimeout>;
}

export class LspServerManager {
  private instances = new Map<string, LspInstance>();
  private sweepTimer: ReturnType<typeof setInterval> | null = null;

  constructor() {
    this.startSweeper();
  }

  private startSweeper(): void {
    this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
    this.sweepTimer.unref?.();
  }

  private findProjectRoot(filePath: string): string | null {
    let dir = filePath;
    const config = getServerConfig(filePath);
    if (!config) return null;
    while (true) {
      for (const pattern of config.rootPatterns) {
        if (existsSync(join(dir, pattern))) return dir;
      }
      const parent = join(dir, '..');
      if (parent === dir) return dir;
      dir = parent;
    }
  }

  async getClient(filePath: string): Promise<LspClient | null> {
    const config = getServerConfig(filePath);
    if (!config) return null;
    const projectRoot = this.findProjectRoot(filePath);
    if (!projectRoot) return null;

    const existing = this.instances.get(projectRoot);
    if (existing) {
      existing.lastUsed = Date.now();
      clearTimeout(existing.timer);
      existing.timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS);
      existing.timer.unref?.();
      return existing.client;
    }

    return this.spawn(projectRoot, config.command, config.args);
  }

  private async spawn(projectRoot: string, command: string, args: string[]): Promise<LspClient | null> {
    const proc = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: projectRoot });
    const client = new LspClient(proc.stdin!, proc.stdout!);

    // A missing binary (e.g. typescript-language-server not on the systemd PATH)
    // emits 'error' on the child and never produces an RPC response, so the
    // initialize request below would hang forever. Race it against a process
    // error and a bounded timeout, and kill the (possibly hung) server on either.
    const spawnFailed = new Promise<never>((_, reject) => {
      proc.on('error', (err) => reject(err));
    });
    let initTimer: ReturnType<typeof setTimeout> | undefined;
    const timeout = new Promise<never>((_, reject) => {
      initTimer = setTimeout(() => reject(new Error('LSP initialize timed out')), INIT_TIMEOUT_MS);
      initTimer.unref?.();
    });

    try {
      await Promise.race([
        client.request('initialize', {
          processId: process.pid,
          rootUri: `file://${projectRoot}`,
          capabilities: {
            textDocument: {
              diagnostic: { dynamicRegistration: false },
              definition: { dynamicRegistration: false },
              references: { dynamicRegistration: false },
            },
          },
        }),
        spawnFailed,
        timeout,
      ]);
    } catch {
      proc.kill('SIGKILL');
      return null;
    } finally {
      clearTimeout(initTimer);
    }
    await client.notify('initialized', {});

    const timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS);
    timer.unref?.();

    this.instances.set(projectRoot, { client, proc, lastUsed: Date.now(), timer });
    proc.on('exit', () => this.instances.delete(projectRoot));

    return client;
  }

  private kill(projectRoot: string): void {
    const inst = this.instances.get(projectRoot);
    if (!inst) return;
    this.instances.delete(projectRoot);
    inst.proc.kill('SIGTERM');
    setTimeout(() => {
      if (inst.proc.exitCode === null) inst.proc.kill('SIGKILL');
    }, 5000);
  }

  private sweep(): void {
    const now = Date.now();
    for (const [root, inst] of this.instances) {
      if (now - inst.lastUsed > IDLE_TIMEOUT_MS) {
        this.kill(root);
      }
    }
  }

  shutdown(): void {
    if (this.sweepTimer) clearInterval(this.sweepTimer);
    for (const root of [...this.instances.keys()]) {
      this.kill(root);
    }
  }

  getActiveCount(): number {
    return this.instances.size;
  }
}

export const lspManager = new LspServerManager();
