"""Generate text command implementation."""

import asyncio
import json

from pathlib import Path
from typing import Annotated, Any, Optional, cast
from uuid import UUID

import typer
from pydantic import BaseModel
from rich.console import Console

from cli.commands.generate.text.concurrent_renderer import ConcurrentRenderer
from cli.commands.generate.text.presenter import GenerateTextPresenter
from cli.commands.generate.text.stream_renderer import StreamRenderer
from cli.commands.flag_utils import validate_reasoning_effort
from cli.commands.shared_flags import (
    AGENT,
    MAX_TOKENS,
    MESSAGE,
    OUTPUT_FILE,
    OUTPUT_MODE,
    REASONING_EFFORT,
    SCHEMA_DEF,
    SCHEMA_FORMAT,
    SCHEMA_REF,
    SHOW_FULL_TRACE,
    SHOW_THINKING,
    STREAM,
    TAGS,
    TEMPERATURE,
)
from cli.infrastructure.command import async_command, authenticated_client
from cli.infrastructure.console import get_console
from cli.infrastructure.editor import EditorError, open_editor
from cli.infrastructure.error_display import display_error_record
from cli.infrastructure.file_content import FileContentError, resolve_content
from cli.infrastructure.output import OutputService
from cli.infrastructure.renderers.entity_renderer import FieldConfig
from cli.infrastructure.spinner import spinner
from alloy_runtime_types.dtos.generation import (
    AgentSource,
    DirectModelSource,
    GenerateTextRequest,
    GenerateTextResponse,
    GenerationParameters,
    InlineOutputSchema,
    ReasoningEffort,
    SchemaReference,
)
from alloy_runtime_types.enums.provider import Provider
from alloy_runtime_types.enums.schema_enums import SchemaFormat as SchemaFormatEnum

# Type alias for repeatable --header option
REQUEST_HEADERS = Annotated[
    Optional[list[str]],
    typer.Option(
        "-H",
        "--header",
        help="Request header (key:value). Repeatable. E.g., -H 'anthropic-beta:context-1m-2025-08-07'",
    ),
]


class TextBlock(BaseModel):
    text: str


