"""Dashboard API endpoints for the Tools tab.

Exposes tool registry, prompt building, job queue, and execution via REST.
"""

import sys
import shutil
from typing import Any

import structlog
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field

from proxym.tools import ToolCategory, ToolRegistry

logger = structlog.get_logger()
router = APIRouter(prefix="/tools", tags=["tools"])


def _aider_install_fix() -> dict[str, str]:
    """Return a safer aider installation hint for the current environment."""
    for py_bin in ("python3.12", "python3.11"):
        if shutil.which(py_bin):
            return {
                "action": "install",
                "label": "Install aider",
                "command": f"{py_bin} -m pip install aider-chat",
                "description": "Install aider CLI in a Python 3.12/3.11 environment (Python 3.13 may fail to build aider-chat).",
            }

    # Fall back to the current interpreter only when it is not 3.13+.
    if sys.version_info < (3, 13):
        return {
            "action": "install",
            "label": "Install aider",
            "command": "python3 -m pip install aider-chat",
            "description": "Install aider CLI in the current Python environment.",
        }

    return {
        "action": "install",
        "label": "Install aider",
        "command": "python3.12 -m pip install aider-chat",
        "description": "Install aider CLI in Python 3.12/3.11 first; Python 3.13 often fails to build aider-chat.",
    }


def _health_diag_base(tool, tool_providers: dict[str, str]) -> dict[str, Any]:
    return {
        "name": tool.name,
        "category": tool.category.value,
        "available": tool.is_available,
        "binary": tool.binary,
        "binary_found": bool(tool.binary and shutil.which(tool.binary)),
        "provider": tool_providers.get(tool.name, "openrouter"),
        "provider_configured": False,
        "api_key_present": False,
        "docker_instance": False,
        "status": "unknown",
        "issues": [],
        "fixes": [],
    }


def _apply_provider_health(diag: dict[str, Any], configured_providers: dict[str, Any]) -> None:
    provider_name = diag["provider"]
    if diag["name"] == "human":
        diag["provider"] = "manual"
        diag["provider_configured"] = True
        diag["api_key_present"] = True
        return

    if provider_name in configured_providers:
        diag["provider_configured"] = True
        diag["api_key_present"] = True
        return

    if "openrouter" in configured_providers and provider_name != "ollama":
        diag["provider_configured"] = True
        diag["api_key_present"] = True
        diag["provider"] = f"{provider_name} (via openrouter)"
        return

    diag["issues"].append(f"Provider '{provider_name}' not configured — no API key")
    diag["fixes"].append({
        "action": "config",
        "label": f"Set {provider_name.upper()}_API_KEY",
        "command": f"export {provider_name.upper()}_API_KEY=<your-key>",
        "description": f"Add API key for {provider_name} in .env or environment",
    })


def _apply_runtime_health(diag: dict[str, Any], tool) -> None:
    if tool.name == "human":
        return

    if tool.binary and not diag["binary_found"]:
        if tool.name == "aider":
            diag["issues"].append(f"{tool.binary} binary not found on PATH")
            diag["fixes"].append(_aider_install_fix())
        elif tool.name == "claude-code":
            diag["issues"].append(f"{tool.binary} binary not found on PATH")
            diag["fixes"].append({
                "action": "install",
                "label": "Install claude-code",
                "command": "npm install -g @anthropic-ai/claude-code",
                "description": "Install Claude Code CLI tool",
            })
        elif tool.name in ("windsurf", "cursor"):
            diag["issues"].append(f"{tool.name} is a desktop IDE — not available in container")
            diag["fixes"].append({
                "action": "info",
                "label": f"{tool.name} runs on host only",
                "command": f"# Install from https://{tool.name}.com — dispatches via echo inside container",
                "description": "Desktop IDE tool — dispatch works but actual execution requires host",
            })

    for inst in tool.instances:
        if inst.kind == "docker" and inst.is_available:
            diag["docker_instance"] = True
            break

    if tool.name in ("vscode", "roo-code", "cline") and not diag["docker_instance"]:
        diag["issues"].append("Docker-backed VS Code container not running")
        diag["fixes"].append({
            "action": "start_container",
            "label": "Start VS Code container",
            "command": "docker compose --profile vscode up -d",
            "description": "Start the VS Code Web container used by roo-code, cline, and vscode tools",
        })

    if tool.name == "browser" and not diag["docker_instance"]:
        diag["issues"].append("Open WebUI container not running")
        diag["fixes"].append({
            "action": "start_container",
            "label": "Start Open WebUI",
            "command": "docker compose --profile webui up -d",
            "description": "Start the browser-based chat container",
        })


