"""Display helper functions for the TUI."""

import ast
import json
import os
import re
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from rich import box
from rich.console import Console
from rich.markup import escape
from rich.panel import Panel
from rich.table import Table

from cadecoder.core.constants import UI_STYLE
from cadecoder.core.logging import get_log_file_path

# Console instance with fixed width for consistent display
console = Console(width=110, stderr=True)


def _format_result_summary(result: str, max_len: int = 80) -> str:
    """Format a tool result into a brief summary.

    Intelligently summarizes JSON structures without hardcoded field names.

    Args:
        result: Raw result string (often JSON)
        max_len: Maximum summary length

    Returns:
        Brief summary string
    """
    if not result:
        return "[dim]empty[/dim]"

    # Check for error patterns
    result_lower = result.lower()
    if "failed" in result_lower[:100] or "error" in result_lower[:100]:
        # Extract HTTP status if present
        for code in ["404", "401", "403", "500", "502", "503"]:
            if code in result:
                return f"[red]{code} error[/red]"
        return "[red]failed[/red]"

    # Try to parse as JSON
    try:
        data = json.loads(result)
        return _summarize_json(data)
    except (json.JSONDecodeError, TypeError):
        pass

    # Fallback: truncate raw string
    preview = result.replace("\n", " ").strip()
    if len(preview) > max_len:
        return preview[: max_len - 3] + "..."
    return preview if preview else "[dim]empty[/dim]"


def _summarize_json(data: Any, depth: int = 0) -> str:
    """Recursively summarize a JSON structure.

    Args:
        data: Parsed JSON data
        depth: Current recursion depth

    Returns:
        Brief summary string
    """
    if depth > 2:
        return "..."

    if data is None:
        return "null"

    if isinstance(data, bool):
        return "✓" if data else "✗"

    if isinstance(data, int | float):
        return str(data)

    if isinstance(data, str):
        if len(data) > 40:
            return f'"{data[:37]}..."'
        return f'"{data}"' if len(data) < 20 else data[:40]

    if isinstance(data, list):
        length = len(data)
        if length == 0:
            return "[]"
        # Peek at first item type
        first = data[0]
        if isinstance(first, dict):
            return f"{length} items"
        if isinstance(first, str):
            return f"{length} strings"
        return f"{length} items"

    if isinstance(data, dict):
        if not data:
            return "{}"

        # Count totals
        total_lists = 0
        total_items = 0
        for v in data.values():
            if isinstance(v, list):
                total_lists += 1
                total_items += len(v)

        # If there are lists, summarize by total items
        if total_lists > 0:
            return f"{total_items} items" if total_items > 0 else "OK"

        # Show field names (safely escaped, truncated if too many)
        key_count = len(data)
        keys = list(data.keys())
        if key_count <= 4:
            # Show all keys for small objects
            safe_keys = [escape(str(k)) for k in keys]
            return f"{key_count} fields: {', '.join(safe_keys)}"
        else:
            # Show first 3 keys + count for larger objects
            safe_keys = [escape(str(k)) for k in keys[:3]]
            return f"{key_count} fields: {', '.join(safe_keys)}, ..."

    return str(type(data).__name__)


# Control signals that should be hidden from display but kept for continuation logic
CONTROL_SIGNAL_PATTERN = re.compile(
    r"\[(?:TASK_COMPLETE|CONTINUE|NEED_USER_INPUT)\]", re.IGNORECASE
)


def strip_control_signals(content: str, strip_whitespace: bool = False) -> str:
    """Remove control signals from content for display.

    Strips [TASK_COMPLETE], [CONTINUE], [NEED_USER_INPUT] markers
    that are used by the continuation strategy but shouldn't be
    shown to the user.

    Args:
        content: Raw content that may contain control signals
        strip_whitespace: If True, also strip leading/trailing whitespace.
                         Set to False when processing streaming chunks to
                         preserve word spacing.

    Returns:
        Content with control signals removed
    """
    if not content:
        return content
    # Remove the control signals
    cleaned = CONTROL_SIGNAL_PATTERN.sub("", content)
    # Clean up any resulting triple+ newlines
    cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
    if strip_whitespace:
        cleaned = cleaned.strip()
    return cleaned


