import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
import { closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { get as httpsGet } from 'node:https';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { createConnection } from 'node:net';

export interface IBrokerSetupOptions {
  readonly toolsDir?: string;
  readonly json?: boolean;
}

export interface IBrokerStartOptions {
  readonly toolsDir?: string;
  readonly port?: string;
  readonly host?: string;
  readonly pidFile?: string;
  readonly logFile?: string;
  readonly json?: boolean;
}

export interface IBrokerStopOptions {
  readonly pidFile?: string;
  readonly json?: boolean;
}

const NATS_VERSION = '2.14.1';

interface INatsPlatform {
  readonly key: string;
  readonly archiveName: string;
  readonly binaryRelativePath: string;
}

interface IBrokerSetupResult {
  readonly ok: true;
  readonly tool: 'nats-server';
  readonly version: string;
  readonly source: 'matrix-cli-managed';
  readonly path: string;
  readonly archiveUrl: string;
}

interface IBrokerStartResult {
  readonly ok: true;
  readonly tool: 'nats-server';
  readonly pid: number;
  readonly url: string;
  readonly pidFile: string;
  readonly logFile: string;
  readonly path: string;
}

interface IBrokerStopResult {
  readonly ok: true;
  readonly pid: number;
  readonly pidFile: string;
}

export async function brokerSetupCommand(options: IBrokerSetupOptions): Promise<void> {
  const result = await ensureNatsServer(options.toolsDir);
  printResult(result, options.json);
}

export async function brokerStartCommand(options: IBrokerStartOptions): Promise<void> {
  const setup = await ensureNatsServer(options.toolsDir);
  const port = parsePort(options.port);
  const host = options.host?.trim() || '127.0.0.1';
  const pidFile = resolve(options.pidFile || join('.matrix', 'broker', 'nats-server.pid'));
  const logFile = resolve(options.logFile || join('.matrix', 'broker', 'nats-server.log'));
  mkdirSync(dirname(pidFile), { recursive: true });
  mkdirSync(dirname(logFile), { recursive: true });

  if (existsSync(pidFile)) {
    const pid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
    if (Number.isFinite(pid) && isProcessAlive(pid)) {
      throw new Error(`MATRIX_BROKER_ALREADY_RUNNING: pid ${pid} from ${pidFile} is still alive`);
    }
    rmSync(pidFile, { force: true });
  }

  const logFd = openSync(logFile, 'a');
  const child = spawn(setup.path, ['-p', String(port), '-a', host], {
    detached: true,
    stdio: ['ignore', logFd, logFd],
  });
  closeSync(logFd);
  child.unref();
  writeFileSync(pidFile, `${child.pid}\n`);
  await waitForTcp(host, port, child);

  const result: IBrokerStartResult = {
    ok: true,
    tool: 'nats-server',
    pid: child.pid ?? 0,
    url: `nats://${host}:${port}`,
    pidFile,
    logFile,
    path: setup.path,
  };
  printResult(result, options.json);
}

export async function brokerStopCommand(options: IBrokerStopOptions): Promise<void> {
  const pidFile = resolve(options.pidFile || join('.matrix', 'broker', 'nats-server.pid'));
  if (!existsSync(pidFile)) {
    throw new Error(`MATRIX_BROKER_PID_FILE_NOT_FOUND: ${pidFile}`);
  }
  const pid = Number.parseInt(readFileSync(pidFile, 'utf8').trim(), 10);
  if (!Number.isFinite(pid)) {
    throw new Error(`MATRIX_BROKER_PID_FILE_INVALID: ${pidFile}`);
  }
  if (isProcessAlive(pid)) {
    process.kill(pid, 'SIGTERM');
    await waitForExit(pid);
  }
  rmSync(pidFile, { force: true });
  const result: IBrokerStopResult = {
    ok: true,
    pid,
    pidFile,
  };
  printResult(result, options.json);
}

async function ensureNatsServer(toolsDirOption?: string): Promise<IBrokerSetupResult> {
  const platform = resolveNatsPlatform();
  const toolsDir = resolve(toolsDirOption || join('.matrix', 'tools'));
  const installRoot = join(toolsDir, 'nats-server', `v${NATS_VERSION}`, platform.key);
  const binaryPath = join(installRoot, platform.binaryRelativePath);
  const archiveUrl = `https://github.com/nats-io/nats-server/releases/download/v${NATS_VERSION}/${platform.archiveName}`;
  if (!existsSync(binaryPath)) {
    mkdirSync(installRoot, { recursive: true });
    const archivePath = join(tmpdir(), `${platform.archiveName}.${process.pid}`);
    await downloadFile(archiveUrl, archivePath);
    extractArchive(archivePath, installRoot, platform.archiveName);
    rmSync(archivePath, { force: true });
  }
  return {
    ok: true,
    tool: 'nats-server',
    version: NATS_VERSION,
    source: 'matrix-cli-managed',
    path: binaryPath,
    archiveUrl,
  };
}

function resolveNatsPlatform(): INatsPlatform {
  const platform = process.platform;
  const arch = process.arch;
  if (platform === 'linux' && arch === 'x64') {
    return {
      key: 'linux-amd64',
      archiveName: `nats-server-v${NATS_VERSION}-linux-amd64.tar.gz`,
      binaryRelativePath: `nats-server-v${NATS_VERSION}-linux-amd64/nats-server`,
    };
  }
  if (platform === 'linux' && arch === 'arm64') {
    return {
      key: 'linux-arm64',
      archiveName: `nats-server-v${NATS_VERSION}-linux-arm64.tar.gz`,
      binaryRelativePath: `nats-server-v${NATS_VERSION}-linux-arm64/nats-server`,
    };
  }
  if (platform === 'darwin' && arch === 'x64') {
    return {
      key: 'darwin-amd64',
      archiveName: `nats-server-v${NATS_VERSION}-darwin-amd64.tar.gz`,
      binaryRelativePath: `nats-server-v${NATS_VERSION}-darwin-amd64/nats-server`,
    };
  }
  if (platform === 'darwin' && arch === 'arm64') {
    return {
      key: 'darwin-arm64',
      archiveName: `nats-server-v${NATS_VERSION}-darwin-arm64.tar.gz`,
      binaryRelativePath: `nats-server-v${NATS_VERSION}-darwin-arm64/nats-server`,
    };
  }
  throw new Error(`MATRIX_BROKER_PLATFORM_UNSUPPORTED: ${platform}/${arch}`);
}

async function downloadFile(url: string, destination: string, redirectCount = 0): Promise<void> {
  await new Promise<void>((resolveDownload, rejectDownload) => {
    const request = httpsGet(url, (response) => {
      if (
        response.statusCode
        && response.statusCode >= 300
        && response.statusCode < 400
        && response.headers.location
      ) {
        response.resume();
        if (redirectCount >= 5) {
          rejectDownload(new Error(`MATRIX_BROKER_DOWNLOAD_REDIRECT_LIMIT: ${url}`));
          return;
        }
        const redirected = new URL(response.headers.location, url).toString();
        downloadFile(redirected, destination, redirectCount + 1).then(resolveDownload, rejectDownload);
        return;
      }
      if (response.statusCode !== 200) {
        rejectDownload(new Error(`MATRIX_BROKER_DOWNLOAD_FAILED: ${url} returned ${response.statusCode}`));
        response.resume();
        return;
      }
      const output = createWriteStream(destination, { mode: 0o600 });
      response.pipe(output);
      output.once('finish', () => output.close(() => resolveDownload()));
      output.once('error', rejectDownload);
    });
    request.once('error', rejectDownload);
  });
}

function extractArchive(archivePath: string, installRoot: string, archiveName: string): void {
  if (!archiveName.endsWith('.tar.gz')) {
    throw new Error(`MATRIX_BROKER_ARCHIVE_UNSUPPORTED: ${archiveName}`);
  }
  const result = spawnSyncChecked('tar', ['-xzf', archivePath, '-C', installRoot]);
  if (result !== 0) {
    throw new Error(`MATRIX_BROKER_EXTRACT_FAILED: tar exited ${result}`);
  }
}

function spawnSyncChecked(command: string, args: readonly string[]): number {
  const result = spawnSync(command, [...args], { stdio: 'inherit' });
  if (result.error) throw result.error;
  return result.status ?? 1;
}

async function waitForTcp(host: string, port: number, child: ChildProcess): Promise<void> {
  const deadline = Date.now() + 10_000;
  let lastError: unknown;
  while (Date.now() < deadline) {
    if (child.exitCode !== null) {
      throw new Error(`MATRIX_BROKER_START_FAILED: nats-server exited ${child.exitCode}`);
    }
    try {
      await new Promise<void>((resolveConnect, rejectConnect) => {
        const socket = createConnection({ host, port });
        socket.once('connect', () => {
          socket.end();
          resolveConnect();
        });
        socket.once('error', rejectConnect);
      });
      return;
    } catch (error) {
      lastError = error;
    }
    await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
  }
  throw lastError instanceof Error ? lastError : new Error('MATRIX_BROKER_START_TIMEOUT');
}

async function waitForExit(pid: number): Promise<void> {
  const deadline = Date.now() + 5_000;
  while (Date.now() < deadline) {
    if (!isProcessAlive(pid)) return;
    await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
  }
  if (isProcessAlive(pid)) {
    process.kill(pid, 'SIGKILL');
  }
}

function isProcessAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch {
    return false;
  }
}

function parsePort(value: string | undefined): number {
  const port = Number.parseInt(value || '4222', 10);
  if (!Number.isInteger(port) || port <= 0 || port > 65535) {
    throw new Error(`MATRIX_BROKER_PORT_INVALID: ${value}`);
  }
  return port;
}

function printResult(value: unknown, json: boolean | undefined): void {
  if (json) {
    console.log(JSON.stringify(value, null, 2));
    return;
  }
  console.log(JSON.stringify(value));
}
