"""Inspection and result collection commands: inspect, logs, collect.

Spec source: plan step ``cli-refactor.cli-refactor-verify-cli-refactor-fix``
extracts oversized command implementations from cli.py without behavior changes.
"""

# @invar:allow function_size: CLI command implementations with output formatting
# @invar:allow file_size: Related command implementations co-located

from __future__ import annotations

import json
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import cast

from pydantic import ValidationError
from rich.console import Console
from rich.panel import Panel

from anima.app import DefaultResultService, TerminalResultPolicyProjection
from anima.core.contracts import RuntimeOutputPolicy
from anima.core.schemas import AgentState
from anima.core.execution import ExecutionOutputPolicyView
from anima.core.talk_history import filter_response_events
from anima.shell.contract_compat import load_persisted_or_legacy_contract
from anima.shell.cli_commands._shared import resolve_agent
from anima.shell.cli_commands.query import STATE_COLORS
from anima.shell.state import (
    LogTailer,
    get_agent_dir,
    read_log,
    read_result,
    read_state,
)

EXIT_AGENT_NOT_FOUND = 2
_TERMINAL_RESULT_TRUNCATED_MARKER = "\n...[truncated for terminal output]"


@dataclass(frozen=True)
class _FallbackOutputPolicyContract:
    """Minimal output-policy view used when legacy contract loading fails."""

    output_policy: RuntimeOutputPolicy


# @invar:allow entry_point_too_thick: Click command with comprehensive output formatting
# @invar:allow function_size: CLI command with comprehensive output formatting
# @shell_complexity: CLI command with multiple Rich panels and state color mapping
def inspect_impl(agent_id: str, json_output: bool, console: Console) -> None:
    """Implementation for anima inspect command.

    Displays comprehensive agent information:
    - Identity: agent_id, instance_id, persona
    - State: lifecycle state, error, result_ref
    - Hierarchy: parent_id, children, depth limits
    - Process: pid, mcp_socket
    - Token usage: prompt/completion/total tokens
    - Budget: total budget, grace budget, turn limits
    - Tools called: breakdown by tool name
    - Timestamps: created_at, updated_at
    - Result: final state (if terminal)

    Exit codes: 0 success, 2 agent not found.
    """
    from returns.result import Success

    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )

    if json_output:
        output = state.model_dump()
        result_data = read_result(state.agent_id)
        if isinstance(result_data, Success):
            output["result_data"] = result_data.unwrap().model_dump()
        console.print_json(data=output)
        sys.exit(0)

    state_color = STATE_COLORS.get(state.state, "white")

    # ── Identity Panel ──
    identity_content = [
        f"[bold]Agent ID:[/] {state.agent_id}",
        f"[bold]Instance ID:[/] {state.instance_id}",
        f"[bold]Persona:[/] {state.persona_name}",
    ]
    if state.persona_ref:
        identity_content.append(f"[bold]Persona Ref:[/] {state.persona_ref}")

    identity_panel = Panel(
        "\n".join(identity_content),
        title="[bold]Identity[/]",
        border_style="cyan",
    )

    # ── State Panel ──
    state_content = [
        f"[bold]State:[/] [{state_color}]{state.state.value}[/{state_color}]",
        f"[bold]Lifecycle:[/] {state.lifecycle.value}",
    ]
    if state.error:
        state_content.append(f"[bold]Error:[/] [red]{state.error}[/red]")
    if state.result_ref:
        state_content.append(f"[bold]Result Ref:[/] {state.result_ref}")

    state_panel = Panel(
        "\n".join(state_content),
        title="[bold]State[/]",
        border_style=state_color,
    )

    # ── Hierarchy Panel ──
    hierarchy_content = [
        f"[bold]Depth:[/] {state.depth} / {state.max_depth}",
    ]
    if state.parent_id:
        hierarchy_content.append(f"[bold]Parent:[/] {state.parent_id}")
    else:
        hierarchy_content.append("[bold]Parent:[/] [dim]none (root agent)[/dim]")

    children_str = ", ".join(state.children) if state.children else "[dim]none[/dim]"
    hierarchy_content.append(f"[bold]Children:[/] {children_str}")

    hierarchy_panel = Panel(
        "\n".join(hierarchy_content),
        title="[bold]Hierarchy[/]",
        border_style="blue",
    )

    # ── Process Panel ──
    process_content = [
        f"[bold]PID:[/] {state.pid if state.pid else '[dim]none[/dim]'}",
        f"[bold]MCP Socket:[/] {state.mcp_socket or '[dim]none[/dim]'}",
    ]

    process_panel = Panel(
        "\n".join(process_content),
        title="[bold]Process[/]",
        border_style="cyan",
    )

    # ── Budget Panel ──
    budget_content = []
    if state.budget_total is not None:
        budget_content.append(f"[bold]Total Budget:[/] {state.budget_total}")
    budget_content.append(f"[bold]Grace Budget:[/] {state.grace_budget}")
    if state.max_turns is not None:
        budget_content.append(f"[bold]Max Turns:[/] {state.max_turns}")

    budget_panel = Panel(
        "\n".join(budget_content)
        if budget_content
        else "[dim]No budget constraints[/dim]",
        title="[bold]Budget[/]",
        border_style="green",
    )

    # Display panels in columns for better layout

    console.print(identity_panel)
    console.print(state_panel)
    console.print(hierarchy_panel)
    console.print(process_panel)
    console.print(budget_panel)
    sys.exit(0)


