/**
 * HTTP 请求工具（1 个）
 *
 * http_request — 发送 HTTP/REST API 请求
 *
 * 使用 Node.js 18+ 内置 fetch API。
 * 响应体截断到 50KB 防止结果过大。
 *
 * #10 修复: SSRF 防护 — 解析 URL 域名并校验 IP，拒绝内网/回环/元数据地址。
 */

import { lookup } from 'node:dns/promises';
import { isIP } from 'node:net';
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
import type { MetonaToolDef } from '../../../harness/types';
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';

const ALLOWED_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'] as const;
const MAX_BODY_BYTES = 50 * 1024; // 50KB

/**
 * #10 修复: 检查 IP 是否为私有/内网/回环/元数据地址
 *
 * 覆盖：
 * - IPv4: 127.0.0.0/8 (回环)、10.0.0.0/8、192.168.0.0/16、172.16.0.0/12、
 *         169.254.0.0/16 (链路本地，含云元数据 169.254.169.254)、0.0.0.0/8、
 *         224.0.0.0/4 (组播)、240.0.0.0/4 (保留)
 * - IPv6: ::1 (回环)、fe80::/10 (链路本地)、fc00::/7 (唯一本地)、::ffff: 映射的 IPv4
 */
function isPrivateIP(ip: string): boolean {
  // IPv4 直接检测
  if (isIP(ip) === 4) {
    const parts = ip.split('.').map(Number);
    if (parts[0] === 127) return true;          // 回环
    if (parts[0] === 10) return true;           // 内网
    if (parts[0] === 192 && parts[1] === 168) return true; // 内网
    if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true; // 内网
    if (parts[0] === 169 && parts[1] === 254) return true;  // 链路本地（含云元数据）
    if (parts[0] === 0) return true;            // 0.0.0.0/8
    if (parts[0] >= 224) return true;           // 组播 + 保留
    return false;
  }

  // IPv6 检测
  if (isIP(ip) === 6) {
    const lower = ip.toLowerCase();
    if (lower === '::1') return true;                        // 回环
    if (lower.startsWith('fe80:')) return true;              // 链路本地
    if (lower.startsWith('fc') || lower.startsWith('fd')) return true; // 唯一本地
    // ::ffff: 映射的 IPv4 — 提取 IPv4 部分递归检测
    const v4MappedMatch = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
    if (v4MappedMatch) return isPrivateIP(v4MappedMatch[1]);
    return false;
  }

  // 非 IP 格式（域名等），由调用方 DNS 解析后再检测
  return false;
}

/**
 * #10 修复: SSRF 校验 — 解析 URL 域名并校验 IP
 *
 * 1. 协议白名单：仅允许 http/https
 * 2. DNS 解析域名，获取所有 IP 地址
 * 3. 逐个检测 IP 是否为私有/内网/回环/元数据地址
 * 4. 任意一个 IP 为私有即拒绝（防止 DNS rebinding 中只校验第一个 IP）
 *
 * 审查修复 (M7) — 已知限制：DNS rebinding 窗口
 * ---------------------------------------------------------------
 * validateSSRF 在校验阶段 DNS 解析得到 IP，fetch 内部会再次 DNS 解析，
 * 两次解析之间存在 DNS rebinding 攻击窗口（攻击者可在校验通过后切换
 * DNS 记录到内网 IP）。
 *
 * 完全防护需要 "DNS pinning"（用校验通过的 IP 替换 URL hostname），
 * 但在 Node.js fetch 实现下不可行：
 *   1. HTTPS 请求时 fetch 会基于 URL hostname 校验证书 SAN，
 *      用 IP 替换会导致证书校验失败（除非目标证书 SAN 包含该 IP）。
 *   2. Node fetch 将 Host 列为 forbidden header，无法通过设置
 *      Host header 保留原始域名。
 *   3. fetch API 不暴露 SNI 自定义入口。
 *
 * 当前实现的缓解措施：
 *   - 校验所有 DNS 返回的 IP（防只校验第一个 IP 的绕过）
 *   - redirect: 'manual' 禁用自动重定向（防重定向到内网）
 *   - 窗口虽存在，但需要攻击者控制权威 DNS 并在毫秒级切换记录，
 *     实际利用难度较高。
 *
 * 彻底防护建议：在 Electron 主进程层使用自定义 lookup 钩子实现
 * DNS pinning（例如 undici 的 dispatcher.agent.connect lookup）。
 *
 * @throws 如果 URL 指向私有/内网/回环地址
 */
