from __future__ import annotations

from contextlib import contextmanager
from itertools import chain
from pathlib import Path
from typing import Any, Callable
import operator
import random
import re

from chinus_tools import yaml, deep_get

__all__ = ["YmlRenderer"]

# 연산자 매핑과 우선순위(긴 연산자 우선)
_OPERATOR_MAP: dict[str, Callable[[Any, Any], bool]] = {
    ">": operator.gt,
    "<": operator.lt,
    ">=": operator.ge,
    "<=": operator.le,
    "==": operator.eq,
    "!=": operator.ne,
    "in": lambda a, b: a in b,
    "not in": lambda a, b: a not in b,
}
_OPERATORS: list[str] = sorted(_OPERATOR_MAP.keys(), key=len, reverse=True)

# 가장 안쪽 괄호를 찾는 정규식
_PAREN_PATTERN = re.compile(r"\(([^()]+)\)")
# 논리 연산자 분리(공백/대소문자에 유연)
_OR_SPLIT = re.compile(r"\s+or\s+", flags=re.IGNORECASE)
_AND_SPLIT = re.compile(r"\s+and\s+", flags=re.IGNORECASE)

_MISSING = object()


class YmlRenderer:
    """
    YAML 파일을 로드하고 데이터 컨텍스트에 따라 조건부 렌더링을 수행하는 클래스.

    주요 기능
    - if/then/elif/else 조건문 처리
    - match/case 구문 처리
    - CHOICES/WEIGHT/K 가중치 기반 랜덤 선택
    - conditions 키를 이용한 렌더링 여부 제어

    :param __yml_root_path: YAML 루트 디렉터리 경로
    :param __data: 렌더링에 사용할 데이터 컨텍스트
    """

    # region: Const
    _IF_KEY, _THEN_KEY, _ELIF_KEY, _ELSE_KEY = "if", "then", "elif", "else"
    _MATCH_KEY, _CASE_KEY = "match", "case"
    _DEFAULT_CASE_KEY = "_"
    _CONDITIONS_KEY = "conditions"
    _TRUE_KEY, _FALSE_KEY = "TRUE", "FALSE"
    _CHOICES_KEY, _WEIGHT_KEY, _K_KEY = "CHOICES", "WEIGHT", "K"
    # endregion

    def __init__(self, __yml_root_path: str | Path, __data: dict | None = None):
        self.data: dict = {} if __data is None else dict(__data)
        self.yml_cache: dict[str, Any] = self._build_yaml_cache(Path(__yml_root_path))

    def _load_and_filter_yaml(self, file_path: Path) -> Any:
        """
        단일 YAML 파일을 로드하고 규칙에 따라 내부 딕셔너리를 필터링합니다.

        규칙
        - 키가 정수이면 포함
        - 키가 문자열이면 '.'으로 시작하지 않는 경우에만 포함

        :param file_path: YAML 파일 경로
        :return: 로드된 객체(필터링 반영)
        :raises ValueError: 파일 로드 또는 파싱 실패
        """
        try:
            loaded_data = yaml.safe_load(file_path)
        except (FileNotFoundError, yaml.YAMLError) as e:
            raise ValueError(f"YAML 파일을 처리할 수 없습니다: {file_path} (원인: {e})") from e

        if isinstance(loaded_data, dict):
            return {
                k: v
                for k, v in loaded_data.items()
                if isinstance(k, int) or (isinstance(k, str) and not k.startswith("."))
            }
        return loaded_data

    def _build_yaml_cache(self, root_dir: Path) -> dict[str, Any]:
        """
        지정된 경로와 하위 디렉터리에서 YAML 파일을 찾아 캐시를 빌드합니다.

        :param root_dir: 루트 디렉터리
        :return: 파일 스템명 → YAML 내용 매핑
        :raises FileNotFoundError: 디렉터리가 아닌 경우
        :raises KeyError: 파일 스템명이 중복될 경우
        :raises ValueError: 개별 YAML 로드 실패
        """
        if not root_dir.is_dir():
            raise FileNotFoundError(f"지정된 경로가 디렉터리가 아닙니다: '{root_dir}'")

        ymls_cache: dict[str, Any] = {}
        yaml_files = chain(root_dir.rglob("*.yaml"), root_dir.rglob("*.yml"))

        for file_path in yaml_files:
            processed = self._load_and_filter_yaml(file_path)
            file_stem = file_path.stem

            if file_stem in ymls_cache:
                raise KeyError(f"중복된 파일 이름(스텀) 발견: '{file_stem}' ({file_path})")

            ymls_cache[file_stem] = processed

        return ymls_cache

    def _evaluate(self, expr_str: str) -> bool:
        """
        조건문 문자열을 평가하여 True/False를 반환합니다.

        - 괄호는 가장 안쪽부터 순차 치환하여 평가합니다.
        - 'or'는 어떤 항목이라도 참이면 참입니다.

        :param expr_str: 조건식 문자열
        :return: 평가 결과
        :raises TypeError: 입력이 문자열이 아닌 경우
        """
        if not isinstance(expr_str, str):
            raise TypeError("조건문은 문자열이어야 합니다.")

        # 괄호 내부부터 재귀 평가
        expr = expr_str
        while "(" in expr:
            expr = _PAREN_PATTERN.sub(lambda m: str(self._evaluate(m.group(1))), expr)

        # or 단위 평가
        return any(self._evaluate_and_clause(part) for part in _OR_SPLIT.split(expr))

    def _evaluate_and_clause(self, and_str: str) -> bool:
        """
        'and' 로 연결된 부분 문자열을 평가합니다. 모두 참이어야 True입니다.

        :param and_str: and 절 문자열
        :return: 평가 결과
        """
        return all(self._evaluate_atomic_expr(part) for part in _AND_SPLIT.split(and_str))

    def _resolve_operand(self, token: str) -> Any:
        """
        좌변/우변 토큰을 실제 값으로 해석합니다.

        우선순위
        1) 데이터 컨텍스트의 변수(딥 키 지원)
        2) 리터럴(리스트/문자열/불리언/숫자)
        3) 미정의 변수면 NameError

        :param token: 피연산자 토큰 문자열
        :return: 해석된 값
        :raises NameError: 변수로도, 해석 가능한 리터럴로도 볼 수 없을 때
        """
        token = token.strip()

        # 1) 변수 조회(딥 키 지원)
        value = deep_get(self.data, token, default=_MISSING)
        if value is not _MISSING:
            return value

        # 2) 리터럴 파싱
        parsed = self._parse_value(token)
        # _parse_value가 원문을 그대로 되돌린 경우(비인용 문자열 등) ⇒ 미정의 변수로 간주
        if parsed == token and not self._looks_like_literal(token):
            raise NameError(f"변수 '{token}'를 찾을 수 없습니다.")
        return parsed

    @staticmethod
    def _looks_like_literal(token: str) -> bool:
        """
        토큰이 인용 문자열/불리언/숫자/리스트처럼 보이는지 간단히 판정합니다.

        :param token: 토큰
        :return: 리터럴로 보이면 True
        """
        t = token.strip()
        if not t:
            return False
        if (t.startswith("'") and t.endswith("'")) or (t.startswith('"') and t.endswith('"')):
            return True
        low = t.lower()
        if low in {"true", "false"}:
            return True
        if t.startswith("[") and t.endswith("]"):
            return True
        try:
            int(t)
            return True
        except ValueError:
            try:
                float(t)
                return True
            except ValueError:
                return False

    def _evaluate_atomic_expr(self, atomic_str: str) -> bool:
        """
        'a > 10', 'role in roles', 'not X', 'rand 30%' 등의 최소 단위 표현식을 평가합니다.

        :param atomic_str: 원자 표현식
        :return: 평가 결과
        :raises NameError: 미정의 변수 접근 시
        :raises TypeError, ValueError: 타입 부적합 등
        """
        s = atomic_str.strip()
        if not s:
            return False

        # rand p%
        if s.lower().startswith("rand "):
            value_str = s.split(maxsplit=1)[1].strip()
            value_str = value_str.removesuffix("%").strip()
            probability = float(value_str) / 100.0
            return random.random() < probability

        # not <expr>
        if s.lower().startswith("not "):
            return not self._evaluate_atomic_expr(s[4:])

        # 이항 연산자
        for op in _OPERATORS:
            # 공백 유무에 강건하게 분리
            padded = f" {s} "
            needle = f" {op} "
            if needle in padded:
                left, right = [p.strip() for p in s.split(op, 1)]
                left_val = self._resolve_operand(left)
                right_val = self._resolve_operand(right)
                return _OPERATOR_MAP[op](left_val, right_val)

        # 단항(값 자체의 truthy 여부)
        return bool(self._parse_value(s))

    def _parse_value(self, val_str: str) -> Any:
        """
        문자열 값을 실제 Python 타입으로 변환합니다.

        지원
        - 리스트: "['warrior', 10]"
        - 인용 문자열: 'text' 또는 "text"
        - 불리언: true/false (대소문자 무관)
        - 숫자: int → float 순으로 시도
        - 변수: self.data.get(name, name)로 폴백(좌변 해석은 _resolve_operand를 사용)

        :param val_str: 원문 문자열
        :return: 파싱된 값
        """
        s = val_str.strip()

        # 1) 리스트
        if s.startswith("[") and s.endswith("]"):
            inner = s[1:-1].strip()
            if not inner:
                return []
            # 간단한 쉼표 분리(중첩/인용 복잡도는 고려하지 않음)
            return [self._parse_value(part) for part in inner.split(",")]

        # 2) 인용 문자열
        if (s.startswith("'") and s.endswith("'")) or (s.startswith('"') and s.endswith('"')):
            return s[1:-1]

        # 3) 불리언
        low = s.lower()
        if low == "true":
            return True
        if low == "false":
            return False

        # 4) 숫자
        try:
            return int(s)
        except ValueError:
            try:
                return float(s)
            except ValueError:
                # 5) 변수처럼 보일 수 있으나, 일반 값 파싱 컨텍스트에서는 보수적으로 반환
                return self.data.get(s, s)

    def _should_render(self, value: Any) -> bool:
        """
        값 렌더링 여부를 결정합니다.

        규칙
        - None: 렌더링
        - dict가 아니거나 conditions 키 없음: 렌더링
        - 그 외: conditions 평가

        :param value: 검사할 값
        :return: 렌더링 여부
        """
        if value is None:
            return True
        if not isinstance(value, dict) or self._CONDITIONS_KEY not in value:
            return True
        return self._check_conditions(value[self._CONDITIONS_KEY])

    def _check_conditions(self, conditions: dict[str, Any]) -> bool:
        """
        conditions 블록을 평가합니다.

        조건 키 예시
        - "flag": TRUE/FALSE/값 또는 리스트
        - "not flag": 위와 동일하나 부정
        - 딥 키 경로 허용

        :param conditions: 조건 딕셔너리
        :return: 모든 조건 통과 시 True
        :raises ValueError: 형식 오류
        """
        if not isinstance(conditions, dict):
            raise ValueError("조건은 딕셔너리 형태여야 합니다.")

        for cond_key, required in conditions.items():
            is_not = cond_key.startswith("not ")
            var_name = cond_key.removeprefix("not ")

            current_value = deep_get(self.data, var_name, default=_MISSING)
            if current_value is _MISSING:
                return False

            if required == self._TRUE_KEY:
                is_match = bool(current_value)
            elif required == self._FALSE_KEY:
                is_match = not bool(current_value)
            else:
                req_list = required if isinstance(required, list) else [required]
                is_match = current_value in req_list

            # 긍정은 is_match True, 부정은 is_match False가 통과 조건
            if is_not == is_match:
                return False

        return True

    def _render_if_then_else(self, cache: dict[str, Any]) -> Any:
        """
        if/then/elif/else 블록을 처리합니다.

        :param cache: 블록 딕셔너리
        :return: 렌더링 결과 또는 None
        :raises ValueError, NameError, TypeError: 조건 평가 중 발생한 오류
        """
        try:
            if self._evaluate(cache[self._IF_KEY]):
                return self._render(cache[self._THEN_KEY])

            for elif_block in cache.get(self._ELIF_KEY, []):
                if self._evaluate(elif_block[self._IF_KEY]):
                    return self._render(elif_block[self._THEN_KEY])

            if self._ELSE_KEY in cache:
                return self._render(cache[self._ELSE_KEY])

            return None
        except (ValueError, NameError, TypeError) as e:
            condition = cache.get(self._IF_KEY) or next(
                (b.get(self._IF_KEY) for b in cache.get(self._ELIF_KEY, [])), "N/A"
            )
            raise type(e)(f"조건문 '{condition}' 평가 중 오류: {e}") from e

    def _render_match_case(self, cache: dict[str, Any]) -> Any:
        """
        match/case 블록을 처리합니다.

        :param cache: 블록 딕셔너리
        :return: 렌더링 결과
        :raises TypeError: 형식 오류
        """
        match_var_name = cache[self._MATCH_KEY]
        if not isinstance(match_var_name, str):
            raise TypeError(f"'match' 값은 변수 이름(문자열)이어야 합니다: {match_var_name}")

        value_to_match = deep_get(self.data, match_var_name, default=_MISSING)
        if value_to_match is _MISSING:
            value_to_match = self.data.get(match_var_name)

        cases = cache.get(self._CASE_KEY, {})
        if not isinstance(cases, dict):
            raise TypeError(f"'case' 값은 딕셔너리여야 합니다: {cases}")

        case = cases.get(value_to_match, cases.get(self._DEFAULT_CASE_KEY, {}))
        return self._render(case)

    def _render_choices(self, cache: dict[str, Any]) -> Any:
        """
        CHOICES/WEIGHT/K 블록을 처리하여 가중치 기반 랜덤 선택을 수행합니다.

        :param cache: 블록 딕셔너리
        :return: 선택 결과(K==1이면 단일 값, 그 외 리스트)
        :raises TypeError, ValueError: 형식 오류
        """
        choices = self._render(cache.get(self._CHOICES_KEY))
        weights = cache.get(self._WEIGHT_KEY)
        k = cache.get(self._K_KEY, 1)

        if not choices:
            return None

        if isinstance(choices, dict):
            choices = [{key: value} for key, value in choices.items()]

        if not isinstance(choices, list):
            # 리스트가 아니면 그대로 반환
            return choices

        if not isinstance(k, int) or k < 0:
            raise TypeError(f"'K' 값은 0 이상의 정수여야 합니다: {k}")

        if weights is not None:
            if not isinstance(weights, list):
                raise TypeError("'WEIGHT' 값은 리스트여야 합니다.")
            if len(weights) != len(choices):
                raise ValueError("CHOICES와 WEIGHT의 길이가 일치해야 합니다.")

        results = random.choices(choices, weights=weights, k=k)
        return results[0] if k == 1 else results

    def _render(self, node: Any) -> Any:
        """
        YAML 노드를 재귀적으로 처리합니다.

        :param node: 처리 대상 노드(dict/list/단일 값)
        :return: 렌더링 결과
        """
        if isinstance(node, list):
            return [self._render(item) for item in node]

        if isinstance(node, dict):
            if self._IF_KEY in node and self._THEN_KEY in node:
                return self._render_if_then_else(node)
            if self._MATCH_KEY in node and self._CASE_KEY in node:
                return self._render_match_case(node)
            if self._CHOICES_KEY in node:
                return self._render_choices(node)

            # 일반 딕셔너리: 조건 필터 후 재귀 렌더
            return {
                key: self._render(value)
                for key, value in node.items()
                if self._should_render(value)
            }

        return node

    @contextmanager
    def _temporary_data(self, new_data: dict | None):
        """
        임시로 self.data를 변경하고, 블록 종료 시 원복합니다.

        :param new_data: 임시 데이터(없으면 변경 없음)
        """
        if new_data is None:
            yield
            return

        original_data = self.data
        self.data = new_data
        try:
            yield
        finally:
            self.data = original_data

    def render(self, key_path: str, temp_data: dict | None = None) -> Any:
        """
        주어진 키 경로의 YAML 노드를 렌더링합니다.

        :param key_path: 딥 키 경로(예: "root.section.key")
        :param temp_data: 임시 데이터 컨텍스트
        :return: 렌더링 결과
        """
        with self._temporary_data(temp_data):
            rendering = self._render(deep_get(self.yml_cache, key_path, {}))
        return rendering

    def random_render(self, key_path: str, temp_data: dict | None = None) -> Any:
        """
        렌더링 결과에서 무작위 요소를 반환합니다.

        규칙
        - dict: 무작위 키 하나를 선택하여 {key: value} 반환
        - list/tuple: 무작위 요소 반환
        - 그 외: 그대로 반환

        :param key_path: 딥 키 경로
        :param temp_data: 임시 데이터 컨텍스트
        :return: 무작위 선택 결과
        """
        rendering = self.render(key_path, temp_data=temp_data)

        match rendering:
            case dict() as d:
                key = random.choice(list(d.keys()))
                return {key: d.get(key)}
            case list() | tuple() as seq:
                return random.choice(seq)
            case _:
                return rendering