def clear_screen() -> None:
    """Clear the terminal screen."""
    os.system("cls" if os.name == "nt" else "clear")


def display_thread_header(thread_name: str) -> None:
    """Display the thread header/title."""
    if UI_STYLE == "minimal":
        console.print(f"[bold green]Chat Thread:[/bold green] {thread_name}")
        console.print("[dim]/help for commands, Ctrl+C to exit[/dim]")
        console.print()
    else:
        console.print(
            Panel(
                f"[bold green]{thread_name}[/bold green]",
                title="Chat Thread",
                border_style="green",
            )
        )


def display_git_branch_info(branch_name: str | None) -> None:
    """Display the current git branch information."""
    if branch_name:
        console.print(f"[dim]Git branch: {branch_name}[/dim]")


def display_help() -> None:
    """Display help information."""
    from rich.text import Text

    text = Text()
    text.append("Commands\n\n", style="cyan")
    commands = [
        ("/exit", "", "Exit the session"),
        ("/help", "", "Show this help message"),
        ("/clear", "", "Clear the screen"),
        ("/tools", "", "Show available tools"),
        ("/logs", "", "Show log file location and recent logs"),
        ("/context", "", "Show context window status and token usage"),
        ("/history", "", "Show conversation history"),
        ("/model", " [list|name]", "Show, list, or switch model"),
        ("/thread", "", "Show current thread info"),
        ("/pin", " [name] [file]", "Pin context (from file or paste mode)"),
        ("/unpin", " <name>", "Remove pinned context"),
        ("/pwd", "", "Show current directory"),
        ("/cd", " <path>", "Change directory"),
        ("/!", " <cmd>", "Execute shell command (unsanitized, raw shell access)"),
    ]
    for cmd, args, desc in commands:
        label = cmd + args
        padding = " " * max(1, 24 - len(label))
        text.append(f"  {cmd}", style="bold")
        text.append(args)
        text.append(f"{padding}{desc}\n")
    text.append("\nKeyboard Shortcuts\n\n", style="cyan")
    shortcuts = [
        ("Ctrl+C", "Cancel current operation"),
        ("Ctrl+C Ctrl+C", "Exit chat (press twice quickly)"),
        ("Up/Down", "Navigate message history"),
    ]
    for key, desc in shortcuts:
        padding = " " * max(1, 24 - len(key))
        text.append(f"  {key}{padding}{desc}\n")
    console.print(Panel(text, title="Help", border_style="cyan", width=72))


def display_logs(num_lines: int = 20) -> None:
    """Display the log file location and recent log entries."""
    log_path = get_log_file_path()

    console.print(f"[bold cyan]Log file:[/bold cyan] {log_path}")
    console.print()

    try:
        if log_path.exists():
            with open(log_path, encoding="utf-8") as f:
                lines = f.readlines()
                recent_lines = lines[-num_lines:] if len(lines) > num_lines else lines

            if recent_lines:
                console.print(f"[dim]Last {len(recent_lines)} log entries:[/dim]")
                console.print()
                for line in recent_lines:
                    line = line.strip()
                    if not line:
                        continue
                    # Color code by log level
                    if "ERROR" in line:
                        console.print(f"[red]{line}[/red]")
                    elif "WARNING" in line:
                        console.print(f"[yellow]{line}[/yellow]")
                    elif "INFO" in line:
                        console.print(f"[green]{line}[/green]")
                    else:
                        console.print(f"[dim]{line}[/dim]")
            else:
                console.print("[dim]Log file is empty.[/dim]")
        else:
            console.print("[yellow]Log file does not exist yet.[/yellow]")
    except Exception as e:
        console.print(f"[red]Error reading log file: {escape(str(e))}[/red]")


def display_messages(messages: list[Any], limit: int = 10) -> None:
    """Display recent messages."""
    if not messages:
        console.print("[dim]No messages in this thread.[/dim]")
        return

    recent = messages[-limit:]
    for msg in recent:
        role = getattr(msg, "role", "unknown")
        content = getattr(msg, "content", "")

        if role == "user":
            console.print(f"[bold blue]You:[/bold blue] {content}")
        elif role == "assistant":
            console.print(f"[bold green]Assistant:[/bold green] {content[:500]}...")
        elif role == "system":
            console.print(f"[dim]System: {content[:100]}...[/dim]")


