r"""
AutoPOE2 Pickit DSL Parser (Phase P2 - Doc 29 mục 9)
Phân tích cú pháp các quy tắc lọc đồ .ipd thành cấu trúc AST và danh sách PickitRule.
Cú pháp:
  [KEY] OP "VALUE" (&&|\|\|) ... # [KEY] OP "VALUE" ...
  - Bên trái dấu '#': Pre-condition (trên sàn, chưa ID).
  - Bên phải dấu '#': Post-condition và các hành động (Stash, Run, Sell, Salvage...).
  - '//': Chú thích dòng hoặc trailing comment.
  - '//[' hoặc '//!': Quy tắc bị vô hiệu hóa (disabled).
"""

from __future__ import annotations
import re
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Union, Dict, Set


class PickitParseError(SyntaxError):
    """Lỗi cú pháp khi phân tích file luật .ipd."""
    pass


@dataclass
class Clause:
    """Mệnh đề so sánh đơn: [KEY] OP "VALUE" """
    key: str
    op: str
    value: str

    def __repr__(self) -> str:
        return f"[{self.key}] {self.op} \"{self.value}\""


@dataclass
class WeightedSumClause:
    """Mệnh đề tổng có trọng số: [WeightedSum] 2*[mod1] + 3*[mod2] OP "VALUE" hoặc [WeightedSum(mod1:2,mod2:3)] OP "VALUE" """
    terms: List[Tuple[float, str]]  # list of (weight, key)
    op: str
    value: float

    def __repr__(self) -> str:
        t_str = " + ".join(f"{w}*[{k}]" for w, k in self.terms)
        return f"[WeightedSum] {t_str} {self.op} \"{self.value}\""


@dataclass
class BinaryOp:
    """Toán tử logic kết hợp: left && right hoặc left || right"""
    op: str  # '&&' hoặc '||'
    left: ASTNode
    right: ASTNode

    def __repr__(self) -> str:
        return f"({self.left} {self.op} {self.right})"


ASTNode = Union[Clause, WeightedSumClause, BinaryOp]


@dataclass
class PickitRule:
    """Một quy tắc lọc đồ .ipd hoàn chỉnh."""
    line_num: int
    raw_line: str
    enabled: bool
    pre_raw: str
    post_raw: str
    pre_ast: Optional[ASTNode] = None
    post_ast: Optional[ASTNode] = None
    actions: Set[str] = field(default_factory=set)
    requires_post_mods: bool = False

    def __repr__(self) -> str:
        status = "ENABLED" if self.enabled else "DISABLED"
        return f"<PickitRule line={self.line_num} {status} actions={self.actions}>"


# Danh mục các key hành động hợp lệ (Post-Actions) chuẩn Doc 29 mục 9.4
KNOWN_ACTIONS = {
    "StashItem", "StashUnid", "SellItem", "SalvageItem", "DisenchantItem",
    "Sell", "Salvage", "Disenchant", "IgnoreRitual",
    "RunMap", "IgnoreMap", "UpgradeToRare", "Augment", "Exalt", "Corrupt", "UseTablet"
}

# Regex tokenizing
CLAUSE_RE = re.compile(
    r'\[(?P<key>[^\]]+)\]\s*(?P<op>==|!=|<=|>=|<|>)\s*"(?P<val>[^"]*)"'
)
WEIGHTED_SUM_FUNC_RE = re.compile(
    r'\[WeightedSum\((?P<args>[^)]+)\)\]\s*(?P<op>==|!=|<=|>=|<|>)\s*"(?P<val>[^"]*)"'
)
WEIGHTED_SUM_RE = re.compile(
    r'\[WeightedSum\]\s*(?P<expr>[^=!<>\n]+)\s*(?P<op>==|!=|<=|>=|<|>)\s*"(?P<val>[^"]*)"'
)
WEIGHT_TERM_RE = re.compile(
    r'([+-]?\s*\d+(?:\.\d+)?)\s*\*\s*\[([^\]]+)\]'
)


