import { spawnSync } from 'node:child_process';
import { createWriteStream, existsSync, mkdirSync, readdirSync, rmSync, statSync } from 'node:fs';
import { chmod, copyFile } from 'node:fs/promises';
import { get } from 'node:https';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

export const NATS_SERVER_VERSION = '2.14.1';

const thisFile = fileURLToPath(import.meta.url);
export const repoRoot = resolve(dirname(thisFile), '..', '..');

function platformName() {
  if (process.platform === 'linux') return 'linux';
  if (process.platform === 'darwin') return 'darwin';
  if (process.platform === 'win32') return 'windows';
  throw new Error(`Unsupported platform for managed nats-server install: ${process.platform}`);
}

function archName() {
  if (process.arch === 'x64') return 'amd64';
  if (process.arch === 'arm64') return 'arm64';
  throw new Error(`Unsupported architecture for managed nats-server install: ${process.arch}`);
}

function executableName() {
  return process.platform === 'win32' ? 'nats-server.exe' : 'nats-server';
}

export function managedNatsServerPath(root = repoRoot) {
  return join(root, '.tools', 'nats-server', `v${NATS_SERVER_VERSION}`, `${platformName()}-${archName()}`, 'bin', executableName());
}

function pathEntries() {
  return (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
}

function executableExists(path) {
  try {
    return existsSync(path) && statSync(path).isFile();
  } catch {
    return false;
  }
}

function findOnPath(name) {
  for (const entry of pathEntries()) {
    const candidate = join(entry, name);
    if (executableExists(candidate)) return candidate;
  }
  return null;
}

export function resolveNatsServer(root = repoRoot) {
  const explicit = process.env.MATRIX_NATS_SERVER;
  if (explicit) {
    const resolved = resolve(explicit);
    if (!executableExists(resolved)) {
      throw new Error(`MATRIX_NATS_SERVER points to a missing file: ${resolved}`);
    }
    return { path: resolved, source: 'MATRIX_NATS_SERVER' };
  }

  const managed = managedNatsServerPath(root);
  if (executableExists(managed)) return { path: managed, source: 'repo-managed' };

  const onPath = findOnPath(executableName()) ?? findOnPath('nats-server');
  if (onPath) return { path: onPath, source: 'PATH' };

  return null;
}

export function natsServerInstallUrl(version = NATS_SERVER_VERSION) {
  const format = process.platform === 'win32' ? 'zip' : 'tar.gz';
  const asset = `nats-server-v${version}-${platformName()}-${archName()}.${format}`;
  return {
    asset,
    url: `https://github.com/nats-io/nats-server/releases/download/v${version}/${asset}`,
    format,
  };
}

function download(url, destination) {
  return new Promise((resolveDownload, rejectDownload) => {
    const request = get(url, (response) => {
      if ([301, 302, 303, 307, 308].includes(response.statusCode ?? 0) && response.headers.location) {
        response.resume();
        download(response.headers.location, destination).then(resolveDownload, rejectDownload);
        return;
      }

      if (response.statusCode !== 200) {
        response.resume();
        rejectDownload(new Error(`Download failed with HTTP ${response.statusCode}: ${url}`));
        return;
      }

      const file = createWriteStream(destination);
      response.pipe(file);
      file.on('finish', () => {
        file.close(resolveDownload);
      });
      file.on('error', rejectDownload);
    });
    request.on('error', rejectDownload);
  });
}

function walkFiles(dir) {
  const out = [];
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const path = join(dir, entry.name);
    if (entry.isDirectory()) out.push(...walkFiles(path));
    else out.push(path);
  }
  return out;
}

function extractArchive(archive, destination, format) {
  mkdirSync(destination, { recursive: true });
  if (format === 'tar.gz') {
    const result = spawnSync('tar', ['-xzf', archive, '-C', destination], { stdio: 'inherit' });
    if (result.error) throw result.error;
    if (result.status !== 0) throw new Error(`tar extraction failed with exit ${result.status}`);
    return;
  }

  const script = `Expand-Archive -LiteralPath ${JSON.stringify(archive)} -DestinationPath ${JSON.stringify(destination)} -Force`;
  const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', script], { stdio: 'inherit' });
  if (result.error) throw result.error;
  if (result.status !== 0) throw new Error(`zip extraction failed with exit ${result.status}`);
}

export async function installManagedNatsServer(root = repoRoot) {
  const existing = resolveNatsServer(root);
  if (existing?.source === 'repo-managed') {
    return { ...existing, installed: false };
  }

  const target = managedNatsServerPath(root);
  const installRoot = dirname(dirname(target));
  const tmpRoot = join(root, '.tools', 'tmp', `nats-server-v${NATS_SERVER_VERSION}-${Date.now()}`);
  const { asset, url, format } = natsServerInstallUrl();
  const archive = join(tmpRoot, asset);
  const extractDir = join(tmpRoot, 'extract');

  rmSync(installRoot, { recursive: true, force: true });
  mkdirSync(tmpRoot, { recursive: true });

  try {
    await download(url, archive);
    extractArchive(archive, extractDir, format);
    const binary = walkFiles(extractDir).find((path) => path.endsWith(executableName()) || path.endsWith('/nats-server'));
    if (!binary) throw new Error(`Downloaded ${asset} did not contain ${executableName()}`);

    mkdirSync(dirname(target), { recursive: true });
    await copyFile(binary, target);
    if (process.platform !== 'win32') await chmod(target, 0o755);
    return { path: target, source: 'repo-managed', installed: true, url };
  } finally {
    rmSync(tmpRoot, { recursive: true, force: true });
  }
}

export function readNatsServerVersion(binaryPath) {
  const result = spawnSync(binaryPath, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
  if (result.error) throw result.error;
  if (result.status !== 0) throw new Error(`${binaryPath} --version failed with exit ${result.status}`);
  return `${result.stdout}${result.stderr}`.trim();
}