async def display_tools_async(tool_manager: Any) -> None:
    """Display available tools."""
    if not tool_manager:
        console.print("[yellow]No tool manager available.[/yellow]")
        return

    try:
        tools = await tool_manager.get_tools()
        if not tools:
            console.print("[yellow]No tools available.[/yellow]")
            return

        # Get source map if available (from CompositeToolManager)
        source_map = {}
        if hasattr(tool_manager, "get_tool_source"):
            for tool in tools:
                tool_name = tool.get("function", {}).get("name", "")
                source = tool_manager.get_tool_source(tool_name)
                if source:
                    source_map[tool_name] = source

        table = Table(title="Available Tools", box=box.ROUNDED)
        table.add_column("Name", style="cyan", max_width=45)
        table.add_column("Source", style="dim", max_width=8)
        table.add_column("Description", style="white", max_width=50)

        # Group by source for better organization
        local_tools = []
        arcade_tools = []
        mcp_tools = []

        for tool in tools:
            func_info = tool.get("function", {})
            name = func_info.get("name", "Unknown")
            desc = func_info.get("description", "No description")

            # Clean up description
            desc = desc.replace("[Arcade Cloud] ", "")
            if len(desc) > 50:
                desc = desc[:47] + "..."

            # Determine source from map or fallback to description check
            source = source_map.get(name, "")
            if not source:
                source = (
                    "arcade" if "[Arcade Cloud]" in func_info.get("description", "") else "local"
                )

            entry = (name, source, desc)
            if source == "local":
                local_tools.append(entry)
            elif source == "arcade" or source == "remote":
                arcade_tools.append(entry)
            elif source == "mcp":
                mcp_tools.append(entry)
            else:
                local_tools.append(entry)

        # Add tools in order: local, then arcade/remote (as mcp), then mcp
        for name, source, desc in local_tools:
            table.add_row(name, "local", desc)
        for name, source, desc in arcade_tools:
            table.add_row(name, "mcp", desc)
        for name, source, desc in mcp_tools:
            table.add_row(name, "mcp", desc)

        console.print(table)
        total_mcp = len(arcade_tools) + len(mcp_tools)
        console.print(
            f"\n[dim]Total: {len(tools)} tools (Local: {len(local_tools)}, MCP: {total_mcp})[/dim]"
        )
    except Exception as e:
        console.print(f"[red]Error loading tools: {escape(str(e))}[/red]")


# -----------------------------------------------------------------------------
# Tool display registry: (server, tool) -> handler for context and success
# -----------------------------------------------------------------------------

_LOCAL_PREFIX = "Local_"


def _quote(s: str, max_len: int = 60) -> str:
    if len(s) > max_len:
        s = s[: max_len - 3] + "..."
    return f'("{s}")'


def _parse_tool_name(full_name: str) -> tuple[str, str]:
    """Split tool names into (server, tool) tuple.

    Handles multiple formats:
    - 'Local_ReadFile' -> ('Local', 'ReadFile')
    - 'LocalReadfile' -> ('Local', 'Readfile')  (arcade-mcp-server format)
    - 'Librarian_AddToLibrary' -> ('Librarian', 'AddToLibrary')
    """
    # Handle underscore-separated format
    if "_" in full_name:
        idx = full_name.index("_")
        return (full_name[:idx], full_name[idx + 1 :])

    # Handle arcade-mcp-server format: LocalBash, LocalReadfile, etc.
    if full_name.startswith("Local") and len(full_name) > 5:
        return ("Local", full_name[5:])

    return ("", full_name)


@dataclass(frozen=True)
class ToolDisplayHandler:
    """Per-tool display: context for 'Calling tool' line and success detection for result line."""

    get_context: Callable[[dict[str, Any]], str]
    is_success: Callable[[str, Any], bool]  # (status, parsed_content) -> bool


def _local_bash_context(args: dict[str, Any]) -> str:
    script = args.get("script", "") or args.get("command", "")
    return _quote(script) if script else ""


def _local_bash_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict) and "success" in data:
        return bool(data["success"])
    return True


