"""Local rule-based plan templates when plan-v2 returns a get_status stub."""

from __future__ import annotations

import re
from pathlib import Path
from typing import Any

from iuve_connect_agents.shared.feature_flags import (
    avatar_desktop_vision_enabled,
    avatar_desktop_vision_local_enabled,
)

from ..active.artifact_generation_intent import detect_artifact_generation_intent
from ..active.explicit_text_extractor import extract_explicit_post_intent, should_generate_post_content
from ..active.content_creation_intent import (
    detect_content_creation_intent,
    extract_content_topic,
    extract_landing_folder_name,
    extract_literal_post_text,
    extract_markdown_filename,
    extract_web_post_generation_topic,
    is_document_generation_intent,
    is_html_landing_intent,
    is_web_post_intent,
    sanitize_content_topic,
    user_requests_explicit_publish,
)
from ..active.local_notepad_intent import extract_notepad_text, is_local_notepad_intent
from ..active.web_operator import (
    build_composer_focus_payload,
    build_login_detect_prompt,
    default_page_wait_sec,
    draft_verify_snippet,
    honest_composer_limitation,
    publish_stop_zone_payload,
    resolve_target_site_for_prompt,
    url_host_label,
    user_expects_logged_in,
    user_requests_explicit_publish,
)
from ..active.goal_memory import goal_memory_store
from .avatar_intent_router import (
    detect_compound_intent,
    extract_app_post_launch_goal,
    extract_app_query_from_text,
    extract_app_type_text,
    extract_browser_question_text,
    extract_cursor_question_text,
    extract_routing_mode_from_text,
    extract_vision_verify_expected_text,
    is_active_window_analysis_intent,
    is_browser_search_navigation_intent,
    is_chatgpt_browser_intent,
    is_downloads_file_search_intent,
    is_folder_analysis_intent,
    is_full_system_metrics_query,
    is_generic_app_launch_intent,
    is_job_search_intent,
    is_local_computer_job_search_intent,
    is_plan_only_intent,
    is_research_write_compound_intent,
    is_multi_app_launch_command,
    is_web_research_intent,
    is_workspace_file_creation_intent,
    resolve_browser_url_from_text,
    should_submit_browser_chat_message,
    split_multi_command_segments,
    wants_browser_load_completion,
    wants_browser_open_and_type,
    wants_cursor_launch_and_type,
    wants_generic_app_launch_and_type,
    wants_local_cursor_launch,
    wants_screen_verification,
    _is_actionable_launch_segment,
)
from iuve_connect_agents.shared.url_normalizer import extract_normalized_url_from_text, normalize_avatar_url

_DOCUMENT_TOKENS = (
    "документ",
    "document",
    "doc",
    "word",
    "ворд",
    "libreoffice",
    "writer",
    "напиш",
    "write",
    "summary",
    "резюме",
    "кратко",
    "открой документ",
)
_PUBLISH_TOKENS = (
    "опублик",
    "publish",
    "linkedin",
    "linked in",
    "social post",
    "пост в",
)
_MAINTENANCE_TOKENS = (
    "корзин",
    "trash",
    "recycle bin",
    "recycle",
    "очист",
    "empty trash",
)
_EMPTY_TRASH_VERB_TOKENS = (
    "очисти",
    "очисть",
    "очистить",
    "очистк",
    "empty ",
    "empty\t",
    "clear ",
    "clear\t",
    "опорожн",
)
_EMPTY_TRASH_TARGET_TOKENS = (
    "корзин",
    "trash",
    "recycle bin",
    "recycle",
    "wastebasket",
    "мусор",
)
_MEMORY_TOKENS = (
    "memory",
    "ram",
    "памят",
    "оператив",
)

_FOLDER_NAME_QUOTED_RE = re.compile(
    r"[«“\"']([A-Za-zА-Яа-яЁё0-9_-]+)[»”\"']",
)
_FOLDER_NAME_EXPLICIT_RE = re.compile(
    r"(?:папк[уаеи]|folder)\s+(?:с\s+именем\s+|named\s+)?([A-Za-zА-Яа-яЁё0-9_-]+)",
    re.IGNORECASE,
)
_FOLDER_STOPWORDS = frozenset(
    {
        "на",
        "в",
        "in",
        "the",
        "a",
        "an",
        "рабочем",
        "рабочий",
        "столе",
        "стол",
        "desktop",
        "safe",
        "balanced",
        "trusted",
        "сейф",
        "баланс",
        "траст",
    }
)
_FILE_COUNT_RE = re.compile(
    r"(?:(\d+)|(три|два|четыре|пять|two|three|four|five))\s+(?:текстовых\s+)?файл",
    re.IGNORECASE,
)
_NAMED_FILE_RE = re.compile(
    r"файл\s+([A-Za-z0-9_.-]+)",
    re.IGNORECASE,
)
_FILE_CONTENT_RE = re.compile(
    r"(?:строку|текст|text)\s*:\s*(.+?)(?:\.|$)",
    re.IGNORECASE,
)
_COUNT_WORDS = {
    "два": 2,
    "two": 2,
    "три": 3,
    "three": 3,
    "четыре": 4,
    "four": 4,
    "пять": 5,
    "five": 5,
}

_CONSENT = {"user_consent_state": "consented_via_connect_flow"}


def build_plan_only_preview(request_text: str) -> dict[str, Any] | None:
    """Informational backup/migration plan — no executor run."""
    if not is_plan_only_intent(request_text):
        return None
    lower = str(request_text or "").strip().lower()
    steps: list[dict[str, Any]] = []
    if "резерв" in lower or "backup" in lower:
        goal = "План резервного копирования рабочего стола (без выполнения)"
        descriptions = [
            "Оценить объём данных на рабочем столе и в IuVeWorkspace",
            "Выбрать целевой носитель (внешний диск или облако)",
            "Создать структуру папок backup-YYYYMMDD",
            "Скопировать файлы с проверкой контрольных сумм",
            "Проверить целостность архива (выборочная сверка)",
            "Задокументировать расположение резервной копии",
            "Выполнить восстановление одного тестового файла (после явного подтверждения)",
        ]
    else:
        goal = "Информационный план действий (без выполнения)"
        descriptions = [
            "Уточнить цель и ограничения",
            "Подготовить шаги и необходимые разрешения",
            "Выполнить после явного подтверждения владельца",
        ]
    for index, description in enumerate(descriptions, start=1):
        steps.append(
            {
                "step_id": f"s{index}",
                "command_type": "plan_step",
                "payload": {"description": description, **_CONSENT},
            }
        )
    return {
        "goal_summary": goal,
        "execution_mode": "plan_only",
        "no_execution_required": True,
        "verifier": "plan_generated",
        "steps": steps,
    }


def build_active_window_plan(request_text: str) -> dict[str, Any] | None:
    """Vision route: capture screenshot and describe active window."""
    if not is_active_window_analysis_intent(request_text):
        return None
    if is_folder_analysis_intent(request_text):
        return None
    return {
        "goal_summary": "Определить активное окно и описать его содержимое",
        "steps": [
            _vision_capture_step(step_id="s1", depends_on=[]),
            {
                "step_id": "s2",
                "command_type": "describe_active_window",
                "depends_on": ["s1"],
                "payload": {
                    "depends_on_step": "s1",
                    "prompt": "Кратко опиши активное окно и его видимое содержимое.",
                    **_CONSENT,
                },
            },
        ],
    }


def build_browser_load_verify_plan(request_text: str) -> dict[str, Any] | None:
    """Open browser URL, wait, verify page loaded via vision."""
    if not wants_browser_load_completion(request_text):
        return None
    url = resolve_browser_url_from_text(request_text)
    if "iuve" in str(request_text or "").lower() and "iuve.eu" not in url:
        url = "https://www.iuve.eu/connect"
    host = url.replace("https://", "").replace("http://", "").split("/")[0]
    expected = "IuVe" if "iuve" in host.lower() else host.split(".")[0]
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "browser_open_url",
            "payload": {"url": url, "new_window": True, "browser": "default", **_CONSENT},
        },
        _vision_capture_step(step_id="s2", depends_on=["s1"]),
        _vision_verify_step(
            step_id="s3",
            depends_on=["s2"],
            expected_text=expected,
            verify_mode="page_loaded",
        ),
    ]
    return {
        "goal_summary": f"Открыть {url} и подтвердить загрузку страницы",
        "steps": steps,
    }


