#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：curl_parser.py
模块描述：性能测试 curl 命令解析器（纯函数，无第三方依赖）

  - 解析 curl -X/--request、-H/--header、-d/--data* 及 URL
  - 自动识别 Authorization Bearer（置 auth_required）与签名头（置 sign_request）并剥离
  - Cookie 头保留在 headers 中
  - 不支持的参数（-F/-u/--data-urlencode）收集到 errors 友好提示

作者：czj
创建日期：2026-08-19
最后修改：2026-08-19
"""

import json
import logging
import shlex
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional, Tuple

logger = logging.getLogger(__name__)

# 签名相关请求头（剥离并置 sign_request=True）
_SIGN_HEADERS = ("X-RANDOM", "X-TIMESTAMP", "X-SIGN")

# 不支持的参数 → 友好提示（带值参数，如 -F file=@x）
_UNSUPPORTED_VALUE_FLAGS = {
    "-F": "暂不支持文件上传参数 -F/--form，已忽略",
    "--form": "暂不支持文件上传参数 -F/--form，已忽略",
    "-u": "暂不支持 Basic 认证参数 -u/--user，已忽略",
    "--user": "暂不支持 Basic 认证参数 -u/--user，已忽略",
    "--data-urlencode": "暂不支持 URL 编码参数 --data-urlencode，已忽略",
}

# 不支持的布尔参数（无需取值）→ 友好提示
_UNSUPPORTED_BOOL_FLAGS = {}


def _preprocess_cmd_style(curl_command: str) -> str:
    """
    预处理 Windows CMD 风格的 curl 命令，兼容 '^' 续行符与 '^"' 转义引号。

    CMD 中：
      - '^' 为行续行符（行尾），去掉后可还原为单行命令
      - '^"' 为转义的双引号（等价于 '"'），必须还原为普通引号
    不做还原的话，shlex 会把 '^' 当作字面量拼进 token（如 ^Accept），
    导致请求头识别失败、认证/签名头无法剥离。
    """
    if "^" not in curl_command:
        return curl_command

    # 第一步：去掉行尾续行符 '^'，并合并为单行（保留空格分隔）
    lines = []
    for line in curl_command.splitlines():
        stripped = line.rstrip()
        if stripped.endswith("^"):
            stripped = stripped[:-1]
        lines.append(stripped)
    cleaned = " ".join(lines)

    # 第二步：把 CMD 转义引号 '^"' 还原为 '"'
    cleaned = cleaned.replace('^"', '"')

    return cleaned.strip()


@dataclass
class CurlParseResult:
    """curl 解析结果"""

    method: str = "GET"
    target_url: str = ""
    headers: Dict[str, str] = field(default_factory=dict)
    body: Optional[Any] = None
    auth_required: bool = False
    sign_request: bool = False
    cookie: Optional[str] = None
    errors: List[str] = field(default_factory=list)


def _split_header(header: str) -> Optional[Tuple[str, str]]:
    """将 'Key: Value' 拆分为 (key, value)，无冒号返回 None"""
    if ":" not in header:
        return None
    key, value = header.split(":", 1)
    key = key.strip()
    value = value.strip()
    if not key:
        return None
    return key, value


def _parse_json(value: str) -> Any:
    """尝试 JSON 解析，失败返回原始字符串"""
    try:
        return json.loads(value)
    except (json.JSONDecodeError, TypeError):
        return value


def _merge_header(existing: Dict[str, str], key: str, value: str) -> None:
    """合并请求头，同一键多个值用 '; ' 拼接"""
    if key in existing and existing[key]:
        existing[key] = f"{existing[key]}; {value}"
    else:
        existing[key] = value


def parse_curl(curl_command: str) -> CurlParseResult:
    """
    解析 curl 命令，返回结构化结果。

    Args:
        curl_command: 完整的 curl 命令字符串

    Returns:
        CurlParseResult: 解析结果（method/target_url/headers/body/认证开关/errors）

    Raises:
        ValueError: 命令为空、引号不匹配或无法识别目标 URL 时抛出
    """
    if not curl_command or not curl_command.strip():
        raise ValueError("curl 命令不能为空")

    # 兼容 Windows CMD 文本：还原 '^' 续行与 '^"' 转义引号后再分词
    curl_command = _preprocess_cmd_style(curl_command)

    # shlex 分词：统一 posix=True 兼容单/双引号与反斜杠续行
    try:
        tokens = shlex.split(curl_command)
    except ValueError as e:
        raise ValueError(f"curl 命令无法解析（引号不匹配）: {e}")

    # 去掉开头的 curl 可执行文件名
    if tokens and tokens[0].lower() in ("curl", "curl.exe"):
        tokens = tokens[1:]

    result = CurlParseResult()
    method_explicit = False
    body_parts: List[str] = []

    i = 0
    while i < len(tokens):
        token = tokens[i]
        next_token = tokens[i + 1] if i + 1 < len(tokens) else None

        # ---------- 裸露参数 → 目标 URL（取第一个） ----------
        if not token.startswith("-"):
            if not result.target_url:
                result.target_url = token
            i += 1
            continue

        # ---------- --key=value 形式（长选项内联赋值） ----------
        if token.startswith("--") and "=" in token:
            flag, _, inline_value = token.partition("=")
            if flag in ("--request", "-X"):
                result.method = inline_value.upper()
                method_explicit = True
            elif flag in ("--header", "-H"):
                parsed = _split_header(inline_value)
                if parsed:
                    _merge_header(result.headers, parsed[0], parsed[1])
            elif flag in ("--data", "--data-raw", "--data-binary", "--data-json", "-d"):
                body_parts.append(inline_value)
            elif flag in _UNSUPPORTED_VALUE_FLAGS:
                result.errors.append(_UNSUPPORTED_VALUE_FLAGS[flag])
            # 其余未知长选项（--compressed 等布尔旗标）忽略
            i += 1
            continue

        # ---------- 带值选项（下一 token 为值） ----------
        if token in ("-X", "--request"):
            if next_token is None:
                raise ValueError(f"参数 {token} 缺少值")
            result.method = next_token.upper()
            method_explicit = True
            i += 2
            continue

        if token in ("-H", "--header"):
            if next_token is None:
                raise ValueError(f"参数 {token} 缺少值")
            parsed = _split_header(next_token)
            if parsed:
                _merge_header(result.headers, parsed[0], parsed[1])
            i += 2
            continue

        if token in ("-d", "--data", "--data-raw", "--data-binary", "--data-json"):
            if next_token is None:
                raise ValueError(f"参数 {token} 缺少值")
            body_parts.append(next_token)
            i += 2
            continue

        if token in ("-b", "--cookie"):
            if next_token is None:
                raise ValueError(f"参数 {token} 缺少值")
            # 将 cookie 值设为 Cookie 请求头（同时保留到 result.cookie）
            result.cookie = next_token
            _merge_header(result.headers, "Cookie", next_token)
            i += 2
            continue

        if token in _UNSUPPORTED_VALUE_FLAGS:
            result.errors.append(_UNSUPPORTED_VALUE_FLAGS[token])
            if next_token is not None and not next_token.startswith("-"):
                # 带值参数跨过值
                i += 2
            else:
                i += 1
            continue

        # ---------- 未知参数（-k/-L/-s/--compressed 等）忽略 ----------
        i += 1

    if not result.target_url:
        raise ValueError("未识别到目标 URL")

    # 方法推断：未显式指定 -X 时，有 body 默认 POST，否则 GET
    if body_parts and not method_explicit:
        result.method = "POST"

    # 合并 body：多个 -d 按 & 拼接（与 curl 行为一致），再尝试 JSON 解析
    if body_parts:
        result.body = _parse_json("&".join(body_parts))

    # 认证识别：剥离 Authorization / 签名头，Cookie 保留在 headers 并记录值
    for key in list(result.headers.keys()):
        upper_key = key.upper()
        if upper_key == "AUTHORIZATION":
            result.auth_required = True
            del result.headers[key]
        elif upper_key in _SIGN_HEADERS:
            result.sign_request = True
            del result.headers[key]
        elif upper_key == "COOKIE":
            result.cookie = result.headers[key]

    logger.debug("parse_curl result: method=%s url=%s headers=%s body=%s errors=%s",
                 result.method, result.target_url, result.headers, result.body, result.errors)
    return result