/**
 * Server lifecycle management for emdash-core.
 *
 * The ServerManager handles:
 * - Per-repo server instances (each repo gets its own server)
 * - Discovering running servers via port file
 * - Starting new servers when needed
 * - Health checking servers
 * - Graceful shutdown on CLI exit
 * - Cleanup of stale servers
 */

import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, readdirSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, resolve } from 'node:path';
import { execSync, spawn, type ChildProcess } from 'node:child_process';
import { createServer } from 'node:net';

const SERVERS_DIR = join(homedir(), '.emdash', 'servers');
const STARTUP_TIMEOUT = 30000; // 30 seconds
const HEALTH_TIMEOUT = 2000; // 2 seconds

export class ServerManager {
  private repoRoot: string;
  private process: ChildProcess | null = null;
  private port: number | null = null;
  private startedByUs = false;

  constructor(repoRoot?: string) {
    this.repoRoot = repoRoot ?? this.detectRepoRoot();

    // Create servers directory
    mkdirSync(SERVERS_DIR, { recursive: true });

    // Cleanup stale servers on init
    this.cleanupStaleServers();
  }

  private get repoHash(): string {
    const pathStr = resolve(this.repoRoot);
    return createHash('sha256').update(pathStr).digest('hex').slice(0, 12);
  }

  private get portFile(): string {
    return join(SERVERS_DIR, `${this.repoHash}.port`);
  }

  private get pidFile(): string {
    return join(SERVERS_DIR, `${this.repoHash}.pid`);
  }

  private get repoFile(): string {
    return join(SERVERS_DIR, `${this.repoHash}.repo`);
  }

  /**
   * Get URL of running server for this repo, starting one if needed.
   */
  async getServerUrl(): Promise<string> {
    // Check if server already running for THIS repo
    if (existsSync(this.portFile)) {
      try {
        const port = parseInt(readFileSync(this.portFile, 'utf-8').trim(), 10);
        if (await this.checkHealth(port)) {
          this.port = port;
          return `http://localhost:${port}`;
        }
      } catch {
        // Server not healthy, clean up stale files
      }
      this.cleanupFiles();
    }

    // Start new server for this repo
    this.port = await this.findFreePort();
    await this.spawnServer();
    return `http://localhost:${this.port}`;
  }

  /**
   * Ensure server is running and return URL.
   * Alias for getServerUrl() for clearer intent.
   */
  async ensureServer(): Promise<string> {
    return this.getServerUrl();
  }

  /**
   * Shutdown the server if we started it.
   */
  shutdown(): void {
    if (this.startedByUs && this.process) {
      try {
        this.process.kill('SIGTERM');
      } catch {
        try {
          this.process.kill('SIGKILL');
        } catch {
          // Process already dead
        }
      }
      this.cleanupFiles();
      this.process = null;
    }
  }

  private detectRepoRoot(): string {
    try {
      const result = execSync('git rev-parse --show-toplevel', {
        encoding: 'utf-8',
        stdio: ['pipe', 'pipe', 'pipe'],
      });
      return result.trim();
    } catch {
      return process.cwd();
    }
  }

  private async findFreePort(): Promise<number> {
    return new Promise((resolve, reject) => {
      const server = createServer();
      server.listen(0, '127.0.0.1', () => {
        const address = server.address();
        if (address && typeof address === 'object') {
          const port = address.port;
          server.close(() => resolve(port));
        } else {
          server.close(() => reject(new Error('Failed to get port')));
        }
      });
      server.on('error', reject);
    });
  }

  private async spawnServer(): Promise<void> {
    if (this.port === null) {
      throw new Error('Port must be set before spawning server');
    }

    // Find emdash-core module
    const coreModule = this.findCoreModule();

    // For Python server
    const pythonCmd = 'python';
    const pythonArgs = [
      '-m',
      'emdash_core.server',
      '--port',
      String(this.port),
      '--host',
      '127.0.0.1',
      '--repo-root',
      this.repoRoot,
    ];

    // Set environment
    const env = { ...process.env };
    if (coreModule) {
      env['PYTHONPATH'] = coreModule + (env['PYTHONPATH'] ? `:${env['PYTHONPATH']}` : '');
    }

    this.process = spawn(pythonCmd, pythonArgs, {
      env,
      stdio: ['ignore', 'pipe', 'pipe'],
      detached: false,
    });

    this.startedByUs = true;

    // Write port, PID, and repo files
    writeFileSync(this.portFile, String(this.port));
    writeFileSync(this.pidFile, String(this.process.pid));
    writeFileSync(this.repoFile, this.repoRoot);

    // Register cleanup for normal exit
    process.on('exit', () => this.shutdown());
    process.on('SIGINT', () => {
      this.shutdown();
      process.exit(0);
    });
    process.on('SIGTERM', () => {
      this.shutdown();
      process.exit(0);
    });

    // Wait for server ready
    await this.waitForReady();
  }

  private findCoreModule(): string | null {
    // Check relative to this file (for development)
    const cliDir = join(__dirname, '..');
    const coreDir = join(cliDir, '..', 'core');
    if (existsSync(join(coreDir, 'emdash_core'))) {
      return coreDir;
    }
    return null;
  }

  private async checkHealth(port: number): Promise<boolean> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), HEALTH_TIMEOUT);

      const response = await fetch(`http://localhost:${port}/api/health`, {
        signal: controller.signal,
      });

      clearTimeout(timeoutId);
      return response.status === 200;
    } catch {
      return false;
    }
  }

  private async waitForReady(): Promise<void> {
    if (this.port === null) {
      throw new Error('Port must be set before waiting for ready');
    }

    const start = Date.now();
    while (Date.now() - start < STARTUP_TIMEOUT) {
      if (await this.checkHealth(this.port)) {
        return;
      }

      // Check if process died
      if (this.process && this.process.exitCode !== null) {
        throw new Error('Server process died');
      }

      await new Promise((resolve) => setTimeout(resolve, 100));
    }

    throw new Error(`Server failed to start within ${STARTUP_TIMEOUT / 1000}s`);
  }

  private cleanupFiles(): void {
    for (const file of [this.portFile, this.pidFile, this.repoFile]) {
      try {
        if (existsSync(file)) {
          unlinkSync(file);
        }
      } catch {
        // Ignore errors
      }
    }
  }

  private cleanupStaleServers(): void {
    if (!existsSync(SERVERS_DIR)) {
      return;
    }

    const files = readdirSync(SERVERS_DIR);
    for (const file of files) {
      if (!file.endsWith('.pid')) continue;

      const pidPath = join(SERVERS_DIR, file);
      try {
        const pid = parseInt(readFileSync(pidPath, 'utf-8').trim(), 10);

        // Check if process exists
        try {
          process.kill(pid, 0);
        } catch {
          // Process doesn't exist, clean up files
          const hashPrefix = file.replace('.pid', '');
          for (const ext of ['.port', '.pid', '.repo']) {
            const staleFile = join(SERVERS_DIR, `${hashPrefix}${ext}`);
            if (existsSync(staleFile)) {
              try {
                unlinkSync(staleFile);
              } catch {
                // Ignore
              }
            }
          }
        }
      } catch {
        // Invalid PID file, remove it
        try {
          unlinkSync(pidPath);
        } catch {
          // Ignore
        }
      }
    }
  }
}

// Global singleton for CLI commands
let serverManager: ServerManager | null = null;

/**
 * Get or create the global server manager.
 */
export function getServerManager(repoRoot?: string): ServerManager {
  if (!serverManager) {
    serverManager = new ServerManager(repoRoot);
  }
  return serverManager;
}