def build_system_metrics_plan(request_text: str) -> dict[str, Any] | None:
    if not is_full_system_metrics_query(request_text):
        return None
    if is_folder_analysis_intent(request_text):
        return None
    return {
        "goal_summary": "Собрать CPU, RAM и диск — таблица метрик",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "get_diagnostics",
                "payload": {
                    "include_memory_summary": True,
                    "include_cpu": True,
                    "include_disk": True,
                    "format_table": True,
                    **_CONSENT,
                },
            }
        ],
    }


def build_downloads_search_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.local_search_resolver import build_local_search_plan

    return build_local_search_plan(request_text)


def build_folder_analysis_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.folder_analysis_resolver import build_folder_analysis_plan as _build

    return _build(request_text)


def build_web_research_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.web_research_resolver import build_web_research_plan as _build

    return _build(request_text)


def build_research_then_artifact_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.web_research_resolver import build_research_then_artifact_plan as _build

    return _build(request_text)


def build_compound_site_local_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.compound_intent_resolver import build_compound_site_local_plan as _build

    return _build(request_text)


def build_local_computer_job_search_plan(request_text: str) -> dict[str, Any] | None:
    from ..active.web_research_resolver import build_local_computer_job_search_plan as _build

    return _build(request_text)


def build_system_monitor_plan(request_text: str) -> dict[str, Any] | None:
    """Launch Linux system monitor with optional post-launch goal."""
    lower = str(request_text or "").strip().lower()
    if not any(token in lower for token in ("монитор", "monitor", "system monitor", "системн")):
        return None
    if not any(verb in lower for verb in ("открой", "open", "запусти", "launch", "start")):
        return None
    from ..active.device_capability_scanner import (
        load_profile,
        resolve_app_launch_from_text,
        resolve_system_monitor_launch,
    )

    resolved = resolve_app_launch_from_text(request_text, load_profile())
    if resolved is None:
        monitor = resolve_system_monitor_launch()
        if monitor is None:
            return None
        matched, candidates = monitor
    else:
        matched, candidates = resolved
    app_name = str(matched.get("name") or "System Monitor")
    app_slug = str(matched.get("slug") or app_name).strip().lower()
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "application_launch",
            "payload": {
                "application": app_slug,
                "application_name": app_name,
                "application_query": extract_app_query_from_text(request_text) or app_name,
                "launch_candidates": [list(item) for item in candidates],
                **_CONSENT,
            },
        }
    ]
    post_goal = extract_app_post_launch_goal(request_text)
    if post_goal:
        steps.append(
            {
                "step_id": "s2",
                "command_type": "describe_active_window",
                "depends_on": ["s1"],
                "payload": {
                    "prompt": f"Опиши окно приложения и ответь на задачу: {post_goal}",
                    "delay_sec": 4.0,
                    **_CONSENT,
                },
            }
        )
    return {"goal_summary": f"Запустить {app_name}", "steps": steps}


def build_app_launch_with_goal_plan(request_text: str) -> dict[str, Any] | None:
    """Launch named app (e.g. Stacer) and continue with post-launch goal."""
    if not is_generic_app_launch_intent(request_text):
        return None
    post_goal = extract_app_post_launch_goal(request_text)
    if not post_goal:
        return None
    from ..active.device_capability_scanner import load_profile, resolve_app_launch_from_text

    resolved = resolve_app_launch_from_text(request_text, load_profile())
    if resolved is None:
        return None
    matched, candidates = resolved
    app_name = str(matched.get("name") or extract_app_query_from_text(request_text) or "application")
    app_slug = str(matched.get("slug") or app_name).strip().lower()
    return {
        "goal_summary": f"Запустить {app_name} и выполнить: {post_goal}",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "application_launch",
                "payload": {
                    "application": app_slug,
                    "application_name": app_name,
                    "application_query": extract_app_query_from_text(request_text) or app_name,
                    "launch_candidates": [list(item) for item in candidates],
                    **_CONSENT,
                },
            },
            {
                "step_id": "s2",
                "command_type": "describe_active_window",
                "depends_on": ["s1"],
                "payload": {
                    "prompt": post_goal,
                    "delay_sec": 5.0,
                    **_CONSENT,
                },
            },
        ],
    }


def _vision_capture_step(*, step_id: str, depends_on: list[str], present_to_chat: bool = False) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "region": "full",
        "delay_ms": 500,
        "user_consent_state": "consented_via_connect_flow",
    }
    if present_to_chat:
        payload["present_to_chat"] = True
    return {
        "step_id": step_id,
        "command_type": "capture_screenshot",
        "depends_on": depends_on,
        "payload": payload,
    }


def _vision_verify_step(
    *,
    step_id: str,
    depends_on: list[str],
    expected_text: str,
    verify_mode: str = "contains",
    required_url_host: str = "",
) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "expected_text": expected_text,
        "depends_on_step": depends_on[0] if depends_on else "",
        "match_mode": "contains",
        "verify_mode": verify_mode,
        "min_confidence": 0.7,
        "capture_if_missing": False,
        "user_consent_state": "consented_via_connect_flow",
    }
    host = str(required_url_host or "").strip().lower().removeprefix("www.")
    if host:
        payload["required_url_host"] = host
    return {
        "step_id": step_id,
        "command_type": "verify_screen_contains_text",
        "depends_on": depends_on,
        "payload": payload,
    }


def _should_append_vision_capture(*, submit_after: bool, request_text: str) -> bool:
    if not avatar_desktop_vision_enabled():
        return False
    if submit_after:
        return True
    return wants_screen_verification(request_text)


def _append_vision_verify_if_needed(
    steps: list[dict[str, Any]],
    *,
    capture_step_id: str,
    expected_text: str,
    verify_mode: str = "contains",
) -> list[dict[str, Any]]:
    if not avatar_desktop_vision_enabled() or not avatar_desktop_vision_local_enabled():
        return steps
    if not str(expected_text or "").strip():
        return steps
    verify_id = f"{capture_step_id}_verify"
    if any(str(step.get("command_type") or "") == "verify_screen_contains_text" for step in steps):
        return steps
    return [
        *steps,
        _vision_verify_step(
            step_id=verify_id,
            depends_on=[capture_step_id],
            expected_text=str(expected_text).strip(),
            verify_mode=verify_mode,
        ),
    ]


def _append_vision_capture_if_needed(
    steps: list[dict[str, Any]],
    *,
    depends_on_step_id: str,
    request_text: str,
    submit_after: bool = False,
    expected_text: str = "",
) -> list[dict[str, Any]]:
    if not _should_append_vision_capture(submit_after=submit_after, request_text=request_text):
        return steps
    capture_id = f"{depends_on_step_id}_capture"
    if any(str(step.get("command_type") or "") == "capture_screenshot" for step in steps):
        return steps
    steps = [*steps, _vision_capture_step(step_id=capture_id, depends_on=[depends_on_step_id])]
    verify_text = extract_vision_verify_expected_text(
        request_text=request_text,
        typed_text=expected_text,
    )
    verify_mode = "chat_response" if wants_screen_verification(request_text) else "contains"
    return _append_vision_verify_if_needed(
        steps,
        capture_step_id=capture_id,
        expected_text=verify_text,
        verify_mode=verify_mode,
    )


def is_get_status_stub_plan(plan: dict[str, Any] | None) -> bool:
    """True when plan is a single-step get_status placeholder from cloud planner."""
    if not isinstance(plan, dict):
        return False
    steps = plan.get("steps")
    if not isinstance(steps, list) or len(steps) != 1:
        return False
    step = steps[0]
    if not isinstance(step, dict):
        return False
    return str(step.get("command_type") or "").strip().lower() == "get_status"


def _is_get_status_stub(plan: dict[str, Any]) -> bool:
    return is_get_status_stub_plan(plan)


def _is_diagnostics_stub(plan: dict[str, Any]) -> bool:
    steps = plan.get("steps") if isinstance(plan, dict) else None
    if not isinstance(steps, list) or len(steps) != 1:
        return False
    step = steps[0]
    if not isinstance(step, dict):
        return False
    return str(step.get("command_type") or "").strip().lower() == "get_diagnostics"


