from typing import List, Union

from .ir import (
    Pipeline,
    Stage,
    SearchStage,
    FieldsStage,
    StatsStage,
    SortStage,
    LimitStage,
    FilterStage,
    BoolExpr,
    Filter,
    AggFunc,
)


def _quote(value: str) -> str:
    if value is None:
        return ''
    if any(ch.isspace() for ch in value) or '"' in value or '|' in value:
        escaped = value.replace('"', '\\"')
        return '"' + escaped + '"'
    return value


def _quote_always(value: str) -> str:
    v = '' if value is None else str(value)
    escaped = v.replace('"', '\\"')
    return '"' + escaped + '"'


def _is_expression(value: str) -> bool:
    try:
        v = str(value)
    except Exception:
        return False
    # Heuristic: treat as expression only if it clearly looks like one
    # - function calls: word followed by '('
    # - arithmetic between tokens (alnum/underscore) with operators
    # - presence of parentheses
    import re
    if '(' in v or ')' in v:
        return True
    if re.search(r"\b\w+\(", v):
        return True
    # Treat arithmetic as expression only when operator has whitespace around it (avoid flagging file paths like a/b)
    if re.search(r"[A-Za-z0-9_]\s+[-+*/]\s+[A-Za-z0-9_]", v):
        return True
    return False


def _prec(op: str) -> int:
    return 3 if op == 'not' else 2 if op == 'and' else 1


def render_filter_atom(atom: Union[Filter, BoolExpr], parent_op: str = None) -> str:
    if isinstance(atom, Filter):
        if atom.op in ("eq", "neq", "gt", "gte", "lt", "lte") and atom.field is not None:
            op_map = {
                "eq": "=",
                "neq": "!=",
                "gt": ">",
                "gte": ">=",
                "lt": "<",
                "lte": "<=",
            }
            lhs = f"'{atom.field}'"
            rhs = str(atom.value) if _is_expression(atom.value) else _quote_always(atom.value)
            return f"{lhs} {op_map[atom.op]} {rhs}"
        elif atom.op == "text":
            return _quote_always(str(atom.value))
        elif atom.op == "in" and atom.field is not None:
            vals = ",".join([_quote_always(str(v)) for v in (atom.value or [])])
            return f"in({atom.field}, {vals})"
        elif atom.op == "regex" and atom.field is not None:
            return f"regex({atom.field}, {_quote_always(str(atom.value))})"
        else:
            if atom.field:
                return f"{atom.field}={_quote(str(atom.value))}"
            return _quote(str(atom.value))
    else:
        # BoolExpr
        if atom.op == "not":
            inner = render_filter_expr(atom.children[0], parent_op=atom.op) if atom.children else ''
            return f"NOT ({inner})".strip()
        if atom.op == "and":
            # Group text filters to be concatenated by spaces; structured by AND
            structured_parts: List[str] = []
            text_parts: List[str] = []
            for ch in atom.children:
                if isinstance(ch, Filter) and ch.op == "text":
                    text_parts.append(render_filter_atom(ch, parent_op=atom.op))
                else:
                    structured_parts.append(render_filter_atom(ch, parent_op=atom.op) if isinstance(ch, Filter) else render_filter_expr(ch, parent_op=atom.op))
            groups: List[str] = []
            if structured_parts:
                groups.append(" AND ".join([p for p in structured_parts if p]))
            if text_parts:
                groups.append(" ".join([p for p in text_parts if p]))
            joined = " AND ".join([g for g in groups if g])
            if parent_op and _prec(atom.op) < _prec(parent_op):
                return f"({joined})"
            return joined
        sep = " AND " if atom.op == "and" else " OR "
        parts = [render_filter_atom(ch, parent_op=atom.op) if isinstance(ch, Filter) else render_filter_expr(ch, parent_op=atom.op) for ch in atom.children]
        joined = sep.join([p for p in parts if p])
        if parent_op and _prec(atom.op) < _prec(parent_op):
            return f"({joined})"
        return joined


def render_filter_expr(expr: BoolExpr, parent_op: str = None) -> str:
    return render_filter_atom(expr, parent_op=parent_op)


def render_search2(stage: SearchStage) -> str:
    parts: List[str] = ["search2"]
    if stage.start:
        parts.append(f'start="{stage.start}"')
    if stage.end:
        parts.append(f'end="{stage.end}"')
    if stage.repo:
        parts.append(f'repo="{stage.repo}"')
    if stage.query:
        q = render_filter_expr(stage.query)
        if q:
            parts.append(q)
    return ' '.join(parts)


def render_fields(stage: FieldsStage) -> str:
    return "fields " + ",".join([f.strip() for f in stage.fields])


def render_stats(stage: StatsStage) -> str:
    func_tokens: List[str] = []
    for f in stage.funcs:
        tok = f"{f.name}(" + (f.field or '') + ")"
        if f.alias:
            tok += f" as {f.alias}"
        func_tokens.append(tok)
    parts = ["stats", ", ".join(func_tokens) if func_tokens else ""]
    if stage.by:
        parts.append("by " + ",".join(stage.by))
    return ' '.join([p for p in parts if p])


def render_sort(stage: SortStage) -> str:
    pairs = [f"{f} {o}" for (f, o) in stage.by]
    size = getattr(stage, 'params', {}).get('size')
    if pairs:
        return f"sort {size} by " + ", ".join(pairs) if size else "sort by " + ", ".join(pairs)
    return f"sort {size}" if size else "sort"


def render_limit(stage: LimitStage) -> str:
    return f"limit {stage.size}"


def render_filter(stage: FilterStage) -> str:
    body = render_filter_expr(stage.expr) if stage.expr else ""
    header = "where"  # SPL uses 'where' not 'filter'
    return f"{header} " + body if body else header


def to_spl(pipeline: Pipeline) -> str:
    out: List[str] = []
    for st in pipeline.stages:
        if isinstance(st, SearchStage):
            out.append(render_search2(st))
        elif isinstance(st, FieldsStage):
            out.append(render_fields(st))
        elif isinstance(st, StatsStage):
            out.append(render_stats(st))
        elif isinstance(st, SortStage):
            out.append(render_sort(st))
        elif isinstance(st, LimitStage):
            out.append(render_limit(st))
        elif isinstance(st, FilterStage):
            out.append(render_filter(st))
        else:
            # Fallback to raw generic; avoid duplicating head
            raw = getattr(st, 'params', {}).get('raw', '')
            out.append(raw or getattr(st, 'type', 'generic'))
    return ' | '.join(out)