def _finalize_health_status(diag: dict[str, Any]) -> None:
    if not diag["issues"]:
        diag["status"] = "ok"
    elif diag["available"] or diag["provider_configured"]:
        diag["status"] = "degraded"
    else:
        diag["status"] = "unavailable"


# ── Request models ────────────────────────────────────────

class DispatchRequest(BaseModel):
    ticket_id: str = Field(..., description="Ticket ID to dispatch")
    tool: str = Field(..., description="Tool name (aider, claude-code, ...)")
    mode: str = Field("", description="Agent mode (code, architect, debug, ask)")
    model: str = Field("", description="Model alias or full name")
    project_dir: str = Field("", description="Project directory")


class PreviewPromptRequest(BaseModel):
    ticket_id: str = Field(..., description="Ticket ID")
    tool: str = Field(..., description="Tool name")
    mode: str = Field("", description="Agent mode")


# ── Tool registry endpoints ──────────────────────────────

@router.get("/")
async def list_tools(
    category: str | None = Query(None, description="Filter by category: ide, agent_cli, browser"),
    available_only: bool = Query(False, description="Only show tools with an available host or docker-backed instance"),
) -> dict[str, Any]:
    """List all registered tools."""
    reg = ToolRegistry.get()
    cat = ToolCategory(category) if category else None
    tools = reg.list_tools(category=cat, available_only=available_only)
    return {"tools": [t.summary() for t in tools], "count": len(tools)}


@router.get("/summary")
async def tools_summary() -> dict[str, Any]:
    """Summary of tool availability."""
    return ToolRegistry.get().summary()


# ── Prompt preview ───────────────────────────────────────

@router.post("/prompt/preview")
async def preview_prompt(req: PreviewPromptRequest) -> dict[str, Any]:
    """Preview the prompt that would be sent to a tool for a ticket."""
    tool = ToolRegistry.get().get_tool(req.tool)
    if not tool:
        raise HTTPException(404, f"Tool '{req.tool}' not found")

    from proxym.tools.prompt_builder import build_prompt_dict
    ticket = _get_ticket(req.ticket_id)
    return build_prompt_dict(ticket, tool, req.mode)


# ── Job dispatch ─────────────────────────────────────────

@router.post("/dispatch")
async def dispatch_job(req: DispatchRequest) -> dict[str, Any]:
    """Dispatch a tool to work on a ticket. Enqueues a job."""
    tool = ToolRegistry.get().get_tool(req.tool)
    if not tool:
        raise HTTPException(404, f"Tool '{req.tool}' not found")

    ticket = _get_ticket(req.ticket_id)
    if tool.name == "human":
        from proxym.dashboard._tickets_api import _tickets
        from proxym.dashboard.tickets import ToolAssignment

        mgr = _tickets()
        mgr.assign_tool(ticket.id, ToolAssignment(tool=tool.name, agent_mode=req.mode, model=req.model))
        updated = mgr.get_ticket(ticket.id)
        return {
            "job": None,
            "ticket": updated.summary() if updated else ticket.summary(),
            "message": f"Human task assigned for ticket {ticket.id}",
        }

    project_dir = req.project_dir or _default_project_dir(ticket.project_id)

    from proxym.tools.executor import ToolExecutor
    executor = ToolExecutor.get()
    # Make dispatch self-contained: if the worker isn't running yet, start it
    # before enqueueing so the job doesn't stay queued forever.
    await executor.start()
    job = executor.enqueue(ticket, tool, project_dir, req.mode, req.model)
    return {"job": job.summary(), "message": f"Job {job.id} enqueued for {tool.name}"}


# ── Job queue endpoints (sub-router to avoid /{tool_name} shadow) ──

queue_router = APIRouter(prefix="/queue", tags=["tools"])


def _job_snapshot(job) -> dict[str, Any]:
    """Return a rich job payload for the dashboard UI and exports."""
    data = job.summary()
    if job.result:
        data["result"] = {
            **job.result.summary(),
            "stdout": job.result.stdout,
            "stderr": job.result.stderr,
        }
    if job.ticket_id:
        try:
            data["ticket"] = _get_ticket(job.ticket_id).summary()
        except HTTPException:
            data["ticket"] = None
    return data


