import * as net from 'node:net';
import { SocksClient } from 'socks';

/**
 * 本地 HTTP→SOCKS5 桥：在 127.0.0.1:<port> 起一个 HTTP CONNECT 代理服务，
 * 把 chromium 的 HTTPS 请求通过 CONNECT 隧道转发到带认证的 SOCKS5 上游。
 *
 * 解决 chromium 不支持 SOCKS5 用户名/密码认证的问题
 * （Browser does not support socks5 proxy authentication）。
 *
 * 使用方法：
 *   const bridge = await createSocks5HttpBridge({ host, port, username, password });
 *   // 启动 chromium 时用 proxy: { server: bridge.localUrl }
 *   ...
 *   bridge.close();
 */
export interface Socks5UpstreamOpts {
  host: string;
  port: number;
  username?: string;
  password?: string;
}

export interface Socks5Bridge {
  /** 本地 HTTP 代理 URL，传给 chromium proxy.server 用 */
  localUrl: string;
  localPort: number;
  close(): void;
}

export async function createSocks5HttpBridge(upstream: Socks5UpstreamOpts): Promise<Socks5Bridge> {
  const server = net.createServer();
  // 失败统计（按目标域名聚合，便于诊断代理白名单问题）
  const failStats = new Map<string, number>();

  server.on('connection', (clientSocket) => {
    let buf = Buffer.alloc(0);
    const onFirstData = (chunk: Buffer) => {
      buf = Buffer.concat([buf, chunk]);
      const headerEnd = buf.indexOf('\r\n\r\n');
      if (headerEnd < 0) return; // 等更多数据
      clientSocket.removeListener('data', onFirstData);

      const head = buf.subarray(0, headerEnd).toString('utf8');
      const firstLine = head.split('\r\n')[0];
      const m = firstLine.match(/^CONNECT\s+([^:\s]+):(\d+)\s+HTTP\/[\d.]+/i);
      if (!m) {
        clientSocket.end('HTTP/1.1 405 Method Not Allowed\r\n\r\n');
        return;
      }
      const [, targetHost, targetPortStr] = m;
      const targetPort = parseInt(targetPortStr, 10);

      // 通过上游 SOCKS5 拨号到目标，失败后重试 1 次（上游偶尔抖动）
      const dial = (attempt: number): Promise<any> => SocksClient.createConnection({
        proxy: {
          host: upstream.host,
          port: upstream.port,
          type: 5,
          userId: upstream.username,
          password: upstream.password,
        },
        command: 'connect',
        destination: { host: targetHost, port: targetPort },
        timeout: 15000,
      }).catch((err) => {
        if (attempt === 0) {
          // 第一次失败，等 200ms 重试
          return new Promise<any>(r => setTimeout(r, 200)).then(() => dial(1));
        }
        throw err;
      });

      dial(0).then(({ socket: upstreamSocket }) => {
        clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
        clientSocket.pipe(upstreamSocket);
        upstreamSocket.pipe(clientSocket);
        const cleanup = () => {
          try { clientSocket.destroy(); } catch {}
          try { upstreamSocket.destroy(); } catch {}
        };
        clientSocket.on('error', cleanup);
        upstreamSocket.on('error', cleanup);
        clientSocket.on('close', cleanup);
        upstreamSocket.on('close', cleanup);
      }).catch((err: Error) => {
        // 按域名聚合统计失败次数，便于诊断哪些域名被代理拒绝
        const cnt = (failStats.get(targetHost) ?? 0) + 1;
        failStats.set(targetHost, cnt);
        if (cnt === 1 || cnt % 5 === 0) {
          // eslint-disable-next-line no-console
          console.warn(`[socks5-bridge] ${targetHost}:${targetPort} 失败 (累计 ${cnt} 次): ${err.message}`);
        }
        if (!clientSocket.destroyed) {
          clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\nSOCKS5 upstream failed: ${err.message}`);
        }
      });
    };
    clientSocket.on('data', onFirstData);
    clientSocket.on('error', () => clientSocket.destroy());
  });

  // 监听 127.0.0.1 随机端口
  await new Promise<void>((resolve, reject) => {
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => resolve());
  });
  const addr = server.address() as net.AddressInfo;

  return {
    localUrl: `http://127.0.0.1:${addr.port}`,
    localPort: addr.port,
    close: () => {
      // 关闭时打印失败统计帮诊断
      if (failStats.size > 0) {
        const top = [...failStats.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
        // eslint-disable-next-line no-console
        console.log('[socks5-bridge] 失败域名 TOP10:', top.map(([d, c]) => `${d}=${c}`).join(', '));
      }
      try { server.close(); } catch {}
    },
  };
}
