"""Shared lazy proxies, adapter factories, and utility functions."""

import base64
import time

from common.capabilities import (
    ACTIONS_REQUIRING_EXTRA_VALUE,
    GLOBAL_ACTIONS,
    SUPPORTED_ACTIONS,
)


class _LazyProxy:
    def __init__(self, loader):
        object.__setattr__(self, "_loader", loader)
        object.__setattr__(self, "_value", None)

    def _load(self):
        value = object.__getattribute__(self, "_value")
        if value is None:
            value = object.__getattribute__(self, "_loader")()
            object.__setattr__(self, "_value", value)
        return value

    def __getattr__(self, name):
        return getattr(self._load(), name)

    def __setattr__(self, name, value):
        if name in {"_loader", "_value"}:
            object.__setattr__(self, name, value)
            return
        setattr(self._load(), name, value)


def _load_config_module():
    import config.config as _config

    return _config


def _load_log_object():
    from common.logs import log as _log

    return _log


config = _LazyProxy(_load_config_module)
log = _LazyProxy(_load_log_object)
UIExecutor = None
get_actual_element = None
StepHistoryManager = None
run_preflight = None
RunReporter = None
compress_web_dom = None
compress_android_xml = None
compress_ios_xml = None
AndroidU2Adapter = None
IosWdaAdapter = None
WebPlaywrightAdapter = None
AutonomousBrain = None
load_workflow_file = None
WorkflowLoadError = None
parse_workflow_var_overrides = None
resolve_workflow_definition = None
SUPPORTED_INLINE_ACTIONS = SUPPORTED_ACTIONS
GLOBAL_INLINE_ACTIONS = GLOBAL_ACTIONS
INLINE_ACTIONS_REQUIRING_EXTRA_VALUE = ACTIONS_REQUIRING_EXTRA_VALUE


def _slug_to_test_name(label: str) -> str:
    """Turn a human goal/action label into a valid pytest function name.

    Keeps unicode word chars (PEP 3131 allows unicode identifiers, so Chinese
    goals survive instead of being stripped to nothing); collapses everything
    else to single underscores. Always prefixed with `test_` so pytest collects
    it, and guarded so a symbols-only label can't yield a bare `test_`.
    """
    import re
    import unicodedata

    # Lowercase ASCII for conventional pytest names; unicode word chars (e.g.
    # Chinese) pass through .lower() unchanged and stay as-is. NFKC folds
    # compatibility chars so "½"/"²" don't survive as non-identifier \w chars.
    normalized = unicodedata.normalize("NFKC", str(label).strip().lower())
    slug = re.sub(r"\W+", "_", normalized, flags=re.UNICODE).strip("_")
    if not slug:
        return "test_auto_generated_case"
    # A leading digit is a valid word char but not a valid identifier start.
    if slug[0].isdigit():
        slug = f"case_{slug}"
    name = f"test_{slug}"
    # Final guard: \w matches some chars (fractions, superscripts) that are NOT
    # valid Python identifier chars. If the slug still isn't a clean identifier,
    # fall back rather than emit a file that won't parse.
    if not name.isidentifier():
        return "test_auto_generated_case"
    return name


def get_initial_header(label: str | None = None) -> list:
    # NOTE: no `import pytest` here — no generated step references pytest, so
    # emitting it makes every generated file fail ruff's F401 (unused import).
    # allure + log are both actually used by the step bodies.
    #
    # label (optional): the user's goal / action / workflow name. When given,
    # the test is named + documented after it so the generated file is
    # discoverable instead of an opaque `test_auto_generated_case`. None keeps
    # the legacy name (main.py interactive recording + public-surface contract).
    import re as _re

    func_name = _slug_to_test_name(label) if label else "test_auto_generated_case"
    safe_label = str(label).strip() if label else "AI Auto-recorded Scenario"
    # Collapse ALL control chars (incl. \r and NUL) to spaces FIRST — an
    # unescaped \r or \0 in a string literal makes the emitted file unparseable.
    safe_label = _re.sub(r"[\x00-\x1f\x7f]", " ", safe_label).strip() or "AI Auto-recorded Scenario"
    # allure.story / docstring are plain string literals — escape backslash+quote.
    story = safe_label.replace("\\", "\\\\").replace("'", "\\'")
    doc = safe_label.replace("\\", "\\\\").replace('"', '\\"')
    return [
        "# -*- coding: utf-8 -*-\n",
        "# Auto-generated by ScreenForge AI Agent\n",
        "import allure\n\n",
        "from common.logs import log\n\n",
        "@allure.feature('Core Business Flow')\n",
        f"@allure.story('{story}')\n",
        f"def {func_name}(d):\n",
        f'    """{doc}\n\n'
        '    Replay auto-recorded UI steps. For Web, param d is the Playwright page object."""\n',
    ]