def _local_editfile_context(args: dict[str, Any]) -> str:
    path = args.get("file_path_arg", "") or args.get("file_path", "")
    if path and len(path) > 50:
        path = "..." + path[-47:]
    return f'("{path}")' if path else ""


def _local_editfile_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict):
        return data.get("success", "error" not in data)
    return True


def _local_editfileinsert_context(args: dict[str, Any]) -> str:
    return _local_editfile_context(args)


def _local_editfileinsert_is_success(status: str, data: Any) -> bool:
    return _local_editfile_is_success(status, data)


def _local_executecommand_context(args: dict[str, Any]) -> str:
    cmd = args.get("command", "")
    return _quote(cmd) if cmd else ""


def _local_executecommand_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict) and "success" in data:
        return bool(data["success"])
    return True


def _local_gitbranch_context(args: dict[str, Any]) -> str:
    branch = args.get("branch_name", "")
    return f'("{branch}")' if branch else ""


def _local_gitbranch_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict) and "error" in data:
        return False
    return True


def _local_gitdiff_context(args: dict[str, Any]) -> str:
    return "(staged)" if args.get("staged", False) else ""


def _local_gitdiff_is_success(status: str, data: Any) -> bool:
    return status == "success"


def _local_gitlog_context(args: dict[str, Any]) -> str:
    max_count = args.get("max_count", 10)
    return f"(last {max_count} commits)"


def _local_gitlog_is_success(status: str, data: Any) -> bool:
    return status == "success"


def _local_gitstatus_context(args: dict[str, Any]) -> str:
    return ""


def _local_gitstatus_is_success(status: str, data: Any) -> bool:
    return status == "success"


def _local_grep_context(args: dict[str, Any]) -> str:
    pattern = args.get("pattern", "")
    directory = args.get("path", ".")
    if pattern:
        if len(pattern) > 40:
            pattern = pattern[:37] + "..."
        if directory and directory != ".":
            return f'("{pattern}", "{directory}")'
        return _quote(pattern, max_len=40)
    return f'("{directory}")' if directory != "." else ""


def _local_grep_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict) and "error" in data:
        return False
    return True


def _local_listfiles_context(args: dict[str, Any]) -> str:
    directory = args.get("directory", "") or args.get("path", ".")
    if len(directory) > 50:
        directory = "..." + directory[-47:]
    return f'("{directory}")'


def _local_listfiles_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict) and "error" in data:
        return False
    return True


def _local_readfile_context(args: dict[str, Any]) -> str:
    path = args.get("file_path_arg", "") or args.get("file_path", "")
    if path and len(path) > 50:
        path = "..." + path[-47:]
    return f'("{path}")' if path else ""


def _local_readfile_is_success(status: str, data: Any) -> bool:
    """Success = status from executor; do not infer from content (file may contain 'error')."""
    if status != "success":
        return False
    if isinstance(data, dict) and "error" in data:
        return False
    return True


def _local_ripgrep_context(args: dict[str, Any]) -> str:
    return _local_grep_context(args)


def _local_ripgrep_is_success(status: str, data: Any) -> bool:
    return _local_grep_is_success(status, data)


def _local_searchcode_context(args: dict[str, Any]) -> str:
    pattern = args.get("pattern", "")
    directory = args.get("directory", "") or args.get("path", ".")
    if pattern:
        if len(pattern) > 40:
            pattern = pattern[:37] + "..."
        if directory and directory != ".":
            return f'("{pattern}", "{directory}")'
        return _quote(pattern, max_len=40)
    return f'("{directory}")' if directory != "." else ""


def _local_searchcode_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict):
        summary = data.get("summary", {})
        if isinstance(summary, dict) and "error" in summary:
            return False
    return True


def _local_writefile_context(args: dict[str, Any]) -> str:
    return _local_readfile_context(args)


def _local_writefile_is_success(status: str, data: Any) -> bool:
    if status != "success":
        return False
    if isinstance(data, dict):
        return data.get("success", "error" not in data)
    return True


def _local_searchrecentcontext_context(args: dict[str, Any]) -> str:
    query = args.get("query", "")
    if query:
        return _quote(query, max_len=40)
    return "(listing backups)"