# @invar:allow entry_point_too_thick: Click command with polling loop
# @invar:allow function_size: CLI command with result polling and error handling
# @shell_complexity: Wait/poll pattern with result file auto-read and error envelope handling
def collect_impl(
    agent_id: str, wait: bool, timeout: int | None, json_output: bool, console: Console
) -> None:
    """Implementation for anima collect command.

    Harvests result from a terminal-state agent. Supports:
    - ID prefix matching (e.g., 'ag_abc' matches 'ag_abc123...')
    - --wait to poll until terminal state
    - --timeout for wait duration in seconds

    Exit codes: 0 success, 1 error (non-terminal without --wait, timeout), 2 agent not found.
    """
    from rich.panel import Panel
    from returns.result import Failure

    # 1. Resolve ID (prefix match)
    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )
    instance_id = state.instance_id

    # Terminal states
    terminal = {AgentState.completed, AgentState.failed, AgentState.killed}

    contract_work_dir: object = None
    contract_project_root: object = None
    try:
        contract = load_persisted_or_legacy_contract(state)
        result_projection = DefaultResultService().project_terminal_result_policy(
            contract,
            adapter_surface="cli_terminal_presenter",
        )
        contract_work_dir = contract.model_context.values.get("work_dir")
        contract_project_root = contract.model_context.values.get("project_root")
    except (TypeError, ValueError, ValidationError):
        fallback_policy = RuntimeOutputPolicy(
            provider_profile="legacy-default",
            per_call_tool_output_chars=None,
            per_turn_tool_output_chars=None,
            truncation_strategy="truncate",
        )
        result_projection = DefaultResultService().project_terminal_result_policy(
            cast(
                ExecutionOutputPolicyView,
                _FallbackOutputPolicyContract(output_policy=fallback_policy),
            ),
            adapter_surface="cli_terminal_presenter",
        )

    # 2. Check if terminal
    if state.state not in terminal:
        if not wait:
            # Non-terminal without --wait: error envelope + exit 1
            if json_output:
                console.print_json(
                    data={
                        "error": {
                            "code": "AGENT_NOT_TERMINAL",
                            "numeric_code": 315,
                            "message": f"Agent is still {state.state.value}. Use --wait to block.",
                            "details": {
                                "instance_id": instance_id,
                                "state": state.state.value,
                            },
                        }
                    }
                )
            else:
                console.print(
                    f"[yellow]Agent {instance_id} is still {state.state.value}. "
                    f"Use --wait to block.[/yellow]"
                )
            sys.exit(1)

        # --wait: poll until terminal or timeout
        timeout_seconds: float | None = float(timeout) if timeout else None

        start_time = time.time()
        while True:
            # Check timeout
            if timeout_seconds is not None:
                elapsed = time.time() - start_time
                if elapsed >= timeout_seconds:
                    if json_output:
                        console.print_json(
                            data={
                                "error": {
                                    "code": "TIMEOUT",
                                    "numeric_code": 3,  # per contracts/errors.yaml
                                    "message": f"Timed out waiting for agent after {timeout} seconds",
                                    "details": {
                                        "instance_id": instance_id,
                                        "state": state.state.value,
                                        "timeout": timeout,
                                    },
                                }
                            }
                        )
                    else:
                        console.print(
                            f"[red]Timed out waiting for agent {instance_id}[/red]"
                        )
                    sys.exit(1)  # Timeout -> error

            # Read state again
            state_result = read_state(instance_id)
            if isinstance(state_result, Failure):
                # Should not happen, but handle gracefully
                if json_output:
                    console.print_json(
                        data={
                            "error": {
                                "code": "INTERNAL",
                                "numeric_code": 10,
                                "message": f"Failed to read state: {state_result.failure()}",
                                "details": {"instance_id": instance_id},
                            }
                        }
                    )
                else:
                    console.print(
                        f"[red]Error: Failed to read state: {state_result.failure()}[/red]"
                    )
                sys.exit(1)  # Internal error

            state = state_result.unwrap()

            if state.state in terminal:
                break

            # Poll every 1 second
            time.sleep(1)

    result_data = read_result(instance_id)

    # 4. Handle missing result file
    if isinstance(result_data, Failure):
        # Result file missing but state is terminal
        if json_output:
            console.print_json(
                data={
                    "instance_id": instance_id,
                    "agent_id": instance_id,  # Backward compat alias
                    "state": state.state.value,
                    "result_ref": None,
                    "token_usage": None,
                    "error": state.error,
                    "warning": "No result file found",
                }
            )
        else:
            console.print(
                f"[yellow]Warning: No result file found for agent {instance_id}[/yellow]"
            )
            panel = Panel(
                f"[bold]Agent ID:[/] {instance_id}\n"
                f"[bold]State:[/] {state.state.value}\n"
                f"[bold]Error:[/] {state.error or 'None'}",
                title="Agent State (No Result)",
                border_style="yellow",
            )
            console.print(panel)
        sys.exit(0)

    agent_result = result_data.unwrap()

    # Helper to read result_ref content if it's a file
    result_ref_content, result_content_truncated = _read_result_ref_file_content(
        agent_result.result_ref,
        result_projection=result_projection,
        work_dir=contract_work_dir,
        project_root=contract_project_root,
    )

    # 5. Display result
    if json_output:
        output = {
            "instance_id": instance_id,
            "agent_id": instance_id,  # Backward compat alias
            "final_state": agent_result.final_state.value,
            "result_ref": agent_result.result_ref,
            "token_usage": {
                "prompt_tokens": agent_result.token_usage.prompt_tokens
                if agent_result.token_usage
                else 0,
                "completion_tokens": agent_result.token_usage.completion_tokens
                if agent_result.token_usage
                else 0,
                "total_tokens": agent_result.token_usage.total_tokens
                if agent_result.token_usage
                else 0,
            },
            "error": agent_result.error,
        }
        if result_ref_content is not None:
            output["result_content"] = result_ref_content
        if result_content_truncated:
            output["result_content_truncated"] = True
        console.print_json(data=output)
    else:
        # Determine panel color based on terminal state
        if agent_result.final_state == AgentState.completed:
            border_color = "green"
        elif agent_result.final_state == AgentState.failed:
            border_color = "red"
        else:  # killed
            border_color = "yellow"

        token_str = (
            f"{agent_result.token_usage.prompt_tokens} prompt / "
            f"{agent_result.token_usage.completion_tokens} completion / "
            f"{agent_result.token_usage.total_tokens} total"
            if agent_result.token_usage
            else "N/A"
        )

        panel_lines = [
            f"[bold]Agent ID:[/] {instance_id}",
            f"[bold]Final State:[/] {agent_result.final_state.value}",
            f"[bold]Result Ref:[/] {agent_result.result_ref or 'None'}",
        ]

        if result_ref_content is not None:
            panel_lines.append(f"[bold]Result Content:[/] {result_ref_content}")
            if result_content_truncated:
                panel_lines.append(
                    "[dim]Result content truncated to output-policy cap[/dim]"
                )

        panel_lines.extend(
            [
                f"[bold]Tokens:[/] {token_str}",
                f"[bold]Error:[/] {agent_result.error or 'None'}",
            ]
        )

        panel = Panel(
            "\n".join(panel_lines),
            title="Agent Result",
            border_style=border_color,
        )
        console.print(panel)

    sys.exit(0)