def save_to_disk(file_path: str, content: list) -> None:
    import os
    os.makedirs(os.path.dirname(file_path), exist_ok=True)
    temp_path = file_path + ".tmp"
    with open(temp_path, "w", encoding="utf-8") as f:
        f.writelines(content)
    os.replace(temp_path, file_path)


def launch_app(device, env_name="dev", system="android"):
    app_target = config.APP_ENV_CONFIG.get(env_name, {}).get(system)
    if not app_target:
        return
    if system == "android":
        device.app_start(app_target)
        log.info(f"✅ Android App launched: {app_target}")
    elif system == "web":
        device.goto(app_target)
        log.info(f"✅ Web browser navigated to: {app_target}")


def _ensure_executor_runtime() -> None:
    global UIExecutor, get_actual_element
    if UIExecutor is None or get_actual_element is None:
        from common.executor import (
            UIExecutor as _UIExecutor,
        )
        from common.executor import (
            get_actual_element as _get_actual_element,
        )

        if UIExecutor is None:
            UIExecutor = _UIExecutor
        if get_actual_element is None:
            get_actual_element = _get_actual_element


def _ensure_history_manager() -> None:
    global StepHistoryManager
    if StepHistoryManager is None:
        from common.history_manager import StepHistoryManager as _StepHistoryManager

        StepHistoryManager = _StepHistoryManager


def _ensure_preflight_runner() -> None:
    global run_preflight
    if run_preflight is None:
        from common.preflight import run_preflight as _run_preflight

        run_preflight = _run_preflight


def _ensure_reporter_class() -> None:
    global RunReporter
    if RunReporter is None:
        from common.run_reporter import RunReporter as _RunReporter

        RunReporter = _RunReporter


def _ensure_ui_compressors() -> None:
    global compress_web_dom, compress_android_xml, compress_ios_xml
    if compress_web_dom is None:
        from utils.utils_web import compress_web_dom as _compress_web_dom

        compress_web_dom = _compress_web_dom
    if compress_android_xml is None:
        from utils.utils_xml import compress_android_xml as _compress_android_xml

        compress_android_xml = _compress_android_xml
    if compress_ios_xml is None:
        from utils.utils_ios import compress_ios_xml as _compress_ios_xml

        compress_ios_xml = _compress_ios_xml


def _ensure_adapter_factories() -> None:
    global AndroidU2Adapter, IosWdaAdapter, WebPlaywrightAdapter
    if (
        AndroidU2Adapter is None
        or IosWdaAdapter is None
        or WebPlaywrightAdapter is None
    ):
        from common.adapters import (
            AndroidU2Adapter as _AndroidU2Adapter,
        )
        from common.adapters import (
            IosWdaAdapter as _IosWdaAdapter,
        )
        from common.adapters import (
            WebPlaywrightAdapter as _WebPlaywrightAdapter,
        )

        if AndroidU2Adapter is None:
            AndroidU2Adapter = _AndroidU2Adapter
        if IosWdaAdapter is None:
            IosWdaAdapter = _IosWdaAdapter
        if WebPlaywrightAdapter is None:
            WebPlaywrightAdapter = _WebPlaywrightAdapter


def _ensure_runtime_classes() -> None:
    global AutonomousBrain
    if AutonomousBrain is None:
        from common.ai_autonomous import AutonomousBrain as _AutonomousBrain

        AutonomousBrain = _AutonomousBrain


def _ensure_workflow_loader() -> None:
    global load_workflow_file
    global WorkflowLoadError
    global parse_workflow_var_overrides
    global resolve_workflow_definition
    if (
        load_workflow_file is None
        or WorkflowLoadError is None
        or parse_workflow_var_overrides is None
        or resolve_workflow_definition is None
    ):
        from common.workflow_schema import (
            WorkflowLoadError as _WorkflowLoadError,
        )
        from common.workflow_schema import (
            load_workflow_file as _load_workflow_file,
        )
        from common.workflow_schema import (
            parse_workflow_var_overrides as _parse_workflow_var_overrides,
        )
        from common.workflow_schema import (
            resolve_workflow_definition as _resolve_workflow_definition,
        )

        if load_workflow_file is None:
            load_workflow_file = _load_workflow_file
        if WorkflowLoadError is None:
            WorkflowLoadError = _WorkflowLoadError
        if parse_workflow_var_overrides is None:
            parse_workflow_var_overrides = _parse_workflow_var_overrides
        if resolve_workflow_definition is None:
            resolve_workflow_definition = _resolve_workflow_definition


def _create_adapter(platform: str, args=None):
    _ensure_adapter_factories()
    if platform == "android":
        adapter = AndroidU2Adapter()
        if args and getattr(args, "device_serial", ""):
            adapter._serial = args.device_serial
        return adapter
    if platform == "ios":
        adapter = IosWdaAdapter()
        if args and getattr(args, "device_url", ""):
            adapter._wda_url = args.device_url
        if args and getattr(args, "device_serial", ""):
            adapter._udid = args.device_serial
        return adapter
    if platform == "web":
        return WebPlaywrightAdapter()
    raise ValueError(f"Unsupported platform: {platform}")