def generate_text_command(
    # Message (required - one of message or message-template)
    message: Optional[str] = MESSAGE,
    message_template: Optional[str] = typer.Option(
        None,
        "--message-template",
        help="Template name/UUID for user message",
    ),
    message_vars: Annotated[
        Optional[list[str]],
        typer.Option(
            "-v",
            "--var",
            help="Variable (key=value or key:=json). Repeatable.",
        ),
    ] = None,
    edit: bool = typer.Option(
        False,
        "-e",
        "--edit",
        help="Open $EDITOR to compose message",
    ),
    # Model Selection (mutually exclusive with agent)
    agent: Optional[str] = AGENT,
    # Multi-agent comparison (mutually exclusive with agent and provider/model)
    agents: Annotated[
        Optional[list[str]],
        typer.Option(
            "-A",
            "--agents",
            help="Compare multiple agents. Repeatable.",
        ),
    ] = None,
    provider: Optional[str] = typer.Option(
        None,
        "-p",
        "--provider",
        help="Provider (requires -M/--model)",
    ),
    model: Optional[str] = typer.Option(
        None,
        "-M",
        "--model",
        help="Model name (requires -p/--provider)",
    ),
    # System Instruction (optional)
    system_instruction: Optional[str] = typer.Option(
        None,
        "--system",
        help="System instruction (or @file)",
    ),
    system_template: Optional[str] = typer.Option(
        None,
        "--system-template",
        help="Template name/UUID for system instruction",
    ),
    system_vars: Annotated[
        Optional[list[str]],
        typer.Option(
            "--system-var",
            help="System template var (key=value). Repeatable.",
        ),
    ] = None,
    # Generation Parameters
    temperature: Optional[float] = TEMPERATURE,
    max_tokens: Optional[int] = MAX_TOKENS,
    reasoning_effort: str | None = REASONING_EFFORT,
    # Additional Generation Parameters
    top_p: Optional[float] = typer.Option(
        None,
        "--top-p",
        min=0.0,
        max=1.0,
        help="Nucleus sampling parameter (0.0-1.0)",
    ),
    top_k: Optional[int] = typer.Option(
        None,
        "--top-k",
        min=1,
        help="Top-k sampling parameter",
    ),
    frequency_penalty: Optional[float] = typer.Option(
        None,
        "--frequency-penalty",
        min=-2.0,
        max=2.0,
        help="Frequency penalty (-2.0 to 2.0)",
    ),
    presence_penalty: Optional[float] = typer.Option(
        None,
        "--presence-penalty",
        min=-2.0,
        max=2.0,
        help="Presence penalty (-2.0 to 2.0)",
    ),
    seed: Optional[int] = typer.Option(
        None,
        "--seed",
        help="Random seed for reproducible outputs",
    ),
    stop: Optional[str] = typer.Option(
        None,
        "--stop",
        help="Stop sequence(s) - comma-separated for multiple",
    ),
    logprobs: Optional[bool] = typer.Option(
        None,
        "--logprobs/--no-logprobs",
        help="Include log probabilities in response",
    ),
    top_logprobs: Optional[int] = typer.Option(
        None,
        "--top-logprobs",
        min=1,
        max=20,
        help="Number of top log probabilities to return (1-20)",
    ),
    user: Optional[str] = typer.Option(
        None,
        "--user",
        help="End-user ID for tracking and abuse monitoring",
    ),
    # Session and Context
    session_id: Optional[UUID] = typer.Option(
        None,
        "--session",
        help="Session ID for multi-turn",
    ),
    tags: Optional[str] = TAGS,
    # Display Options
    stream: bool = STREAM,
    show_thinking: bool = SHOW_THINKING,
    show_full_trace: bool = SHOW_FULL_TRACE,
    # Concurrent Execution
    count: int = typer.Option(
        1,
        "--count",
        min=1,
        max=10,
        help="Concurrent executions (1-10)",
    ),
    # Output Schema Options
    schema: Optional[str] = SCHEMA_REF,
    schema_format: Optional[str] = SCHEMA_FORMAT,
    schema_def: Optional[str] = SCHEMA_DEF,
    # Output Options
    output: str = OUTPUT_MODE,
    output_file: Optional[str] = OUTPUT_FILE,
    # Request Headers (for direct model mode)
    headers: REQUEST_HEADERS = None,
    # Async Execution Options
    run_async: bool = typer.Option(
        False,
        "--async",
        help="Submit for background processing. Returns execution_id for polling.",
    ),
    external_id: Optional[str] = typer.Option(
        None,
        "--external-id",
        help="External identifier for idempotency and tracking",
    ),
    billing_project_id: Optional[str] = typer.Option(
        None,
        "--billing-project",
        help="Billing project name or UUID for cost tracking",
    ),
    parent_execution_id: Optional[UUID] = typer.Option(
        None,
        "--parent-execution",
        help="Parent execution ID for lineage tracking",
    ),
    execution_purpose: Optional[str] = typer.Option(
        None,
        "--execution-purpose",
        help="Label for why this execution exists (e.g., 'batch_export', 'testing')",
    ),
    max_history_messages: Optional[int] = typer.Option(
        None,
        "--max-history",
        min=1,
        max=1000,
        help="Limit chat history to N most recent messages",
    ),
    regenerate: bool = typer.Option(
        False,
        "--regenerate",
        help="Regenerate response to the last user message in the session",
    ),
    chat_history: Optional[str] = typer.Option(
        None,
        "--chat-history",
        help="Chat history as JSON or @file.json",
    ),
    user_metadata: Optional[str] = typer.Option(
        None,
        "--user-metadata",
        help="User metadata as JSON or @file.json",
    ),
    webhook_url: Optional[str] = typer.Option(
        None,
        "--webhook-url",
        help="Webhook URL for completion notification",
    ),
    tools: Optional[str] = typer.Option(
        None,
        "--tools",
        help="Tool definitions as JSON or @file.json (direct model mode)",
    ),
) -> None:
    """Generate text using AI models.

    Use a pre-configured agent or specify provider/model directly.
    Supports streaming, concurrent execution, and multi-agent comparison.

    Examples:

        # With agent
        alloy generate text -a my-agent -m "Write a haiku"

        # With provider/model
        alloy generate text -p anthropic -M claude-3-sonnet -m "Explain decorators"

        # Concurrent executions
        alloy generate text -a my-agent -m "Random fact" --count 3

        # Multi-agent comparison
        alloy generate text -A agent1 -A agent2 -m "Explain AI"

        # With template and variables
        alloy generate text -a my-agent --message-template my-template -v topic=AI

        # Compose in editor
        alloy generate text -a my-agent -e

        # Output to clipboard or file
        alloy generate text -a my-agent -m "Story" -o clipboard
        alloy generate text -a my-agent -m "Story" -o file -O out.txt

        # Structured output with schema
        alloy generate text -p openai -M gpt-4o -m "List colors" --schema my-schema

        # With custom headers (e.g., Anthropic 1M context beta)
        alloy generate text -p anthropic -M claude-sonnet-4-5 -m @big-file.txt -H 'anthropic-beta:context-1m-2025-08-07'
    """
    # Resolve @file references for message, system_instruction, and schema_def
    try:
        message = resolve_content(message)
        system_instruction = resolve_content(system_instruction)
        schema_def = resolve_content(schema_def)
    except FileContentError as e:
        raise typer.BadParameter(str(e))

    # Handle --edit flag: open editor to compose message
    if edit:
        if message_template:
            raise typer.BadParameter(
                "Cannot use --edit with --message-template. "
                "Use --edit with --message or alone."
            )
        result_output = OutputService.get()
        try:
            edited_message = open_editor(initial_content=message or "")
            if edited_message is None:
                raise typer.Exit(code=1)
            # Strip whitespace and check for empty content
            edited_message = edited_message.strip()
            if not edited_message:
                result_output.result(
                    action="compose",
                    resource="message",
                    status="failed",
                    identifier=None,
                    name=None,
                    extra_fields={"Reason": "No message entered"},
                )
                raise typer.Exit(code=1)
            message = edited_message
        except EditorError as e:
            result_output.result(
                action="compose",
                resource="message",
                status="failed",
                identifier=None,
                name=None,
                extra_fields={"Reason": str(e)},
            )
            raise typer.Exit(code=1)

    # Validate output options
    if output not in ("console", "clipboard", "file"):
        raise typer.BadParameter(
            f"Invalid output '{output}'. Valid options: console, clipboard, file"
        )
    if output == "file" and not output_file:
        raise typer.BadParameter("--output-file is required when using --output=file")

    # Validate output schema options
    if schema and (schema_format or schema_def):
        raise typer.BadParameter(
            "--schema cannot be combined with --schema-format/--schema-def. "
            "Use --schema for existing schemas, or --schema-format + --schema-def for inline."
        )
    if (schema_format and not schema_def) or (schema_def and not schema_format):
        raise typer.BadParameter(
            "--schema-format and --schema-def must be used together"
        )
    if schema_format and schema_format not in ("json_schema", "regex"):
        valid = ", ".join(f.value for f in SchemaFormatEnum)
        raise typer.BadParameter(
            f"Invalid schema format '{schema_format}'. Valid: {valid}"
        )
    # Output schema cannot be used with agents (agents have their own)
    if (schema or schema_format) and (agent or agents):
        raise typer.BadParameter(
            "--schema/--schema-format cannot be used with --agent or --agents. "
            "Agents have their own output schema configuration."
        )

    # Headers can only be used with direct model mode (not agents)
    if headers and (agent or agents):
        raise typer.BadParameter(
            "--header cannot be used with --agent or --agents. "
            "For agents, configure request_headers in the agent's settings."
        )

    # Validate --async and --stream are mutually exclusive
    if run_async and stream:
        raise typer.BadParameter(
            "--async cannot be used with --stream. "
            "Use --async for background processing or --stream for real-time responses."
        )

    # Validate --async incompatible with concurrent execution
    if run_async and count > 1:
        raise typer.BadParameter(
            "--async cannot be used with --count. "
            "Async execution does not support concurrent runs."
        )

    # Validate --async incompatible with multi-agent mode
    if run_async and agents:
        raise typer.BadParameter(
            "--async cannot be used with --agents. "
            "Async execution does not support multi-agent comparison."
        )

    # Validate --tools can only be used with direct model mode (not agents)
    if tools and (agent or agents):
        raise typer.BadParameter(
            "--tools cannot be used with --agent or --agents. "
            "For agents, configure tools in the agent's settings."
        )

    # Validate mutually exclusive options
    validate_arguments(
        message=message,
        message_template=message_template,
        agent=agent,
        agents=agents,
        provider=provider,
        model=model,
        count=count,
    )

    # Execute the command
    # Parse variables from key=value format to dict
    from cli.infrastructure.kv_parser import parse_kv_pairs

    parsed_message_vars = parse_kv_pairs(message_vars)
    parsed_system_vars = parse_kv_pairs(system_vars)
    validated_reasoning_effort = (
        validate_reasoning_effort(reasoning_effort)
        if reasoning_effort is not None
        else None
    )

    # Parse headers from key:value format to dict
    parsed_headers: dict[str, str] | None = None
    if headers:
        parsed_headers = {}
        for h in headers:
            if ":" not in h:
                raise typer.BadParameter(
                    f"Invalid header format '{h}'. Use 'key:value' format."
                )
            key, value = h.split(":", 1)
            parsed_headers[key.strip()] = value.strip()

    # Helper function to parse JSON or @file content
    def parse_json_or_file(value: str | None, param_name: str) -> Any | None:
        if value is None:
            return None
        try:
            resolved = resolve_content(value)
        except FileContentError as e:
            raise typer.BadParameter(str(e))
        if resolved is None:
            return None
        try:
            return json.loads(resolved)
        except json.JSONDecodeError as e:
            raise typer.BadParameter(f"Invalid JSON for {param_name}: {e}")

    # Parse JSON options
    parsed_chat_history = parse_json_or_file(chat_history, "--chat-history")
    parsed_user_metadata = parse_json_or_file(user_metadata, "--user-metadata")
    parsed_tools = parse_json_or_file(tools, "--tools")

    # Parse stop sequences (can be single string or comma-separated)
    parsed_stop: str | list[str] | None = None
    if stop:
        if "," in stop:
            parsed_stop = [s.strip() for s in stop.split(",") if s.strip()]
        else:
            parsed_stop = stop

    _execute_generate_text(
        message=message,
        message_template=message_template,
        message_vars=parsed_message_vars,
        agent=agent,
        agents=agents,
        provider=provider,
        model=model,
        system_instruction=system_instruction,
        system_template=system_template,
        system_vars=parsed_system_vars,
        temperature=temperature,
        max_tokens=max_tokens,
        reasoning_effort=validated_reasoning_effort,
        top_p=top_p,
        top_k=top_k,
        frequency_penalty=frequency_penalty,
        presence_penalty=presence_penalty,
        seed=seed,
        stop=parsed_stop,
        logprobs=logprobs,
        top_logprobs=top_logprobs,
        user=user,
        session_id=session_id,
        tags=tags,
        stream=stream,
        show_thinking=show_thinking,
        show_full_trace=show_full_trace,
        count=count,
        output_mode=output,
        output_file=output_file,
        schema=schema,
        schema_format=schema_format,
        schema_def=schema_def,
        request_headers=parsed_headers,
        run_async=run_async,
        external_id=external_id,
        billing_project_id=billing_project_id,
        max_history_messages=max_history_messages,
        regenerate=regenerate,
        chat_history=parsed_chat_history,
        user_metadata=parsed_user_metadata,
        webhook_url=webhook_url,
        tools=parsed_tools,
        parent_execution_id=parent_execution_id,
        execution_purpose=execution_purpose,
    )