def _local_searchrecentcontext_is_success(status: str, data: Any) -> bool:
    return status == "success"


# Registry: (server, tool) -> ToolDisplayHandler. Tool name matches MCP tool suffix (e.g. ReadFile, ExecuteCommand).
TOOL_DISPLAY_REGISTRY: dict[tuple[str, str], ToolDisplayHandler] = {
    ("Local", "Bash"): ToolDisplayHandler(_local_bash_context, _local_bash_is_success),
    ("Local", "EditFile"): ToolDisplayHandler(_local_editfile_context, _local_editfile_is_success),
    ("Local", "EditFileInsert"): ToolDisplayHandler(
        _local_editfileinsert_context, _local_editfileinsert_is_success
    ),
    ("Local", "ExecuteCommand"): ToolDisplayHandler(
        _local_executecommand_context, _local_executecommand_is_success
    ),
    ("Local", "GitBranch"): ToolDisplayHandler(
        _local_gitbranch_context, _local_gitbranch_is_success
    ),
    ("Local", "GitDiff"): ToolDisplayHandler(_local_gitdiff_context, _local_gitdiff_is_success),
    ("Local", "GitLog"): ToolDisplayHandler(_local_gitlog_context, _local_gitlog_is_success),
    ("Local", "GitStatus"): ToolDisplayHandler(
        _local_gitstatus_context, _local_gitstatus_is_success
    ),
    ("Local", "Grep"): ToolDisplayHandler(_local_grep_context, _local_grep_is_success),
    ("Local", "ListFiles"): ToolDisplayHandler(
        _local_listfiles_context, _local_listfiles_is_success
    ),
    ("Local", "ReadFile"): ToolDisplayHandler(_local_readfile_context, _local_readfile_is_success),
    ("Local", "Ripgrep"): ToolDisplayHandler(_local_ripgrep_context, _local_ripgrep_is_success),
    ("Local", "SearchCode"): ToolDisplayHandler(
        _local_searchcode_context, _local_searchcode_is_success
    ),
    ("Local", "SearchRecentContext"): ToolDisplayHandler(
        _local_searchrecentcontext_context, _local_searchrecentcontext_is_success
    ),
    ("Local", "WriteFile"): ToolDisplayHandler(
        _local_writefile_context, _local_writefile_is_success
    ),
}


def _find_registry_handler(server: str, tool: str) -> ToolDisplayHandler | None:
    """Find handler in registry with case-insensitive tool name matching."""
    # Exact match first
    key = (server, tool)
    if key in TOOL_DISPLAY_REGISTRY:
        return TOOL_DISPLAY_REGISTRY[key]

    # Case-insensitive match for tool name (arcade-mcp-server lowercases)
    tool_lower = tool.lower()
    for (reg_server, reg_tool), handler in TOOL_DISPLAY_REGISTRY.items():
        if reg_server == server and reg_tool.lower() == tool_lower:
            return handler

    return None


def get_display_tool_name(tool_name: str, args: dict[str, Any]) -> str:
    """Return the effective display name for a tool call.

    For Arcade_UseTool, substitutes the inner tool name (e.g. 'Gmail.ListEmails')
    so the display shows what's actually being invoked rather than the wrapper.
    """
    if tool_name in ("Arcade_UseTool", "Arcade_ExecuteTool"):
        inner = args.get("tool_name") or args.get("name") or args.get("tool")
        if inner:
            # Normalize dot-separated to underscore (Gmail.ListEmails -> Gmail_ListEmails)
            # Strip version suffix (GoogleShopping_SearchProducts@3_1_2 -> GoogleShopping_SearchProducts)
            display = str(inner).replace(".", "_")
            at_idx = display.find("@")
            return display[:at_idx] if at_idx > 0 else display
    return tool_name


def get_tool_context(tool_name: str, args: dict[str, Any]) -> str:
    """Return parenthesized context for the tool (used by session for 'Calling tool' line)."""
    server, tool = _parse_tool_name(tool_name)
    handler = _find_registry_handler(server, tool)
    if handler:
        return handler.get_context(args)
    # Fallback for unregistered tools (e.g. Librarian_*, legacy names)
    return _fallback_tool_context(tool_name, args)


