"""Agent update command implementation."""

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

import typer

from cli.commands.agents.create.command import parse_tools
from cli.commands.agents.update.presenter import present_agent_update_success
from cli.commands.flag_utils import (
    parse_tags,
    require_exclusive,
    require_together,
    require_update_flags,
    validate_provider,
    validate_reasoning_effort,
)
from cli.commands.shared_flags import REASONING_EFFORT
from cli.infrastructure.command import async_command, authenticated_client
from cli.infrastructure.file_content import FileContentError, resolve_content
from alloy_runtime_types.dtos.agents import AgentModelConfig, UpdateAgentRequest
from alloy_runtime_types.dtos.generation import GenerationParameters, ReasoningEffort
from alloy_runtime_sdk.exceptions.errors import NotFoundError


REQUEST_HEADERS = Annotated[
    Optional[list[str]],
    typer.Option(
        "-H",
        "--header",
        help="Request header (key:value). Repeatable.",
    ),
]


def agents_update_command(
    agent: str = typer.Argument(..., help="Agent name or UUID"),
    name: Optional[str] = typer.Option(
        None,
        "-n",
        "--name",
        help="New name",
    ),
    description: Optional[str] = typer.Option(
        None,
        "-d",
        "--description",
        help="New description",
    ),
    provider: Optional[str] = typer.Option(
        None,
        "-p",
        "--provider",
        help="New provider (requires --model)",
    ),
    model: Optional[str] = typer.Option(
        None,
        "-M",
        "--model",
        help="New model (requires --provider)",
    ),
    tags: Optional[str] = typer.Option(
        None,
        "-t",
        "--tags",
        help="Comma-separated tags (replaces all existing tags)",
    ),
    clear_tags: bool = typer.Option(
        False,
        "--clear-tags",
        help="Remove all tags from agent",
    ),
    temperature: Optional[float] = typer.Option(
        None,
        "-T",
        "--temperature",
        help="Temperature (0.0-2.0)",
    ),
    max_tokens: Optional[int] = typer.Option(
        None,
        "--max-tokens",
        help="Max tokens to generate",
    ),
    top_p: Optional[float] = typer.Option(
        None,
        "--top-p",
        help="Nucleus sampling (0.0-1.0)",
    ),
    top_k: Optional[int] = typer.Option(
        None,
        "--top-k",
        help="Top-k sampling",
    ),
    frequency_penalty: Optional[float] = typer.Option(
        None,
        "--frequency-penalty",
        help="Frequency penalty (-2.0 to 2.0)",
    ),
    presence_penalty: Optional[float] = typer.Option(
        None,
        "--presence-penalty",
        help="Presence penalty (-2.0 to 2.0)",
    ),
    reasoning_effort: str | None = REASONING_EFFORT,
    seed: Optional[int] = typer.Option(
        None,
        "--seed",
        help="Random seed for reproducibility",
    ),
    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",
        help="Number of top log probs to return (1-20)",
    ),
    user: Optional[str] = typer.Option(
        None,
        "--user",
        help="End-user ID for tracking",
    ),
    system_template_id: Optional[str] = typer.Option(
        None,
        "--system-template",
        help="System template name or UUID",
    ),
    system_version_id: Optional[UUID] = typer.Option(
        None,
        "--system-version",
        help="System instruction version UUID",
    ),
    input_schema_id: Optional[str] = typer.Option(
        None,
        "--input-schema",
        help="Input schema name or UUID",
    ),
    output_schema_id: Optional[str] = typer.Option(
        None,
        "--output-schema",
        help="Output schema name or UUID (mutually exclusive with --output-schema-def)",
    ),
    output_schema_def: Optional[str] = typer.Option(
        None,
        "--output-schema-def",
        help="Inline output schema JSON (or @file.json). Mutually exclusive with --output-schema.",
    ),
    clear_description: bool = typer.Option(
        False,
        "--clear-description",
        help="Clear description",
    ),
    headers: REQUEST_HEADERS = None,
    tools: Optional[str] = typer.Option(
        None,
        "--tools",
        help="JSON array of tool configs (or @file.json). Replaces all existing tools.",
    ),
    tools_file: Optional[Path] = typer.Option(
        None,
        "--tools-file",
        help="Path to JSON file with tool configs. Replaces all existing tools.",
        exists=True,
        readable=True,
    ),
    clear_tools: bool = typer.Option(
        False,
        "--clear-tools",
        help="Remove all tools from agent",
    ),
) -> None:
    """Update an existing agent.

    Only specified fields are updated; others are preserved.
    To update the model, provide both --provider and --model.

    Examples:
        alloy agents update my-agent -n "New Name"
        alloy agents update my-agent -d "Updated description"
        alloy agents update my-agent --clear-description
        alloy agents update my-agent -p anthropic -M claude-3-5-sonnet-20241022
        alloy agents update my-agent -T 0.9 --max-tokens 2000
        alloy agents update my-agent -t "new,tags"
        alloy agents update my-agent --clear-tags
        alloy agents update my-agent --tools '[{"tool_id": "web_search"}]'
        alloy agents update my-agent --clear-tools
        alloy agents update my-agent --top-p 0.9 --frequency-penalty 0.5
        alloy agents update my-agent --output-schema-def '{"type": "object"}'
        alloy agents update my-agent -H 'anthropic-beta:context-1m-2025-08-07'
    """
    require_together(("--provider", provider), ("--model", model))

    require_exclusive(
        ("--tools", tools),
        ("--tools-file", tools_file),
        ("--clear-tools", clear_tools),
    )

    require_exclusive(
        ("--tags", tags),
        ("--clear-tags", clear_tags),
    )

    require_exclusive(
        ("--output-schema", output_schema_id),
        ("--output-schema-def", output_schema_def),
    )

    try:
        resolved_output_schema_def = resolve_content(output_schema_def)
    except FileContentError as e:
        raise typer.BadParameter(str(e))

    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()

    stop_sequences: str | list[str] | None = None
    if stop:
        if "," in stop:
            stop_sequences = [s.strip() for s in stop.split(",") if s.strip()]
        else:
            stop_sequences = stop

    output_schema_dict: dict[str, Any] | None = None
    if resolved_output_schema_def:
        try:
            parsed = json.loads(resolved_output_schema_def)
            if not isinstance(parsed, dict):
                raise typer.BadParameter("--output-schema-def must be a JSON object")
            output_schema_dict = cast(dict[str, Any], parsed)
        except json.JSONDecodeError as e:
            raise typer.BadParameter(f"Invalid JSON in --output-schema-def: {e}")

    require_update_flags(
        ("--name", name),
        ("--description", description),
        ("--provider", provider),
        ("--tags", tags),
        ("--clear-tags", clear_tags),
        ("--temperature", temperature),
        ("--max-tokens", max_tokens),
        ("--top-p", top_p),
        ("--top-k", top_k),
        ("--frequency-penalty", frequency_penalty),
        ("--presence-penalty", presence_penalty),
        ("--reasoning-effort", reasoning_effort),
        ("--seed", seed),
        ("--stop", stop),
        ("--logprobs", logprobs),
        ("--top-logprobs", top_logprobs),
        ("--user", user),
        ("--system-template", system_template_id),
        ("--system-version", system_version_id),
        ("--input-schema", input_schema_id),
        ("--output-schema", output_schema_id),
        ("--output-schema-def", output_schema_def),
        ("--clear-description", clear_description),
        ("--header", headers),
        ("--tools", tools),
        ("--tools-file", tools_file),
        ("--clear-tools", clear_tools),
    )

    validated_reasoning_effort = (
        validate_reasoning_effort(reasoning_effort)
        if reasoning_effort is not None
        else None
    )

    _execute_update(
        agent_identifier=agent,
        name=name,
        description=description,
        provider=provider,
        model=model,
        tags=tags,
        clear_tags=clear_tags,
        temperature=temperature,
        max_tokens=max_tokens,
        top_p=top_p,
        top_k=top_k,
        frequency_penalty=frequency_penalty,
        presence_penalty=presence_penalty,
        reasoning_effort=validated_reasoning_effort,
        seed=seed,
        stop_sequences=stop_sequences,
        logprobs=logprobs,
        top_logprobs=top_logprobs,
        user=user,
        system_template_id=system_template_id,
        system_version_id=system_version_id,
        input_schema_id=input_schema_id,
        output_schema_id=output_schema_id,
        output_schema_dict=output_schema_dict,
        clear_description=clear_description,
        headers=parsed_headers,
        tools=tools,
        tools_file=tools_file,
        clear_tools=clear_tools,
    )