def _is_misrouted_diagnostics_plan(request_text: str, plan: dict[str, Any] | None) -> bool:
    if not isinstance(plan, dict) or not _is_diagnostics_stub(plan):
        return False
    if is_folder_analysis_intent(request_text):
        return True
    from ..active.local_search_resolver import is_local_search_intent

    if is_local_search_intent(request_text):
        return True
    return False


def _is_maintenance_goal(lower: str) -> bool:
    has_memory = any(token in lower for token in _MEMORY_TOKENS)
    has_maintenance = any(token in lower for token in _MAINTENANCE_TOKENS)
    return has_memory and has_maintenance


def is_empty_trash_intent(text: str) -> bool:
    """Standalone empty recycle-bin / trash intent (RU/EN, including typo очисть)."""
    lower = str(text or "").strip().lower()
    if not lower:
        return False
    if _is_maintenance_goal(lower):
        # Compound memory+trash handled by maintenance plan.
        return False
    has_target = any(token in lower for token in _EMPTY_TRASH_TARGET_TOKENS)
    if not has_target:
        return False
    if "empty trash" in lower or "empty recycle" in lower or "clear trash" in lower:
        return True
    if any(token in lower for token in _EMPTY_TRASH_VERB_TOKENS):
        return True
    # Bare «корзина» / trash with clear verbs already covered; allow «корзину от мусора».
    if "корзин" in lower and any(token in lower for token in ("мусор", "очист", "опорожн")):
        return True
    return False


def _is_publish_goal(lower: str) -> bool:
    has_document = any(token in lower for token in _DOCUMENT_TOKENS)
    has_publish = any(token in lower for token in _PUBLISH_TOKENS)
    return has_document and has_publish


def _extract_publish_topic(text: str) -> str:
    lower = str(text or "").strip().lower()
    match = re.search(r"про\s+([^,.]+)", lower)
    if match:
        return match.group(1).strip().title() or "IuVe AI"
    if "iuve" in lower:
        return "IuVe AI"
    return "IuVe AI"


def _build_empty_trash_plan() -> dict[str, Any]:
    return {
        "goal_summary": "Empty recycle bin / trash",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "empty_trash",
                "payload": {
                    "app_id": "trash_manager",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
        ],
    }


def build_empty_trash_plan(request_text: str) -> dict[str, Any] | None:
    if not is_empty_trash_intent(request_text):
        return None
    return _build_empty_trash_plan()


def _build_maintenance_plan() -> dict[str, Any]:
    return {
        "goal_summary": "Check device memory and empty trash",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "get_diagnostics",
                "payload": {
                    "user_consent_state": "consented_via_connect_flow",
                    "include_memory_summary": True,
                },
            },
            {
                "step_id": "s2",
                "command_type": "empty_trash",
                "depends_on": ["s1"],
                "payload": {
                    "app_id": "trash_manager",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
        ],
    }


def _build_publish_plan(text: str) -> dict[str, Any]:
    topic = _extract_publish_topic(text)
    lower = str(text or "").strip().lower()
    platform = "linkedin" if "linkedin" in lower or "linked in" in lower else "social"
    draft = f"Post about {topic} — draft prepared by IuVe Avatar."
    return {
        "goal_summary": f"Draft and publish {topic} post to {platform}",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "social_post_draft",
                "payload": {
                    "user_consent_state": "consented_via_connect_flow",
                    "content": draft,
                    "topic": topic,
                },
            },
            {
                "step_id": "s2",
                "command_type": "social_post_publish",
                "depends_on": ["s1"],
                "payload": {
                    "user_consent_state": "consented_via_connect_flow",
                    "content_from_step": "s1",
                    "platform": platform,
                    "rollback_hint": "Recall draft; do not publish if unsure",
                    "rollback_instruction": "Delete the draft post manually if publish was unintended",
                },
            },
        ],
    }


def _build_cursor_launch_type_plan(text: str) -> dict[str, Any]:
    question = extract_cursor_question_text(text)
    return {
        "goal_summary": "Launch Cursor IDE and type question into IDE chat",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "application_launch",
                "payload": {
                    "app_id": "code_editor",
                    "application": "cursor",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
            {
                "step_id": "s2",
                "command_type": "keyboard_type_text",
                "depends_on": ["s1"],
                "payload": {
                    "text": question,
                    "app_id": "code_editor",
                    "target_app": "cursor",
                    "window_name": "Cursor",
                    "delay_sec": 3.5,
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
        ],
    }


def build_cursor_launch_type_plan(request_text: str) -> dict[str, Any] | None:
    """Two-step local plan: launch Cursor, wait, type question into IDE chat."""
    if is_chatgpt_browser_intent(request_text):
        return None
    if not wants_cursor_launch_and_type(request_text):
        return None
    return _build_cursor_launch_type_plan(request_text)


def _build_browser_open_type_plan(text: str) -> dict[str, Any]:
    question = extract_browser_question_text(text)
    url = resolve_browser_url_from_text(text)
    submit_after = should_submit_browser_chat_message(text)
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "browser_open_url",
            "payload": {
                "app_id": "web_browser",
                "url": url,
                "new_window": True,
                "browser": "default",
                "user_consent_state": "consented_via_connect_flow",
            },
        },
        {
            "step_id": "s2",
            "command_type": "keyboard_type_text",
            "depends_on": ["s1"],
            "payload": {
                "text": question,
                "source_request_text": text,
                "app_id": "web_browser",
                "target_app": "browser",
                "window_name": "ChatGPT",
                "delay_sec": 5.0,
                "focus_tab_count": 3,
                "submit_after": submit_after,
                "submit_message": submit_after,
                "user_consent_state": "consented_via_connect_flow",
            },
        },
    ]
    steps = _append_vision_capture_if_needed(
        steps,
        depends_on_step_id="s2",
        request_text=text,
        submit_after=submit_after,
        expected_text=question,
    )
    return {
        "goal_summary": "Open website in browser and type question into chat input",
        "steps": steps,
    }


def build_browser_open_type_plan(request_text: str) -> dict[str, Any] | None:
    """Two-step local plan: open URL in browser, wait, type question into chat input."""
    if not wants_browser_open_and_type(request_text):
        return None
    return _build_browser_open_type_plan(request_text)


def _extract_workspace_folder_name(text: str) -> str | None:
    raw = str(text or "").strip()
    if not raw:
        return None
    quoted = _FOLDER_NAME_QUOTED_RE.search(raw)
    if quoted:
        name = str(quoted.group(1) or "").strip()
        if name and name.lower() not in _FOLDER_STOPWORDS:
            return name
    for match in _FOLDER_NAME_EXPLICIT_RE.finditer(raw):
        candidate = str(match.group(1) or "").strip()
        if not candidate or candidate.lower() in _FOLDER_STOPWORDS:
            continue
        tail = raw[match.end() : match.end() + 24].lower()
        if candidate.lower() == "на" and ("рабоч" in tail or "desktop" in tail):
            continue
        return candidate
    # TestAvatar-style: «Создай папку TestAvatar»
    test_match = re.search(
        r"\b(?:создай|создать|create)\s+папк[уаеи]\s+([A-Za-zА-Яа-яЁё][A-Za-zА-Яа-яЁё0-9_-]*)",
        raw,
        re.IGNORECASE,
    )
    if test_match:
        name = str(test_match.group(1) or "").strip()
        if name.lower() not in _FOLDER_STOPWORDS:
            return name
    return None


def _is_quoted_follow_up_command(text: str) -> bool:
    return bool(re.search(r"\bзатем\s+команд\w*\b", str(text or "").lower()))


def _resolve_folder_creation_target(
    request_text: str,
    *,
    folder_name: str,
    trust_state: str = "",
) -> tuple[Path, str, str]:
    from ..active.capability_resolver import resolve_user_folder

    routing_mode = extract_routing_mode_from_text(request_text)
    resolved = resolve_user_folder(
        request_text,
        for_read=False,
        routing_mode=routing_mode,  # type: ignore[arg-type]
        trust_state=trust_state,
    )
    if resolved is None:
        from ..active.capability_resolver import resolve_workspace_path

        base = resolve_workspace_path()
        hint = "IuVeWorkspace (default)"
        mode = "safe"
    else:
        base = resolved.path
        hint = resolved.policy_hint or str(resolved.scope)
        mode = resolved.routing_mode
    target = base / folder_name
    return target, hint, mode


