import re
from typing import List, Optional, Tuple

from .ir import (
    Pipeline,
    SearchStage,
    FieldsStage,
    StatsStage,
    FilterStage,
    AggFunc,
    Filter,
    BoolExpr,
    GenericStage,
)
from .spl_generator import to_spl

# --- Zabbix trigger expression pattern ---
# Example: {web01:net.if.in[eth0].last()}=0
TRIGGER_RE = re.compile(r"\{([^:}]+):([^}]+)\}\s*(>=|<=|=|!=|>|<)\s*([^\s]+)")

# Item.function(args) splitter
FUNC_SPLIT_RE = re.compile(r"^(?P<item>.+)\.(?P<func>[A-Za-z_][\w]*)\((?P<args>[^)]*)\)$")
# Parse item key and parameter like key[param]
ITEM_ARG_RE = re.compile(r"^(?P<key>[A-Za-z0-9_.]+)\[(?P<arg>[^\]]+)\]$")

# Loose key-value / comparisons for event filters
KV_RE = re.compile(r"([A-Za-z_][\w.]*)=(\"[^\"]*\"|[^\s|)]+)")
CMP_RE = re.compile(r"([A-Za-z_][\w.]*)\s*(>=|<=|=|!=|>|<)\s*([^\s|)]+)")

OP_MAP = {"=": "eq", "!=": "neq", ">": "gt", ">=": "gte", "<": "lt", "<=": "lte"}

SUPPORTED_FUNCS = {"avg", "min", "max", "count", "strlen", "nodata", "change", "delta", "latest", "latest_time", "sum", "prev", "last", "dayofweek", "time"}

REGSUB_SPLIT_RE = re.compile(r'^(?P<base>.+)\.regsub\((?P<pat>"[^"]*"|[^,]+)\s*,\s*(?P<rep>"[^"]*"|[^)]+)\)$')
REGEX_SPLIT_RE = re.compile(r'^(?P<base>.+)\.(?P<f>iregexp|regexp)\((?P<pat>"[^"]*"|[^)]*)\)$')
STR_SPLIT_RE = re.compile(r'^(?P<base>.+)\.str\(\)$')

# Known encodings to avoid treating as text keywords
ENCODINGS = {"UTF-8", "GB2312", "GBK", "GB18030"}


def _wildcard_to_regex(pattern: str) -> str:
    """Convert shell-style wildcard to regex string anchoring whole match.
    Example: "/var/log/app_*.log" -> ^/var/log/app_.*\\.log$
    """
    import re as _re
    # Escape regex specials, then replace wildcards
    escaped = ''.join([_re.escape(ch) for ch in pattern])
    escaped = escaped.replace(r'\*', '.*').replace(r'\?', '.')
    return f"^{escaped}$"


def _strip_quotes(v: str) -> str:
    if v is None:
        return v
    v = str(v)
    if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
        return v[1:-1]
    return v


def _parse_trigger_segment(seg: str) -> Optional[Tuple[str, str, str, str, Optional[str], Optional[str]]]:
    """
    Parse a single trigger expression segment like:
      {host:item.func(args)} <op> <value>
    Returns: (host, item, func, op, value, window, func_args)
    window may be derived from args for windowed functions like avg(5m).
    """
    m = TRIGGER_RE.search(seg)
    if not m:
        return None
    host = m.group(1).strip()
    item_func = m.group(2).strip()
    op_symbol = m.group(3)
    value = _strip_quotes(m.group(4))

    func = "last"
    args = ""
    item = item_func
    window = None
    fm = FUNC_SPLIT_RE.match(item_func)
    if fm:
        item = fm.group("item").strip()
        func = fm.group("func").strip().lower()
        args = fm.group("args").strip()
        # pick first arg as window if looks like duration
        if args:
            first_arg = args.split(',')[0].strip()
            if re.match(r"^[-+]?\d+[smhdw]$", first_arg):
                window = first_arg
    return (host, item, func, OP_MAP.get(op_symbol, op_symbol), value, window, args)


