"""
Валидатор схем UIRouter — проверка целостности ссылок, достижимости сцен и т.д.
"""

from collections import deque
from collections.abc import Generator
from enum import StrEnum
from typing import Any

from pydantic import BaseModel

from .schema import (
    ActionInstruction,
    ActionType,
    Button,
    DynamicKeyboard,
    EventType,
    GotoSceneAction,
    Keyboard,
    MessageActionBase,
    STATELESS_EVENT_TYPES,
    Scene,
    SendMessageAction,
    EditMessageAction,
    SendPhotoAction,
    SendVideoAction,
    SendDocumentAction,
    UIRouter,
)


class ValidationSeverity(StrEnum):
    ERROR = "error"
    WARNING = "warning"
    INFO = "info"


class ValidationCategory(StrEnum):
    BROKEN_SCENE_REF = "broken_scene_ref"
    BROKEN_CALLBACK_REF = "broken_callback_ref"
    BROKEN_GLOBAL_REF = "broken_global_ref"
    BROKEN_PARENT_REF = "broken_parent_ref"
    BROKEN_ALLOWED_REF = "broken_allowed_ref"
    UNREACHABLE_SCENE = "unreachable_scene"
    DEAD_END_SCENE = "dead_end_scene"
    UNUSED_GLOBAL_HANDLER = "unused_global_handler"
    BROKEN_FUNCTION_REF = "broken_function_ref"
    BROKEN_EVENT_REF = "broken_event_ref"
    SCHEMA_PROTECTION = "schema_protection"
    INVALID_THROTTLE = "invalid_throttle"
    INVALID_WEB_APP_URL = "invalid_web_app_url"
    INCOMPATIBLE_ACTION = "incompatible_action"
    EMPTY_ACTION_CONTENT = "empty_action_content"


class ValidationIssue(BaseModel):
    severity: ValidationSeverity
    category: ValidationCategory
    message: str
    path: str


class ValidationReport(BaseModel):
    issues: list[ValidationIssue]

    @property
    def errors(self) -> list[ValidationIssue]:
        return [i for i in self.issues if i.severity == ValidationSeverity.ERROR]

    @property
    def warnings(self) -> list[ValidationIssue]:
        return [i for i in self.issues if i.severity == ValidationSeverity.WARNING]

    @property
    def infos(self) -> list[ValidationIssue]:
        return [i for i in self.issues if i.severity == ValidationSeverity.INFO]

    @property
    def is_valid(self) -> bool:
        return len(self.errors) == 0