def build_browser_search_navigation_plan(request_text: str) -> dict[str, Any] | None:
    """Google → type query → open official site → vision/URL verify."""
    if not is_browser_search_navigation_intent(request_text):
        return None
    lower = str(request_text or "").lower()
    search_term = "OpenAI"
    if "openai" in lower:
        search_term = "OpenAI"
    target_url = "https://openai.com/"
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "browser_open_url",
            "payload": {
                "app_id": "web_browser",
                "url": "https://www.google.com/",
                "new_window": True,
                "browser": "default",
                **_CONSENT,
            },
        },
        {
            "step_id": "s2",
            "command_type": "keyboard_type_text",
            "depends_on": ["s1"],
            "payload": {
                "text": search_term,
                "app_id": "web_browser",
                "target_app": "browser",
                "window_name": "Google",
                "delay_sec": 3.0,
                "submit_after": True,
                "focus_recovery": True,
                **_CONSENT,
            },
        },
        {
            "step_id": "s3",
            "command_type": "browser_open_url",
            "depends_on": ["s2"],
            "payload": {
                "app_id": "web_browser",
                "url": target_url,
                "new_window": False,
                "browser": "default",
                **_CONSENT,
            },
        },
        _vision_capture_step(step_id="s4", depends_on=["s3"]),
        _vision_verify_step(
            step_id="s5",
            depends_on=["s4"],
            expected_text="openai.com",
            verify_mode="url_domain",
            required_url_host="openai.com",
        ),
    ]
    return {
        "goal_summary": f"Открыть Google, найти {search_term}, открыть официальный сайт",
        "steps": steps,
    }


def _extract_file_count(text: str) -> int:
    lower = str(text or "").strip().lower()
    match = _FILE_COUNT_RE.search(lower)
    if match:
        if match.group(1):
            return max(1, int(match.group(1)))
        word = str(match.group(2) or "").lower()
        return _COUNT_WORDS.get(word, 3)
    if "файл" in lower or "file" in lower:
        return 1
    return 3


def _extract_named_file(text: str) -> tuple[str, str] | None:
    match = _NAMED_FILE_RE.search(str(text or ""))
    if not match:
        return None
    filename = str(match.group(1) or "").strip().rstrip(".")
    if not filename:
        return None
    content_match = _FILE_CONTENT_RE.search(str(text or ""))
    content = str(content_match.group(1) or "").strip() if content_match else "Hello from IuVe Avatar"
    return filename, content


def _request_explicitly_mentions_file_writes(text: str) -> bool:
    """True when NL asks for file content — not folder-only (TestAvatar2 over-expansion guard)."""
    if _extract_named_file(text) is not None:
        return True
    lower = str(text or "").strip().lower()
    if _FILE_COUNT_RE.search(lower):
        return True
    if "файл" in lower or re.search(r"\bfile\b", lower):
        return True
    return False


def build_landing_page_plan(request_text: str) -> dict[str, Any] | None:
    """HTML landing: folder → generate_content → write index.html → verify → report."""
    if not is_html_landing_intent(request_text):
        return None
    intent = detect_content_creation_intent(request_text)
    folder = extract_landing_folder_name(request_text)
    topic = extract_content_topic(request_text, default="услуги аналитики")
    content_type = intent.content_type if intent else "html_landing"
    filename = intent.output_filename if intent else "index.html"

    target_path, policy_hint, routing_mode = _resolve_folder_creation_target(
        request_text,
        folder_name=folder,
    )
    rel_path = f"{folder}/{filename}"
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "open_folder",
            "payload": {
                "absolute_path": str(target_path),
                "routing_mode": routing_mode,
                "policy_hint": policy_hint,
                "folder_name": folder,
                **_CONSENT,
            },
        },
        {
            "step_id": "s2",
            "command_type": "generate_content",
            "depends_on": ["s1"],
            "payload": {
                "content_type": content_type,
                "topic": topic,
                "goal_summary": f"HTML landing about {topic}",
                **_CONSENT,
            },
        },
        {
            "step_id": "s3",
            "command_type": "write_file_in_workspace",
            "depends_on": ["s2"],
            "payload": {
                "relative_path": rel_path,
                "absolute_path": str(target_path / filename),
                "content_from_step": "s2",
                "routing_mode": routing_mode,
                **_CONSENT,
            },
        },
        {
            "step_id": "s4",
            "command_type": "verify_workspace_file",
            "depends_on": ["s3"],
            "payload": {
                "relative_path": rel_path,
                "absolute_path": str(target_path / filename),
                "topic": topic,
                "verify_mode": "html_structure",
                **_CONSENT,
            },
        },
    ]
    return {
        "goal_summary": f"Создать лендинг {folder}/{filename} — {topic}",
        "routing_mode": routing_mode,
        "verifier": "landing_html_created",
        "steps": steps,
    }


def build_artifact_generation_plan(request_text: str) -> dict[str, Any] | None:
    """Code/HTML artifact: generate_content → write_file → verify (no screenshot)."""
    intent = detect_artifact_generation_intent(request_text)
    if intent is None:
        return None
    filename = intent.output_filename
    topic = intent.topic
    content_type = intent.content_type
    rel_path = f"{intent.folder_name}/{filename}"
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "generate_content",
            "payload": {
                "content_type": content_type,
                "topic": topic,
                "goal_summary": f"Generate {filename} — {topic}",
                **_CONSENT,
            },
        },
        {
            "step_id": "s2",
            "command_type": "write_file_in_workspace",
            "depends_on": ["s1"],
            "payload": {
                "relative_path": rel_path,
                "content_from_step": "s1",
                **_CONSENT,
            },
        },
        {
            "step_id": "s3",
            "command_type": "verify_workspace_file",
            "depends_on": ["s2"],
            "payload": {
                "relative_path": rel_path,
                "topic": topic,
                "verify_mode": "exists",
                **_CONSENT,
            },
        },
    ]
    verifier = "code_file_created" if intent.subtype == "code" else "artifact_generated"
    return {
        "goal_summary": f"Создать файл {filename} — {topic}",
        "verifier": verifier,
        "steps": steps,
    }


def build_document_generation_plan(request_text: str) -> dict[str, Any] | None:
    """Workspace markdown/document: generate_content → write_file → verify (no screenshot)."""
    if not is_document_generation_intent(request_text):
        return None
    from ..active.web_research_resolver import dossier_filename_from_topic, extract_research_topic

    intent = detect_content_creation_intent(request_text)
    mem = goal_memory_store().snapshot()
    filename = extract_markdown_filename(request_text)
    if intent and intent.output_filename and filename == "document.md":
        filename = intent.output_filename
    topic = extract_content_topic(request_text, default=filename.replace(".md", "").replace("_", " "))
    if (not topic or topic in {"document", "тема"}) and mem.last_artifact_topic:
        topic = mem.last_artifact_topic
    if "досье" in str(request_text or "").lower() or "dossier" in str(request_text or "").lower():
        filename = dossier_filename_from_topic(topic)
    elif mem.last_artifact_plan and "досье" in mem.last_artifact_plan.lower():
        filename = dossier_filename_from_topic(topic)
    elif filename == "document.md" and mem.last_artifact_topic:
        filename = dossier_filename_from_topic(mem.last_artifact_topic)
    content_type = intent.content_type if intent else "document"
    rel_path = f"IuVeWorkspace/{filename}"
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "generate_content",
            "payload": {
                "content_type": content_type,
                "topic": topic,
                "goal_summary": f"Generate document {filename} about {topic}",
                **_CONSENT,
            },
        },
        {
            "step_id": "s2",
            "command_type": "write_file_in_workspace",
            "depends_on": ["s1"],
            "payload": {
                "relative_path": rel_path,
                "content_from_step": "s1",
                **_CONSENT,
            },
        },
        {
            "step_id": "s3",
            "command_type": "verify_workspace_file",
            "depends_on": ["s2"],
            "payload": {
                "relative_path": rel_path,
                "topic": topic,
                "verify_mode": "exists",
                **_CONSENT,
            },
        },
    ]
    return {
        "goal_summary": f"Сформировать документ {filename} — {topic}",
        "verifier": "document_generated",
        "steps": steps,
    }