def _fallback_tool_context(tool_name: str, args: dict[str, Any]) -> str:
    """Legacy context extraction for tools not in the registry."""
    raw = tool_name.replace(_LOCAL_PREFIX, "").lower()
    name = raw.replace("_", "")
    name_u = raw
    if name == "bash" or name_u == "bash_tool":
        script = args.get("script", "") or args.get("command", "")
        return _quote(script) if script else ""
    if name in ("readfile", "writefile", "edit") or name_u in (
        "read_file",
        "read_file_tool",
        "write_file",
        "write_file_tool",
        "edit_tool",
    ):
        path = args.get("file_path_arg", "") or args.get("file_path", "")
        if path:
            if len(path) > 50:
                path = "..." + path[-47:]
            return f'("{path}")'
        return ""
    if name == "listfiles" or name_u in ("list_files", "list_files_tool"):
        directory = args.get("directory", "") or args.get("path", ".")
        if len(directory) > 50:
            directory = "..." + directory[-47:]
        return f'("{directory}")'
    if name == "search" or name_u in ("search_tool",):
        pattern = args.get("pattern", "")
        directory = args.get("directory", "") or args.get("path", ".")
        if pattern:
            if len(pattern) > 40:
                pattern = pattern[:37] + "..."
            if directory and directory != ".":
                return f'("{pattern}", "{directory}")'
            return _quote(pattern, max_len=40)
        return f'("{directory}")' if directory != "." else ""
    if name == "git" or name_u == "git_tool":
        action = args.get("action", "status")
        if action == "log":
            return f"(log, last {args.get('count', 10)} commits)"
        if action == "diff":
            return "(diff, staged)" if args.get("staged", False) else "(diff)"
        if action == "branch":
            branch_name = args.get("name", "")
            return f'(branch, "{branch_name}")' if branch_name else "(branch)"
        return f"({action})"
    return ""


def _parse_tool_content(content: Any) -> Any:
    """Parse tool result content as JSON or Python literal (repr-style); return None if not parseable."""
    content_str = str(content).strip()
    if not content_str or (not content_str.startswith("{") and not content_str.startswith("[")):
        return None
    try:
        return json.loads(content_str)
    except (json.JSONDecodeError, TypeError):
        pass
    try:
        return ast.literal_eval(content_str)
    except (ValueError, SyntaxError):
        pass
    return None


def _tool_is_success(tool_name: str, status: str, content: Any) -> bool:
    """Use registry to decide success; fallback: status only (do not infer from content)."""
    server, tool = _parse_tool_name(tool_name)
    parsed = _parse_tool_content(content)
    handler = _find_registry_handler(server, tool)
    if handler:
        return handler.is_success(status, parsed)
    return status == "success"


def display_tool_result(
    name: str,
    content: Any,
    status: str = "success",
    args: dict[str, Any] | None = None,
    authorization_url: str | None = None,
) -> None:
    """Display a tool execution result as a single compact line.

    Success: checkmark and tool name. Error: cross, tool name, and brief summary.
    Success/error is determined by the registered handler for (server, tool), or by status only.
    """
    display_name = get_display_tool_name(name, args or {})

    # Handle authorization errors prominently
    if authorization_url:
        console.print(f"[red]✗ {display_name}[/red]: [yellow]Authorization required[/yellow]")
        console.print(f"  [bold]Please visit this URL to authorize:[/bold]\n  {authorization_url}")
        return

    content_str = str(content)
    is_error = not _tool_is_success(name, status, content)

    if is_error:
        icon = "[red]✗[/red]"
        name_style = f"[red]{escape(display_name)}[/red]"
        summary = _format_result_summary(content_str)
        console.print(f"{icon} {name_style}: {summary}")
    else:
        icon = "[green]✓[/green]"
        name_style = f"[cyan]{display_name}[/cyan]"
        console.print(f"{icon} {name_style}")


def display_tool_error(name: str, error: str) -> None:
    """Display a tool execution error as a compact line."""
    # Extract brief error message
    brief_error = error
    if len(brief_error) > 80:
        brief_error = brief_error[:77] + "..."
    brief_error = brief_error.replace("\n", " ")

    console.print(f"[red]✗ {escape(name)}[/red]: [dim]{escape(brief_error)}[/dim]")