def validate_arguments(
    message: str | None,
    message_template: str | None,
    agent: str | None,
    agents: list[str] | None,
    provider: str | None,
    model: str | None,
    count: int,
) -> None:
    """Validate command arguments.

    Args:
        message: User message text
        message_template: User message template name or UUID
        agent: Agent name or UUID
        agents: List of agent names/UUIDs for multi-agent comparison
        provider: Provider key
        model: Model name
        count: Concurrent execution count

    Raises:
        typer.BadParameter: If validation fails
    """
    # Require message OR message_template
    if not message and not message_template:
        raise typer.BadParameter(
            "Either --message (-m) or --message-template is required"
        )

    if message and message_template:
        raise typer.BadParameter("Cannot specify both --message and --message-template")

    # Multi-agent mode validation
    if agents:
        if agent:
            raise typer.BadParameter(
                "Cannot specify both --agent (-a) and --agents (-A)"
            )
        if provider or model:
            raise typer.BadParameter(
                "Cannot specify both --agents (-A) and --provider/--model"
            )
        if len(agents) < 2:
            raise typer.BadParameter(
                "--agents (-A) requires at least 2 agents for comparison"
            )
        if len(agents) > 10:
            raise typer.BadParameter("--agents (-A) supports a maximum of 10 agents")
        if count > 1:
            raise typer.BadParameter(
                "Cannot use --count with --agents. Each agent runs once."
            )
        return  # Skip other validations in multi-agent mode

    # Require agent OR (provider AND model)
    if not agent and not (provider and model):
        raise typer.BadParameter(
            "Either --agent (-a) or both --provider (-p) and --model are required"
        )

    if agent and (provider or model):
        raise typer.BadParameter("Cannot specify both --agent and --provider/--model")

    # Validate count range
    if count < 1 or count > 10:
        raise typer.BadParameter("--count must be between 1 and 10")