def _parse_expression(expr_str: str, line_num: int) -> Optional[ASTNode]:
    """Phân tích biểu thức điều kiện (chuỗi các clause kết hợp bởi && hoặc ||)."""
    s = expr_str.strip()
    if not s:
        return None

    # Tách chuỗi theo toán tử || ở mức cao nhất (or-level)
    or_parts = [p.strip() for p in s.split("||")]
    if len(or_parts) > 1:
        current_node = _parse_expression(or_parts[0], line_num)
        for part in or_parts[1:]:
            next_node = _parse_expression(part, line_num)
            if current_node and next_node:
                current_node = BinaryOp("||", current_node, next_node)
        return current_node

    # Tách chuỗi theo toán tử && (and-level)
    and_parts = [p.strip() for p in s.split("&&")]
    if len(and_parts) > 1:
        current_node = _parse_expression(and_parts[0], line_num)
        for part in and_parts[1:]:
            next_node = _parse_expression(part, line_num)
            if current_node and next_node:
                current_node = BinaryOp("&&", current_node, next_node)
        return current_node

    # Đơn biểu thức: WeightedSum dạng hàm [WeightedSum(stat1:w1,stat2:w2)]
    w_func_match = WEIGHTED_SUM_FUNC_RE.search(s)
    if w_func_match:
        args_str = w_func_match.group("args")
        op = w_func_match.group("op")
        val_str = w_func_match.group("val")
        terms: List[Tuple[float, str]] = []
        for term_item in args_str.split(","):
            term_item = term_item.strip()
            if not term_item:
                continue
            if ":" in term_item:
                k, w_str = term_item.split(":", 1)
                terms.append((float(w_str.strip()), k.strip()))
            elif "*" in term_item:
                w_str, k = term_item.split("*", 1)
                terms.append((float(w_str.strip()), k.strip()))
            else:
                terms.append((1.0, term_item))
        if not terms:
            raise PickitParseError(f"Dòng {line_num}: Không tìm thấy terms hợp lệ trong [WeightedSum(...)]: {s}")
        try:
            val = float(val_str)
        except ValueError:
            raise PickitParseError(f"Dòng {line_num}: Giá trị [WeightedSum] phải là số: '{val_str}'")
        return WeightedSumClause(terms=terms, op=op, value=val)

    # Đơn biểu thức: WeightedSum dạng biểu thức [WeightedSum] w1*[stat1] + w2*[stat2]
    w_match = WEIGHTED_SUM_RE.search(s)
    if w_match:
        terms_str = w_match.group("expr")
        op = w_match.group("op")
        val_str = w_match.group("val")
        terms: List[Tuple[float, str]] = []
        for term_match in WEIGHT_TERM_RE.finditer(terms_str):
            w = float(term_match.group(1).replace(" ", ""))
            k = term_match.group(2).strip()
            terms.append((w, k))
        if not terms:
            raise PickitParseError(f"Dòng {line_num}: Không tìm thấy terms hợp lệ trong [WeightedSum]: {s}")
        try:
            val = float(val_str)
        except ValueError:
            raise PickitParseError(f"Dòng {line_num}: Giá trị [WeightedSum] phải là số: '{val_str}'")
    # Tách chuỗi theo dấu phẩy ',' (nếu không nằm trong ngoặc đơn của WeightedSum)
    # Hỗ trợ danh sách actions phân tách bởi phẩy như: [UpgradeToRare], [RunMap]
    if "," in s and "(" not in s:
        comma_parts = [p.strip() for p in s.split(",") if p.strip()]
        if len(comma_parts) > 1:
            current_node = _parse_expression(comma_parts[0], line_num)
            for part in comma_parts[1:]:
                next_node = _parse_expression(part, line_num)
                if current_node and next_node:
                    current_node = BinaryOp("&&", current_node, next_node)
            return current_node

    c_match = CLAUSE_RE.search(s)
    if c_match:
        key = c_match.group("key").strip()
        op = c_match.group("op").strip()
        val = c_match.group("val")
        return Clause(key=key, op=op, value=val)

    # Hỗ trợ cú pháp shorthand action: [Action] (ví dụ: [IgnoreMap], [RunMap], [UseTablet])
    bare_match = re.match(r'^\[(?P<action>[^\]]+)\]$', s)
    if bare_match:
        act = bare_match.group("action").strip()
        if act in KNOWN_ACTIONS:
            return Clause(key=act, op="==", value="true")

    raise PickitParseError(f"Dòng {line_num}: Cú pháp mệnh đề không hợp lệ: '{s}'")


