#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：ui_context.py
模块描述：UI 用例执行上下文 — 维护用例级上下文变量，支持模板变量渲染

语义化用例执行机制的一部分：
- 用例参数（parameters）在 execute_case 入口初始化为上下文变量
- 步骤 params / semantic 目标 / api_call 请求体在 execute_step 渲染
- 支持两种占位符：
    - 内置生成器（复用 TemplateResolver）：{__NOW__} / {__NOW_+N_H__} /
      {__RANDOM_NAME_N__} / {__UUID__} / {__RANDOM_INT_N_M__} / {__INDEX__} / {__REF:task:key__}
    - 上下文变量：{__CTX:key__} 引用用例参数或前面步骤 save_as 写入的值

渲染原则：不含 "__" 的字符串直接返回（老用例零影响）；
未知占位符保留原文；渲染返回新容器，不修改原模板。

作者：czj
创建日期：2026-08-18
"""

import re
from typing import Any, Dict, Optional

from app.executors.template_resolver import TemplateResolver

logger = __import__("logging").getLogger(__name__)


# 上下文变量占位符：{__CTX:key__}
CTX_PATTERN = re.compile(r"\{__CTX:([^}]+?)__\}")


class ExecutionContext:
    """
    用例级执行上下文

    维护变量表（初始化为用例 parameters + 内置变量），
    提供递归渲染：dict/list/str 中的占位符 → 实际值。

    Usage:
        >>> ctx = ExecutionContext({"meetingName": "自动化-{__RANDOM_NAME_8__}"})
        >>> ctx.render("主题：{__CTX:meetingName__}")
        '主题：自动化-aBcD1234'
        >>> ctx.set("createdId", "meeting_123")
        >>> ctx.get("createdId")
        'meeting_123'
    """

    def __init__(self, parameters: Optional[Dict[str, Any]] = None):
        """
        Args:
            parameters: 用例参数（来自 case["parameters"]）
                        其值中的占位符在初始化时渲染（支持跨键引用，迭代收敛）
        """
        self._resolver = TemplateResolver()
        self._values: Dict[str, Any] = dict(parameters or {})

        # 内置变量缺省值（execute_case 入口会覆盖为真实值）
        self._values.setdefault("caseName", "")
        self._values.setdefault("caseId", "")

        # 渲染 parameters 自身的占位符（如 {"meetingName": "自动化-{__RANDOM_NAME_8__}"}）
        # 两阶段，避免 CTX 引用的值被二次独立渲染生成不一致的随机值：
        #   阶段 A：渲染所有键的"内置生成器"（{__NOW__}/{__RANDOM_NAME_N__} 等），
        #           {__CTX:key__} 占位符保留原样，作为 CTX 查找表。
        #   阶段 B：仅做 {__CTX:key__} 替换（不调用生成器），引用的是阶段 A 已渲染值。
        #           迭代最多 5 轮：支持前向引用（引用定义在后面的键）跨轮收敛。
        lookup: Dict[str, Any] = {
            key: self._render_value(value, {})  # 空查找表：生成器解析，CTX 保留
            for key, value in self._values.items()
        }
        for _ in range(5):
            new_lookup: Dict[str, Any] = dict(lookup)  # 以本轮起点作查找表：前向引用跨轮收敛
            changed = False
            for key, value in lookup.items():
                substituted = self._substitute_recursive(value, new_lookup)
                new_lookup[key] = substituted
                if substituted != value:
                    changed = True
            lookup = new_lookup
            if not changed:
                break
        self._values = lookup

    # ---------- 变量读写 ----------

    def set(self, key: str, value: Any) -> None:
        """写入上下文变量（save_as 使用）"""
        self._values[key] = value

    def get(self, key: str) -> Optional[Any]:
        """读取上下文变量；不存在返回 None"""
        return self._values.get(key)

    def all(self) -> Dict[str, Any]:
        """返回当前全部上下文变量快照（调试用）"""
        return dict(self._values)

    # ---------- 渲染 ----------

    def render(self, template: Any) -> Any:
        """
        递归渲染模板中的占位符，返回新容器（不修改原模板）

        先由 TemplateResolver 处理内置生成器（{__NOW__} 等），
        再替换 {__CTX:key__} 上下文变量。

        Args:
            template: dict/list/str，支持嵌套

        Returns:
            渲染后的 dict/list/str
        """
        return self._render_value(template)

    def _render_value(self, value: Any, lookup: Optional[Dict[str, Any]] = None) -> Any:
        """单值递归渲染；lookup 为 CTX 变量查找表（缺省用 self._values）"""
        if lookup is None:
            lookup = self._values
        if isinstance(value, dict):
            return {k: self._render_value(v, lookup) for k, v in value.items()}
        if isinstance(value, list):
            return [self._render_value(v, lookup) for v in value]
        if isinstance(value, str) and "__" in value:
            # 1. 内置生成器（CTX 占位符会被 TemplateResolver 原样保留）
            resolved = self._resolver.resolve(value)
            if not isinstance(resolved, str):
                return resolved
            # 2. 上下文变量替换
            return self._substitute_ctx(resolved, lookup)
        return value

    @staticmethod
    def _substitute_ctx(text: str, lookup: Dict[str, Any]) -> str:
        """替换 {__CTX:key__} → 上下文变量值；未知键保留原文"""

        def _repl(match: "re.Match[str]") -> str:
            key = match.group(1)
            if key in lookup:
                return str(lookup[key])
            return match.group(0)  # 未知键保留占位符，调用方记录警告

        return CTX_PATTERN.sub(_repl, text)

    @staticmethod
    def _substitute_recursive(value: Any, lookup: Dict[str, Any]) -> Any:
        """
        仅递归替换 {__CTX:key__}（不调用生成器）

        用于 parameters 初始化阶段 B：保证被引用的值已是阶段 A 渲染结果，
        避免已渲染随机值被二次独立重新生成导致引用不一致。
        """
        if isinstance(value, dict):
            return {k: ExecutionContext._substitute_recursive(v, lookup) for k, v in value.items()}
        if isinstance(value, list):
            return [ExecutionContext._substitute_recursive(v, lookup) for v in value]
        if isinstance(value, str):
            return ExecutionContext._substitute_ctx(value, lookup)
        return value