def _present_text_record(
    output: OutputService, title: str, label: str, value: str
) -> None:
    output.entity(
        TextBlock(text=value),
        title,
        [FieldConfig("text", label)],
        raw_payload={"text": value},
    )


def _emit_destination_result(
    output: OutputService,
    *,
    action: str,
    destination: str,
    status: str,
    characters: int | None = None,
    reason: str | None = None,
) -> None:
    extra_fields: dict[str, Any] = {"Destination": destination}
    if characters is not None:
        extra_fields["Characters"] = characters
    if reason is not None:
        extra_fields["Reason"] = reason

    output.result(
        action=action,
        resource="text_generation",
        status=status,
        identifier=None,
        name=None,
        extra_fields=extra_fields,
    )


def _emit_batch_result(
    output: OutputService,
    *,
    resource: str,
    total: int,
    successful: int,
    failed: int,
) -> None:
    output.result(
        action="generate",
        resource=resource,
        status="success" if failed == 0 else "partial",
        identifier=None,
        name=None,
        extra_fields={
            "Count": total,
            "Successful": successful,
            "Failed": failed,
        },
    )


def _emit_batch_error(label: str, details: list[str]) -> None:
    display_error_record(
        {
            "Type": "generation_error",
            "Message": f"{label} failed",
            "Details": "\n".join(details),
            "Hint": "Review the failing executions and retry them individually.",
        }
    )


