import re
from typing import List, Tuple, Optional

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

# --- helpers ---------------------------------------------------------------

def _strip_quotes(s: Optional[str]) -> str:
    if s is None:
        return ""
    s = s.strip()
    if len(s) >= 2 and ((s[0] == '"' and s[-1] == '"') or (s[0] == "'" and s[-1] == "'")):
        return s[1:-1]
    return s


def _is_number(x: Optional[str]) -> bool:
    if x is None:
        return False
    try:
        float(str(x))
        return True
    except Exception:
        return False


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


def _to_bool_expr(filters: List[Filter]) -> BoolExpr:
    return BoolExpr(op='and', children=filters)


def _split_args(text: str) -> List[str]:
    # Split by comma but respect quotes
    parts = []
    cur = []
    in_q = False
    qch = ''
    i = 0
    while i < len(text):
        ch = text[i]
        if in_q:
            cur.append(ch)
            if ch == qch:
                in_q = False
            i += 1
            continue
        if ch in ('"', "'"):
            in_q = True
            qch = ch
            cur.append(ch)
            i += 1
            continue
        if ch == ',':
            parts.append(''.join(cur).strip())
            cur = []
            i += 1
            continue
        cur.append(ch)
        i += 1
    if cur:
        parts.append(''.join(cur).strip())
    return parts

# --- tokenizer -------------------------------------------------------------

TOK_AND = 'AND'
TOK_OR = 'OR'

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

class Term:
    def __init__(self,
                 host: str,
                 item_key: str,
                 item_args: List[str],
                 func: Optional[str],
                 func_args: List[str],
                 cmp_op: str,
                 cmp_val: Optional[str]):
        self.host = host
        self.item_key = item_key
        self.item_args = item_args
        self.func = func
        self.func_args = func_args
        self.cmp_op = cmp_op
        self.cmp_val = cmp_val


def tokenize_expr(text: str) -> List[Tuple[Optional[Term], Optional[str]]]:
    """Tokenize into (Term, connective) pairs; connective is 'AND'/'OR'/None.
    Simple brace-based scanner tailored for Zabbix trigger patterns.
    """
    i = 0
    n = len(text)
    out: List[Tuple[Optional[Term], Optional[str]]] = []
    def skip_ws():
        nonlocal i
        while i < n and text[i].isspace():
            i += 1
    while i < n:
        skip_ws()
        if i >= n:
            break
        if text[i] == '{':
            # parse {host:item}
            i += 1
            start = i
            brace_end = text.find('}', start)
            if brace_end == -1:
                break
            inside = text[start:brace_end]
            i = brace_end + 1
            # host:item[args].optionalFunc(args) within braces
            host, item_full = inside.split(':', 1)
            # Robustly parse key[args].func(args) allowing ']' inside args (e.g., file paths)
            item_key = item_full.strip()
            item_args: List[str] = []
            func_name: Optional[str] = None
            func_args: List[str] = []
            if '[' in item_full and ']' in item_full:
                key, tail = item_full.split('[', 1)
                closing = tail.rfind(']')
                if closing != -1:
                    item_key = key.strip()
                    args_str = tail[:closing]
                    rest = tail[closing+1:]
                    item_args = _split_args(args_str)
                    if rest.startswith('.'):
                        mfunc = re.match(r"^\.(?P<func>[A-Za-z0-9_.]+)\((?P<fargs>[^)]*)\)$", rest)
                        if mfunc:
                            func_name = mfunc.group('func')
                            func_args = _split_args(mfunc.group('fargs') or '')
            # fallback: parse key.func(args) without [args]
            if func_name is None:
                m2 = re.match(r"^(?P<key>[A-Za-z0-9_.]+)\.(?P<func>[A-Za-z0-9_.]+)\((?P<fargs>[^)]*)\)$", item_full)
                if m2:
                    item_key = m2.group('key')
                    func_name = m2.group('func')
                    func_args = _split_args(m2.group('fargs') or '')
            # comparator
            skip_ws()
            cmp_map = {'=': 'eq', '!=': 'neq', '<>': 'neq', '>=': 'gte', '<=': 'lte', '>': 'gt', '<': 'lt'}
            cmp = None
            # check multi-char operators first to avoid partial matches
            for sym in ['>=', '<=', '!=', '<>', '=', '>', '<']:
                if text.startswith(sym, i):
                    cmp = cmp_map[sym]
                    i += len(sym)
                    break
            skip_ws()
            # value until next space or connective
            val_start = i
            while i < n and not text[i].isspace():
                i += 1
            val = text[val_start:i] if cmp else None
            term = Term(host.strip(), item_key.strip(), item_args, func_name, func_args, cmp, val)
            out.append((term, None))
            continue
        # connective
        if text[i:i+3].lower() == 'and':
            out.append((None, TOK_AND))
            i += 3
            continue
        if text[i:i+2].lower() == 'or':
            out.append((None, TOK_OR))
            i += 2
            continue
        i += 1
    return out