def _to_bool_expr(filters: List[Filter]) -> Optional[BoolExpr]:
    if not filters:
        return None
    if len(filters) == 1:
        return BoolExpr(op="and", children=[filters[0]])
    return BoolExpr(op="and", children=filters)


def _duration_value(w: Optional[str]) -> int:
    if not w:
        return 0
    try:
        num = int(str(w).rstrip('smhdw'))
    except Exception:
        return 0
    unit = str(w)[-1]
    mult = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400, 'w': 604800}.get(unit, 60)
    return num * mult


def _op_to_eval(op: str) -> str:
    return {
        "eq": "==",
        "neq": "!=",
        "gt": ">",
        "gte": ">=",
        "lt": "<",
        "lte": "<=",
    }.get(op, "==")


def _is_number(v: str) -> bool:
    try:
        float(str(v))
        return True
    except Exception:
        return False


def parse_zabbix(text: str, default_repo: str = "zabbix_events", default_start: str = "", file_field: str = "origin", host_field: str = "host") -> Pipeline:
    """
    Parse Zabbix rule text into IR Pipeline.
    Supported forms:
      - Trigger expressions: "{host:item.func(args)} <op> <value>" possibly combined by AND/OR (first version treats as sequential filters)
      - Event filters: loose key-values like "group=web severity>=4 ack=0"; defaults to events repo
      - Optional window parameter: "window=-5m" to override start time
      - Special case: item key "log[filename,pattern]" with func "strlen()" maps to text search and eval
    """
    # Local defaults (do not expose via parameters)
    s = text.strip()
    pipeline = Pipeline()

    # Detect explicit window=... parameter
    start_override = None
    mw = re.search(r"\bwindow\s*=\s*(\"[^\"]*\"|[^\s|]+)", s)
    if mw:
        start_override = _strip_quotes(mw.group(1))
        # remove it from body
        s = s.replace(mw.group(0), "").strip()

    # Find all trigger segments
    triggers = [m.group(0) for m in TRIGGER_RE.finditer(s)]

    # Special join case: two log triggers (nodata + aggregate) on same file/pattern
    if len(triggers) >= 2:
        parsed_segs = [_parse_trigger_segment(seg) for seg in triggers]
        # pick first two
        a, b = parsed_segs[0], parsed_segs[1]
        if a and b:
            def _extract_log(item_str: str):
                mitem = ITEM_ARG_RE.match(item_str)
                if not mitem or mitem.group("key") not in ("log", "logrt"):
                    return None
                parts = [p.strip() for p in mitem.group("arg").split(',')]
                file_name = _strip_quotes(parts[0]) if parts else ""
                # locate mode token from tail
                mode_idx = None
                for idx in range(len(parts) - 1, -1, -1):
                    pv = _strip_quotes(parts[idx])
                    if pv in ("regexp", "iregexp", "str"):
                        mode_idx = idx
                        break
                pattern = ""
                if mode_idx is not None:
                    for j in range(mode_idx - 1, 0, -1):
                        cand = _strip_quotes(parts[j])
                        if cand and cand.upper() not in ENCODINGS:
                            pattern = cand
                            break
                if not pattern and len(parts) > 1:
                    cand = _strip_quotes(parts[1])
                    pattern = cand if cand.upper() not in ENCODINGS else ""
                tokens = [t.strip() for t in (pattern or '').split('|') if t.strip()]
                return file_name, tokens
            la = _extract_log(a[1])
            lb = _extract_log(b[1])
            if la and lb:
                host_a, item_a, func_a, op_a, value_a, win_a, args_a = a
                host_b, item_b, func_b, op_b, value_b, win_b, args_b = b
                file_a, toks_a = la
                file_b, toks_b = lb
                # both log and same file and tokens
                if host_a == host_b and file_a == file_b and toks_a == toks_b:
                    # build base filter string
                    origin = file_a
                    tokens = toks_a
                    # choose windows: agg as main, nodata as sub
                    win_nodata = win_a if func_a == 'nodata' else win_b if func_b == 'nodata' else None
                    win_agg = win_b if func_b in ("count","avg","min","max","sum","last","prev") else win_a if func_a in ("count","avg","min","max","sum","last","prev") else None
                    if win_nodata and win_agg:
                        # Main search2
                        cnt_main_alias = f"cnt_{win_agg}" if win_agg else "cnt_main"
                        cnt_sub_alias = f"cnt_{win_nodata}" if win_nodata else "cnt_2m"
                        def _filters_for(host):
                            parts = [f"'host' = \"{host}\"", f"'origin' = \"{origin}\""]
                            parts += [f'"{t}"' for t in tokens]
                            return ' AND '.join(parts)
                        main_search = SearchStage()
                        main_search.repo = default_repo
                        main_search.start = start_override or (f"-{win_agg}" if win_agg and not str(win_agg).startswith('-') else win_agg)
                        # attach query
                        base_filters: List[Filter] = [Filter(op="eq", field=host_field, value=str(host_a)), Filter(op="eq", field=file_field, value=origin)] + [Filter(op="text", field=None, value=t) for t in tokens]
                        main_search.query = _to_bool_expr(base_filters)
                        pipeline.stages.append(main_search)
                        # use timechart then aggregate over cnt
                        pipeline.stages.append(GenericStage(type="generic", params={"raw": "timechart span=\"1m\" count() as cnt"}))
                        pipeline.stages.append(StatsStage(funcs=[AggFunc(name=("sum" if (func_a == 'count' or func_b == 'count') else 'max'), field='cnt', alias=cnt_main_alias)], by=[]))
                        pipeline.stages.append(GenericStage(type="generic", params={"raw": "eval join_tmp=1"}))
                        # build subsearch pipeline and render
                        sub_p = Pipeline()
                        sub_s = SearchStage()
                        sub_s.repo = default_repo
                        sub_s.start = start_override or (f"-{win_nodata}" if win_nodata and not str(win_nodata).startswith('-') else win_nodata)
                        sub_s.query = _to_bool_expr(base_filters)
                        sub_p.stages.append(sub_s)
                        sub_p.stages.append(GenericStage(type="generic", params={"raw": "timechart span=\"1m\" count() as cnt"}))
                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name=('sum' if (func_a == 'count' or func_b == 'count') else 'max'), field='cnt', alias=cnt_sub_alias)], by=[]))
                        sub_p.stages.append(GenericStage(type="generic", params={"raw": "eval join_tmp=1"}))
                        sub_spl = to_spl(sub_p)
                        pipeline.stages.append(GenericStage(type="generic", params={"raw": f"join type=\"left\" join_tmp [{sub_spl}]"}))
                        pipeline.stages.append(GenericStage(type="generic", params={"raw": f"eval need_alert=if({cnt_main_alias} > 0 and {cnt_sub_alias} == 0, true, false)"}))
                        pipeline.stages.append(FieldsStage(fields=["need_alert"]))
                        return pipeline

    if triggers:
        # Metrics/log path
        search = SearchStage()
        search.repo = default_repo
        
        base_filters: List[Filter] = []
        stats_and_filters: List[Tuple[StatsStage, FilterStage]] = []
        handled_log = False
        for seg in triggers:
            parsed = _parse_trigger_segment(seg)
            if not parsed:
                continue
            host, item, func, op, value, window, func_args = parsed
            # Adjust start if window provided or override is set
            if window and not start_override:
                search.start = f"-{window}" if not str(window).startswith('-') else str(window)
            elif start_override:
                search.start = start_override

            # log/logrt item special handling
            mitem_full = str(item)
            # detect chains like .regsub(...), .regexp(...), .iregexp(...), .str()
            regsub_pat = None
            regsub_rep = None
            regex_pat = None
            regex_ci = False
            m = REGSUB_SPLIT_RE.match(mitem_full)
            if m:
                mitem_full = m.group("base")
                regsub_pat = _strip_quotes(m.group("pat"))
                regsub_rep = _strip_quotes(m.group("rep"))
            m2 = REGEX_SPLIT_RE.match(mitem_full)
            if m2:
                mitem_full = m2.group("base")
                regex_pat = _strip_quotes(m2.group("pat"))
                regex_ci = (m2.group("f") == "iregexp")
                # override detected function for downstream handling
                func = m2.group("f")
            m3 = STR_SPLIT_RE.match(mitem_full)
            if m3:
                mitem_full = m3.group("base")
                # override func to str when .str() detected in chain
                func = "str"
            mitem = ITEM_ARG_RE.match(mitem_full)
            if mitem and mitem.group("key") in ("log", "logrt", "log.count", "logrt.count"):
                key_type = mitem.group("key")
                # 主机名过滤
                if host:
                    base_filters.append(Filter(op="eq", field=host_field, value=str(host)))
                parts = [p.strip() for p in mitem.group("arg").split(',')]
                # 文件名
                file_name = _strip_quotes(parts[0]) if parts else ""
                if file_name:
                    base_filters.append(Filter(op="eq", field=file_field, value=file_name))
                # 解析模式：优先识别 item 参数中的 regexp/iregexp 模式
                mode_idx = None
                mode_val = None
                encoding_val = None
                for idx in range(len(parts) - 1, -1, -1):
                    pv = _strip_quotes(parts[idx])
                    if pv.upper() in ENCODINGS:
                        encoding_val = pv
                        continue
                    if pv in ("regexp", "iregexp", "str"):
                        mode_idx = idx
                        mode_val = pv
                        break
                pattern = ""
                if mode_idx is not None:
                    # 从模式参数向前回溯，找到第一个非空参数作为模式
                    for j in range(mode_idx - 1, 0, -1):
                        cand = _strip_quotes(parts[j])
                        if cand and cand.upper() not in ENCODINGS:
                            pattern = cand
                            break
                # 回退：第二参数作为模式（兼容旧格式）
                if not pattern and len(parts) > 1:
                    cand = _strip_quotes(parts[1])
                    pattern = cand if cand.upper() not in ENCODINGS else ""
                # 函数链上的 regexp/iregexp 优先
                if regex_pat:
                    val = ("(?i)" + regex_pat) if regex_ci else regex_pat
                    base_filters.append(Filter(op="regex", field="_raw", value=val))
                elif mode_val in ("regexp", "iregexp") and pattern:
                    # 将 OR 分隔的关键字拆成独立文本过滤词（过滤掉编码）
                    for tok in [t.strip() for t in pattern.split('|') if t.strip() and t.upper() not in ENCODINGS]:
                        base_filters.append(Filter(op="text", field=None, value=tok))
                elif func in ("regexp", "iregexp") and func_args:
                    pat = _strip_quotes(func_args)
                    for tok in [t.strip() for t in pat.split('|') if t.strip() and t.upper() not in ENCODINGS]:
                        base_filters.append(Filter(op="text", field=None, value=tok))
                elif pattern:
                    # 普通文本模式（过滤掉编码）
                    if pattern.upper() not in ENCODINGS:
                        base_filters.append(Filter(op="text", field=None, value=pattern))
                # Build pipeline
                pipeline.stages.append(search)
                # override log handling
                # Special join case: general AND between first two triggers: render as join
                if len(triggers) >= 2:
                    spans = [(m.start(), m.end()) for m in TRIGGER_RE.finditer(s)]
                    if len(spans) >= 2:
                        between = s[spans[0][1]:spans[1][0]].lower()
                        if 'and' in between:
                            parsed_segs = [_parse_trigger_segment(seg) for seg in triggers[:2]]
                            a, b = parsed_segs[0], parsed_segs[1]
                            if a and b:
                                def _extract_log(item_str: str):
                                    mitem = ITEM_ARG_RE.match(item_str)
                                    if not mitem or mitem.group('key') not in ('log','logrt','log.count','logrt.count'):
                                        return None
                                    parts = [p.strip() for p in mitem.group('arg').split(',')]
                                    file_name = _strip_quotes(parts[0]) if parts else ''
                                    # locate mode token from tail
                                    mode_idx = None
                                    for idx in range(len(parts) - 1, -1, -1):
                                        pv = _strip_quotes(parts[idx])
                                        if pv in ('regexp','iregexp','str'):
                                            mode_idx = idx
                                            break
                                    pattern = ''
                                    if mode_idx is not None:
                                        for j in range(mode_idx - 1, 0, -1):
                                            cand = _strip_quotes(parts[j])
                                            if cand and cand.upper() not in ENCODINGS:
                                                pattern = cand
                                                break
                                    if not pattern and len(parts) > 1:
                                        cand = _strip_quotes(parts[1])
                                        pattern = cand if cand.upper() not in ENCODINGS else ''
                                def _build_sub_pipeline(seg, cond_alias_name: str):
                                    host, item, func, op, value, window, func_args = seg
                                    mitem = ITEM_ARG_RE.match(str(item))
                                    # base filters
                                    base_filters: List[Filter] = []
                                    if host:
                                        base_filters.append(Filter(op='eq', field=host_field, value=str(host)))
                                    origin = None
                                    tokens: List[str] = []
                                    if mitem:
                                        key = mitem.group('key')
                                        arg = mitem.group('arg')
                                        parts = [p.strip() for p in arg.split(',')]
                                        origin = _strip_quotes(parts[0]) if parts else ''
                                        if key in ('log','logrt','log.count','logrt.count'):
                                            if origin:
                                                base_filters.append(Filter(op='eq', field=file_field, value=origin))
                                            # derive tokens from item args
                                            mode_idx = None
                                            for idx in range(len(parts) - 1, -1, -1):
                                                pv = _strip_quotes(parts[idx])
                                                if pv in ('regexp','iregexp','str','strlen'):
                                                    mode_idx = idx
                                                    break
                                            pattern_item = ''
                                            if mode_idx is not None:
                                                for j in range(mode_idx - 1, 0, -1):
                                                    cand = _strip_quotes(parts[j])
                                                    if cand and cand.upper() not in ENCODINGS:
                                                        pattern_item = cand
                                                        break
                                            if not pattern_item and len(parts) > 1:
                                                cand = _strip_quotes(parts[1])
                                                pattern_item = cand if cand.upper() not in ENCODINGS else ''
                                            if pattern_item:
                                                tokens.extend([t.strip() for t in pattern_item.split('|') if t.strip() and t.upper() not in ENCODINGS])
                                    # add tokens from function args for regex-like funcs
                                    if func in ('regexp','iregexp','str') and func_args:
                                        func_pat = _strip_quotes(func_args)
                                        if func_pat:
                                            tokens.extend([t.strip() for t in func_pat.split('|') if t.strip() and t.upper() not in ENCODINGS])
                                    # attach text tokens
                                    for t in tokens:
                                        base_filters.append(Filter(op='text', field=None, value=t))
                                    # build search
                                    sub_s = SearchStage()
                                    sub_s.repo = default_repo
                                    sub_s.start = start_override or (f"-{window}" if window and not str(window).startswith('-') else window)
                                    sub_s.query = _to_bool_expr(base_filters)
                                    sub_p = Pipeline()
                                    sub_p.stages.append(sub_s)
                                    # function handling
                                    if func == 'strlen':
                                        alias = 'cnt'
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias=alias)], by=[]))
                                        if value is not None:
                                            op_sym = _op_to_eval(op)
                                            rhs = str(value) if _is_number(value) else f'"{value}"'
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} {op_sym} {rhs})"}))
                                        else:
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} > 0)"}))
                                    elif func == 'nodata':
                                        alias = 'cnt'
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias=alias)], by=[]))
                                        # nodata(2m)=0 -> 有数据，计数>0
                                        if str(value) == '0' and op == 'eq':
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} > 0)"}))
                                        elif str(value) == '1' and op == 'eq':
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} == 0)"}))
                                        else:
                                            op_sym = _op_to_eval(op)
                                            rhs = str(value) if _is_number(value) else f'"{value}"'
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} {op_sym} {rhs})"}))
                                    elif func in ('regexp','iregexp','str'):
                                        alias = 'cnt'
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias=alias)], by=[]))
                                        if value is not None:
                                            op_sym = _op_to_eval(op)
                                            rhs = str(value) if _is_number(value) else f'"{value}"'
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} {op_sym} {rhs})"}))
                                        else:
                                            sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} > 0)"}))
                                    elif func in ('count','avg','min','max','sum'):
                                        # 统一时间序列聚合：timechart + stats over cnt
                                        agg_name = 'sum' if func == 'count' else func
                                        alias = f"{agg_name}_val"
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'timechart span="1m" count() as cnt'}))
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name=agg_name, field='cnt', alias=alias)], by=[]))
                                        op_sym = _op_to_eval(op)
                                        rhs = str(value) if _is_number(value) else f'"{value}"'
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=({alias} {op_sym} {rhs})"}))
                                    elif func in ('last','prev') and (str(value).strip().lower().startswith('prev')):
                                        # 前后值比较：当前计数与上一时点计数
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'timechart span="1m" count() as cnt'}))
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'streamstats current=f window=2 global=f'}))
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'eval prev_val=lag(cnt, 1)'}))
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'where isnotnull(prev_val)'}))
                                        op_sym = _op_to_eval(op)
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=(cnt {op_sym} prev_val)"}))
                                    elif func in ('last','prev'):
                                        # 默认按存在性处理
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias='cnt')], by=[]))
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=(cnt > 0)"}))
                                    else:
                                        # default: existence
                                        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias='cnt')], by=[]))
                                        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=(cnt > 0)"}))
                                    sub_p.stages.append(GenericStage(type='generic', params={'raw': 'eval join_tmp=1'}))
                                    return sub_p
                                main_p = _build_sub_pipeline(a, 'cond_a')
                                sub_p = _build_sub_pipeline(b, 'cond_b')
                                main_spl = to_spl(main_p)
                                sub_spl = to_spl(sub_p)
                                # ensure we don't duplicate earlier search stage
                                pipeline.stages = []
                                pipeline.stages.append(GenericStage(type='generic', params={'raw': main_spl}))
                                pipeline.stages.append(GenericStage(type='generic', params={'raw': f'join type="left" join_tmp [{sub_spl}]'}))
                                pipeline.stages.append(GenericStage(type='generic', params={'raw': 'eval need_alert=(cond_a and cond_b)'}))
                                pipeline.stages.append(FieldsStage(fields=['need_alert']))
                                return pipeline
                # fallback: only search
                handled_log = True
                break

            # Normal metrics path (non-log)
            base_filters.append(Filter(op="eq", field=host_field, value=str(host)))
            if mitem:
                key = mitem.group("key")
                arg = _strip_quotes(mitem.group("arg"))
                base_filters.append(Filter(op="eq", field=key, value=str(arg)))
                item_field_for_stats = f"'{key}'"
            else:
                base_filters.append(Filter(op="eq", field="item", value=str(item)))
                item_field_for_stats = str(item)
            
            func_name = 'latest' if func == 'last' else (func if func in SUPPORTED_FUNCS else 'latest')
            alias = f"{func_name}_val"
            stats = StatsStage(funcs=[AggFunc(name=func_name, field=item_field_for_stats, alias=alias)], by=[host_field, item_field_for_stats])
            filt = FilterStage(expr=BoolExpr(op="and", children=[Filter(op=op, field=alias, value=value)]))
            stats_and_filters.append((stats, filt))
        
        if handled_log:
            # attach built search.query for log case
            search.query = _to_bool_expr(base_filters)
            return pipeline
        
        # assign combined base filters to search for metrics
        search.query = _to_bool_expr(base_filters)
        pipeline.stages.append(search)
        # Standard fields: host, metric field, _time
        if stats_and_filters:
            last_stats = stats_and_filters[-1][0]
            metric_field = last_stats.funcs[0].field or "value"
        else:
            metric_field = "value"
        pipeline.stages.append(FieldsStage(fields=[host_field, metric_field, "_time"]))
        if stats_and_filters:
            last_stats, last_filt = stats_and_filters[-1]
            pipeline.stages.append(last_stats)
            f = last_filt.expr.children[0]
            op_sym = _op_to_eval(f.op)
            rhs = str(f.value) if _is_number(f.value) else f'"{f.value}"'
            alias = last_stats.funcs[0].alias or "val"
            # 特殊处理：与 prev() 比较，使用 streamstats 获取上一值
            if str(f.value).strip().lower().startswith('prev'):
                pipeline.stages.append(GenericStage(type='generic', params={'raw': 'streamstats current=f window=2 global=f'}))
                pipeline.stages.append(GenericStage(type='generic', params={'raw': f'eval prev_val=lag({alias}, 1)'}))
                pipeline.stages.append(GenericStage(type='generic', params={'raw': 'where isnotnull(prev_val)'}))
                pipeline.stages.append(GenericStage(type='generic', params={'raw': f"eval need_alert=({alias} {op_sym} prev_val)"}))
            else:
                pipeline.stages.append(GenericStage(type="generic", params={"raw": f"eval need_alert=({alias} {op_sym} {rhs})"}))
            pipeline.stages.append(FieldsStage(fields=["need_alert"]))
        return pipeline

    # Else, treat as event filters line
    search = SearchStage()
    search.repo = default_repo
    search.start = start_override or default_start
    expr_filters: List[Filter] = []
    pos = 0
    body = s
    while pos < len(body):
        m = re.match(r"\s+", body[pos:])
        if m:
            pos += m.end()
            continue
        mc = CMP_RE.match(body[pos:])
        if mc:
            field, op_symbol, val_raw = mc.groups()
            val = _strip_quotes(val_raw)
            expr_filters.append(Filter(op=OP_MAP.get(op_symbol, op_symbol), field=field, value=val))
            pos += mc.end()
            continue
        mk = KV_RE.match(body[pos:])
        if mk:
            field, val_raw = mk.groups()
            val = _strip_quotes(val_raw)
            expr_filters.append(Filter(op="eq", field=field, value=val))
            pos += mk.end()
            continue
        mword = re.match(r"[A-Za-z0-9_\-\.]+", body[pos:])
        if mword:
            expr_filters.append(Filter(op="text", field=None, value=mword.group(0)))
            pos += mword.end()
            continue
        pos += 1
    search.query = _to_bool_expr(expr_filters)
    pipeline.stages.append(search)
    pipeline.stages.append(FieldsStage(fields=["_time", host_field, "trigger", "severity", "ack", "message"]))
    # 对事件过滤行也输出 need_alert：统计匹配数量
    pipeline.stages.append(StatsStage(funcs=[AggFunc(name="count", field="", alias="cnt")], by=[]))
    pipeline.stages.append(GenericStage(type="generic", params={"raw": "eval need_alert=(cnt > 0)"}))
    pipeline.stages.append(FieldsStage(fields=["need_alert"]))
    return pipeline