def _extract_actions(node: Optional[ASTNode]) -> Tuple[Set[str], bool]:
    """
    Rút trích danh sách action và kiểm tra xem rule có yêu cầu post mods hay không.
    """
    actions = set()
    requires_post_mods = False

    if node is None:
        return actions, requires_post_mods

    stack = [node]
    while stack:
        curr = stack.pop()
        if isinstance(curr, Clause):
            if curr.key in KNOWN_ACTIONS:
                if curr.op == "==" and curr.value.lower() == "true":
                    actions.add(curr.key)
            else:
                # Nếu có mệnh đề không phải action ở phía post-ID -> cần mods
                requires_post_mods = True
        elif isinstance(curr, WeightedSumClause):
            requires_post_mods = True
        elif isinstance(curr, BinaryOp):
            stack.append(curr.left)
            stack.append(curr.right)

    return actions, requires_post_mods


def parse_line(raw_line: str, line_num: int) -> Optional[PickitRule]:
    """Phân tích 1 dòng text .ipd thành PickitRule (hoặc None nếu dòng trống/comment thuần)."""
    line = raw_line.strip()
    if not line:
        return None

    # Kiểm tra trạng thái kích hoạt
    enabled = True
    if line.startswith("//[") or line.startswith("//!"):
        enabled = False
        line = line[2:].strip()
    elif line.startswith("//"):
        # Chú thích đơn thuần
        return None

    # Loại bỏ trailing comment
    # Cần cẩn thận không cắt nhầm chuỗi trong nháy kép
    if "//" in line:
        parts = line.split("//")
        # Nếu dấu // nằm ngoài nháy kép
        reconstructed = ""
        in_quote = False
        for ch in line:
            if ch == '"':
                in_quote = not in_quote
            elif ch == '/' and not in_quote:
                # Kiểm tra ký tự kế
                break
            reconstructed += ch
        line = reconstructed.strip()

    if not line:
        return None

    # Bắt buộc phải có dấu '#' phân tách pre và post
    if "#" not in line:
        raise PickitParseError(
            f"Dòng {line_num}: Thiếu dấu '#' phân tách pre-identify và post-identify: '{raw_line}'"
        )

    parts = line.split("#", 1)
    pre_str = parts[0].strip()
    post_str = parts[1].strip()

    if not pre_str and not post_str:
        return None

    pre_ast = _parse_expression(pre_str, line_num) if pre_str else None
    post_ast = _parse_expression(post_str, line_num) if post_str else None

    actions, requires_post_mods = _extract_actions(post_ast)

    return PickitRule(
        line_num=line_num,
        raw_line=raw_line,
        enabled=enabled,
        pre_raw=pre_str,
        post_raw=post_str,
        pre_ast=pre_ast,
        post_ast=post_ast,
        actions=actions,
        requires_post_mods=requires_post_mods,
    )


def parse_ipd(content: str) -> List[PickitRule]:
    """Phân tích toàn bộ nội dung file .ipd thành danh sách các rule."""
    rules: List[PickitRule] = []
    lines = content.splitlines()
    for idx, raw_line in enumerate(lines, start=1):
        rule = parse_line(raw_line, idx)
        if rule is not None:
            rules.append(rule)
    return rules
