import { spawn, execSync, type ChildProcess } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { paths } from '../shared/paths.js';
import { log } from '../shared/logger.js';

let proc: ChildProcess | null = null;

const MIN_CF_SIZE = 10 * 1024 * 1024; // 10 MB — valid cloudflared is ~30-50 MB

function findBinary(): string | null {
  // Check system-wide install
  const which = process.platform === 'win32' ? 'where cloudflared' : 'which cloudflared';
  try { execSync(which, { stdio: 'ignore' }); return 'cloudflared'; } catch {}

  // Check local install (validate by file size, never execute — avoids Windows popup)
  if (!fs.existsSync(paths.cloudflared)) return null;
  const size = fs.statSync(paths.cloudflared).size;
  if (size < MIN_CF_SIZE) {
    fs.unlinkSync(paths.cloudflared);
    return null;
  }
  return paths.cloudflared;
}

export async function installCloudflared(): Promise<string> {
  const existing = findBinary();
  if (existing) return existing;

  const dir = path.dirname(paths.cloudflared);
  fs.mkdirSync(dir, { recursive: true });

  const p = os.platform();
  const a = p === 'win32'
    ? (process.env.PROCESSOR_ARCHITECTURE || os.arch()).toLowerCase()
    : os.arch();
  const base = 'https://github.com/cloudflare/cloudflared/releases/latest/download';

  let url: string;
  if (p === 'win32') url = `${base}/cloudflared-windows-${a.includes('arm') ? 'arm64' : 'amd64'}.exe`;
  else if (p === 'darwin') url = `${base}/cloudflared-darwin-${a === 'arm64' ? 'arm64' : 'amd64'}.tgz`;
  else if (a === 'arm64' || a === 'aarch64') url = `${base}/cloudflared-linux-arm64`;
  else if (a.startsWith('arm')) url = `${base}/cloudflared-linux-arm`;
  else url = `${base}/cloudflared-linux-amd64`;

  log.info('Installing cloudflared...');
  if (p === 'win32') {
    execSync(`curl.exe -fsSL -o "${paths.cloudflared}" "${url}"`, { stdio: 'ignore' });
  } else if (url.endsWith('.tgz')) {
    execSync(`curl -fsSL "${url}" | tar xz -C "${dir}"`, { stdio: 'ignore' });
  } else {
    execSync(`curl -fsSL -o "${paths.cloudflared}" "${url}"`, { stdio: 'ignore' });
  }
  if (p !== 'win32') fs.chmodSync(paths.cloudflared, 0o755);
  log.ok('cloudflared installed');
  return paths.cloudflared;
}

export function startTunnel(port: number): Promise<string> {
  return new Promise(async (resolve, reject) => {
    const bin = await installCloudflared();
    const timeout = setTimeout(() => reject(new Error('Tunnel timeout')), 30_000);

    proc = spawn(bin, ['tunnel', '--url', `http://localhost:${port}`, '--no-autoupdate'], {
      stdio: ['ignore', 'pipe', 'pipe'],
      windowsHide: true,
    });

    let buf = '';
    const onData = (d: Buffer) => {
      buf += d.toString();
      const m = buf.match(/https:\/\/[^\s]+\.trycloudflare\.com/);
      if (m) { clearTimeout(timeout); resolve(m[0]); }
    };

    proc.stdout?.on('data', onData);
    proc.stderr?.on('data', onData);
    proc.on('error', (e) => { clearTimeout(timeout); reject(e); });
    proc.on('exit', (code) => { if (code !== 0) { clearTimeout(timeout); reject(new Error(`cloudflared exited with code ${code}`)); } });
  });
}

export async function startNamedTunnel(configPath: string, name: string): Promise<void> {
  const bin = await installCloudflared();

  proc = spawn(bin, ['tunnel', '--config', configPath, 'run', name], {
    stdio: ['ignore', 'pipe', 'pipe'],
    windowsHide: true,
  });

  proc.stdout?.on('data', (d: Buffer) => log.info(`[named-tunnel] ${d.toString().trim()}`));
  proc.stderr?.on('data', (d: Buffer) => log.info(`[named-tunnel] ${d.toString().trim()}`));
  proc.on('error', (e) => log.error(`Named tunnel error: ${e.message}`));
  proc.on('exit', (code) => { if (code !== 0) log.warn(`Named tunnel exited with code ${code}`); });
}

export async function startTunnelForMode(
  mode: 'off' | 'quick' | 'named',
  port: number,
  configPath?: string,
  name?: string,
): Promise<string | null> {
  if (mode === 'off') return null;
  if (mode === 'named') {
    if (!configPath || !name) throw new Error('Named tunnel requires configPath and name');
    await startNamedTunnel(configPath, name);
    return null; // URL is known from config.tunnel.domain
  }
  // mode === 'quick'
  return startTunnel(port);
}

export function stopTunnel(): void {
  proc?.kill();
  proc = null;
}

/** Check if the cloudflared child process is still running */
export function isTunnelProcessAlive(): boolean {
  return proc !== null && proc.exitCode === null && proc.signalCode === null;
}

export async function isTunnelAlive(url: string, localPort?: number): Promise<boolean> {
  // 1. Process check — if cloudflared exited, tunnel is definitely dead
  if (!isTunnelProcessAlive()) return false;

  // 2. Local reachability check — confirm the local server is still responding.
  //    We probe localhost directly instead of the tunnel URL because on macOS
  //    the server can't reach itself through the Cloudflare tunnel (DNS/firewall).
  //    If localhost responds and cloudflared is running, the tunnel is alive.
  if (localPort) {
    try {
      const res = await fetch(`http://127.0.0.1:${localPort}/api/health?_cb=${Date.now()}`, {
        signal: AbortSignal.timeout(3000),
      });
      if (res.ok) return true;
    } catch {}
    // Local server not responding — but cloudflared is alive, so tunnel is still up.
    // The worker may be restarting. Don't kill the tunnel for a local issue.
    return true;
  }

  // No local port provided — process alive is enough
  return true;
}

export async function restartTunnel(port: number): Promise<string> {
  stopTunnel();
  return startTunnel(port);
}

export async function restartNamedTunnel(configPath: string, name: string): Promise<void> {
  stopTunnel();
  await startNamedTunnel(configPath, name);
}