def _copy_to_clipboard(content: str, output: OutputService) -> bool:
    try:
        import pyperclip

        pyperclip.copy(content)
        _emit_destination_result(
            output,
            action="copy",
            destination="clipboard",
            status="success",
            characters=len(content),
        )
        return True
    except ImportError:
        _emit_destination_result(
            output,
            action="copy",
            destination="clipboard",
            status="failed",
            reason="pyperclip not installed. Install with: pip install pyperclip",
        )
        GenerateTextPresenter(output).present_text_output(content)
        return False
    except Exception as e:
        _emit_destination_result(
            output,
            action="copy",
            destination="clipboard",
            status="failed",
            reason=str(e),
        )
        GenerateTextPresenter(output).present_text_output(content)
        return False


def _write_output_file(output_file: str, content: str, output: OutputService) -> None:
    try:
        path = Path(output_file)
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(content, encoding="utf-8")
        _emit_destination_result(
            output,
            action="write",
            destination=str(path.resolve()),
            status="success",
            characters=len(content),
        )
    except OSError as e:
        _emit_destination_result(
            output,
            action="write",
            destination=output_file,
            status="failed",
            reason=str(e),
        )
        GenerateTextPresenter(output).present_text_output(content)
        raise typer.Exit(code=1)


def _get_numbered_filepath(base_path: str, index: int) -> str:
    """Generate a numbered filepath for concurrent outputs.

    Args:
        base_path: Base file path (e.g., "output.txt")
        index: Execution index (1-based)

    Returns:
        Numbered filepath (e.g., "output_1.txt")
    """
    path = Path(base_path)
    stem = path.stem
    suffix = path.suffix
    return str(path.parent / f"{stem}_{index}{suffix}")


def _write_concurrent_outputs(
    output_file: str,
    results: list[tuple[str, str, str | None, str | None]],
    output: OutputService,
) -> None:
    for i, (content, _, error, _) in enumerate(results, 1):
        if error is None and content:
            filepath = _get_numbered_filepath(output_file, i)
            _write_output_file(filepath, content, output)


def _write_concurrent_responses(
    output_file: str,
    responses: list[tuple[int, GenerateTextResponse]],
    output: OutputService,
) -> None:
    for index, response in responses:
        filepath = _get_numbered_filepath(output_file, index)
        _write_output_file(filepath, response.output_text, output)


def _get_agent_filepath(base_path: str, agent_id: str) -> str:
    """Generate a filepath with agent identifier for multi-agent outputs.

    Args:
        base_path: Base file path (e.g., "output.txt")
        agent_id: Agent name or UUID

    Returns:
        Filepath with agent identifier (e.g., "output_my-agent.txt")
    """
    path = Path(base_path)
    stem = path.stem
    suffix = path.suffix
    # Sanitize agent_id for use in filename (replace problematic chars)
    safe_agent_id = agent_id.replace("/", "_").replace("\\", "_").replace(":", "_")
    return str(path.parent / f"{stem}_{safe_agent_id}{suffix}")


def _write_multi_agent_outputs(
    output_file: str,
    agents: list[str],
    results: list[tuple[str, str, str | None, str | None]],
    output: OutputService,
) -> None:
    for i, (content, _, error, _) in enumerate(results):
        if error is None and content:
            filepath = _get_agent_filepath(output_file, agents[i])
            _write_output_file(filepath, content, output)


def _write_multi_agent_responses(
    output_file: str,
    successful_responses: list[tuple[str, GenerateTextResponse]],
    output: OutputService,
) -> None:
    for agent_id, response in successful_responses:
        filepath = _get_agent_filepath(output_file, agent_id)
        _write_output_file(filepath, response.output_text, output)