# --- IR builder ------------------------------------------------------------

def _build_log_filters(default_repo: Optional[str], host_field: str, file_field: str,
                        host: str, item_key: str, item_args: List[str],
                        func: Optional[str], func_args: List[str]) -> Tuple[SearchStage, List[Filter]]:
    base_filters: List[Filter] = []
    if host:
        base_filters.append(Filter(op='eq', field=host_field, value=str(host)))
    origin = ''
    tokens: List[str] = []
    # item args: [file, token(s), encoding, ..., mode]
    if item_args:
        origin = _strip_quotes(item_args[0])
        if origin:
            base_filters.append(Filter(op='eq', field=file_field, value=origin))
        # find mode token from tail
        mode_idx = None
        for idx in range(len(item_args) - 1, -1, -1):
            pv = _strip_quotes(item_args[idx])
            if pv.upper() in ENCODINGS:
                continue
            if pv.lower().rstrip('()') in ('regexp','iregexp','str','strlen','skip'):
                mode_idx = idx
                break
        pattern_item = ''
        if mode_idx is not None:
            for j in range(mode_idx - 1, 0, -1):
                cand = _strip_quotes(item_args[j])
                if cand and cand.upper() not in ENCODINGS:
                    pattern_item = cand
                    break
        if not pattern_item and len(item_args) > 1:
            cand = _strip_quotes(item_args[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])
    # function args tokens
    if func and func.lower() in ('regexp','iregexp','str') and func_args:
        func_pat = _strip_quotes(','.join(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 tokens
    for t in tokens:
        base_filters.append(Filter(op='text', field=None, value=t))
    # search stage
    s = SearchStage()
    s.repo = default_repo
    s.start = None
    s.query = _to_bool_expr(base_filters)
    return s, base_filters


def _build_metric_filters(default_repo: Optional[str], host_field: str, key_field: str,
                          host: str, item_key: str) -> Tuple[SearchStage, List[Filter]]:
    base_filters: List[Filter] = []
    if host:
        base_filters.append(Filter(op='eq', field=host_field, value=str(host)))
    if item_key:
        base_filters.append(Filter(op='eq', field=key_field, value=str(item_key)))
    s = SearchStage()
    s.repo = default_repo
    s.query = _to_bool_expr(base_filters)
    return s, base_filters


def _build_sub_pipeline(default_repo: Optional[str], host_field: str, file_field: str,
                         seg: Term, cond_alias_name: str) -> Pipeline:
    host, item_key, item_args, func, func_args = seg.host, seg.item_key, seg.item_args, seg.func, seg.func_args
    op = seg.cmp_op
    value = seg.cmp_val
    # base search: choose log vs metric
    is_log_source = (str(item_key or '').lower().startswith('log')) or bool(item_args)
    if is_log_source:
        sub_s, _ = _build_log_filters(default_repo, host_field, file_field, host, item_key, item_args, func, func_args)
    else:
        sub_s, _ = _build_metric_filters(default_repo, host_field, 'key', host, item_key)
    # window from func_args if duration present (first func arg like 5m)
    start = None
    if func_args and re.match(r"^\d+[smhd]$", _strip_quotes(func_args[0] or '')):
        w = _strip_quotes(func_args[0])
        start = f"-{w}" if not str(w).startswith('-') else w
    sub_s.start = start
    sub_p = Pipeline()
    sub_p.stages.append(sub_s)
    # function handling
    f = (func or '').lower()
    if not is_log_source and f == 'last':
        # Metric last(): time series last over _value
        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'timechart span="1m" last(_value) as last_val'}))
        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='last', field='last_val', alias='final_val')], 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}=(final_val {op_sym} {rhs})"}))
    elif not is_log_source and f == 'nodata':
        alias = 'cnt'
        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias=alias)], by=[]))
        if 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 f == '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 f == 'nodata':
        alias = 'cnt'
        sub_p.stages.append(StatsStage(funcs=[AggFunc(name='count', field='', alias=alias)], by=[]))
        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 f 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 f in ('count','avg','min','max','sum'):
        # 统一时间序列聚合：timechart + stats over cnt
        agg_name = 'sum' if f == 'count' else f
        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 f in ('last','prev') and (str(value or '').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 f 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)"}))
    elif f == 'dayofweek':
        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'eval dow=dayofweek(_time)'}))
        op_sym = _op_to_eval(op)
        rhs = str(value)
        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=(dow {op_sym} {rhs})"}))
    elif f == 'time':
        sub_p.stages.append(GenericStage(type='generic', params={'raw': 'eval hhmm=hour(_time)*10000 + minute(_time)*100 + second(_time)'}))
        op_sym = _op_to_eval(op)
        rhs = str(value)
        sub_p.stages.append(GenericStage(type='generic', params={'raw': f"eval {cond_alias_name}=(hhmm {op_sym} {rhs})"}))
    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 API --------------------------------------------------------------