def _connect_adapter(args, reporter):
    adapter = _create_adapter(args.platform, args)
    adapter.setup()
    device = adapter.driver
    log.info(f"✅ {args.platform} platform connected and initialized")
    try:
        launch_app(device, args.env, args.platform)
        reporter.emit_event("adapter_ready", platform=args.platform)
        return adapter
    except Exception:
        try:
            adapter.teardown()
        except Exception as e:
            log.warning(f"⚠️ [Warning] Cleanup failed during teardown: {e}")
        raise


def current_url(adapter, platform: str) -> str:
    """Web page URL for agent-facing payloads; "" off-web or on error.

    `adapter.driver` is the Playwright page (exposes `.url`); mobile adapters
    have no URL concept, so we honestly return "".
    """
    if platform != "web":
        return ""
    try:
        return adapter.driver.url or ""
    except Exception:
        return ""


def _wait_for_platform_idle(platform: str, device) -> None:
    try:
        if platform == "android":
            device.wait_activity(device.app_current()["activity"], timeout=3)
        elif platform == "web":
            device.wait_for_load_state("domcontentloaded")
    except Exception:
        time.sleep(1)


def _capture_ui_state(args, adapter, reporter, step_index: int):
    device = adapter.driver
    _wait_for_platform_idle(args.platform, device)
    _ensure_ui_compressors()

    ui_json = "{}"
    if args.platform == "android":
        try:
            ui_json = compress_android_xml(device.dump_hierarchy())
        except Exception as e:
            log.warning(f"⚠️ Failed to capture UI tree: {e}")
    elif args.platform == "ios":
        try:
            ui_json = compress_ios_xml(device.source())
        except Exception as e:
            log.warning(f"⚠️ Failed to capture iOS UI tree: {e}")
    elif args.platform == "web":
        try:
            ui_json = compress_web_dom(device)
        except Exception as e:
            log.warning(f"⚠️ Failed to capture Web DOM: {e}")

    screenshot_base64 = None
    if args.vision:
        try:
            img_bytes = adapter.take_screenshot()
            reporter.save_screenshot(img_bytes, step_index)
            screenshot_base64 = base64.b64encode(img_bytes).decode("utf-8")
            log.info("📸 Screenshot captured, sending to vision model.")
        except Exception as e:
            log.warning(f"⚠️ Screenshot failed, falling back to text-only UI tree: {e}")

    return ui_json, screenshot_base64


class _SharedAdapterManager:
    """Shared adapter manager for MCP sessions.

    Each platform adapter is created once and reused across requests.
    Call teardown_all() on MCP server exit to clean up.
    """

    def __init__(self):
        self._adapters: dict[str, object] = {}
        # One UIExecutor per platform, created lazily alongside the adapter and
        # living exactly as long as it. The executor owns the web ref cache, so
        # an inspect_ui and a follow-up `ref @N` action in the same MCP session
        # share state via get_executor() — without any process-global.
        self._executors: dict[str, object] = {}

    def get_or_create(self, platform: str, env: str = "dev"):
        if platform in self._adapters:
            adapter = self._adapters[platform]
            log.info(f"♻️ [System] Reusing existing {platform} adapter")
            return adapter
        from cli.parser import build_parser
        from cli.tool_protocol_handlers import _NullRunReporter

        parser = build_parser()
        args = parser.parse_args([])
        args.platform = platform
        args.env = env
        adapter = _connect_adapter(args, _NullRunReporter())
        self._adapters[platform] = adapter
        return adapter

    def get_executor(self, platform: str, env: str = "dev"):
        """Return the shared UIExecutor for a platform, creating it (and its
        adapter) on first use. Reused across inspect_ui / action requests so the
        ref cache survives between them within one MCP session."""
        _ensure_executor_runtime()
        adapter = self.get_or_create(platform, env)
        existing = self._executors.get(platform)
        if existing is not None:
            # Re-sync to the live driver: if the adapter reconnected or made a
            # new page, adapter.driver may have changed under us. Cheap defensive
            # rebind so the cached executor never drives a stale/closed handle.
            existing.d = adapter.driver
            return existing
        executor = UIExecutor(adapter.driver, platform=platform)
        self._executors[platform] = executor
        return executor

    def teardown_all(self):
        for platform, adapter in self._adapters.items():
            try:
                adapter.teardown()
                log.info(f"✅ [System] Cleaned up {platform} adapter")
            except Exception as e:
                log.warning(f"⚠️ [Warning] Failed to clean up {platform} adapter: {e}")
        self._adapters.clear()
        self._executors.clear()