@async_command
async def _execute_generate_text(
    message: str | None,
    message_template: str | None,
    message_vars: dict[str, Any],
    agent: str | None,
    agents: list[str] | None,
    provider: str | None,
    model: str | None,
    system_instruction: str | None,
    system_template: str | None,
    system_vars: dict[str, Any],
    temperature: float | None,
    max_tokens: int | None,
    reasoning_effort: ReasoningEffort | None,
    top_p: float | None,
    top_k: int | None,
    frequency_penalty: float | None,
    presence_penalty: float | None,
    seed: int | None,
    stop: str | list[str] | None,
    logprobs: bool | None,
    top_logprobs: int | None,
    user: str | None,
    session_id: UUID | None,
    tags: str | None,
    stream: bool,
    show_thinking: bool,
    count: int,
    output_mode: str,
    output_file: str | None,
    schema: str | None,
    schema_format: str | None,
    schema_def: str | None,
    request_headers: dict[str, str] | None = None,
    run_async: bool = False,
    external_id: str | None = None,
    billing_project_id: str | None = None,
    max_history_messages: int | None = None,
    regenerate: bool = False,
    chat_history: list[dict[str, str]] | None = None,
    user_metadata: dict[str, Any] | None = None,
    webhook_url: str | None = None,
    tools: list[Any] | None = None,
    parent_execution_id: UUID | None = None,
    execution_purpose: str | None = None,
    show_full_trace: bool = False,
) -> None:
    """Execute the generate text command.

    Args:
        message: User message text
        message_template: User message template name or UUID
        message_vars: User message template variables (parsed from key=value)
        agent: Agent name or UUID
        agents: List of agent names/UUIDs for multi-agent comparison
        provider: Provider key
        model: Model name
        system_instruction: System instruction text
        system_template: System instruction template name or UUID
        system_vars: System instruction template variables (parsed from key=value)
        temperature: Temperature parameter
        max_tokens: Max tokens parameter
        reasoning_effort: Reasoning effort level
        session_id: Chat session ID
        tags: Comma-separated tags
        stream: Whether to stream
        show_thinking: Whether to show thinking blocks
        count: Number of concurrent executions
        output_mode: Output destination - 'console', 'clipboard', or 'file'
        output_file: File path for 'file' output mode
        schema: Schema reference (name or UUID) for structured output
        schema_format: Inline schema format ('json_schema' or 'regex')
        schema_def: Inline schema definition string
        request_headers: Custom HTTP headers for direct model mode
    """
    console = get_console()
    output = OutputService.get()

    # Parse provider enum if specified
    provider_enum = None
    if provider:
        try:
            provider_enum = Provider(provider)
        except ValueError:
            valid = ", ".join(
                p.value for p in Provider if not p.name.startswith("TESTING")
            )
            raise typer.BadParameter(
                f"Invalid provider '{provider}'. Valid providers: {valid}"
            )

    # Get template variables (already parsed by callback, convert empty dict to None)
    message_variables = message_vars if message_vars else None
    system_variables = system_vars if system_vars else None

    # Parse tags
    tag_list = [t.strip() for t in tags.split(",")] if tags else []

    # Build generation parameters
    gen_params = None
    if any(
        [
            temperature is not None,
            max_tokens is not None,
            reasoning_effort is not None,
            top_p is not None,
            top_k is not None,
            frequency_penalty is not None,
            presence_penalty is not None,
            seed is not None,
            stop is not None,
            logprobs is not None,
            top_logprobs is not None,
            user is not None,
        ]
    ):
        gen_params = GenerationParameters(
            temperature=temperature,
            max_tokens=max_tokens,
            reasoning_effort=reasoning_effort,
            top_p=top_p,
            top_k=top_k,
            frequency_penalty=frequency_penalty,
            presence_penalty=presence_penalty,
            seed=seed,
            stop=stop,
            logprobs=logprobs,
            top_logprobs=top_logprobs,
            user=user,
        )

    # Build output schema (already validated in command function)
    output_schema: InlineOutputSchema | SchemaReference | None = None
    if schema:
        output_schema = SchemaReference(schema_id=schema)
    elif schema_format and schema_def:
        output_schema = InlineOutputSchema(
            schema_format=SchemaFormatEnum(schema_format),
            schema_definition=schema_def,
        )

    # Multi-agent comparison mode
    if agents:
        await _execute_multi_agent(
            agents=agents,
            message=message,
            message_template=message_template,
            message_variables=message_variables,
            system_instruction=system_instruction,
            system_template=system_template,
            system_variables=system_variables,
            gen_params=gen_params,
            tag_list=tag_list,
            stream=stream,
            show_thinking=show_thinking,
            show_full_trace=show_full_trace,
            console=console,
            _output=output,
            output_mode=output_mode,
            output_file=output_file,
        )
        return

    # Build model_source based on user input
    if agent:
        model_source = AgentSource(agent=agent, generation_params=gen_params)
    else:
        # Must be provider + model (validated earlier)
        assert provider_enum is not None and model is not None
        model_source = DirectModelSource(
            provider_key=provider_enum,
            provider_model_name=model,
            generation_params=gen_params,
            request_headers=request_headers,
        )

    # Build request
    request = GenerateTextRequest(
        model_source=model_source,
        user_message=message,
        user_message_template=message_template,
        user_message_variables=message_variables,
        system_instruction=system_instruction,
        system_instruction_template=system_template,
        system_instruction_variables=system_variables,
        chat_session_id=session_id,
        tags=tag_list,
        stream=stream,
        output_schema=output_schema,
        show_full_trace=show_full_trace,
        external_id=external_id,
        billing_project_id=billing_project_id,
        max_history_messages=max_history_messages,
        regenerate=regenerate,
        chat_history=chat_history,
        webhook_url=webhook_url,
        tools=tools,
        parent_execution_id=parent_execution_id,
        execution_purpose=execution_purpose,
    )

    # Handle async execution mode
    if run_async:
        await _execute_async(
            request=request,
            user_metadata=user_metadata,
            webhook_url=webhook_url,
            output=output,
        )
        return

    # Execute based on count and streaming mode
    request = GenerateTextRequest(
        model_source=model_source,
        user_message=message,
        user_message_template=message_template,
        user_message_variables=message_variables,
        system_instruction=system_instruction,
        system_instruction_template=system_template,
        system_instruction_variables=system_variables,
        chat_session_id=session_id,
        tags=tag_list,
        stream=stream,
        show_full_trace=show_full_trace,
        output_schema=output_schema,
        billing_project_id=billing_project_id,
        parent_execution_id=parent_execution_id,
        execution_purpose=execution_purpose,
    )

    # Execute based on count and streaming mode
    if count == 1:
        await _execute_single(
            request, stream, show_thinking, console, output, output_mode, output_file
        )
    else:
        await _execute_concurrent(
            request,
            stream,
            show_thinking,
            count,
            console,
            output,
            output_mode,
            output_file,
        )