class SchemaValidator:
    """Валидатор UIRouter схемы"""

    def validate(self, schema: UIRouter, *, registry: Any = None) -> ValidationReport:
        issues: list[ValidationIssue] = []
        scene_ids = {s.id for s in schema.scenes}
        scene_map = {s.id: s for s in schema.scenes}

        self._check_broken_scene_refs(schema, scene_ids, issues)
        self._check_broken_callback_refs(schema, issues)
        self._check_broken_global_refs(schema, issues)
        self._check_broken_parent_refs(schema, scene_ids, issues)
        self._check_broken_allowed_refs(schema, scene_ids, issues)
        self._check_unreachable_scenes(schema, scene_ids, scene_map, issues)
        self._check_dead_end_scenes(schema, issues)
        self._check_unused_global_handlers(schema, issues)
        self._check_broken_function_refs(schema, registry, issues)
        self._check_broken_event_refs(schema, issues)
        self._check_stateless_event_types(schema, issues)
        self._check_action_event_compatibility(schema, issues)
        self._check_fallback_handlers(schema, issues)
        self._check_throttle_config(schema, issues)
        self._check_web_app_urls(schema, issues)
        self._check_empty_action_content(schema, issues)

        return ValidationReport(issues=issues)

    def _check_broken_scene_refs(
        self,
        schema: UIRouter,
        scene_ids: set[str],
        issues: list[ValidationIssue],
    ) -> None:
        for action, path in self._iter_all_actions(schema):
            if isinstance(action, GotoSceneAction) and action.scene_id and action.scene_id not in scene_ids:
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.ERROR,
                        category=ValidationCategory.BROKEN_SCENE_REF,
                        message=f"GOTO_SCENE references non-existent scene '{action.scene_id}'",
                        path=path,
                    )
                )

    def _check_broken_callback_refs(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for scene in schema.scenes:
            handler_names = {h.name for h in scene.handlers if h.event_type == EventType.CALLBACK}
            for btn, path in self._iter_scene_buttons(scene):
                if btn.callback_action and btn.callback_action not in handler_names:
                    msg = f"Button callback_action '{btn.callback_action}' not found in scene '{scene.id}' handlers"
                    issues.append(
                        ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.BROKEN_CALLBACK_REF,
                            message=msg,
                            path=path,
                        )
                    )

    def _check_broken_global_refs(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        global_names = {h.name for h in schema.global_handlers}
        for btn, path in self._iter_all_buttons(schema):
            if btn.callback_global and btn.callback_global not in global_names:
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.ERROR,
                        category=ValidationCategory.BROKEN_GLOBAL_REF,
                        message=f"Button callback_global '{btn.callback_global}' not found in global_handlers",
                        path=path,
                    )
                )

    def _check_broken_parent_refs(
        self,
        schema: UIRouter,
        scene_ids: set[str],
        issues: list[ValidationIssue],
    ) -> None:
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.ERROR,
                category=ValidationCategory.BROKEN_PARENT_REF,
                message=f"parent_scene '{scene.parent_scene}' does not exist",
                path=f"scenes[{scene.id}].parent_scene",
            )
            for scene in schema.scenes
            if scene.parent_scene and scene.parent_scene not in scene_ids
        )

    def _check_broken_allowed_refs(
        self,
        schema: UIRouter,
        scene_ids: set[str],
        issues: list[ValidationIssue],
    ) -> None:
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.ERROR,
                category=ValidationCategory.BROKEN_ALLOWED_REF,
                message=f"allowed_scenes references non-existent scene '{ref}'",
                path=f"scenes[{scene.id}].allowed_scenes",
            )
            for scene in schema.scenes
            for ref in scene.allowed_scenes
            if ref not in scene_ids
        )

    def _check_unreachable_scenes(
        self,
        schema: UIRouter,
        scene_ids: set[str],
        scene_map: dict[str, Scene],
        issues: list[ValidationIssue],
    ) -> None:
        graph: dict[str, set[str]] = {sid: set() for sid in scene_ids}

        for scene in schema.scenes:
            targets = self._extract_goto_targets_from_scene(scene)
            graph[scene.id].update(targets & scene_ids)

        global_targets: set[str] = set()
        for gh in schema.global_handlers:
            for action in gh.actions:
                if isinstance(action, GotoSceneAction) and action.scene_id:
                    global_targets.add(action.scene_id)
        for eh in schema.event_handlers:
            for action in eh.actions:
                if isinstance(action, GotoSceneAction) and action.scene_id:
                    global_targets.add(action.scene_id)
        global_targets &= scene_ids

        for sid in scene_ids:
            graph[sid].update(global_targets)

        visited: set[str] = set()
        queue: deque[str] = deque([schema.initial_scene])
        visited.add(schema.initial_scene)

        while queue:
            current = queue.popleft()
            for neighbor in graph.get(current, set()):
                if neighbor not in visited:
                    visited.add(neighbor)
                    queue.append(neighbor)

        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.UNREACHABLE_SCENE,
                message=f"Scene '{sid}' is unreachable from initial_scene '{schema.initial_scene}'",
                path=f"scenes[{sid}]",
            )
            for sid in scene_ids - visited
        )

    def _check_dead_end_scenes(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for scene in schema.scenes:
            has_navigation = any(
                action.type in {ActionType.GOTO_SCENE, ActionType.BACK}
                for action, _ in self._iter_scene_actions(scene)
            )
            if not has_navigation:
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.INFO,
                        category=ValidationCategory.DEAD_END_SCENE,
                        message=f"Scene '{scene.id}' has no GOTO_SCENE or BACK actions (dead end)",
                        path=f"scenes[{scene.id}]",
                    )
                )

    def _check_unused_global_handlers(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        used_globals: set[str] = set()
        for btn, _ in self._iter_all_buttons(schema):
            if btn.callback_global:
                used_globals.add(btn.callback_global)

        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.INFO,
                category=ValidationCategory.UNUSED_GLOBAL_HANDLER,
                message=f"Global handler '{gh.name}' (callback) is not referenced by any button",
                path=f"global_handlers[{gh.name}]",
            )
            for gh in schema.global_handlers
            if gh.event_type == EventType.CALLBACK and gh.name not in used_globals
        )

    def _check_broken_function_refs(
        self,
        schema: UIRouter,
        registry: Any,
        issues: list[ValidationIssue],
    ) -> None:
        if registry is None:
            return

        registered = self._collect_registered_names(registry)
        self._check_condition_refs(schema, registered["conditions"], issues)
        self._check_getter_refs(schema, registered["getters"], issues)
        self._check_validator_refs(schema, registered["validators"], issues)

    def _collect_registered_names(self, registry: Any) -> dict[str, set[str]]:
        result: dict[str, set[str]] = {"conditions": set(), "getters": set(), "validators": set()}
        for key in result:
            sub = getattr(registry, key, None)
            if sub and hasattr(sub, "list_registered"):
                result[key] = set(sub.list_registered())
        return result

    def _check_condition_refs(self, schema: UIRouter, registered: set[str], issues: list[ValidationIssue]) -> None:
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.BROKEN_FUNCTION_REF,
                message=f"Condition '{cond_name}' is not registered",
                path=f"scenes[{scene.id}].handlers[{handler.name}].conditions",
            )
            for scene in schema.scenes
            for handler in scene.handlers
            for cond_name in handler.conditions
            if cond_name not in registered
        )
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.BROKEN_FUNCTION_REF,
                message=f"Condition '{cond_name}' is not registered",
                path=f"global_handlers[{gh.name}].conditions",
            )
            for gh in schema.global_handlers
            for cond_name in gh.conditions
            if cond_name not in registered
        )

    def _check_getter_refs(self, schema: UIRouter, registered: set[str], issues: list[ValidationIssue]) -> None:
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.BROKEN_FUNCTION_REF,
                message=f"Getter '{flag.getter.function}' is not registered",
                path=f"scenes[{scene.id}].flags[{flag.name}].getter",
            )
            for scene in schema.scenes
            for flag in scene.flags
            if flag.getter.type == "function" and flag.getter.function and flag.getter.function not in registered
        )
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.BROKEN_FUNCTION_REF,
                message=f"Getter '{flag.getter.function}' is not registered",
                path=f"global_flags[{flag.name}].getter",
            )
            for flag in schema.global_flags
            if flag.getter.type == "function" and flag.getter.function and flag.getter.function not in registered
        )

    def _check_validator_refs(self, schema: UIRouter, registered: set[str], issues: list[ValidationIssue]) -> None:
        issues.extend(
            ValidationIssue(
                severity=ValidationSeverity.WARNING,
                category=ValidationCategory.BROKEN_FUNCTION_REF,
                message=f"Validator '{scene.input_validator}' is not registered",
                path=f"scenes[{scene.id}].input_validator",
            )
            for scene in schema.scenes
            if scene.input_validator and scene.input_validator not in registered
        )

    def _check_broken_event_refs(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        defined_events = {e.name for e in schema.events}
        for i, eh in enumerate(schema.event_handlers):
            if eh.event_name not in defined_events:
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.ERROR,
                        category=ValidationCategory.BROKEN_EVENT_REF,
                        message=f"EventHandler references undefined event '{eh.event_name}'",
                        path=f"event_handlers[{i}]",
                    )
                )

    def _check_stateless_event_types(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for gh in schema.global_handlers:
            for j, action in enumerate(gh.actions):
                if action.type == ActionType.ANSWER_INLINE_QUERY and gh.event_type != EventType.INLINE_QUERY:
                    issues.append(
                        ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.BROKEN_SCENE_REF,
                            message=(
                                f"Global handler '{gh.name}' uses answer_inline_query "
                                f"but event_type is '{gh.event_type.value}' (requires inline_query)"
                            ),
                            path=f"global_handlers[{gh.name}].actions[{j}]",
                        )
                    )

            if gh.event_type not in STATELESS_EVENT_TYPES:
                continue
            for j, action in enumerate(gh.actions):
                if action.type in {ActionType.GOTO_SCENE, ActionType.BACK}:
                    issues.append(
                        ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.BROKEN_SCENE_REF,
                            message=(
                                f"Global handler '{gh.name}' uses {action.type.value} "
                                f"but event_type '{gh.event_type.value}' does not support scene navigation"
                            ),
                            path=f"global_handlers[{gh.name}].actions[{j}]",
                        )
                    )

        for scene in schema.scenes:
            issues.extend(ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.BROKEN_SCENE_REF,
                            message=(
                                f"Scene handler '{handler.name}' uses event_type "
                                f"'{handler.event_type.value}' which is only valid in global_handlers"
                            ),
                            path=f"scenes[{scene.id}].handlers[{handler.name}]",
                        ) for handler in scene.handlers if handler.event_type in STATELESS_EVENT_TYPES)

    _COMMON_ACTIONS: set[ActionType] = {
        ActionType.BUSINESS_ACTION, ActionType.CUSTOM,
        ActionType.SET_VARIABLE, ActionType.EMIT_EVENT, ActionType.SCHEDULE_EVENT,
    }

    _EVENT_ALLOWED_ACTIONS: dict[EventType, set[ActionType]] = {
        EventType.MESSAGE: {
            ActionType.SEND_MESSAGE, ActionType.GOTO_SCENE,
            ActionType.BACK, ActionType.SAVE_INPUT, ActionType.CLEAR_DATA,
            ActionType.DELETE_MESSAGE, ActionType.SEND_PHOTO, ActionType.SEND_VIDEO,
            ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.CALLBACK: {
            ActionType.SEND_MESSAGE, ActionType.EDIT_MESSAGE, ActionType.GOTO_SCENE,
            ActionType.BACK, ActionType.ANSWER_CALLBACK, ActionType.CLEAR_DATA,
            ActionType.DELETE_MESSAGE, ActionType.SEND_PHOTO, ActionType.SEND_VIDEO,
            ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.EDITED_MESSAGE: {
            ActionType.SEND_MESSAGE, ActionType.EDIT_MESSAGE, ActionType.GOTO_SCENE,
            ActionType.BACK, ActionType.SAVE_INPUT, ActionType.CLEAR_DATA,
            ActionType.DELETE_MESSAGE, ActionType.SEND_PHOTO, ActionType.SEND_VIDEO,
            ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.SCENE_ENTER: {
            ActionType.SEND_MESSAGE, ActionType.EDIT_MESSAGE, ActionType.GOTO_SCENE,
            ActionType.BACK, ActionType.CLEAR_DATA, ActionType.DELETE_MESSAGE,
            ActionType.SEND_PHOTO, ActionType.SEND_VIDEO, ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.SCENE_EXIT: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
        } | _COMMON_ACTIONS,
        EventType.INLINE_QUERY: {
            ActionType.ANSWER_INLINE_QUERY,
        } | _COMMON_ACTIONS,
        EventType.CHOSEN_INLINE_RESULT: _COMMON_ACTIONS,
        EventType.MY_CHAT_MEMBER: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
            ActionType.SEND_PHOTO, ActionType.SEND_VIDEO, ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.CHAT_MEMBER: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
            ActionType.SEND_PHOTO, ActionType.SEND_VIDEO, ActionType.SEND_DOCUMENT,
        } | _COMMON_ACTIONS,
        EventType.CHAT_BOOST: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
        } | _COMMON_ACTIONS,
        EventType.REMOVED_CHAT_BOOST: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
        } | _COMMON_ACTIONS,
        EventType.PRE_CHECKOUT_QUERY: {
            ActionType.SEND_MESSAGE, ActionType.CLEAR_DATA,
        } | _COMMON_ACTIONS,
        EventType.CHAT_JOIN_REQUEST: {
            ActionType.SEND_MESSAGE, ActionType.APPROVE_JOIN_REQUEST,
            ActionType.DECLINE_JOIN_REQUEST, ActionType.CLEAR_DATA,
        } | _COMMON_ACTIONS,
    }

    def _check_action_event_compatibility(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for gh in schema.global_handlers:
            allowed = self._EVENT_ALLOWED_ACTIONS.get(gh.event_type)
            if allowed is None:
                continue
            for j, action in enumerate(gh.actions):
                if action.type not in allowed:
                    issues.append(
                        ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.INCOMPATIBLE_ACTION,
                            message=(
                                f"Global handler '{gh.name}': action '{action.type.value}' "
                                f"is not compatible with event_type '{gh.event_type.value}'"
                            ),
                            path=f"global_handlers[{gh.name}].actions[{j}]",
                        )
                    )

        for scene in schema.scenes:
            for handler in scene.handlers:
                allowed = self._EVENT_ALLOWED_ACTIONS.get(handler.event_type)
                if allowed is None:
                    continue
                for j, action in enumerate(handler.actions):
                    if action.type not in allowed:
                        issues.append(
                            ValidationIssue(
                                severity=ValidationSeverity.ERROR,
                                category=ValidationCategory.INCOMPATIBLE_ACTION,
                                message=(
                                    f"Handler '{handler.name}' in scene '{scene.id}': "
                                    f"action '{action.type.value}' is not compatible "
                                    f"with event_type '{handler.event_type.value}'"
                                ),
                                path=f"scenes[{scene.id}].handlers[{handler.name}].actions[{j}]",
                            )
                        )

    _FALLBACK_EXPECTED_EVENT_TYPE: dict[str, EventType] = {
        "unexpected_message": EventType.MESSAGE,
        "unexpected_callback": EventType.CALLBACK,
        "unexpected_edited_message": EventType.EDITED_MESSAGE,
        "unexpected_inline_query": EventType.INLINE_QUERY,
    }

    def _check_fallback_handlers(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        if not schema.fallback:
            return
        for fb_key, expected_et in self._FALLBACK_EXPECTED_EVENT_TYPE.items():
            handlers = getattr(schema.fallback, fb_key, [])
            for i, handler in enumerate(handlers):
                if handler.event_type != expected_et:
                    issues.append(
                        ValidationIssue(
                            severity=ValidationSeverity.ERROR,
                            category=ValidationCategory.INCOMPATIBLE_ACTION,
                            message=(
                                f"Fallback handler '{handler.name}' in {fb_key} "
                                f"has event_type '{handler.event_type.value}' "
                                f"but expected '{expected_et.value}'"
                            ),
                            path=f"fallback.{fb_key}[{i}]",
                        )
                    )
                allowed = self._EVENT_ALLOWED_ACTIONS.get(expected_et)
                if allowed is None:
                    continue
                for j, action in enumerate(handler.actions):
                    if action.type not in allowed:
                        issues.append(
                            ValidationIssue(
                                severity=ValidationSeverity.ERROR,
                                category=ValidationCategory.INCOMPATIBLE_ACTION,
                                message=(
                                    f"Fallback handler '{handler.name}' in {fb_key}: "
                                    f"action '{action.type.value}' is not compatible "
                                    f"with event_type '{expected_et.value}'"
                                ),
                                path=f"fallback.{fb_key}[{i}].actions[{j}]",
                            )
                        )

    def _check_throttle_config(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for scene in schema.scenes:
            for handler in scene.handlers:
                if handler.throttle:
                    path = f"scenes[{scene.id}].handlers[{handler.name}].throttle"
                    self._validate_throttle(handler.throttle, path, issues)

        for gh in schema.global_handlers:
            if gh.throttle:
                self._validate_throttle(gh.throttle, f"global_handlers[{gh.name}].throttle", issues)

    def _validate_throttle(
        self,
        throttle: object,
        path: str,
        issues: list[ValidationIssue],
    ) -> None:
        if throttle.max_calls <= 0:
            issues.append(
                ValidationIssue(
                    severity=ValidationSeverity.ERROR,
                    category=ValidationCategory.INVALID_THROTTLE,
                    message=f"throttle.max_calls must be > 0 (got {throttle.max_calls})",
                    path=path,
                )
            )
        if throttle.period_seconds <= 0:
            issues.append(
                ValidationIssue(
                    severity=ValidationSeverity.ERROR,
                    category=ValidationCategory.INVALID_THROTTLE,
                    message=f"throttle.period_seconds must be > 0 (got {throttle.period_seconds})",
                    path=path,
                )
            )

    def _check_web_app_urls(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for btn, path in self._iter_all_buttons(schema):
            if btn.web_app_url and not btn.web_app_url.startswith("https://"):
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.ERROR,
                        category=ValidationCategory.INVALID_WEB_APP_URL,
                        message=f"Web App URL must use HTTPS: {btn.web_app_url}",
                        path=path,
                    )
                )

    _MESSAGE_ACTION_TYPES = (
        SendMessageAction, EditMessageAction,
        SendPhotoAction, SendVideoAction, SendDocumentAction,
    )

    def _check_empty_action_content(
        self,
        schema: UIRouter,
        issues: list[ValidationIssue],
    ) -> None:
        for action, path in self._iter_all_actions(schema):
            if not isinstance(action, self._MESSAGE_ACTION_TYPES):
                continue
            content = action.content
            has_content = (
                content is not None
                and (content.text is not None or content.media is not None)
            )
            has_keyboard = action.keyboard is not None
            if not has_content and not has_keyboard:
                issues.append(
                    ValidationIssue(
                        severity=ValidationSeverity.ERROR,
                        category=ValidationCategory.EMPTY_ACTION_CONTENT,
                        message=(
                            f"Action '{action.type.value}' has no content "
                            f"(text, media, or keyboard are all empty)"
                        ),
                        path=path,
                    )
                )

    def _iter_all_actions(self, schema: UIRouter) -> Generator[tuple[ActionInstruction, str]]:
        for scene in schema.scenes:
            yield from self._iter_scene_actions(scene)

        for gh in schema.global_handlers:
            for j, action in enumerate(gh.actions):
                yield action, f"global_handlers[{gh.name}].actions[{j}]"

        for i, eh in enumerate(schema.event_handlers):
            for j, action in enumerate(eh.actions):
                yield action, f"event_handlers[{i}].actions[{j}]"

        for i, handler in enumerate(schema.fallback.unexpected_message):
            for j, action in enumerate(handler.actions):
                yield action, f"fallback.unexpected_message[{i}].actions[{j}]"
        for i, handler in enumerate(schema.fallback.unexpected_callback):
            for j, action in enumerate(handler.actions):
                yield action, f"fallback.unexpected_callback[{i}].actions[{j}]"
        for i, handler in enumerate(schema.fallback.unexpected_edited_message):
            for j, action in enumerate(handler.actions):
                yield action, f"fallback.unexpected_edited_message[{i}].actions[{j}]"

    def _iter_scene_actions(self, scene: Scene) -> Generator[tuple[ActionInstruction, str]]:
        for handler in scene.handlers:
            for j, action in enumerate(handler.actions):
                yield action, f"scenes[{scene.id}].handlers[{handler.name}].actions[{j}]"

        for j, action in enumerate(scene.on_enter):
            yield action, f"scenes[{scene.id}].on_enter[{j}]"

        for j, action in enumerate(scene.on_exit):
            yield action, f"scenes[{scene.id}].on_exit[{j}]"

    def _iter_all_buttons(self, schema: UIRouter) -> Generator[tuple[Button, str]]:
        for scene in schema.scenes:
            yield from self._iter_scene_buttons(scene)

        for action, action_path in self._iter_all_actions(schema):
            if isinstance(action, MessageActionBase) and action.keyboard:
                yield from self._iter_keyboard_buttons(action.keyboard, f"{action_path}.keyboard")

    def _iter_scene_buttons(self, scene: Scene) -> Generator[tuple[Button, str]]:
        if scene.default_keyboard:
            yield from self._iter_keyboard_buttons(
                scene.default_keyboard,
                f"scenes[{scene.id}].default_keyboard",
            )

        for i, cc in enumerate(scene.conditional_content):
            if cc.keyboard:
                yield from self._iter_keyboard_buttons(
                    cc.keyboard,
                    f"scenes[{scene.id}].conditional_content[{i}].keyboard",
                )

        for handler in scene.handlers:
            for j, action in enumerate(handler.actions):
                if isinstance(action, MessageActionBase) and action.keyboard:
                    yield from self._iter_keyboard_buttons(
                        action.keyboard,
                        f"scenes[{scene.id}].handlers[{handler.name}].actions[{j}].keyboard",
                    )

    def _iter_keyboard_buttons(
        self, keyboard: Keyboard | DynamicKeyboard, base_path: str
    ) -> Generator[tuple[Button, str]]:
        if isinstance(keyboard, Keyboard):
            for r, row in enumerate(keyboard.buttons):
                for c, btn in enumerate(row):
                    yield btn, f"{base_path}.buttons[{r}][{c}]"
        elif isinstance(keyboard, DynamicKeyboard):
            for r, row in enumerate(keyboard.header_buttons):
                for c, btn in enumerate(row):
                    yield btn, f"{base_path}.header_buttons[{r}][{c}]"
            for r, row in enumerate(keyboard.footer_buttons):
                for c, btn in enumerate(row):
                    yield btn, f"{base_path}.footer_buttons[{r}][{c}]"

    def _extract_goto_targets_from_scene(self, scene: Scene) -> set[str]:
        targets: set[str] = set()
        for action, _ in self._iter_scene_actions(scene):
            if isinstance(action, GotoSceneAction) and action.scene_id:
                targets.add(action.scene_id)
        return targets