@async_command
async def _execute_update(
    agent_identifier: str,
    name: Optional[str],
    description: Optional[str],
    provider: Optional[str],
    model: Optional[str],
    tags: Optional[str],
    clear_tags: bool,
    temperature: Optional[float],
    max_tokens: Optional[int],
    top_p: Optional[float],
    top_k: Optional[int],
    frequency_penalty: Optional[float],
    presence_penalty: Optional[float],
    reasoning_effort: ReasoningEffort | None,
    seed: Optional[int],
    stop_sequences: str | list[str] | None,
    logprobs: Optional[bool],
    top_logprobs: Optional[int],
    user: Optional[str],
    system_template_id: Optional[str],
    system_version_id: Optional[UUID],
    input_schema_id: Optional[str],
    output_schema_id: Optional[str],
    output_schema_dict: dict[str, Any] | None,
    clear_description: bool,
    headers: dict[str, str] | None,
    tools: Optional[str],
    tools_file: Optional[Path],
    clear_tools: bool,
) -> None:
    """Execute the agent update operation."""
    models: list[AgentModelConfig] | None = None
    if provider is not None and model is not None:
        provider_enum = validate_provider(provider)
        models = [
            AgentModelConfig(
                provider_key=provider_enum,
                provider_model_name=model,
                preference_order=0,
            )
        ]

    request_data: dict[str, Any] = {}

    if name is not None:
        request_data["name"] = name

    if description is not None:
        request_data["description"] = description
    elif clear_description:
        request_data["description"] = None

    if models is not None:
        request_data["models"] = models

    if clear_tags:
        request_data["tags"] = []
    elif tags is not None:
        request_data["tags"] = parse_tags(tags)

    if system_template_id is not None:
        request_data["system_instruction_template"] = system_template_id

    if system_version_id is not None:
        request_data["system_instruction_version_id"] = str(system_version_id)

    if input_schema_id is not None:
        request_data["input_schema_id"] = input_schema_id

    if output_schema_id is not None:
        request_data["output_schema_id"] = output_schema_id

    if output_schema_dict is not None:
        request_data["output_schema"] = output_schema_dict

    if headers is not None:
        request_data["default_request_headers"] = headers

    gen_params_fields = [
        temperature,
        max_tokens,
        top_p,
        top_k,
        frequency_penalty,
        presence_penalty,
        reasoning_effort,
        seed,
        stop_sequences,
        logprobs,
        top_logprobs,
        user,
    ]
    if any(p is not None for p in gen_params_fields):
        request_data["generation_params"] = GenerationParameters(
            temperature=temperature,
            max_tokens=max_tokens,
            top_p=top_p,
            top_k=top_k,
            frequency_penalty=frequency_penalty,
            presence_penalty=presence_penalty,
            reasoning_effort=reasoning_effort,
            seed=seed,
            stop=stop_sequences,
            logprobs=logprobs,
            top_logprobs=top_logprobs,
            user=user,
        )

    if clear_tools:
        request_data["tools"] = []
    elif tools is not None or tools_file is not None:
        tool_configs = parse_tools(tools, tools_file)
        if tool_configs is not None:
            request_data["tools"] = tool_configs

    request = UpdateAgentRequest(**request_data)

    async with authenticated_client() as (_, client):
        try:
            agent_data = await client.get_agent(agent_identifier)
        except NotFoundError:
            raise typer.BadParameter(
                f"Agent '{agent_identifier}' not found. Use 'alloy agents list' to see available agents."
            )

        response = await client.update_agent(agent_data.id, request)

    present_agent_update_success(response)