def build_notepad_text_plan(request_text: str) -> dict[str, Any] | None:
    """Launch local text editor (gedit) and type verbatim text — not web composer."""
    if not is_local_notepad_intent(request_text):
        return None
    text_to_type = extract_notepad_text(request_text)
    if not text_to_type:
        return None
    from ..active.device_capability_scanner import load_profile, resolve_app_launch_from_text

    resolved = resolve_app_launch_from_text("открой gedit", load_profile())
    if resolved is None:
        app_name = "gedit"
        app_slug = "gedit"
        candidates: list[list[str]] = []
    else:
        matched, raw_candidates = resolved
        app_name = str(matched.get("name") or "gedit")
        app_slug = str(matched.get("slug") or "gedit").strip().lower()
        candidates = [list(item) for item in raw_candidates]
    return {
        "goal_summary": f"Написать в блокноте: {text_to_type}",
        "verifier": "notepad_text_typed",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "application_launch",
                "payload": {
                    "application": app_slug,
                    "application_name": app_name,
                    "application_query": app_name,
                    "launch_candidates": candidates,
                    **_CONSENT,
                },
            },
            {
                "step_id": "s2",
                "command_type": "keyboard_type_text",
                "depends_on": ["s1"],
                "payload": {
                    "text": text_to_type,
                    "target_app": app_slug,
                    "window_name": app_name,
                    "delay_sec": 3.0,
                    **_CONSENT,
                },
            },
        ],
    }


def _has_existing_web_draft(*, runtime_context: dict[str, Any] | None = None) -> bool:
    try:
        from ..active.runtime_context import runtime_context_store

        runtime = dict(runtime_context or runtime_context_store().load().safe_metadata())
    except Exception:
        runtime = dict(runtime_context or {})
    mem = goal_memory_store().snapshot()
    for source in (runtime, mem.as_dict()):
        draft = str(source.get("composer_draft") or source.get("draft_snippet") or "").strip()
        if draft:
            return True
        if source.get("composer_focused"):
            return True
    return bool(str(mem.active_composer or "").strip())


def is_publish_only_web_post_intent(text: str) -> bool:
    """Publish existing draft without reopening composer or regenerating content."""
    raw = str(text or "").strip()
    if not raw or not user_requests_explicit_publish(raw):
        return False
    lower = raw.lower()
    create_verbs = ("создай", "создать", "create", "напиш", "write", "открой", "open", "сделай", "make")
    if any(verb in lower for verb in create_verbs):
        return False
    return bool(re.search(r"^(?:опублику\w*|publish)\b", raw, re.IGNORECASE))


def build_web_publish_only_plan(
    request_text: str,
    *,
    runtime_context: dict[str, Any] | None = None,
) -> dict[str, Any] | None:
    """Minimal publish plan when composer draft already exists (DEF-009)."""
    if not is_publish_only_web_post_intent(request_text):
        return None
    if not _has_existing_web_draft(runtime_context=runtime_context):
        return None
    mem = goal_memory_store().snapshot()
    memory_site = str(mem.current_target_site or mem.active_site or mem.last_browser_url or "")
    site = resolve_target_site_for_prompt(request_text, memory_site=memory_site)
    if not site:
        return None
    url = site.url
    host = url_host_label(url)
    publish_payload = {
        "active_site": url,
        "window_name": host,
        "button_label": "Опубликовать",
        "confirm_modal_text": "Опубликовать сейчас?",
        "source_request_text": request_text,
        "deny_message": "Черновик готов. Для публикации требуется подтверждение.",
        "publish_requested": True,
        **publish_stop_zone_payload(),
        **_CONSENT,
    }
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "browser_click_publish",
            "payload": publish_payload,
        },
    ]
    if avatar_desktop_vision_enabled() and avatar_desktop_vision_local_enabled():
        steps.append(
            {
                "step_id": "s2",
                "command_type": "verify_published",
                "depends_on": ["s1"],
                "payload": {
                    "depends_on_step": "s1",
                    "active_site": url,
                    "window_name": host,
                    "optional": False,
                    "require_publish_verified": True,
                    **_CONSENT,
                },
            }
        )
    return {
        "goal_summary": f"Опубликовать пост на {host}",
        "verifier": "web_post_published",
        "steps": steps,
    }


def build_web_post_plan(request_text: str) -> dict[str, Any] | None:
    """Generic web operator: open URL → composer → draft text → verify → [confirm] publish."""
    publish_only = build_web_publish_only_plan(request_text)
    if publish_only is not None:
        return publish_only
    mem = goal_memory_store().snapshot()
    memory_site = str(mem.current_target_site or mem.active_site or mem.last_browser_url or "")
    if not is_web_post_intent(request_text, memory_site=memory_site):
        return None
    site = resolve_target_site_for_prompt(request_text, memory_site=memory_site)
    if not site:
        return None
    url = site.url
    intent = detect_content_creation_intent(request_text)
    explicit = extract_explicit_post_intent(request_text)
    literal_text = extract_literal_post_text(request_text)
    use_exact = bool(literal_text) and not (explicit and explicit.generate_content)
    topic = literal_text or extract_web_post_generation_topic(request_text) or sanitize_content_topic(
        extract_content_topic(request_text, default="новость")
    )
    content_type = intent.content_type if intent else "social_post"
    logged_in = user_expects_logged_in(request_text)
    wait_sec = default_page_wait_sec(logged_in_hint=logged_in)
    host = url_host_label(url)
    wants_publish = user_requests_explicit_publish(request_text)
    composer = build_composer_focus_payload(url=url, delay_sec=wait_sec, window_name=host)

    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "browser_open_url",
            "payload": {"url": url, "new_window": True, "browser": "default", **_CONSENT},
        },
        {
            "step_id": "s2",
            "command_type": "describe_active_window",
            "depends_on": ["s1"],
            "payload": {
                "prompt": build_login_detect_prompt(url=url),
                "delay_sec": wait_sec,
                "active_site": url,
                "detect_composer": True,
                **_CONSENT,
            },
        },
    ]

    composer_step_id = "s2b"
    steps.append(
        {
            "step_id": composer_step_id,
            "command_type": "open_web_composer",
            "depends_on": ["s2"],
            "payload": {
                "active_site": url,
                "window_name": host,
                "source_request_text": request_text,
                "delay_sec": 1.5,
                "site_hint": host,
                "verify_composer_open": True,
                "prefer_feed_composer": True,
                "composer_kind": "feed_post",
                **_CONSENT,
            },
        }
    )

    content_step_id = "s3"
    content_dep = composer_step_id
    if use_exact:
        steps.append(
            {
                "step_id": content_step_id,
                "command_type": "insert_exact_text",
                "depends_on": [content_dep],
                "payload": {
                    "exact_text": literal_text,
                    "generate_content": False,
                    "topic": literal_text,
                    "content_type": content_type,
                    **_CONSENT,
                },
            }
        )
    else:
        gen_topic = extract_web_post_generation_topic(request_text) or topic
        steps.append(
            {
                "step_id": content_step_id,
                "command_type": "generate_content",
                "depends_on": [content_dep],
                "payload": {
                    "content_type": content_type,
                    "topic": gen_topic,
                    "goal_summary": f"Social post draft for {host} — {gen_topic}",
                    **_CONSENT,
                },
            }
        )

    type_step_id = "s4"
    steps.append(
        {
            "step_id": type_step_id,
            "command_type": "keyboard_type_text",
            "depends_on": [content_step_id],
            "payload": {
                "content_from_step": content_step_id,
                "text_from_step": content_step_id,
                "source_request_text": request_text,
                "submit_after": False,
                "submit_message": False,
                "active_composer": host,
                "active_site": url,
                "web_post_operator": True,
                "mvp_limitation": honest_composer_limitation(),
                **composer,
                **_CONSENT,
            },
        }
    )

    verify_step_id = "s5"
    last_dep = type_step_id
    if avatar_desktop_vision_enabled() and avatar_desktop_vision_local_enabled():
        steps.append(
            {
                "step_id": verify_step_id,
                "command_type": "verify_web_draft",
                "depends_on": [type_step_id],
                "payload": {
                    "expected_text_from_step": content_step_id,
                    "type_step_id": type_step_id,
                    "depends_on_step": type_step_id,
                    "source_request_text": request_text,
                    "active_site": url,
                    "window_name": host,
                    "enable_recovery": True,
                    "capture_if_missing": True,
                    "delay_ms": 500,
                    **_CONSENT,
                },
            }
        )
        last_dep = verify_step_id

    if wants_publish:
        publish_payload = {
            "active_site": url,
            "window_name": host,
            "button_label": "Опубликовать",
            "confirm_modal_text": "Опубликовать сейчас?",
            "content_from_step": content_step_id,
            "source_request_text": request_text,
            "deny_message": "Черновик готов. Для публикации требуется подтверждение.",
            "publish_requested": True,
            **publish_stop_zone_payload(),
            **_CONSENT,
        }
        publish_step_id = f"s{len(steps) + 1}"
        steps.append(
            {
                "step_id": publish_step_id,
                "command_type": "browser_click_publish",
                "depends_on": [last_dep],
                "payload": publish_payload,
            }
        )
        last_dep = publish_step_id
        if avatar_desktop_vision_enabled() and avatar_desktop_vision_local_enabled():
            verify_pub_id = f"s{len(steps) + 1}"
            steps.append(
                {
                    "step_id": verify_pub_id,
                    "command_type": "verify_published",
                    "depends_on": [publish_step_id],
                    "payload": {
                        "expected_text_from_step": content_step_id,
                        "depends_on_step": publish_step_id,
                        "active_site": url,
                        "window_name": host,
                        "optional": False,
                        "require_publish_verified": True,
                        **_CONSENT,
                    },
                }
            )
            last_dep = verify_pub_id

    goal_action = "опубликовать пост" if wants_publish else "черновик поста"
    return {
        "goal_summary": f"Создать {goal_action} на {host} — {topic}",
        "verifier": "web_post_published" if wants_publish else "web_post_draft",
        "steps": steps,
    }