# @invar:allow entry_point_too_thick: Click command with log formatting and JSON output
# @invar:allow function_size: CLI command with log streaming and JSON output
# @shell_complexity: CLI command with follow mode, JSONL streaming, and relative timestamps
def logs_impl(
    agent_id: str,
    tail: int | None,
    json_output: bool,
    follow: bool,
    responses: bool,
    console: Console,
) -> None:
    """Implementation for anima logs command.

    Reads log.jsonl from the agent's state directory. Supports ID prefix matching.
    With --responses flag, filters and displays response events as conversation history.

    Exit codes: 0 success, 1 error, 2 agent not found.
    """
    from returns.result import Failure

    state = resolve_agent(
        agent_id,
        json_output=json_output,
        console=console,
        exit_agent_not_found=EXIT_AGENT_NOT_FOUND,
    )
    instance_id = state.instance_id

    log_result = read_log(instance_id)
    if isinstance(log_result, Failure):
        if json_output:
            console.print_json(
                data={
                    "error": {
                        "code": "INTERNAL",
                        "numeric_code": 10,
                        "message": f"Failed to read log: {log_result.failure()}",
                        "details": {"instance_id": instance_id},
                    }
                }
            )
        else:
            console.print(
                f"[red]Error: Failed to read log: {log_result.failure()}[/red]"
            )
        sys.exit(1)

    initial_entries = log_result.unwrap()

    # Filter for response events if --responses flag is set
    if responses:
        entries = filter_response_events(initial_entries)
    else:
        entries = initial_entries

    entries = entries[-tail:] if tail is not None and tail > 0 else entries

    def _format_response(entry: dict, details: dict) -> str:
        """Format response event for conversation history display."""
        turn_number = details.get("turn_number", "?")
        response_text = details.get("response_text", "")
        ts = entry.get("ts", "N/A")

        try:
            ts_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
            ts_str = ts_dt.strftime("%H:%M:%S")
        except (ValueError, TypeError):
            ts_str = ts[:19] if len(ts) > 19 else ts

        return f"[dim]{ts_str}[/dim] Turn {turn_number}: {response_text}"

    def _format(entry: dict) -> str:
        ts = entry.get("ts", "N/A")
        event = entry.get("event", "unknown")
        details = entry.get("details", {})

        # Special formatting for response events when --responses flag is set
        if responses and event == "response":
            return _format_response(entry, details)

        try:
            ts_dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
            now = datetime.now(timezone.utc)
            delta_seconds = (now - ts_dt).total_seconds()
            if delta_seconds < 60:
                ts_str = f"{int(delta_seconds)}s ago"
            elif delta_seconds < 3600:
                ts_str = f"{int(delta_seconds / 60)}m ago"
            elif delta_seconds < 86400:
                ts_str = f"{int(delta_seconds / 3600)}h ago"
            else:
                ts_str = ts[:19]
        except (ValueError, TypeError):
            ts_str = ts[:19] if len(ts) > 19 else ts

        details_str = ""
        if details:
            detail_parts = [f"{k}={v}" for k, v in details.items() if v is not None]
            if detail_parts:
                details_str = f" [dim]({', '.join(detail_parts)})[/dim]"

        event_colors = {
            "created": "green",
            "running": "green",
            "completed": "blue",
            "failed": "red",
            "killed": "yellow",
            "kill": "yellow",
            "approve": "green",
            "reject": "red",
            "resume": "cyan",
            "suspend": "cyan",
        }
        event_color = event_colors.get(event, "white")
        return (
            f"[dim]{ts_str}[/dim] [{event_color}]{event}[/{event_color}]{details_str}"
        )

    def _emit(entries_to_output: list[dict], stream_mode: bool = False) -> None:
        if json_output:
            for entry in entries_to_output:
                print(json.dumps(entry))
            return

        if not entries_to_output:
            return

        if stream_mode:
            for entry in entries_to_output:
                console.print(_format(entry))
            return

        panel = Panel(
            "\n".join(_format(entry) for entry in entries_to_output),
            title=f"Conversation History: {instance_id[:8]}"
            if responses
            else f"Event Log: {instance_id[:8]}",
            border_style="green" if responses else "blue",
        )
        console.print(panel)

    if not entries and not follow:
        if responses:
            console.print(f"[dim]No response events for agent {instance_id[:8]}[/dim]")
        else:
            console.print(f"[dim]No log entries for agent {instance_id[:8]}[/dim]")
        sys.exit(0)

    _emit(entries, stream_mode=follow)
    if not follow:
        sys.exit(0)

    log_file = get_agent_dir(instance_id) / "log.jsonl"
    if not log_file.exists():
        log_file.parent.mkdir(parents=True, exist_ok=True)
        log_file.touch()

    tailer = LogTailer(log_file)
    try:
        while True:
            new_entries = tailer.read_new()
            if new_entries:
                # Apply same filtering as initial snapshot
                if responses:
                    new_entries = filter_response_events(new_entries)
                if new_entries:
                    _emit(new_entries, stream_mode=True)

            # Check terminal state
            state_result = read_state(instance_id)
            if isinstance(state_result, Failure):
                # Continue following on state read error
                pass
            else:
                current_state = state_result.unwrap()
                if current_state.state in {
                    AgentState.completed,
                    AgentState.failed,
                    AgentState.killed,
                }:
                    break
            time.sleep(0.5)
    except KeyboardInterrupt:
        sys.exit(0)