@queue_router.get("")
async def queue_status() -> dict[str, Any]:
    """Get queue summary and recent jobs."""
    from proxym.tools.executor import ToolExecutor
    executor = ToolExecutor.get()
    summary = executor.queue_summary()
    recent = executor.list_jobs(limit=20)
    summary["recent_jobs"] = [_job_snapshot(j) for j in recent]
    return summary


@queue_router.get("/jobs")
async def list_jobs(
    status: str | None = Query(None, description="Filter: queued, running, done, failed, cancelled"),
    limit: int = Query(50, ge=1, le=200),
) -> dict[str, Any]:
    """List jobs with optional status filter."""
    from proxym.tools.executor import JobStatus, ToolExecutor
    executor = ToolExecutor.get()
    st = JobStatus(status) if status else None
    jobs = executor.list_jobs(status=st, limit=limit)
    return {"jobs": [_job_snapshot(j) for j in jobs], "count": len(jobs)}


@queue_router.get("/jobs/{job_id}")
async def get_job(job_id: str) -> dict[str, Any]:
    """Get details for a specific job."""
    from proxym.tools.executor import ToolExecutor
    job = ToolExecutor.get().get_job(job_id)
    if not job:
        raise HTTPException(404, f"Job '{job_id}' not found")
    return _job_snapshot(job)


@queue_router.post("/jobs/{job_id}/cancel")
async def cancel_job(job_id: str) -> dict[str, Any]:
    """Cancel a queued job."""
    from proxym.tools.executor import ToolExecutor
    if ToolExecutor.get().cancel_job(job_id):
        return {"ok": True, "job_id": job_id}
    raise HTTPException(400, "Job not found or not in queued state")


@queue_router.post("/jobs/{job_id}/retry")
async def retry_job(job_id: str) -> dict[str, Any]:
    """Retry a failed job — re-enqueue with same parameters."""
    from proxym.tools.executor import JobStatus, ToolExecutor
    executor = ToolExecutor.get()
    old_job = executor.get_job(job_id)
    if not old_job:
        raise HTTPException(404, f"Job '{job_id}' not found")
    if old_job.status not in (JobStatus.FAILED, JobStatus.CANCELLED):
        raise HTTPException(400, "Only failed or cancelled jobs can be retried")

    ticket = _get_ticket(old_job.ticket_id)
    tool = ToolRegistry.get().get_tool(old_job.tool_name)
    if not tool:
        raise HTTPException(404, f"Tool '{old_job.tool_name}' no longer registered")

    await executor.start()
    new_job = executor.enqueue(ticket, tool, old_job.project_dir, old_job.mode, old_job.model)
    return {"ok": True, "old_job_id": job_id, "new_job": new_job.summary()}


@queue_router.post("/jobs/{job_id}/repair")
async def repair_job(job_id: str) -> dict[str, Any]:
    """Run auto-diagnosis on a failed job and suggest/apply fixes."""
    from proxym.tools.executor import JobStatus, ToolExecutor
    executor = ToolExecutor.get()
    job = executor.get_job(job_id)
    if not job:
        raise HTTPException(404, f"Job '{job_id}' not found")
    if job.status != JobStatus.FAILED:
        raise HTTPException(400, "Only failed jobs can be repaired")

    quick_fixes = _suggest_quick_fixes(job)
    try:
        from proxym.observability.repair_agent import RepairAgent
        agent = RepairAgent(mode="suggest")
        diagnosis = await agent.diagnose_and_repair(
            error_text=job.error,
            traceback_text=job.result.stderr if job.result else "",
        )
    except Exception as exc:
        diagnosis = {"error": f"Repair diagnosis failed: {exc}"}

    return _finalize_repair_diagnosis(job_id, job, quick_fixes, diagnosis)


# ── Auto-fix execution ────────────────────────────────────

class AutoFixRequest(BaseModel):
    tool_name: str = Field(..., description="Tool name to fix")
    fix_action: str = Field(..., description="Action type: install, config, start_container")
    command: str = Field("", description="Command to execute (optional)")