def build_workspace_files_plan(request_text: str) -> dict[str, Any] | None:
    """Multi-step folder creation with routing mode (Safe/Balanced/Trusted)."""
    if not is_workspace_file_creation_intent(request_text):
        return None

    folder = _extract_workspace_folder_name(request_text)
    needs_clarify = folder is None
    if folder is None:
        folder = "TestFolder"

    quoted_follow_up = _is_quoted_follow_up_command(request_text)
    named = None if quoted_follow_up else _extract_named_file(request_text)
    write_files = False if quoted_follow_up else _request_explicitly_mentions_file_writes(request_text)

    target_path, policy_hint, routing_mode = _resolve_folder_creation_target(
        request_text,
        folder_name=folder,
    )
    steps: list[dict[str, Any]] = []
    prev_id: str | None = None

    def _append_step(step: dict[str, Any]) -> None:
        nonlocal prev_id
        step_id = f"s{len(steps) + 1}"
        step["step_id"] = step_id
        if prev_id:
            step["depends_on"] = [prev_id]
        steps.append(step)
        prev_id = step_id

    open_payload: dict[str, Any] = {
        "absolute_path": str(target_path),
        "routing_mode": routing_mode,
        "policy_hint": policy_hint,
        "folder_name": folder,
        **_CONSENT,
    }
    if needs_clarify:
        open_payload["suggested_folder_name"] = "TestFolder"
        open_payload["clarify_folder_name"] = True

    _append_step({"command_type": "open_folder", "payload": open_payload})

    if named is not None:
        rel_path = f"{folder}/{named[0]}"
        _append_step(
            {
                "command_type": "write_file_in_workspace",
                "payload": {
                    "relative_path": rel_path,
                    "absolute_path": str(target_path / named[0]),
                    "content": named[1],
                    "routing_mode": routing_mode,
                    **_CONSENT,
                },
            }
        )
    elif write_files:
        count = _extract_file_count(request_text)
        for index in range(1, count + 1):
            _append_step(
                {
                    "command_type": "write_file_in_workspace",
                    "payload": {
                        "relative_path": f"{folder}/file{index}.txt",
                        "absolute_path": str(target_path / f"file{index}.txt"),
                        "content": f"Текст файла {index} — подготовлено IuVe Avatar.",
                        "routing_mode": routing_mode,
                        **_CONSENT,
                    },
                }
            )

    if named is not None or write_files:
        _append_step(
            {
                "command_type": "open_folder",
                "payload": {
                    "absolute_path": str(target_path),
                    "routing_mode": routing_mode,
                    **_CONSENT,
                },
            }
        )

    scope_label = policy_hint or routing_mode
    goal = f"Создать папку {folder} ({scope_label})"
    if needs_clarify:
        goal = f"Создать папку на рабочем столе — предложено имя {folder} ({routing_mode})"
    if named is not None:
        goal = f"Создать {folder}/{named[0]} ({routing_mode})"
    elif write_files:
        goal = f"Создать папку {folder} и записать файлы ({routing_mode})"
    return {"goal_summary": goal, "routing_mode": routing_mode, "steps": steps}


def build_local_compound_plan(request_text: str) -> dict[str, Any] | None:
    """Rule-based compound plan for known maintenance/publish goals (offline fallback)."""
    compound_site_local = build_compound_site_local_plan(request_text)
    if compound_site_local is not None:
        return compound_site_local

    workspace_plan = build_workspace_files_plan(request_text)
    if workspace_plan is not None:
        return workspace_plan

    lower = str(request_text or "").strip().lower()
    if _is_maintenance_goal(lower):
        return _build_maintenance_plan()
    empty_trash_plan = build_empty_trash_plan(request_text)
    if empty_trash_plan is not None:
        return empty_trash_plan
    if _is_publish_goal(lower):
        return _build_publish_plan(request_text)

    compound = detect_compound_intent(request_text)
    if not compound.is_compound:
        return None
    return None


def build_generic_app_launch_plan(request_text: str) -> dict[str, Any] | None:
    if not is_generic_app_launch_intent(request_text):
        return None
    from ..active.device_capability_scanner import load_profile, resolve_app_launch_from_text

    resolved = resolve_app_launch_from_text(request_text, load_profile())
    if resolved is None:
        return None
    matched, candidates = resolved
    app_name = str(matched.get("name") or extract_app_query_from_text(request_text) or "application")
    app_slug = str(matched.get("slug") or app_name).strip().lower()
    from iuve_connect_agents.shared.app_resolver import legacy_slug_to_app_id

    canonical_app_id = legacy_slug_to_app_id(app_slug) or legacy_slug_to_app_id(app_name) or app_slug
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "application_launch",
            "payload": {
                "app_id": canonical_app_id,
                "application": app_slug,
                "application_name": app_name,
                "application_query": extract_app_query_from_text(request_text) or app_name,
                "launch_candidates": [list(item) for item in candidates],
                "user_consent_state": "consented_via_connect_flow",
            },
        }
    ]
    steps = _append_vision_capture_if_needed(
        steps,
        depends_on_step_id="s1",
        request_text=request_text,
    )
    return {
        "goal_summary": f"Launch {app_name}",
        "steps": steps,
    }


def _extract_document_topic(text: str) -> str:
    from ..active.libreoffice_document_intent import extract_article_topic

    return extract_article_topic(text)


def _build_document_article_content(topic: str, *, max_chars: int | None = None) -> str:
    body = (
        f"{topic}\n\n"
        f"Статья на тему «{topic}», подготовленная IuVe Avatar для LibreOffice Writer.\n"
        f"Краткое введение, основные тезисы и заключение по теме.\n"
    )
    if max_chars and len(body) > max_chars:
        return body[: max_chars - 1].rstrip() + "…"
    return body


def build_libreoffice_article_retry_plan(
    request_text: str,
    *,
    prior_user_text: str | None = None,
) -> dict[str, Any] | None:
    """Retry LibreOffice article after failure follow-up (screenshot / «статья не написана»)."""
    from ..active.libreoffice_document_intent import (
        extract_article_topic,
        extract_max_article_chars,
        is_document_article_failure_follow_up,
    )

    if not is_document_article_failure_follow_up(request_text):
        return None
    source = str(prior_user_text or request_text or "").strip()
    if not source:
        return None
    return build_document_write_plan(source)


