"""Execution commands: call, read, prompt, exec.

All execution is live against the server — no caching.
"""

import asyncio
import sys
from pathlib import Path

import click

from mcpbundles.args import parse_tool_args, ArgParseError
from mcpbundles.auth_resolve import resolve_auth
from mcpbundles.config import AuthError, CLISettings
from mcpbundles.exit_codes import USER_ERROR, TOOL_ERROR, CONNECTION_ERROR
from mcpbundles.mcp_client import (
    hub_session, call_tool, extract_text,
    ConnectionError, ToolCallError,
)
from mcpbundles.output import render_call_result, render_error, print_json, render_success
from mcpbundles.completion import tool_name_completer, resource_uri_completer, prompt_name_completer


@click.command("call")
@click.argument("tool_name", shell_complete=tool_name_completer)
@click.argument("args", nargs=-1)
@click.option("-f", "--file", "file_path", help="Read arguments from a JSON file")
@click.option("--stdin", "from_stdin", is_flag=True, help="Read JSON arguments from stdin")
@click.option("--raw", is_flag=True, help="Output full MCP response envelope")
@click.option("--pretty", is_flag=True, help="Rich-formatted output with syntax highlighting")
@click.option("--as", "connection", help="Use a named connection")
@click.pass_context
def call_cmd(ctx: click.Context, tool_name: str, args: tuple[str, ...], file_path: str | None, from_stdin: bool, raw: bool, pretty: bool, connection: str | None) -> None:
    """Call an MCP tool by name.

    Arguments can be passed as key=value pairs, key:=json, JSON string, or file.

    Examples:
        mcpbundles call get-contacts limit=10
        mcpbundles call search query="test" include_archived:=true
        mcpbundles call create-note '{"title": "Hello", "body": "World"}'
        mcpbundles call process-data -f payload.json
        echo '{"query":"test"}' | mcpbundles call search --stdin
    """
    try:
        arguments = parse_tool_args(args, file_path=file_path, from_stdin=from_stdin)
    except ArgParseError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)

    mode = "raw" if raw else ("pretty" if pretty else "json")
    asyncio.run(_call_impl(ctx, tool_name, arguments, mode, connection))


async def _call_impl(ctx: click.Context, tool_name: str, arguments: dict, mode: str, connection: str | None) -> None:
    settings: CLISettings = ctx.obj["settings"]
    try:
        auth = resolve_auth(settings, connection)
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)

    try:
        async with hub_session(auth.token, auth.url, auth.endpoint) as session:
            result = await call_tool(session, tool_name, arguments)
            render_call_result(result, mode=mode)
    except ToolCallError as exc:
        render_error(str(exc))
        raise SystemExit(TOOL_ERROR)
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)
    except ConnectionError as exc:
        render_error(str(exc))
        raise SystemExit(CONNECTION_ERROR)


@click.command("read")
@click.argument("uri", shell_complete=resource_uri_completer)
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--as", "connection", help="Use a named connection")
@click.pass_context
def read_cmd(ctx: click.Context, uri: str, as_json: bool, connection: str | None) -> None:
    """Read an MCP resource by URI.

    Examples:
        mcpbundles read config://workspace/settings
        mcpbundles read --json config://workspace/settings
    """
    asyncio.run(_read_impl(ctx, uri, as_json, connection))


async def _read_impl(ctx: click.Context, uri: str, as_json: bool, connection: str | None) -> None:
    settings: CLISettings = ctx.obj["settings"]
    try:
        auth = resolve_auth(settings, connection)
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)

    try:
        async with hub_session(auth.token, auth.url, auth.endpoint) as session:
            result = await session.read_resource(uri)
            for content in result.contents:
                if as_json:
                    print_json({"uri": str(content.uri), "mimeType": content.mimeType, "text": content.text if hasattr(content, "text") else None})
                else:
                    if hasattr(content, "text") and content.text:
                        print(content.text)
                    else:
                        print(f"[Binary content: {content.mimeType}]")
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)
    except ConnectionError as exc:
        render_error(str(exc))
        raise SystemExit(CONNECTION_ERROR)


@click.command("prompt")
@click.argument("prompt_name", shell_complete=prompt_name_completer)
@click.argument("args", nargs=-1)
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
@click.option("--as", "connection", help="Use a named connection")
@click.pass_context
def prompt_cmd(ctx: click.Context, prompt_name: str, args: tuple[str, ...], as_json: bool, connection: str | None) -> None:
    """Get an MCP prompt by name.

    Examples:
        mcpbundles prompt summarize-data dataset=sales_q4
    """
    try:
        arguments = parse_tool_args(args)
    except ArgParseError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)

    asyncio.run(_prompt_impl(ctx, prompt_name, arguments, as_json, connection))


async def _prompt_impl(ctx: click.Context, prompt_name: str, arguments: dict, as_json: bool, connection: str | None) -> None:
    settings: CLISettings = ctx.obj["settings"]
    try:
        auth = resolve_auth(settings, connection)
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)

    try:
        async with hub_session(auth.token, auth.url, auth.endpoint) as session:
            result = await session.get_prompt(prompt_name, arguments)
            if as_json:
                print_json({"description": result.description, "messages": [{"role": m.role, "content": m.content.text if hasattr(m.content, "text") else str(m.content)} for m in result.messages]})
            else:
                if result.description:
                    print(f"# {result.description}\n")
                for msg in result.messages:
                    print(f"[{msg.role}]")
                    if hasattr(msg.content, "text"):
                        print(msg.content.text)
                    else:
                        print(str(msg.content))
                    print()
    except AuthError as exc:
        render_error(str(exc))
        raise SystemExit(USER_ERROR)
    except ConnectionError as exc:
        render_error(str(exc))
        raise SystemExit(CONNECTION_ERROR)


@click.command("exec")
@click.argument("code", required=False)
@click.option("-f", "--file", "file_path", help="Execute code from a file")
@click.option("--raw", is_flag=True, help="Output full MCP response envelope")
@click.option("--pretty", is_flag=True, help="Rich-formatted output")
@click.option("--stream", is_flag=True, help="Stream output line-by-line as it arrives")
@click.option("--as", "connection", help="Use a named connection")
@click.pass_context
def exec_cmd(ctx: click.Context, code: str | None, file_path: str | None, raw: bool, pretty: bool, stream: bool, connection: str | None) -> None:
    """Execute code via code_execution tool (sugar for 'call code_execution').

    Examples:
        mcpbundles exec "result = 2 + 2; print(result)"
        mcpbundles exec -f script.py
        mcpbundles exec -f sync_contacts.py --stream
        echo "print('hello')" | mcpbundles exec --stdin
    """
    if file_path:
        p = Path(file_path)
        if not p.exists():
            render_error(f"File not found: {file_path}")
            raise SystemExit(USER_ERROR)
        code = p.read_text()
    elif not code and not sys.stdin.isatty():
        code = sys.stdin.read()

    if not code:
        render_error("Provide code as argument, -f file, or pipe via stdin.")
        raise SystemExit(USER_ERROR)

    arguments = {"code": code}
    if raw:
        mode = "raw"
    elif stream:
        mode = "stream"
    elif pretty:
        mode = "pretty"
    else:
        mode = "json"
    asyncio.run(_call_impl(ctx, "code_execution", arguments, mode, connection))