@router.post("/autofix")
async def execute_autofix(req: AutoFixRequest) -> dict[str, Any]:
    """Execute an automatic fix for a tool issue.

    Attempts to auto-fix: install missing binaries, start containers, etc.
    If human action is required, creates a ticket assigned to the default user.
    """
    import asyncio
    import subprocess

    result = {
        "tool": req.tool_name,
        "action": req.fix_action,
        "executed": False,
        "output": "",
        "error": "",
        "ticket_created": None,
    }

    # Determine if this fix can be auto-executed
    auto_executable = req.fix_action in ("install", "start_container")
    requires_human = req.fix_action in ("info", "config")

    if requires_human:
        # Create ticket for human action
        ticket = _create_human_action_ticket(req.tool_name, req.fix_action, req.command)
        result["ticket_created"] = ticket.summary() if ticket else None
        result["error"] = f"Fix requires human action. Ticket created: {ticket.id if ticket else 'failed'}"
        return result

    if not auto_executable:
        result["error"] = f"Unknown fix action: {req.fix_action}"
        return result

    # Execute the fix
    try:
        if req.fix_action == "start_container":
            # Execute docker compose command
            cmd = req.command or "docker compose up -d"
            proc = await asyncio.create_subprocess_shell(
                cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
            result["output"] = stdout.decode() if stdout else ""
            result["error"] = stderr.decode() if stderr else ""
            result["exit_code"] = proc.returncode
            result["executed"] = proc.returncode == 0

        elif req.fix_action == "install":
            # Execute install command
            cmd = req.command
            if not cmd:
                result["error"] = "No install command provided"
                return result

            proc = await asyncio.create_subprocess_shell(
                cmd,
                stdout=asyncio.subprocess.PIPE,
                stderr=asyncio.subprocess.PIPE,
            )
            stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=300)
            result["output"] = stdout.decode() if stdout else ""
            result["error"] = stderr.decode() if stderr else ""
            result["exit_code"] = proc.returncode
            result["executed"] = proc.returncode == 0

    except asyncio.TimeoutError:
        result["error"] = "Fix execution timed out"
    except Exception as exc:
        result["error"] = f"Fix execution failed: {exc}"

    return result


def _create_human_action_ticket(tool_name: str, action: str, command: str) -> Any:
    """Create a ticket for human-required actions and assign to default user."""
    from proxym.dashboard._tickets_api import _tickets
    from proxym.dashboard.tickets import Ticket, TicketPriority, TicketType

    action_labels = {
        "info": "Manual setup required",
        "config": "Configuration required",
    }

    title = f"{action_labels.get(action, 'Action required')}: {tool_name}"
    description = f"""Tool: {tool_name}
Action needed: {action}
Command/Info: {command}

This tool requires manual intervention that cannot be automated.
Please review and complete the required setup.
"""

    try:
        mgr = _tickets()
        # Get default user for assignment
        default_user = _get_default_user()

        ticket = Ticket(
            task=title,
            description=description,
            priority=TicketPriority.HIGH,
            type=TicketType.TASK,
            tags=["autofix", "human-required", tool_name],
        )
        created = mgr.create_ticket(ticket)

        # Assign to default user if available
        if default_user:
            # Add comment with assignment
            from proxym.dashboard.tickets import TicketComment
            comment = TicketComment(
                author="system",
                content=f"Auto-assigned to default user: {default_user.get('name', 'unknown')} ({default_user.get('email', 'no email')})",
            )
            mgr.add_comment(created.id, comment)

        return created
    except Exception as exc:
        logger.error("Failed to create human action ticket", tool=tool_name, error=str(exc))
        return None


def _get_default_user() -> dict[str, Any] | None:
    """Get the default/delegated user from users store."""
    from proxym.dashboard.users import UserManager
    try:
        mgr = UserManager.get()
        return mgr.get_default_user()
    except Exception:
        return None


# ── Bulk auto-fix for health check issues ─────────────────

