"""受限 WHERE DSL 解析器。

用户输入一行 where 表达式，翻译成参数化 SQL `WHERE` 片段。所有字段 / 操作符 / 值都走严格白名单，不允许 OR / JOIN / 子查询 / 注释 / 分号 / 引号。

语法（EBNF 简化）:
    expr     := clause (" AND " clause)*
    clause   := scalar_clause | tags_clause
    scalar_clause := FIELD OP VALUE
    tags_clause   := "tags" "CONTAINS" TAG_VALUE

字段 × 合法操作符:
    gender  : = !=
    age     : = != > < >= <=
    city    : = !=
    tags    : CONTAINS  （单标签匹配，多标签请多个 CONTAINS AND 连起来）

值约束:
    gender  ∈ {f, m, nb}
    age     ∈ [18, 99]
    city    字符串，长度 ≤ 30，必须匹配 ^[a-zA-Z0-9_一-龥-]+$
    tags    字符串，长度 ≤ 20，同 city 字符集

限制:
    - 只支持 AND 连接，不支持 OR（商业上：花更多词拼 OR 相当于"自由查整个库"）
    - 不允许空 where（防一次性拉全库）；至少一个 clause
    - clause 数量 ≤ 8
"""

from __future__ import annotations

import re
from dataclasses import dataclass

ALLOWED_OPS_BY_FIELD: dict[str, set[str]] = {
    "gender": {"=", "!="},
    "age": {"=", "!=", ">", "<", ">=", "<="},
    "city": {"=", "!="},
}

MAX_CLAUSES = 8

GENDER_VALUES = {"f", "m", "nb"}
AGE_MIN = 18
AGE_MAX = 99
CITY_MAX_LEN = 30
TAG_MAX_LEN = 20
STR_RE = re.compile(r"^[a-zA-Z0-9_一-龥-]+$")


class DSLError(ValueError):
    """DSL 解析或校验失败。错误码见 code 属性。"""

    def __init__(self, code: str, msg: str):
        super().__init__(f"[{code}] {msg}")
        self.code = code


@dataclass
class ParsedWhere:
    sql: str           # 参数化 SQL 片段，例 "gender = ? AND age >= ?"
    params: list       # 对应参数


def parse(where: str) -> ParsedWhere:
    """解析 where DSL 字符串。"""
    if where is None or not where.strip():
        raise DSLError("dsl_empty", "WHERE 不能为空（防一次性拉全库）")

    if ";" in where or "--" in where or "/*" in where or "'" in where or '"' in where:
        raise DSLError("dsl_forbidden_char", "不允许 ; -- /* ' \" 等字符")

    if re.search(r"\bOR\b", where, re.IGNORECASE):
        raise DSLError("dsl_no_or", "不支持 OR，只允许 AND 连接")

    if re.search(r"\b(JOIN|UNION|SELECT|INSERT|UPDATE|DELETE|DROP|LIMIT|OFFSET|ORDER)\b",
                 where, re.IGNORECASE):
        raise DSLError("dsl_forbidden_keyword",
                       "不允许 SQL 关键字（JOIN/UNION/SELECT/...）")

    clauses_raw = re.split(r"\s+AND\s+", where.strip(), flags=re.IGNORECASE)
    if len(clauses_raw) > MAX_CLAUSES:
        raise DSLError("dsl_too_many_clauses", f"最多 {MAX_CLAUSES} 个 AND 条件")

    sql_parts: list[str] = []
    params: list = []
    for raw in clauses_raw:
        clause = raw.strip()
        if not clause:
            raise DSLError("dsl_empty_clause", "空 clause（多余的 AND？）")
        sql, param = _parse_clause(clause)
        sql_parts.append(sql)
        if isinstance(param, list):
            params.extend(param)
        else:
            params.append(param)

    return ParsedWhere(sql=" AND ".join(sql_parts), params=params)


def _parse_clause(clause: str) -> tuple[str, object]:
    """单个 clause 的处理入口。"""
    # tags CONTAINS xxx
    m = re.match(r"^tags\s+CONTAINS\s+(\S+)$", clause, re.IGNORECASE)
    if m:
        tag = m.group(1)
        _check_tag(tag)
        return ("tags LIKE ?", f"%|{tag}|%")

    # scalar: FIELD OP VALUE
    m = re.match(
        r"^(gender|age|city)\s*(!=|>=|<=|=|>|<)\s*(.+)$",
        clause,
        re.IGNORECASE,
    )
    if not m:
        raise DSLError(
            "dsl_bad_clause",
            f"无法解析 clause: {clause!r}（合法例：`gender=f` / `age>=25` / `city=hangzhou` / `tags CONTAINS 登山`）",
        )
    field = m.group(1).lower()
    op = m.group(2)
    value_raw = m.group(3).strip()

    if op not in ALLOWED_OPS_BY_FIELD[field]:
        raise DSLError(
            "dsl_bad_op",
            f"字段 {field} 不允许操作符 {op}（允许：{sorted(ALLOWED_OPS_BY_FIELD[field])}）",
        )

    if field == "gender":
        if value_raw.lower() not in GENDER_VALUES:
            raise DSLError("dsl_bad_value", f"gender 必须是 f/m/nb，收到 {value_raw!r}")
        return (f"gender {op} ?", value_raw.lower())

    if field == "age":
        try:
            age = int(value_raw)
        except ValueError as e:
            raise DSLError("dsl_bad_value", f"age 必须是整数，收到 {value_raw!r}") from e
        if age < AGE_MIN or age > AGE_MAX:
            raise DSLError(
                "dsl_bad_value",
                f"age 范围 [{AGE_MIN}, {AGE_MAX}]，收到 {age}",
            )
        return (f"age {op} ?", age)

    if field == "city":
        if len(value_raw) > CITY_MAX_LEN:
            raise DSLError("dsl_bad_value", f"city 长度 ≤ {CITY_MAX_LEN}")
        if not STR_RE.match(value_raw):
            raise DSLError("dsl_bad_value", f"city 包含非法字符：{value_raw!r}")
        return (f"city {op} ?", value_raw)

    raise DSLError("dsl_internal", f"未处理的字段：{field}")  # pragma: no cover


def _check_tag(tag: str) -> None:
    if len(tag) > TAG_MAX_LEN:
        raise DSLError("dsl_bad_value", f"tag 长度 ≤ {TAG_MAX_LEN}")
    if not STR_RE.match(tag):
        raise DSLError("dsl_bad_value", f"tag 包含非法字符：{tag!r}")