async def _execute_async(
    request: GenerateTextRequest,
    user_metadata: dict[str, Any] | None,
    webhook_url: str | None,
    output: OutputService,
) -> None:
    async with authenticated_client() as (_config, client):
        async with spinner("Submitting async generation..."):
            response = await client.generate_text_async(
                request,
                webhook_url=webhook_url,
                user_metadata=user_metadata,
            )

    output.result(
        action="queue",
        resource="text_generation",
        status="success",
        identifier=str(response.execution_id),
        name=None,
        extra_fields={
            "Status": response.status,
            "External ID": response.external_id,
            "Message": response.message,
        },
    )


async def _execute_single(
    request: GenerateTextRequest,
    stream: bool,
    show_thinking: bool,
    console: Console,
    output: OutputService,
    output_mode: str = "console",
    output_file: str | None = None,
) -> None:
    async with authenticated_client() as (_config, client):
        if stream:
            renderer = StreamRenderer(console)
            stream_gen = client.generate_text_stream(request)
            content, thinking, session_id = await renderer.render_stream(
                stream_gen, show_thinking
            )

            if output_mode == "clipboard":
                _copy_to_clipboard(content, output)
            elif output_mode == "file" and output_file:
                _write_output_file(output_file, content, output)
            else:
                renderer.present_final_output(
                    content, thinking, session_id, show_thinking
                )

        else:
            async with spinner("Generating text..."):
                response = await client.generate_text(request)

            if output_mode == "clipboard":
                _copy_to_clipboard(response.output_text, output)
            elif output_mode == "file" and output_file:
                _write_output_file(output_file, response.output_text, output)
            else:
                presenter = GenerateTextPresenter(output)
                presenter.present_result(response, show_metadata=True)


async def _execute_concurrent(
    request: GenerateTextRequest,
    stream: bool,
    show_thinking: bool,
    count: int,
    console: Console,
    _output: OutputService,
    output_mode: str = "console",
    output_file: str | None = None,
) -> None:
    async with authenticated_client() as (_config, client):
        if stream:
            renderer = ConcurrentRenderer(console, count)
            streams = [client.generate_text_stream(request) for _ in range(count)]
            results = await renderer.render_streams(streams, show_thinking)

            if output_mode == "clipboard":
                combined = "\n\n---\n\n".join(
                    content
                    for content, _, error, _ in results
                    if error is None and content
                )
                if combined:
                    _copy_to_clipboard(combined, _output)
            elif output_mode == "file" and output_file:
                _write_concurrent_outputs(output_file, results, _output)

            successful = sum(1 for _, _, error, _ in results if error is None)
            failed = count - successful
            _emit_batch_result(
                _output,
                resource="text_generation_batch",
                total=count,
                successful=successful,
                failed=failed,
            )

            session_lines = [
                f"Execution {i}: {session_id}"
                for i, (_, _, error, session_id) in enumerate(results, 1)
                if error is None and session_id
            ]
            if session_lines:
                _present_text_record(
                    _output,
                    "Generation Sessions",
                    "Sessions",
                    "\n".join(session_lines),
                )

            error_lines = [
                f"Execution {i}: {error}"
                for i, (_, _, error, _) in enumerate(results, 1)
                if error is not None
            ]
            if error_lines:
                _emit_batch_error("Concurrent generation", error_lines)
                raise typer.Exit(code=1)

        else:
            async with spinner(f"Generating {count} concurrent executions..."):
                tasks = [client.generate_text(request) for _ in range(count)]
                responses = await asyncio.gather(*tasks, return_exceptions=True)

            successful_responses: list[tuple[int, GenerateTextResponse]] = []
            errors: list[tuple[int, str]] = []
            for i, response in enumerate(responses):
                if isinstance(response, Exception):
                    errors.append((i + 1, str(response)))
                else:
                    successful_responses.append(
                        (i + 1, cast(GenerateTextResponse, response))
                    )

            if output_mode == "clipboard" and successful_responses:
                combined = "\n\n---\n\n".join(
                    response.output_text for _, response in successful_responses
                )
                _copy_to_clipboard(combined, _output)
            elif output_mode == "file" and output_file and successful_responses:
                _write_concurrent_responses(output_file, successful_responses, _output)
            elif successful_responses:
                presenter = GenerateTextPresenter(_output)
                presenter.present_multiple_results(
                    [response for _, response in successful_responses],
                    show_metadata=True,
                )

            _emit_batch_result(
                _output,
                resource="text_generation_batch",
                total=count,
                successful=len(successful_responses),
                failed=len(errors),
            )

            session_lines = [
                f"Execution {index}: {response.chat_session_id}"
                for index, response in successful_responses
            ]
            if session_lines:
                _present_text_record(
                    _output,
                    "Generation Sessions",
                    "Sessions",
                    "\n".join(session_lines),
                )

            if errors:
                _emit_batch_error(
                    "Concurrent generation",
                    [f"Execution {idx}: {error}" for idx, error in errors],
                )
                raise typer.Exit(code=1)