def build_document_write_plan(request_text: str) -> dict[str, Any] | None:
    """Generate article → open LibreOffice Writer → type with Writer focus."""
    from ..active.libreoffice_document_intent import (
        extract_article_topic,
        extract_max_article_chars,
        is_libreoffice_article_intent,
    )
    from .avatar_intent_router import has_document_write_intent

    if wants_browser_open_and_type(request_text) or is_chatgpt_browser_intent(request_text):
        return None
    if is_research_write_compound_intent(request_text):
        return None
    if not is_libreoffice_article_intent(request_text) and not has_document_write_intent(request_text):
        return None
    if not is_libreoffice_article_intent(request_text):
        lower = str(request_text or "").strip().lower()
        if not any(token in lower for token in ("напиш", "write", "стать", "article")):
            return None

    topic = extract_article_topic(request_text)
    max_chars = extract_max_article_chars(request_text)
    relative_path = "IuVeWorkspace/avatar-article.odt"
    generate_payload: dict[str, Any] = {
        "content_type": "article",
        "topic": topic,
        "goal_summary": f"Generate article about {topic} for LibreOffice Writer",
        **_CONSENT,
    }
    if max_chars:
        generate_payload["max_chars"] = max_chars

    return {
        "goal_summary": f"Open LibreOffice Writer and write article about {topic}",
        "verifier": "libreoffice_article_typed",
        "steps": [
            {
                "step_id": "s1",
                "command_type": "generate_content",
                "payload": generate_payload,
            },
            {
                "step_id": "s2",
                "command_type": "open_document_edit",
                "depends_on": ["s1"],
                "payload": {
                    "relative_path": relative_path,
                    "content_from_step": "s1",
                    "app_id": "document_writer",
                    "editor": "libreoffice",
                    "mode": "edit",
                    **_CONSENT,
                },
            },
            {
                "step_id": "s3",
                "command_type": "keyboard_type_text",
                "depends_on": ["s2"],
                "payload": {
                    "content_from_step": "s1",
                    "text_from_step": "s1",
                    "app_id": "document_writer",
                    "target_app": "libreoffice",
                    "window_name": "LibreOffice",
                    "delay_sec": 6.0,
                    "focus_recovery": True,
                    **_CONSENT,
                },
            },
        ],
    }