async function validateSSRF(url: string): Promise<void> {
  let parsed: URL;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`Invalid URL: ${url}`);
  }

  // 协议白名单
  if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
    throw new Error(`Blocked SSRF: protocol "${parsed.protocol}" not allowed (only http/https)`);
  }

  const hostname = parsed.hostname;

  // 如果 hostname 本身就是 IP，直接检测
  if (isIP(hostname)) {
    if (isPrivateIP(hostname)) {
      throw new Error(`Blocked SSRF: ${hostname} is a private/loopback address`);
    }
    return;
  }

  // 域名 — DNS 解析后检测所有 IP
  let addresses: Array<{ address: string }>;
  try {
    addresses = await lookup(hostname, { all: true });
  } catch (err) {
    throw new Error(`Blocked SSRF: DNS resolution failed for ${hostname}: ${(err as Error).message}`);
  }

  if (addresses.length === 0) {
    throw new Error(`Blocked SSRF: no DNS records for ${hostname}`);
  }

  for (const { address } of addresses) {
    if (isPrivateIP(address)) {
      throw new Error(`Blocked SSRF: ${hostname} resolves to private IP ${address}`);
    }
  }
}

export class HttpRequestTool implements IMetonaTool {
  readonly definition: MetonaToolDef = {
    name: 'http_request',
    description: 'Send an HTTP/REST API request. Supports GET/POST/PUT/PATCH/DELETE/HEAD methods with custom headers and body. Response body is truncated to 50KB.',
    parameters: {
      type: 'object',
      properties: {
        url: { type: 'string', description: 'Request URL (must start with http:// or https://)' },
        method: {
          type: 'string',
          description: 'HTTP method (default GET)',
          enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'],
        },
        headers: { type: 'object', description: 'Request headers as key-value pairs' },
        body: { type: 'string', description: 'Request body (string)' },
        timeout: { type: 'number', description: 'Timeout in milliseconds (default 30000, max 60000)' },
      },
      required: ['url'],
    },
    category: MetonaToolCategory.NETWORK,
    riskLevel: MetonaRiskLevel.MEDIUM,
    requiresPermission: true,
    timeoutMs: 30_000,
  };

  async execute(args: Record<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
    try {
      const url = args.url as string;
      const method = ((args.method as string) ?? 'GET').toUpperCase();
      const headers = (args.headers as Record<string, string> | undefined) ?? undefined;
      const body = args.body as string | undefined;
      const timeout = Math.min(60_000, Math.max(1, (args.timeout as number) ?? 30_000));

      // 校验 URL
      if (!url || !/^https?:\/\//i.test(url)) {
        return { error: 'Invalid URL', success: false };
      }

      // #10 修复: SSRF 校验 — 拒绝内网/回环/元数据地址
      try {
        await validateSSRF(url);
      } catch (ssrfErr) {
        return { error: (ssrfErr as Error).message, success: false };
      }

      // 校验 method
      if (!(ALLOWED_METHODS as readonly string[]).includes(method)) {
        return {
          error: `Invalid method: ${method}. Must be one of: ${ALLOWED_METHODS.join(', ')}`,
          success: false,
        };
      }

      // 超时控制
      const controller = new AbortController();
      const timer = setTimeout(() => controller.abort(), timeout);

      try {
        const fetchOptions: RequestInit = {
          method,
          headers,
          signal: controller.signal,
          // #10 修复: 禁用自动重定向跟随 — 防止重定向到内网地址绕过 SSRF 校验
          // 重定向后的 URL 由用户自行处理（响应中会包含 Location 头）
          redirect: 'manual',
        };
        // GET/HEAD 不应携带 body
        if (body !== undefined && method !== 'GET' && method !== 'HEAD') {
          fetchOptions.body = body;
        }

        const response = await fetch(url, fetchOptions);
        const text = await response.text();

        // 截断到 50KB
        const truncated = text.length > MAX_BODY_BYTES;
        const safeBody = truncated ? text.slice(0, MAX_BODY_BYTES) : text;

        // 只返回 content-type 和 content-length
        // 审查修复: redirect:'manual' 后需要返回 Location header，否则 LLM 无法知道重定向目标
        const filteredHeaders: Record<string, string> = {};
        const contentType = response.headers.get('content-type');
        if (contentType) filteredHeaders['content-type'] = contentType;
        const contentLength = response.headers.get('content-length');
        if (contentLength) filteredHeaders['content-length'] = contentLength;
        const location = response.headers.get('location');
        if (location) filteredHeaders['location'] = location;

        return {
          status: response.status,
          statusText: response.statusText,
          headers: filteredHeaders,
          body: safeBody,
          truncated,
          ok: response.ok,
          success: true,  // v0.3.1 修复 WARN-4: 成功路径添加 success 字段
        };
      } finally {
        clearTimeout(timer);
      }
    } catch (error) {
      // 区分超时（AbortError）与其他网络错误
      if (error instanceof Error && error.name === 'AbortError') {
        return { error: 'Request timeout', success: false };
      }
      const errMsg = error instanceof Error ? error.message : String(error);
      return { error: errMsg, success: false };
    }
  }
}