async def _execute_multi_agent(
    agents: list[str],
    message: str | None,
    message_template: str | None,
    message_variables: dict[str, Any] | None,
    system_instruction: str | None,
    system_template: str | None,
    system_variables: dict[str, Any] | None,
    gen_params: GenerationParameters | None,
    tag_list: list[str],
    stream: bool,
    show_thinking: bool,
    console: Console,
    _output: OutputService,
    output_mode: str = "console",
    output_file: str | None = None,
    show_full_trace: bool = False,
) -> None:
    async with authenticated_client() as (_config, client):
        requests = [
            GenerateTextRequest(
                model_source=AgentSource(agent=agent_id, generation_params=gen_params),
                user_message=message,
                user_message_template=message_template,
                user_message_variables=message_variables,
                system_instruction=system_instruction,
                system_instruction_template=system_template,
                system_instruction_variables=system_variables,
                tags=tag_list,
                stream=stream,
                show_full_trace=show_full_trace,
            )
            for agent_id in agents
        ]

        if stream:
            renderer = ConcurrentRenderer(
                console, len(agents), agent_identifiers=agents
            )
            streams = [client.generate_text_stream(req) for req in requests]
            results = await renderer.render_streams(streams, show_thinking)

            if output_mode == "clipboard":
                parts: list[str] = []
                for i, (content, _, error, _) in enumerate(results):
                    if error is None and content:
                        parts.append(f"## {agents[i]}\n\n{content}")
                if parts:
                    combined = "\n\n---\n\n".join(parts)
                    _copy_to_clipboard(combined, _output)
            elif output_mode == "file" and output_file:
                _write_multi_agent_outputs(output_file, agents, results, _output)

            successful = sum(1 for _, _, error, _ in results if error is None)
            failed = len(agents) - successful
            _emit_batch_result(
                _output,
                resource="multi_agent_generation",
                total=len(agents),
                successful=successful,
                failed=failed,
            )

            session_lines = [
                f"{agents[i]}: {session_id}"
                for i, (_, _, error, session_id) in enumerate(results)
                if error is None and session_id
            ]
            if session_lines:
                _present_text_record(
                    _output,
                    "Agent Sessions",
                    "Sessions",
                    "\n".join(session_lines),
                )

            error_lines = [
                f"{agents[i]}: {error}"
                for i, (_, _, error, _) in enumerate(results)
                if error is not None
            ]
            if error_lines:
                _emit_batch_error("Multi-agent generation", error_lines)
                raise typer.Exit(code=1)

        else:
            async with spinner(f"Generating responses from {len(agents)} agents..."):
                tasks = [client.generate_text(req) for req in requests]
                responses = await asyncio.gather(*tasks, return_exceptions=True)

            successful_responses: list[tuple[str, GenerateTextResponse]] = []
            errors: list[tuple[str, str]] = []
            for i, response in enumerate(responses):
                agent_id = agents[i]
                if isinstance(response, Exception):
                    errors.append((agent_id, str(response)))
                else:
                    successful_responses.append(
                        (agent_id, cast(GenerateTextResponse, response))
                    )

            if output_mode == "clipboard" and successful_responses:
                parts = [
                    f"## {agent_id}\n\n{response.output_text}"
                    for agent_id, response in successful_responses
                ]
                combined = "\n\n---\n\n".join(parts)
                _copy_to_clipboard(combined, _output)
            elif output_mode == "file" and output_file and successful_responses:
                _write_multi_agent_responses(output_file, successful_responses, _output)
            elif successful_responses:
                presenter = GenerateTextPresenter(_output)
                for agent_id, response in successful_responses:
                    presenter.present_result(
                        response,
                        show_metadata=True,
                        metadata_title=f"Generation Result: {agent_id}",
                        output_title=f"Generated Text: {agent_id}",
                    )

            _emit_batch_result(
                _output,
                resource="multi_agent_generation",
                total=len(agents),
                successful=len(successful_responses),
                failed=len(errors),
            )

            session_lines = [
                f"{agent_id}: {response.chat_session_id}"
                for agent_id, response in successful_responses
            ]
            if session_lines:
                _present_text_record(
                    _output,
                    "Agent Sessions",
                    "Sessions",
                    "\n".join(session_lines),
                )

            if errors:
                _emit_batch_error(
                    "Multi-agent generation",
                    [f"{agent_id}: {error}" for agent_id, error in errors],
                )
                raise typer.Exit(code=1)
