"""Tool: ``save_plan``. Publish the sandbox plan file for review.

Reads the Markdown plan file the agent created in the sandbox and publishes it to
the plan-review page, where the user and collaborators read it, comment inline,
and approve or request changes. Available in plan mode (it does not modify the
repository under review).
"""

from __future__ import annotations

import logging
from collections.abc import Mapping
from typing import Any

from langgraph.config import get_config

from ..dashboard.plan_store import PLAN_FILE_DIRECTORY, PLAN_STATUS_READY, save_plan_content
from ..utils.sandbox_state import get_sandbox_backend

logger = logging.getLogger(__name__)

_MAX_PLAN_LINES = 20_000
_MARKDOWN_EXTENSIONS = (".md", ".markdown")


async def save_plan(plan_file_path: str) -> dict[str, Any]:
    """Publish a Markdown plan file from the sandbox for review.

    Use this in plan mode once your plan is ready. First create a Markdown file
    under ``/workspace/plans/`` using a dated, descriptive filename, then pass
    that file path here. The file contents are published to the plan-review page
    linked in the conversation, where the user (the owner) and any collaborators
    can read it, leave inline comments, and then approve it or request changes.
    Call it again to publish a revised file when addressing feedback.

    Write the plan in standard Markdown — headings, bullet/numbered lists, and
    fenced code blocks all render. Keep it concise and high level, focusing on
    approach, decisions/tradeoffs, risks, and verification; avoid file/function
    details unless they are unusually tricky or controversial.

    Args:
        plan_file_path: Path to the Markdown plan file in the sandbox.

    Returns:
        ``{success: True, path}`` on success, or ``{success: False, error}``.
    """
    if not isinstance(plan_file_path, str):
        return {"success": False, "error": "plan_file_path must be a string"}
    path = plan_file_path.strip()
    if not path:
        return {"success": False, "error": "plan_file_path cannot be empty"}
    if not _is_markdown_path(path):
        return {
            "success": False,
            "error": f"plan_file_path must point to a Markdown file in {PLAN_FILE_DIRECTORY}",
        }

    try:
        config = get_config()
    except Exception:
        config = {}
    configurable = config.get("configurable", {}) if isinstance(config, dict) else {}
    thread_id = configurable.get("thread_id") if isinstance(configurable, dict) else None
    if not thread_id:
        return {"success": False, "error": "no thread_id in run config"}

    try:
        content = (await _read_plan_file(str(thread_id), path)).strip()
        if not content:
            return {"success": False, "error": "plan file cannot be empty"}
        await _save(str(thread_id), content, path)
    except Exception as exc:  # noqa: BLE001
        logger.exception("save_plan failed for thread %s", thread_id)
        return {"success": False, "error": f"failed to save plan: {exc}"}
    return {"success": True, "path": path}


async def _save(thread_id: str, content: str, path: str) -> None:
    await save_plan_content(
        thread_id, markdown=content, status=PLAN_STATUS_READY, plan_file_path=path
    )


async def _read_plan_file(thread_id: str, path: str) -> str:
    backend = await get_sandbox_backend(thread_id)
    result = await backend.aread(path, offset=0, limit=_MAX_PLAN_LINES)
    error = _value(result, "error")
    if error:
        raise ValueError(error)
    file_data = _value(result, "file_data")
    if file_data is None:
        raise ValueError("plan file could not be read")
    encoding = _value(file_data, "encoding")
    if encoding is not None and encoding != "utf-8":
        raise ValueError("plan file must be UTF-8 text")
    content = _value(file_data, "content")
    if not isinstance(content, str):
        raise ValueError("plan file content was not text")
    if content.count("\n") + 1 >= _MAX_PLAN_LINES:
        raise ValueError("plan file is too large")
    return content


def _value(value: Any, key: str) -> Any:
    if isinstance(value, Mapping):
        return value.get(key)
    return getattr(value, key, None)


def _is_markdown_path(path: str) -> bool:
    if "\x00" in path or not path.startswith(f"{PLAN_FILE_DIRECTORY}/"):
        return False
    filename = path.removeprefix(f"{PLAN_FILE_DIRECTORY}/")
    if not filename or "/" in filename:
        return False
    return filename.lower().endswith(_MARKDOWN_EXTENSIONS)