def build_research_document_plan(request_text: str) -> dict[str, Any] | None:
    """Four-step plan: open site, fetch, summarize, write document (stauceni / iuve.eu flow)."""
    if not is_research_write_compound_intent(request_text):
        return None
    url = extract_normalized_url_from_text(request_text, correct_typos=True)
    if not url:
        match = re.search(
            r"\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}\b",
            str(request_text or ""),
        )
        if match:
            url = normalize_avatar_url(match.group(0))
    if not url:
        return None
    host = url.replace("https://", "").replace("http://", "").split("/")[0]
    safe_name = host.replace(".", "-").lower() or "site"
    doc_path = f"IuVeWorkspace/{safe_name}-summary.odt"
    if "stauceni" in host.lower():
        doc_path = "IuVeWorkspace/stauceni-summary.odt"
        goal = "Research stauceni.md and write summary document"
    else:
        goal = f"Research {host} and write summary document"
    return {
        "goal_summary": goal,
        "steps": [
            {
                "step_id": "s1",
                "command_type": "browser_open_url",
                "payload": {
                    "url": url,
                    "new_window": True,
                    "browser": "default",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
            {
                "step_id": "s2",
                "command_type": "web_fetch_bounded",
                "depends_on": ["s1"],
                "payload": {
                    "url": url,
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
            {
                "step_id": "s3",
                "command_type": "summarize_content",
                "depends_on": ["s2"],
                "payload": {
                    "content_from_step": "s2",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
            {
                "step_id": "s4",
                "command_type": "open_document_edit",
                "depends_on": ["s3"],
                "payload": {
                    "content_from_step": "s3",
                    "relative_path": doc_path,
                    "editor": "libreoffice",
                    "mode": "edit",
                    "user_consent_state": "consented_via_connect_flow",
                },
            },
        ],
    }


def build_multi_app_launch_plan(request_text: str) -> dict[str, Any] | None:
    """Sequential application_launch steps for numbered/chained open/launch commands."""
    if not is_multi_app_launch_command(request_text):
        return None
    from ..active.device_capability_scanner import load_profile, resolve_app_launch_from_text

    segments = split_multi_command_segments(request_text)
    steps: list[dict[str, Any]] = []
    prev_step_id: str | None = None
    launched_names: list[str] = []

    for segment in segments:
        if not _is_actionable_launch_segment(segment):
            continue
        if wants_local_cursor_launch(segment):
            app_name = "Cursor"
            app_slug = "cursor"
            candidates: list[list[str]] = []
        elif is_generic_app_launch_intent(segment):
            resolved = resolve_app_launch_from_text(segment, load_profile())
            if resolved is None:
                continue
            matched, raw_candidates = resolved
            app_name = str(matched.get("name") or extract_app_query_from_text(segment) or "application")
            app_slug = str(matched.get("slug") or app_name).strip().lower()
            candidates = [list(item) for item in raw_candidates]
        else:
            continue

        step_id = f"s{len(steps) + 1}"
        step: dict[str, Any] = {
            "step_id": step_id,
            "command_type": "application_launch",
            "payload": {
                "application": app_slug,
                "application_name": app_name,
                "application_query": extract_app_query_from_text(segment) or app_name,
                "launch_candidates": candidates,
                "user_consent_state": "consented_via_connect_flow",
            },
        }
        if prev_step_id:
            step["depends_on"] = [prev_step_id]
        steps.append(step)
        launched_names.append(app_name)
        prev_step_id = step_id

    if len(steps) < 2:
        return None
    return {
        "goal_summary": f"Launch {' → '.join(launched_names)}",
        "steps": steps,
    }


def build_app_launch_and_type_plan(request_text: str) -> dict[str, Any] | None:
    """Launch a matched desktop app, wait, then type text (e.g. Claude + question)."""
    if not wants_generic_app_launch_and_type(request_text):
        return None
    from ..active.device_capability_scanner import load_profile, resolve_app_launch_from_text

    resolved = resolve_app_launch_from_text(request_text, load_profile())
    if resolved is None:
        return None
    matched, candidates = resolved
    app_name = str(matched.get("name") or extract_app_query_from_text(request_text) or "application")
    app_slug = str(matched.get("slug") or app_name).strip().lower()
    typed_text = extract_app_type_text(request_text)
    lower = str(request_text or "").strip().lower()
    login_requested = any(token in lower for token in ("войди", "login", "sign in", "аккаунт"))
    steps: list[dict[str, Any]] = [
        {
            "step_id": "s1",
            "command_type": "application_launch",
            "payload": {
                "application": app_slug,
                "application_name": app_name,
                "application_query": extract_app_query_from_text(request_text) or app_name,
                "launch_candidates": [list(item) for item in candidates],
                "user_consent_state": "consented_via_connect_flow",
            },
        },
        {
            "step_id": "s2",
            "command_type": "keyboard_type_text",
            "depends_on": ["s1"],
            "payload": {
                "text": typed_text,
                "target_app": app_slug,
                "window_name": app_name,
                "delay_sec": 4.0,
                "user_consent_state": "consented_via_connect_flow",
            },
        },
    ]
    goal = f"Launch {app_name} and type text"
    if login_requested:
        goal = f"Launch {app_name} (login automation not supported; typing only)"
    return {"goal_summary": goal, "steps": steps}


def build_screen_capture_plan(request_text: str) -> dict[str, Any] | None:
    """Single-step capture when user explicitly asks for a screen screenshot."""
    if is_document_generation_intent(request_text):
        return None
    if not wants_screen_verification(request_text):
        return None
    if wants_browser_open_and_type(request_text) or wants_cursor_launch_and_type(request_text):
        return None
    if is_generic_app_launch_intent(request_text):
        return None
    return {
        "goal_summary": "Сделать скриншот экрана",
        "steps": [_vision_capture_step(step_id="s1", depends_on=[], present_to_chat=True)],
    }


def resolve_local_open_type_plan(
    request_text: str,
    *,
    prior_user_text: str | None = None,
) -> dict[str, Any] | None:
    """Prefer specialized owner E2E plans, then multi-launch, browser, workspace, etc."""
    from iuve_connect_agents.shared.device_context_model import get_planning_dcm, planning_dcm_scope
    from iuve_connect_agents.shared.plan_template_router import PlanTemplate, route_plan_template

    try:
        from ..active.capability_resolver import build_capability_snapshot
        from ...core.version import AGENT_MVP_VERSION

        snap = build_capability_snapshot(trust_state="trusted", app_version=AGENT_MVP_VERSION)
        dcm = snap.get("device_context") if isinstance(snap.get("device_context"), dict) else None
    except Exception:
        dcm = get_planning_dcm()

    with planning_dcm_scope(dcm):
        route = route_plan_template(request_text, dcm=get_planning_dcm())

        plan_only = build_plan_only_preview(request_text)
        if plan_only is not None:
            return plan_only

        empty_trash_plan = build_empty_trash_plan(request_text)
        if empty_trash_plan is not None:
            return empty_trash_plan

        retry_doc_plan = build_libreoffice_article_retry_plan(
            request_text,
            prior_user_text=prior_user_text,
        )
        if retry_doc_plan is not None:
            return retry_doc_plan

        if route.template == PlanTemplate.COMPOUND_SITE_LOCAL:
            compound_site_local = build_compound_site_local_plan(request_text)
            if compound_site_local is not None:
                return compound_site_local

        libreoffice_doc_plan = build_document_write_plan(request_text)
        if libreoffice_doc_plan is not None:
            return libreoffice_doc_plan

        if route.template == PlanTemplate.SCREEN_CAPTURE:
            capture = build_screen_capture_plan(request_text)
            if capture is not None:
                return capture

        compound_site_local = build_compound_site_local_plan(request_text)
        if compound_site_local is not None:
            return compound_site_local

        folder_analysis_plan = build_folder_analysis_plan(request_text)
        if folder_analysis_plan is not None:
            return folder_analysis_plan

        landing_plan = build_landing_page_plan(request_text)
        if landing_plan is not None:
            return landing_plan

        research_artifact_plan = build_research_then_artifact_plan(request_text)
        if research_artifact_plan is not None:
            return research_artifact_plan

        document_plan = build_document_generation_plan(request_text)
        if document_plan is not None:
            return document_plan

        notepad_plan = build_notepad_text_plan(request_text)
        if notepad_plan is not None:
            return notepad_plan

        artifact_plan = build_artifact_generation_plan(request_text)
        if artifact_plan is not None:
            return artifact_plan

        if not route.atoms.forbid_web_post:
            web_post_plan = build_web_post_plan(request_text)
            if web_post_plan is not None:
                return web_post_plan

        workspace_files_plan = build_workspace_files_plan(request_text)
        if workspace_files_plan is not None:
            return workspace_files_plan

        web_research_plan = build_web_research_plan(request_text)
        if web_research_plan is not None:
            return web_research_plan

        local_job_plan = build_local_computer_job_search_plan(request_text)
        if local_job_plan is not None:
            return local_job_plan

        active_window_plan = build_active_window_plan(request_text)
        if active_window_plan is not None:
            return active_window_plan

        browser_load_plan = build_browser_load_verify_plan(request_text)
        if browser_load_plan is not None:
            return browser_load_plan

        browser_search_plan = build_browser_search_navigation_plan(request_text)
        if browser_search_plan is not None:
            return browser_search_plan

        metrics_plan = build_system_metrics_plan(request_text)
        if metrics_plan is not None:
            return metrics_plan

        downloads_plan = build_downloads_search_plan(request_text)
        if downloads_plan is not None:
            return downloads_plan

        monitor_plan = build_system_monitor_plan(request_text)
        if monitor_plan is not None:
            return monitor_plan

        app_goal_plan = build_app_launch_with_goal_plan(request_text)
        if app_goal_plan is not None:
            return app_goal_plan

        capture_plan = build_screen_capture_plan(request_text)
        if capture_plan is not None:
            return capture_plan

        multi_plan = build_multi_app_launch_plan(request_text)
        if multi_plan is not None:
            return multi_plan

        if wants_browser_open_and_type(request_text) or (
            is_chatgpt_browser_intent(request_text) and wants_browser_open_and_type(request_text)
        ):
            return build_browser_open_type_plan(request_text)

        app_plan = build_app_launch_and_type_plan(request_text)
        if app_plan is not None:
            return app_plan

        research_plan = build_research_document_plan(request_text)
        if research_plan is not None:
            return research_plan

        plan = build_cursor_launch_type_plan(request_text)
        if plan is not None:
            return plan
        return build_browser_open_type_plan(request_text)


def _is_misrouted_document_plan(request_text: str, plan: dict[str, Any]) -> bool:
    steps = plan.get("steps")
    if not isinstance(steps, list) or not steps:
        return False
    first = steps[0] if isinstance(steps[0], dict) else {}
    first_type = str(first.get("command_type") or "").strip().lower()
    if wants_browser_open_and_type(request_text) or is_chatgpt_browser_intent(request_text):
        return first_type in {"open_document_edit", "application_launch"}
    if is_research_write_compound_intent(request_text):
        return first_type == "open_document_edit" and len(steps) <= 2
    return False


def maybe_prefer_libreoffice_plan(
    request_text: str,
    plan: dict[str, Any] | None,
    *,
    prior_user_text: str | None = None,
) -> dict[str, Any] | None:
    """Replace misrouted cloud/web_post plans with local LibreOffice article plan."""
    from ..active.libreoffice_document_intent import is_misrouted_cloud_plan_for_libreoffice

    local_plan = build_libreoffice_article_retry_plan(
        request_text,
        prior_user_text=prior_user_text,
    ) or build_document_write_plan(request_text)
    if local_plan is None:
        return plan
    if not isinstance(plan, dict):
        return local_plan
    if is_misrouted_cloud_plan_for_libreoffice(request_text, plan):
        return local_plan
    return plan


def maybe_correct_misrouted_plan(
    request_text: str,
    plan: dict[str, Any] | None,
    *,
    prior_user_text: str | None = None,
) -> dict[str, Any] | None:
    """Replace cloud/LLM document stubs when local heuristics know the correct plan."""
    plan = maybe_prefer_libreoffice_plan(
        request_text,
        plan,
        prior_user_text=prior_user_text,
    )
    if isinstance(plan, dict) and _is_misrouted_diagnostics_plan(request_text, plan):
        replacement = resolve_local_open_type_plan(request_text)
        if replacement is not None:
            return replacement
    if not isinstance(plan, dict) or not _is_misrouted_document_plan(request_text, plan):
        return plan
    replacement = resolve_local_open_type_plan(request_text)
    return replacement if replacement is not None else plan


def maybe_replace_get_status_stub(
    request_text: str,
    plan: dict[str, Any] | None,
    *,
    prior_user_text: str | None = None,
) -> dict[str, Any] | None:
    """Return a local compound plan when backend sent a single-step get_status stub."""
    plan = maybe_correct_misrouted_plan(
        request_text,
        plan,
        prior_user_text=prior_user_text,
    )
    if not isinstance(plan, dict) or not _is_get_status_stub(plan):
        return plan

    research_artifact_plan = build_research_then_artifact_plan(request_text)
    if research_artifact_plan is not None:
        return research_artifact_plan

    web_research_plan = build_web_research_plan(request_text)
    if web_research_plan is not None:
        return web_research_plan

    document_plan = build_document_generation_plan(request_text)
    if document_plan is not None:
        return document_plan

    artifact_plan = build_artifact_generation_plan(request_text)
    if artifact_plan is not None:
        return artifact_plan

    capture_plan = build_screen_capture_plan(request_text)
    if capture_plan is not None:
        return capture_plan

    if is_workspace_file_creation_intent(request_text):
        workspace_plan = build_workspace_files_plan(request_text)
        if workspace_plan is not None:
            return workspace_plan

    open_type_plan = resolve_local_open_type_plan(request_text, prior_user_text=prior_user_text)
    if open_type_plan is not None:
        return open_type_plan

    workspace_plan = build_workspace_files_plan(request_text)
    if workspace_plan is not None:
        return workspace_plan

    replacement = build_local_compound_plan(request_text)
    return replacement if replacement is not None else plan