@router.post("/autofix/bulk")
async def bulk_autofix() -> dict[str, Any]:
    """Run auto-fix for all degraded tools that support automatic fixes."""
    from proxym.config import get_settings
    settings = get_settings()
    configured_providers = settings.available_providers()
    tool_providers = settings.tool_providers
    reg = ToolRegistry.get()

    results = []
    executed = 0
    failed = 0
    human_required = 0

    for tool in reg.list_tools():
        diag = _health_diag_base(tool, tool_providers)
        _apply_provider_health(diag, configured_providers)
        _apply_runtime_health(diag, tool)
        _finalize_health_status(diag)

        if diag["status"] != "ok" and diag["fixes"]:
            for fix in diag["fixes"]:
                action = fix.get("action", "")
                if action in ("install", "start_container"):
                    # Auto-execute
                    try:
                        req = AutoFixRequest(
                            tool_name=tool.name,
                            fix_action=action,
                            command=fix.get("command", ""),
                        )
                        result = await execute_autofix(req)
                        results.append({
                            "tool": tool.name,
                            "fix": fix.get("label", ""),
                            "result": result,
                        })
                        if result.get("executed"):
                            executed += 1
                        else:
                            failed += 1
                    except Exception as exc:
                        results.append({
                            "tool": tool.name,
                            "fix": fix.get("label", ""),
                            "error": str(exc),
                        })
                        failed += 1
                elif action in ("info", "config"):
                    # Create ticket for human action
                    ticket = _create_human_action_ticket(tool.name, action, fix.get("command", ""))
                    results.append({
                        "tool": tool.name,
                        "fix": fix.get("label", ""),
                        "ticket_created": ticket.id if ticket else None,
                    })
                    human_required += 1

    return {
        "executed": executed,
        "failed": failed,
        "human_required": human_required,
        "results": results,
    }


def _suggest_quick_fixes(job) -> list[dict[str, str]]:
    """Return actionable fix suggestions for common job failures."""
    fixes: list[dict[str, str]] = []
    error = (job.error or "").lower()
    tool = job.tool_name

    if "not found on path" in error or "no such file or directory" in error:
        if tool == "aider":
            fixes.append(_aider_install_fix())
        elif tool == "vscode":
            fixes.append({
                "action": "start_container",
                "label": "Start VS Code container",
                "command": "docker compose --profile vscode up -d",
                "description": "Start VS Code Web via Docker Compose",
            })
        elif tool in ("windsurf", "cursor"):
            fixes.append({
                "action": "info",
                "label": f"Install {tool}",
                "command": f"# {tool} is a desktop IDE — install from https://{tool}.com",
                "description": f"{tool} is not available as CLI inside containers. Use from host machine.",
            })
        elif tool == "claude-code":
            fixes.append({
                "action": "install",
                "label": "Install Claude Code",
                "command": "npm install -g @anthropic-ai/claude-code",
                "description": "Install Claude Code CLI via npm",
            })
        else:
            fixes.append({
                "action": "info",
                "label": f"Install {tool}",
                "command": f"# Install {tool} binary and ensure it's on PATH",
                "description": f"Binary for {tool} not found in container",
            })

    if "docker" in error:
        fixes.append({
            "action": "info",
            "label": "Docker not available",
            "command": "# Docker socket not mounted in container. Run from host.",
            "description": "This tool requires Docker, which is not available inside the proxym container.",
        })

    if "timeout" in error:
        fixes.append({
            "action": "retry",
            "label": "Retry with longer timeout",
            "command": "",
            "description": "The tool timed out. Try dispatching again — the service may have been starting up.",
        })

    if "queue full" in error:
        fixes.append({
            "action": "retry",
            "label": "Retry later",
            "command": "",
            "description": "Job queue was full. Wait for current jobs to finish and retry.",
        })

    if not fixes:
        fixes.append({
            "action": "diagnose",
            "label": "Run LLM diagnosis",
            "command": "",
            "description": "No known quick fix. Click to run AI-powered diagnosis.",
        })

    return fixes


def _finalize_repair_diagnosis(
    job_id: str,
    job,
    quick_fixes: list[dict[str, str]],
    diagnosis: Any,
) -> dict[str, Any]:
    """Normalize repair agent output and fill in local fallback guidance."""
    normalized = diagnosis if isinstance(diagnosis, dict) else {}
    normalized.setdefault("diagnosis", "")
    normalized.setdefault("suggestion", "")

    if not normalized.get("diagnosis") and not normalized.get("suggestion"):
        if quick_fixes:
            normalized["diagnosis"] = f"Detected a likely environment issue from the job error: {job.error}"
            normalized["suggestion"] = quick_fixes[0].get("description", quick_fixes[0].get("label", ""))
        else:
            normalized["diagnosis"] = f"No known fix found for job error: {job.error}"
            normalized["suggestion"] = "Run a manual diagnosis or retry the job after checking the environment."

    normalized["quick_fixes"] = quick_fixes
    normalized["job_id"] = job_id
    normalized["tool"] = job.tool_name
    normalized["job_error"] = job.error
    return normalized


