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

from chinus_tools import yaml, deep_get

__all__ = ['YmlRenderer']

# 표준 라이브러리 'operator' 및 람다를 사용하여 연산자 함수를 매핑
_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,
}
# 연산자 우선순위를 위해 긴 연산자('not in')부터 확인하도록 정렬
_OPERATORS: list[str] = sorted(_OPERATOR_MAP.keys(), key=len, reverse=True)
# 가장 안쪽 괄호를 찾는 정규식. 미리 컴파일하여 성능 향상
_PAREN_PATTERN = re.compile(r'\(([^()]+)\)')


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

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

    # 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 = {}):
        self.data: dict = __data
        self.yml_cache: dict = self._build_yaml_cache(Path(__yml_root_path))

    def _load_and_filter_yaml(self, file_path: Path) -> Any | None:
        """
        단일 YAML 파일을 로드하고, 규칙에 따라 내부 딕셔너리를 필터링합니다.
        - 키가 정수이면 포함합니다.
        - 키가 문자열이면 '.'으로 시작하지 않는 경우에만 포함합니다.
        """
        try:
            loaded_data = yaml.safe_load(file_path)

            if isinstance(loaded_data, dict):
                return {
                    k: v for k, v in loaded_data.items()
                    # 'isinstance' 검사를 튜플로 묶어 더 깔끔하게 처리
                    if isinstance(k, int) or (isinstance(k, str) and not k.startswith('.'))
                }
            # 딕셔너리가 아니면 (예: 리스트) 그대로 반환
            return loaded_data

        except (FileNotFoundError, yaml.YAMLError) as e:
            # 오류 발생 시 콘솔에 메시지를 출력하고 None을 반환
            print(f"오류: '{file_path}' 파일을 처리할 수 없습니다. 원인: {e}")
            return None

    def _build_yaml_cache(self, root_path: Path | str):
        """지정된 경로와 하위 디렉터리에서 YAML 파일을 찾아 캐시를 빌드합니다."""
        if not root_path.is_dir():
            raise FileNotFoundError(f"지정된 경로 '{root_path}'가 디렉터리가 아닙니다.")

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

        for root_path in yaml_files:
            processed_yml = self._load_and_filter_yaml(root_path)

            if processed_yml is not None:
                # file_path.stem은 파일명에서 확장자를 제거한 부분을 반환합니다.
                file_stem = root_path.stem

                if file_stem in ymls_cache:
                    raise KeyError(f"경고: 중복된 파일 이름('{file_stem}')을 발견했습니다.")

                ymls_cache[file_stem] = processed_yml
            else:
                raise ValueError(f'{root_path}')

        return ymls_cache

    def _evaluate(self, expr_str: str) -> bool:
        """조건문 문자열을 평가하여 True/False를 반환합니다."""
        if not isinstance(expr_str, str):
            raise TypeError("조건문은 문자열이어야 합니다.")

        while '(' in expr_str:
            expr_str = _PAREN_PATTERN.sub(
                lambda match: str(self._evaluate(match.group(1))),
                expr_str
            )

        return any(self._evaluate_and_clause(part) for part in expr_str.split(' or '))

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

    def _evaluate_atomic_expr(self, atomic_str: str) -> bool:
        """'a > 10', 'role in roles' 같은 가장 작은 단위의 표현식을 평가합니다."""
        atomic_str = atomic_str.strip()

        if atomic_str.lower().startswith('rand '):
            # "rand 31.425%" 에서 숫자 부분 추출
            value_str = atomic_str.split(maxsplit=1)[1]
            # '%' 기호 제거 (Python 3.9+), 하위 버전에서는 .rstrip('%')
            value_str = value_str.removesuffix('%').strip()
            # 퍼센트를 0~1 사이의 float으로 변환
            probability = float(value_str) / 100.0
            # 확률 비교 실행
            return random.random() < probability

        elif atomic_str.lower().startswith('not '):
            return not self._evaluate_atomic_expr(atomic_str[4:])

        for op in _OPERATORS:
            # 연산자 양옆에 공백이 없어도 분리할 수 있도록 수정
            if f' {op} ' in f' {atomic_str} ':
                var_name, value_str = [p.strip() for p in atomic_str.split(op, 1)]
                actual_value = self._parse_value(var_name)  # 변수명도 _parse_value를 통해 값 가져오기

                # ### 변경됨: var_name이 변수가 아닐 수도 있으므로 get 대신 직접 조회 ###
                if actual_value is None and var_name not in self.data:
                    # 값을 파싱하려다 실패한 것이 아니라, 정말 없는 변수일 때만 에러 발생
                    try:
                        float(var_name)  # 숫자인지 체크
                    except (ValueError, TypeError):
                        raise NameError(f"변수 '{var_name}'를 찾을 수 없습니다.")

                target_value = self._parse_value(value_str)
                return _OPERATOR_MAP[op](actual_value, target_value)

        return bool(self._parse_value(atomic_str))

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

        # 1. 리스트 (e.g., "['warrior', 10]")
        if val_str.startswith('[') and val_str.endswith(']'):
            list_contents = val_str[1:-1].strip()
            if not list_contents:
                return []
            # 재귀적으로 각 항목을 파싱하여 리스트 생성
            return [self._parse_value(item) for item in list_contents.split(',')]

        # 2. 따옴표로 묶인 문자열
        if (val_str.startswith("'") and val_str.endswith("'")) or \
                (val_str.startswith('"') and val_str.endswith('"')):
            return val_str[1:-1]

        # 3. 불리언
        val_lower = val_str.lower()
        if val_lower == 'true': return True
        if val_lower == 'false': return False

        # 4. 숫자 (정수 -> 실수 순)
        try:
            return int(val_str)
        except ValueError:
            try:
                return float(val_str)
            except ValueError:
                # 5. 변수 또는 변환 실패 시 원본 문자열
                return self.data.get(val_str, val_str)

    def _should_render(self, value):
        """값이 렌더링되어야 하는지 여부를 결정하는 헬퍼 함수"""
        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:
        """
        조건 확인용 함수
        """
        if not isinstance(conditions, dict):
            raise ValueError("조건은 딕셔너리 형태여야 합니다.")

        for cond_key, required_value in conditions.items():
            # 1. 키 파싱
            is_not_condition = cond_key.startswith('not ')
            var_name = cond_key.removeprefix('not ')

            if not deep_get(self.data, var_name):
                return False

            current_value = self.data[var_name]

            # 2. 매치 여부(is_match) 계산 로직 통합
            if required_value == self._TRUE_KEY:
                is_match = bool(current_value)
            elif required_value == self._FALSE_KEY:
                is_match = not bool(current_value)
            else:
                # required_value가 리스트가 아니더라도 'in' 연산을 위해 리스트로 감싸줌
                req_list = required_value if isinstance(required_value, list) else [required_value]
                is_match = current_value in req_list

            # 3. 조건 판정 로직 간소화 (XOR 활용)
            # - 긍정 조건(is_not=False)은 is_match가 True여야 통과
            # - 부정 조건(is_not=True)은 is_match가 False여야 통과
            # 즉, is_not_condition과 is_match가 같으면 실패(False)
            if is_not_condition == is_match:
                return False

        return True  # 모든 조건을 통과

    def _render_if_then_else(self, cahce: dict[str, Any]) -> Any:
        """'if-then-elif-else' 구조를 처리합니다."""
        try:
            if self._evaluate(cahce[self._IF_KEY]):
                return self._render(cahce[self._THEN_KEY])

            for elif_block in cahce.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 cahce:
                return self._render(cahce[self._ELSE_KEY])

            return None  # 모든 조건 불일치 시
        except (ValueError, NameError, TypeError) as e:
            condition = cahce.get(self._IF_KEY) or next((b.get(self._IF_KEY) for b in cahce.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' 구조를 처리합니다."""

        match_var_name = cache[self._MATCH_KEY]
        if not isinstance(match_var_name, str):
            raise TypeError(f"'match' 값은 변수 이름(문자열)이어야 합니다: {match_var_name}")

        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, cahce: dict[str, Any]) -> Any:
        """'CHOICES-WEIGHT-K' 구조를 처리하여 가중치 기반 랜덤 선택을 수행합니다."""
        choices = self._render(cahce.get(self._CHOICES_KEY))
        weights = cahce.get(self._WEIGHT_KEY)
        k = cahce.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}")

        results = random.choices(choices, weights=weights, k=k)

        # 3. K 값에 따라 반환 형식 결정
        if k == 1:
            return results[0] if results else None
        else:
            return results

    def _render(self, cache: dict | list) -> dict | list:
        """
        YAML 내의 조건문을 재귀적으로 처리합니다.
        """
        if isinstance(cache, list):
            return [self._render(item) for item in cache]

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

            return {
                key: self._render(value)
                for key, value in cache.items()
                if self._should_render(value)
            }

        return cache

    @contextmanager
    def _temporary_data(self, new_data: dict | None):
        """임시로 self.data를 변경하고, 끝나면 자동으로 복원하는 컨텍스트 매니저."""
        if new_data is None:
            # 변경할 데이터가 없으면 아무것도 하지 않고 즉시 실행 권한을 넘깁니다.
            yield
            return

        original_data = self.data
        self.data = new_data
        try:
            yield  # with 블록 안의 코드가 여기서 실행됩니다.
        finally:
            self.data = original_data  # with 블록이 끝나면 (성공하든 실패하든) 실행됩니다.

    def render(self, name: str, temp_data: dict = None):
        with self._temporary_data(temp_data):
            rendering = self._render(self.yml_cache.get(name, {}))
        return rendering