def parse_zabbix_lex(text: str, default_repo: Optional[str] = None) -> Pipeline:
    # Special-case: last()<prev() comparison on same log.count source
    s_body = text.strip()
    m = re.search(r"\{([^:}]+):log\.count\[([^\]]+)\]\.last\(\)\}\s*<\s*\{([^:}]+):log\.count\[([^\]]+)\]\.prev\(\)\}", s_body)
    if m:
        host_l = m.group(1)
        args_l = m.group(2)
        host_r = m.group(3)
        args_r = m.group(4)
        # Require same host and file origin for reliable comparison
        left_origin = _strip_quotes((args_l.split(',')[0] if args_l else '').strip())
        right_origin = _strip_quotes((args_r.split(',')[0] if args_r else '').strip())
        if host_l == host_r and left_origin and left_origin == right_origin:
            pipeline = Pipeline()
            # base search
            s = SearchStage()
            s.repo = default_repo
            s.query = _to_bool_expr([
                Filter(op='eq', field='host', value=str(host_l)),
                Filter(op='eq', field='origin', value=str(left_origin)),
            ])
            pipeline.stages.append(s)
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'timechart span="1m" count() as cnt'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'streamstats current=f window=2 global=f'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'eval prev_val=lag(cnt, 1)'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'where isnotnull(prev_val)'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'eval cond_0=(cnt < prev_val)'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'eval join_tmp=1'}))
            pipeline.stages.append(GenericStage(type='generic', params={'raw': 'eval need_alert=cond_0'}))
            pipeline.stages.append(FieldsStage(fields=['need_alert']))
            return pipeline
    pairs = tokenize_expr(text)
    # collect terms with connectives
    terms: List[Term] = []
    connectors: List[str] = []
    for term, conn in pairs:
        if term:
            terms.append(term)
        if conn:
            connectors.append(conn)
    # fields
    host_field = "host"
    file_field = "origin"
    # build main
    pipeline = Pipeline()
    if not terms:
        return pipeline
    # build first as main
    main = _build_sub_pipeline(default_repo, host_field, file_field, terms[0], 'cond_0')
    main_spl = to_spl(main)
    pipeline.stages.append(GenericStage(type='generic', params={'raw': main_spl}))
    # remaining terms as joins
    for idx in range(1, len(terms)):
        sub = _build_sub_pipeline(default_repo, host_field, file_field, terms[idx], f'cond_{idx}')
        sub_spl = to_spl(sub)
        pipeline.stages.append(GenericStage(type='generic', params={'raw': f'join type="left" join_tmp [{sub_spl}]'}))
    # need_alert expression
    # default connectors: if fewer connectors than terms-1, pad with AND
    while len(connectors) < len(terms) - 1:
        connectors.append(TOK_AND)
    expr_parts = [f'cond_{i}' for i in range(len(terms))]
    expr = expr_parts[0]
    for i, conn in enumerate(connectors, start=1):
        op = 'and' if conn == TOK_AND else 'or'
        expr = f'({expr} {op} cond_{i})'
    pipeline.stages.append(GenericStage(type='generic', params={'raw': f'eval need_alert={expr}'}))
    pipeline.stages.append(FieldsStage(fields=['need_alert']))
    return pipeline