@queue_router.post("/start")
async def start_executor() -> dict[str, Any]:
    """Start the background executor loop."""
    from proxym.tools.executor import ToolExecutor
    await ToolExecutor.get().start()
    return {"ok": True, "message": "Executor started"}


@queue_router.post("/stop")
async def stop_executor() -> dict[str, Any]:
    """Stop the background executor loop."""
    from proxym.tools.executor import ToolExecutor
    await ToolExecutor.get().stop()
    return {"ok": True, "message": "Executor stopped"}


router.include_router(queue_router)


# ── Tool health check — tests every tool's readiness ────

@router.get("/healthcheck")
async def tools_healthcheck() -> dict[str, Any]:
    """Comprehensive health check for every registered tool.

    For each tool returns: binary available, provider configured,
    api key present, docker instance available, and actionable diagnosis.
    """
    from proxym.config import get_settings
    settings = get_settings()
    configured_providers = settings.available_providers()
    tool_providers = settings.tool_providers
    reg = ToolRegistry.get()

    results = []
    total_ok = 0
    total_degraded = 0
    total_unavailable = 0

    for tool in reg.list_tools():
        diag = _health_diag_base(tool, tool_providers)
        _apply_provider_health(diag, configured_providers)
        _apply_runtime_health(diag, tool)
        _finalize_health_status(diag)

        if diag["status"] == "ok":
            total_ok += 1
        elif diag["status"] == "degraded":
            total_degraded += 1
        else:
            total_unavailable += 1

        results.append(diag)

    return {
        "tools": results,
        "summary": {
            "total": len(results),
            "ok": total_ok,
            "degraded": total_degraded,
            "unavailable": total_unavailable,
        },
        "providers_configured": list(configured_providers.keys()),
        "tool_providers": tool_providers,
    }


# ── Tool → Provider assignment ───────────────────────────

class ToolProviderRequest(BaseModel):
    tool: str = Field(..., description="Tool name")
    provider: str = Field(..., description="Provider name (openrouter, anthropic, openai, ollama, ...)")


@router.post("/provider")
async def set_tool_provider(req: ToolProviderRequest) -> dict[str, Any]:
    """Assign an LLM provider to a specific tool."""
    from proxym.config import get_settings
    settings = get_settings()

    tool = ToolRegistry.get().get_tool(req.tool)
    if not tool:
        raise HTTPException(404, f"Tool '{req.tool}' not found")

    configured = settings.available_providers()
    if req.provider not in configured and req.provider != "openrouter":
        raise HTTPException(400,
            f"Provider '{req.provider}' not configured. "
            f"Available: {', '.join(configured.keys())}")

    settings.tool_providers[req.tool] = req.provider
    logger.info("tool_provider_set", tool=req.tool, provider=req.provider)
    return {
        "ok": True,
        "tool": req.tool,
        "provider": req.provider,
        "all_mappings": settings.tool_providers,
    }


@router.get("/providers")
async def get_tool_providers() -> dict[str, Any]:
    """Get current tool → provider mappings."""
    from proxym.config import get_settings
    settings = get_settings()
    configured = settings.available_providers()
    return {
        "tool_providers": settings.tool_providers,
        "available_providers": list(configured.keys()),
    }


# NOTE: Dynamic route MUST be after include_router(queue_router).
@router.get("/{tool_name}")
async def get_tool(tool_name: str) -> dict[str, Any]:
    """Get details for a specific tool."""
    tool = ToolRegistry.get().get_tool(tool_name)
    if not tool:
        raise HTTPException(404, f"Tool '{tool_name}' not found")
    return tool.summary()


# ── Helpers ──────────────────────────────────────────────

def _get_ticket(ticket_id: str):
    """Fetch a ticket by ID. Raises 404 if not found."""
    from proxym.dashboard._tickets_api import _tickets
    mgr = _tickets()
    ticket = mgr.get_ticket(ticket_id)
    if not ticket:
        raise HTTPException(404, f"Ticket '{ticket_id}' not found")
    return ticket


def _default_project_dir(project_id: str | None = None) -> str:
    """Return project directory for dispatch."""
    if project_id:
        try:
            from proxym.dashboard._projects_api import _get_registry

            project = _get_registry().get(project_id)
            if project and project.local_path:
                return project.local_path
        except Exception:
            pass
    try:
        from proxym.config import get_settings

        s = get_settings()
        return str(getattr(s, "projects_root", "~/github"))
    except Exception:
        return "~/github"