# @invar:allow shell_result: CLI presentation helper returns optional inline text
# @shell_complexity: Branches distinguish non-file refs, missing files, and read errors
# @invar:allow shell_result: Helper returns shaped text tuple for CLI rendering path
# @shell_orchestration: Terminal-output shaping follows contract projection consumed in CLI adapter
def _shape_terminal_content(
    content: str,
    *,
    result_projection: TerminalResultPolicyProjection,
) -> tuple[str, bool]:
    """Shape terminal content according to projected output policy."""
    cap = result_projection.terminal_result_char_cap
    if cap is None or cap <= 0 or len(content) <= cap:
        return content, False

    marker = _TERMINAL_RESULT_TRUNCATED_MARKER
    if cap <= len(marker):
        return marker[:cap], True
    return content[: cap - len(marker)] + marker, True


# @invar:allow shell_result: CLI helper returns optional inline text plus truncation flag
# @shell_complexity: Branches distinguish URI refs, relative roots, missing files, and decode failures
def _read_result_ref_file_content(
    result_ref: str | None,
    *,
    result_projection: TerminalResultPolicyProjection,
    work_dir: object,
    project_root: object,
) -> tuple[str | None, bool]:
    """Return inline content when ``result_ref`` points to a local file.

    Spec source: ``cli-refactor.enhance-collect-auto-read`` requires ``collect``
    to auto-read and inline file content when ``result_ref`` is a file path.
    Non-file result references must preserve existing behavior.
    """
    if not result_ref:
        return None, False

    # Non-file references such as URIs (s3://, https://, etc.) remain as refs.
    if "://" in result_ref:
        return None, False

    result_path = Path(result_ref).expanduser()
    candidate_paths: list[Path] = []

    if result_path.is_absolute():
        candidate_paths.append(result_path)
    else:
        if isinstance(work_dir, str) and work_dir:
            candidate_paths.append(
                (Path(work_dir).expanduser() / result_path).resolve()
            )
        if isinstance(project_root, str) and project_root:
            candidate_paths.append(
                (Path(project_root).expanduser() / result_path).resolve()
            )
        candidate_paths.append(result_path)

    resolved_path = next((path for path in candidate_paths if path.is_file()), None)
    if resolved_path is None:
        return None, False

    try:
        content = resolved_path.read_text(encoding="utf-8")
        return _shape_terminal_content(content, result_projection=result_projection)
    except (OSError, UnicodeDecodeError):
        return None, False
