"""Alive CLI — the self-evolving AI agent."""

import asyncio
import logging
from typing import Optional

import typer
from rich.console import Console
from rich.panel import Panel
from rich.table import Table

from alive import __version__
from alive.config import ensure_dirs

app = typer.Typer(
    name="alive",
    help="A self-evolving AI agent that builds its own skills.",
    no_args_is_help=True,
    rich_markup_mode="rich",
)
console = Console()
_log = logging.getLogger("alive")


def _setup_logging(verbose: bool) -> None:
    """Configure logging — DEBUG if verbose, WARNING otherwise."""
    level = logging.DEBUG if verbose else logging.WARNING
    logging.basicConfig(level=level, format="%(name)s %(message)s")


def version_callback(value: bool) -> None:
    if value:
        console.print(f"[bold green]alive[/] v{__version__}")
        raise typer.Exit()


@app.callback()
def main(
    version: Optional[bool] = typer.Option(
        None, "--version", "-v", help="Show version and exit.", callback=version_callback, is_eager=True
    ),
) -> None:
    """[bold green]alive[/] — A self-evolving AI agent that builds its own skills."""
    ensure_dirs()


# ── Shared helpers ──────────────────────────────────────────────────────────


def _get_provider(name: str | None = None):
    """Resolve an LLM provider, showing a helpful error on failure."""
    from alive.llm import get_provider

    try:
        return get_provider(name)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Provider Error[/]", border_style="red"))
        raise typer.Exit(1)
    except ImportError as e:
        pkg = str(e).split("'")[-2] if "'" in str(e) else "the required package"
        console.print(Panel(
            f"Missing dependency: {e}\n\n"
            f"Fix: pip install {pkg}\n"
            f"Or switch to a local model: alive config --provider ollama",
            title="[bold red]Missing Package[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


def _verbose_error(e: Exception, verbose: bool) -> str:
    """Format an error — full traceback if verbose, str otherwise."""
    if verbose:
        import traceback
        return traceback.format_exc()
    return str(e)


# ── alive run ────────────────────────────────────────────────────────────────


@app.command()
def run(
    request: str = typer.Argument(..., help="Natural language request for Alive to process."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    simple: bool = typer.Option(False, "--simple", "-s", help="Direct LLM call, skip planning."),
    dry_run: bool = typer.Option(False, "--dry-run", help="Show execution plan without running it."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show LLM prompts, skill code, and full errors."),
    no_battle: bool = typer.Option(False, "--no-battle", help="Skip battle mode for new skill builds."),
    input_json: Optional[str] = typer.Option(None, "--input", "-i", help="JSON input override for chain execution."),
) -> None:
    """Process a natural language request."""
    _setup_logging(verbose)

    # Check if the request matches a chain name — execute directly
    from alive.core.chain import is_chain as _is_chain
    from alive.core.chain import run_chain as _run_chain

    if _is_chain(request):
        import json as _json

        input_override = _json.loads(input_json) if input_json else None
        try:
            result = _run_chain(request, input_override=input_override, console=console)
        except FileNotFoundError as e:
            console.print(Panel(str(e), title="[bold red]Not Found[/]", border_style="red"))
            raise typer.Exit(1)
        except Exception as e:
            console.print(Panel(
                _verbose_error(e, verbose),
                title="[bold red]Chain Error[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

        if result.success:
            import json as _json

            output_text = _json.dumps(result.final_output, indent=2, default=str) if result.final_output else "{}"
            console.print()
            console.print(Panel(
                output_text,
                title=f"[bold green]{request}[/]",
                border_style="green",
                padding=(1, 2),
            ))
        else:
            failed_step = next((s for s in result.step_results if not s.success), None)
            err = failed_step.error if failed_step else "Unknown error"
            console.print(Panel(
                f"Chain failed at step: {failed_step.skill_name if failed_step else '?'}\n\n{err}",
                title="[bold red]Chain Failed[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

        _log_request(request, "success" if result.success else "failed", list(dict.fromkeys(s.skill_name for s in result.step_results)), [])
        return

    llm = _get_provider(provider)

    if simple:
        if dry_run:
            console.print(Panel(
                f"[bold]Request:[/] {request}\n[bold]Mode:[/] simple (direct LLM call)",
                title="[bold cyan]Dry Run[/]",
                border_style="cyan",
            ))
            return

        with console.status("[bold cyan]  Thinking...", spinner="dots"):
            try:
                result = asyncio.run(llm.complete(
                    "You are Alive, a helpful AI assistant. "
                    "Respond concisely and helpfully to the user's request.",
                    request,
                ))
            except Exception as e:
                console.print(Panel(
                    _verbose_error(e, verbose),
                    title="[bold red]LLM Error[/]",
                    border_style="red",
                ))
                raise typer.Exit(1)
        console.print()
        console.print(Panel(result, title="[bold green]alive[/]", border_style="green", padding=(1, 2)))
        _log_request(request, "success", [], [])
        return

    # Full pipeline: plan → compose → result
    from alive.core.composer import CompositionError, compose
    from alive.core.planner import display_plan, plan_request

    try:
        plan = asyncio.run(plan_request(request, provider=llm, console=console))
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Planning Error[/]",
            border_style="red",
        ))
        _log_request(request, "failed", [], [])
        raise typer.Exit(1)

    if dry_run:
        display_plan(plan, console=console)
        console.print(f"\n[dim]Run without --dry-run to execute this plan.[/]")
        return

    try:
        result = asyncio.run(compose(plan, provider=llm, console=console, battle=not no_battle))
    except CompositionError as e:
        console.print(Panel(str(e), title="[bold red]Aborted[/]", border_style="red"))
        _log_request(request, "failed", [], [])
        raise typer.Exit(1)
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Execution Error[/]",
            border_style="red",
        ))
        _log_request(request, "failed", [], [])
        raise typer.Exit(1)

    # Log the request with skill info
    skills_used = []
    skills_built = []
    for sr in result.step_results:
        if sr.step.type == "EXISTING_SKILL" and sr.step.skill:
            skills_used.append(sr.step.skill)
        elif sr.step.type == "NEW_SKILL" and sr.success and sr.step.capability:
            skills_built.append(sr.step.capability)
    status_str = "success" if result.success else "partial"
    _log_request(request, status_str, skills_used, skills_built)

    console.print()
    console.print(Panel(result.final_output, title="[bold green]alive[/]", border_style="green", padding=(1, 2)))


def _log_request(
    request: str, status: str, skills_used: list[str], skills_built: list[str]
) -> None:
    """Best-effort request logging for dream log data."""
    try:
        from alive.core.self_model import record_request

        record_request(request, status, skills_used, skills_built)
    except Exception as e:
        _log.debug("Failed to log request: %s", e)


# ── alive build ──────────────────────────────────────────────────────────────


@app.command()
def build(
    description: str = typer.Argument(..., help="Describe the skill to build."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show generated code and LLM prompts."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    no_battle: bool = typer.Option(False, "--no-battle", help="Skip battle mode (single implementation)."),
) -> None:
    """Build a new skill from a natural language description."""
    _setup_logging(verbose)
    from alive.core.skill_builder import build_skill

    llm = _get_provider(provider) if provider else None

    try:
        asyncio.run(build_skill(
            description, console=console, verbose=verbose, provider=llm,
            battle=not no_battle,
        ))
    except RuntimeError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Try rephrasing or simplifying the description.[/]",
            title="[bold red]Build Failed[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Invalid Input[/]", border_style="red"))
        raise typer.Exit(1)


# ── alive plan ───────────────────────────────────────────────────────────────


@app.command()
def plan(
    request: str = typer.Argument(..., help="Natural language request to plan."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show full LLM responses."),
) -> None:
    """Show the execution plan for a request without running it."""
    _setup_logging(verbose)
    from alive.core.planner import display_plan, plan_request

    llm = _get_provider(provider)

    try:
        result = asyncio.run(plan_request(request, provider=llm, console=console))
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Planning Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    display_plan(result, console=console)


# ── alive skills ─────────────────────────────────────────────────────────────


@app.command()
def skills(
    search: Optional[str] = typer.Option(None, "--search", "-s", help="Search skills by keyword."),
) -> None:
    """List all installed skills."""
    from alive.core.skill_registry import get_registry

    registry = get_registry()
    skill_list = registry.search(search) if search else registry.list_all()
    if not skill_list:
        msg = f"No skills matching '{search}'." if search else "No skills installed yet."
        console.print(Panel(
            f"[dim]{msg}[/]\n\n"
            "[dim]Build one with:[/] alive build \"description of what it should do\"",
            title="[bold]Skills[/]",
            border_style="yellow",
        ))
        return

    table = Table(title="Installed Skills", border_style="cyan", show_lines=False)
    table.add_column("Name", style="bold green", no_wrap=True)
    table.add_column("Confidence", justify="right", style="cyan")
    table.add_column("Used", justify="right")
    table.add_column("Last Used", justify="right", style="dim")
    table.add_column("Description", max_width=50)

    for s in skill_list:
        if s.skill_type == "chain":
            # Load chain to get step count
            try:
                from alive.core.chain import load_chain
                ci = load_chain(s.name)
                step_count = len(ci.steps)
            except Exception:
                step_count = 0
            table.add_row(
                f"🔗 {s.name}",
                f"[cyan]{step_count} steps[/]",
                str(s.times_used),
                s.last_used.split("T")[0] if s.last_used else "never",
                s.description[:50],
            )
        else:
            conf = s.confidence_score
            conf_style = "green" if conf >= 0.8 else "yellow" if conf >= 0.5 else "red"
            table.add_row(
                s.name,
                f"[{conf_style}]{conf:.0%}[/{conf_style}]",
                str(s.times_used),
                s.last_used.split("T")[0] if s.last_used else "never",
                s.description[:50],
            )
    console.print(table)


# ── alive status ─────────────────────────────────────────────────────────────


@app.command()
def status(
    classify: bool = typer.Option(False, "--classify", help="Use LLM to classify skill domains."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider for classification."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show classification details."),
) -> None:
    """Show the agent's self-model — capabilities, gaps, and stats."""
    _setup_logging(verbose)
    from alive.core.self_model import build_self_model, display_self_model

    llm = None
    if classify:
        llm = _get_provider(provider)

    try:
        model = asyncio.run(build_self_model(provider=llm))
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    display_self_model(model, console=console)


# ── alive dream ──────────────────────────────────────────────────────────────


@app.command()
def dream(
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show gathered data and LLM prompt."),
) -> None:
    """Generate a dream log — reflections on what Alive has learned."""
    _setup_logging(verbose)
    from alive.dream.dream_log import generate_dream

    llm = _get_provider(provider)

    try:
        asyncio.run(generate_dream(provider=llm, console=console))
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Dream Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


# ── alive share ──────────────────────────────────────────────────────────────


@app.command()
def share(
    skill_name: str = typer.Argument(..., help="Name of the skill to export."),
    gist: bool = typer.Option(False, "--gist", help="Also publish as a GitHub Gist (requires GITHUB_TOKEN)."),
) -> None:
    """Export a skill as a shareable archive."""
    from alive.sharing.export import export_skill

    try:
        result = export_skill(skill_name, create_gist=gist)
    except FileNotFoundError as e:
        console.print(Panel(
            f"{e}\n\n[dim]See available skills with:[/] alive skills",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Invalid Skill[/]", border_style="red"))
        raise typer.Exit(1)

    lines = f"[bold]{result.archive_path}[/]\n\nInstall command:\n  [cyan]{result.install_command}[/]"
    if result.gist_url:
        lines += f"\n\nGist: [link={result.gist_url}]{result.gist_url}[/link]"
    console.print(Panel(lines, title="[bold green]Skill Exported[/]", border_style="green"))


# ── alive install ────────────────────────────────────────────────────────────


@app.command()
def install(
    source: str = typer.Argument(..., help="Path, URL, or gist URL of a .alive-skill.tar.gz."),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip approval prompt."),
    no_test: bool = typer.Option(False, "--no-test", help="Skip running tests after install."),
) -> None:
    """Import a skill from a local archive, URL, or GitHub gist."""
    from rich.syntax import Syntax

    from alive.sharing.import_skill import import_skill, preview_skill

    # Step 1: Preview
    try:
        with console.status("[bold cyan]  Fetching skill...", spinner="dots"):
            preview = preview_skill(source)
    except FileNotFoundError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Check the path or URL and try again.[/]",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Invalid Archive[/]", border_style="red"))
        raise typer.Exit(1)

    # Step 2: Show preview
    dep_text = ", ".join(preview.dependencies) if preview.dependencies else "[dim]none[/]"
    console.print(Panel(
        f"[bold]{preview.name}[/] — {preview.description}\n"
        f"[bold]Dependencies:[/] {dep_text}",
        title="[bold cyan]Skill Preview[/]",
        border_style="cyan",
    ))

    if "skill.py" in preview.files:
        console.print()
        console.print(Panel(
            Syntax(preview.files["skill.py"], "python", theme="monokai", line_numbers=True),
            title=f"[bold]{preview.name}/skill.py[/]",
            border_style="dim",
        ))

    # Step 3: Ask for approval
    if not yes:
        console.print()
        approve = typer.confirm("Install this skill?", default=True)
        if not approve:
            console.print("[yellow]Installation cancelled.[/]")
            raise typer.Exit(0)

    # Step 4: Install
    try:
        with console.status("[bold cyan]  Installing...", spinner="dots"):
            result = import_skill(source, run_tests=not no_test)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Install Error[/]", border_style="red"))
        raise typer.Exit(1)

    # Step 5: Show results
    if result.tests_passed is not None:
        if result.tests_passed:
            console.print(f"\n  [green]Tests passed[/]")
        else:
            console.print(f"\n  [red]Tests failed[/]")
            if result.test_output:
                console.print(Panel(
                    result.test_output[:1000],
                    title="[bold red]Test Output[/]",
                    border_style="red",
                ))

    console.print(Panel(
        f"Installed [bold green]{result.skill_name}[/]\n\n"
        f"[dim]Run it with:[/] alive run \"use {result.skill_name} to ...\"",
        title="[bold green]Skill Installed[/]",
        border_style="green",
    ))


# ── alive browse ─────────────────────────────────────────────────────────────


@app.command()
def browse(
    search: Optional[str] = typer.Option(None, "--search", "-s", help="Filter skills by keyword."),
) -> None:
    """Browse community skills available for installation."""
    from alive.sharing.browse import browse_skills

    try:
        with console.status("[bold cyan]  Fetching community skills...", spinner="dots"):
            skill_list = browse_skills(search=search)
    except ConnectionError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Check your internet connection and try again.[/]",
            title="[bold red]Connection Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    if not skill_list:
        msg = f"No skills matching '{search}'." if search else "No community skills available yet."
        console.print(Panel(f"[dim]{msg}[/]", title="[bold]Community Skills[/]", border_style="yellow"))
        return

    table = Table(title="Community Skills", border_style="cyan", show_lines=False)
    table.add_column("Name", style="bold green", no_wrap=True)
    table.add_column("Description", max_width=40)
    table.add_column("Author", style="dim")
    table.add_column("Tags", style="cyan")
    table.add_column("Install", style="dim")

    for s in skill_list:
        table.add_row(
            s.name,
            s.description[:40],
            s.author,
            ", ".join(s.tags[:3]) if s.tags else "",
            f"alive install {s.install_url}" if s.install_url else "",
        )
    console.print(table)


# ── alive config ─────────────────────────────────────────────────────────────


@app.command()
def config(
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="Set the LLM provider."),
    claude_key: Optional[str] = typer.Option(None, "--claude-key", help="Set Claude API key."),
    openai_key: Optional[str] = typer.Option(None, "--openai-key", help="Set OpenAI API key."),
    deepseek_key: Optional[str] = typer.Option(None, "--deepseek-key", help="Set DeepSeek API key."),
    tavily_key: Optional[str] = typer.Option(None, "--tavily-key", help="Set Tavily search API key."),
    searxng_url: Optional[str] = typer.Option(None, "--searxng-url", help="Set SearXNG instance URL."),
    show: bool = typer.Option(False, "--show", help="Show current configuration."),
) -> None:
    """Configure Alive — set provider, API keys, and preferences."""
    import json

    from alive.config import CONFIG_FILE

    # Load existing config
    if CONFIG_FILE.exists():
        try:
            cfg = json.loads(CONFIG_FILE.read_text())
        except json.JSONDecodeError:
            console.print("[yellow]Config file was corrupted, starting fresh.[/]")
            cfg = {}
    else:
        cfg = {}

    if show:
        if not cfg:
            console.print(Panel(
                "[dim]No configuration set yet.[/]\n\n"
                "Get started:\n"
                "  alive config --provider claude --claude-key sk-ant-...\n"
                "  alive config --provider ollama  [dim]# no API key needed[/]",
                title="[bold]Config[/]",
                border_style="yellow",
            ))
        else:
            # Mask API keys
            display = {}
            for k, v in cfg.items():
                if "key" in k.lower() and isinstance(v, str) and len(v) > 8:
                    display[k] = v[:4] + "..." + v[-4:]
                else:
                    display[k] = v
            from rich.syntax import Syntax

            console.print(Panel(
                Syntax(json.dumps(display, indent=2), "json", theme="monokai"),
                title="[bold cyan]Current Config[/]",
                border_style="cyan",
            ))
        return

    if not any([provider, claude_key, openai_key, deepseek_key, tavily_key, searxng_url]):
        console.print(
            "[yellow]Usage:[/] alive config --show | --provider <name> | --claude-key <key>\n\n"
            "Available providers: claude, openai, ollama, deepseek\n"
            "Integration keys: --tavily-key, --searxng-url"
        )
        return

    if provider:
        from alive.llm.registry import PROVIDERS

        if provider not in PROVIDERS:
            console.print(
                f"[red]Unknown provider '{provider}'.[/]\n\n"
                f"Choose from: {', '.join(PROVIDERS)}"
            )
            raise typer.Exit(1)
        cfg["provider"] = provider

    if claude_key:
        cfg["claude_key"] = claude_key
    if openai_key:
        cfg["openai_key"] = openai_key
    if deepseek_key:
        cfg["deepseek_key"] = deepseek_key
    if tavily_key:
        cfg["tavily_key"] = tavily_key
    if searxng_url:
        cfg["searxng_url"] = searxng_url

    CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
    CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
    console.print(Panel(
        "[green]Configuration saved.[/]",
        title="[bold green]Config Updated[/]",
        border_style="green",
    ))


# ── alive init ───────────────────────────────────────────────────────────────


@app.command(name="init")
def init_cmd(
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="Set initial LLM provider."),
) -> None:
    """Initialize Alive — create directories, venv, and install seed skills."""
    import json

    from alive.config import ALIVE_HOME, CONFIG_FILE, DREAMS_DIR, SKILLS_DIR, VENV_DIR

    console.print(Panel(
        f"Setting up Alive at [bold]{ALIVE_HOME}[/]",
        title="[bold cyan]Initializing Alive[/]",
        border_style="cyan",
    ))

    # Step 1: Create directories
    for d in [ALIVE_HOME, SKILLS_DIR, DREAMS_DIR]:
        d.mkdir(parents=True, exist_ok=True)
    console.print("  [green]\u2713[/green] Created directories")

    # Step 2: Create venv
    with console.status("[bold cyan]  Creating Python venv...", spinner="dots"):
        from alive.sandbox.dependency import get_dependency_manager

        dm = get_dependency_manager()
        dm.ensure_venv()
    console.print(f"  [green]\u2713[/green] Created venv at [dim]{VENV_DIR}[/]")

    # Step 3: Install seed skills
    from alive.seeds import ensure_seeds
    from alive.seeds._installer import _SEED_NAMES

    ensure_seeds(SKILLS_DIR)
    console.print(f"  [green]\u2713[/green] Installed seed skills: {', '.join(_SEED_NAMES)}")

    # Step 4: Generate agent identity
    from alive.network.identity import get_or_create_identity

    identity = get_or_create_identity()
    (ALIVE_HOME / "network").mkdir(parents=True, exist_ok=True)
    console.print(f"  [green]\u2713[/green] Agent identity: [bold]{identity['agent_id'][:16]}...[/bold] \u2014 ready for the network")

    # Step 5: Set provider if specified
    if provider:
        from alive.llm.registry import PROVIDERS

        if provider not in PROVIDERS:
            console.print(f"\n[yellow]Unknown provider '{provider}'. Skipping.[/]")
        else:
            cfg: dict = {}
            if CONFIG_FILE.exists():
                try:
                    cfg = json.loads(CONFIG_FILE.read_text())
                except json.JSONDecodeError:
                    pass
            cfg["provider"] = provider
            CONFIG_FILE.write_text(json.dumps(cfg, indent=2))
            console.print(f"  [green]\u2713[/green] Set provider to [bold]{provider}[/]")

    # Step 6: Detect available provider
    from alive.llm.registry import _auto_detect_provider, _inject_config_keys, _load_config

    cfg_data = _load_config()
    _inject_config_keys(cfg_data)
    detected = cfg_data.get("provider") or _auto_detect_provider()

    console.print(Panel(
        f"[bold green]Alive is ready![/]\n\n"
        f"[bold]Provider:[/] {detected}\n"
        f"[bold]Skills:[/] {len(_SEED_NAMES)} seed skills installed\n"
        f"[bold]Home:[/] {ALIVE_HOME}\n\n"
        f"[dim]Get started:[/]\n"
        f"  alive run \"what is 2 + 2\"\n"
        f"  alive build \"a skill that generates random passwords\"\n"
        f"  alive skills",
        title="[bold green]Setup Complete[/]",
        border_style="green",
    ))


# ── alive replay ────────────────────────────────────────────────────────────


@app.command()
def replay(
    skill_name: str = typer.Argument(..., help="Name of the skill to replay the build story for."),
) -> None:
    """Replay a skill's birth story — the full build journey from request to working code."""
    from alive.core.replay import display_birth_story, load_birth_story

    try:
        story = load_birth_story(skill_name)
    except FileNotFoundError as e:
        console.print(Panel(
            f"{e}\n\n[dim]See available skills with:[/] alive skills",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    display_birth_story(story, console=console)


# ── alive evolve ────────────────────────────────────────────────────────────


@app.command()
def evolve(
    skill_name: str = typer.Argument(..., help="Name of the skill to evolve."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    yes: bool = typer.Option(False, "--yes", "-y", help="Apply without confirmation."),
    rollback: bool = typer.Option(False, "--rollback", help="Rollback to previous version."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show full analysis."),
) -> None:
    """Evolve a skill — analyze and improve it with LLM suggestions."""
    _setup_logging(verbose)

    if rollback:
        from alive.core.evolve import rollback_skill

        if rollback_skill(skill_name):
            console.print(Panel(
                f"[bold green]{skill_name}[/] rolled back to previous version.",
                title="[bold green]Rollback Complete[/]",
                border_style="green",
            ))
        else:
            console.print(Panel(
                f"No previous version found for '{skill_name}'.\n\n"
                "[dim]Skills can only be rolled back after an evolution.[/]",
                title="[bold yellow]Nothing to Rollback[/]",
                border_style="yellow",
            ))
        return

    llm = _get_provider(provider)

    from alive.core.evolve import (
        analyze_skill,
        apply_evolution,
        display_evolution_result,
        display_suggestion,
    )

    # Analyze
    with console.status("[bold cyan]  Analyzing skill for improvements...", spinner="dots"):
        try:
            suggestion = asyncio.run(analyze_skill(skill_name, provider=llm))
        except FileNotFoundError as e:
            console.print(Panel(
                f"{e}",
                title="[bold red]Not Found[/]",
                border_style="red",
            ))
            raise typer.Exit(1)
        except Exception as e:
            console.print(Panel(
                _verbose_error(e, verbose),
                title="[bold red]Analysis Error[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    if suggestion is None:
        console.print(Panel(
            f"[bold]{skill_name}[/] looks good — no improvements suggested.\n\n"
            "[dim]The skill is well-written and doesn't need changes right now.[/]",
            title="[bold green]No Changes Needed[/]",
            border_style="green",
        ))
        return

    # Show suggestion
    display_suggestion(suggestion, console)

    # Confirm
    if not yes:
        console.print()
        approve = typer.confirm("Apply this evolution?", default=True)
        if not approve:
            console.print("[yellow]Evolution cancelled.[/]")
            return

    # Apply
    with console.status("[bold cyan]  Testing and applying evolution...", spinner="dots"):
        result = apply_evolution(suggestion)

    display_evolution_result(result, console)


# ── alive watch ─────────────────────────────────────────────────────────────


@app.command()
def watch(
    lines: int = typer.Option(200, "--lines", "-n", help="Number of history lines to analyze."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show raw history and LLM prompt."),
) -> None:
    """Watch your shell history and suggest skills to automate repeated patterns."""
    _setup_logging(verbose)
    llm = _get_provider(provider)

    from alive.core.watch import analyze_history, display_suggestions

    with console.status("[bold cyan]  Analyzing your shell history...", spinner="dots"):
        try:
            suggestions = asyncio.run(analyze_history(provider=llm, n_lines=lines))
        except FileNotFoundError as e:
            console.print(Panel(
                f"{e}",
                title="[bold red]No History Found[/]",
                border_style="red",
            ))
            raise typer.Exit(1)
        except ValueError as e:
            console.print(Panel(
                f"{e}",
                title="[bold yellow]Not Enough Data[/]",
                border_style="yellow",
            ))
            raise typer.Exit(1)
        except Exception as e:
            console.print(Panel(
                _verbose_error(e, verbose),
                title="[bold red]Analysis Error[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    display_suggestions(suggestions, console)


# ── alive graph ────────────────────────────────────────────────────────────


@app.command()
def graph(
    dot: bool = typer.Option(False, "--dot", help="Output Graphviz DOT format instead of rich display."),
    classify: bool = typer.Option(False, "--classify", help="Use LLM to classify skill domains."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider for classification."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show edge details."),
) -> None:
    """Show a visual map of how skills relate to each other."""
    _setup_logging(verbose)

    from alive.core.graph import build_graph as _build_graph
    from alive.core.graph import display_graph as _display_graph
    from alive.core.graph import export_dot as _export_dot

    llm = None
    if classify:
        llm = _get_provider(provider)

    try:
        skill_graph = asyncio.run(_build_graph(provider=llm))
    except Exception as e:
        console.print(Panel(
            _verbose_error(e, verbose),
            title="[bold red]Graph Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    if dot:
        # Output DOT to stdout for piping: alive graph --dot > skills.dot
        print(_export_dot(skill_graph))
    else:
        _display_graph(skill_graph, console=console)


# ── alive teach ────────────────────────────────────────────────────────────


@app.command()
def teach(
    source: str = typer.Argument(..., help="Path to a Python script or URL."),
    name: str = typer.Option(..., "--name", "-n", help="Name for the new skill (snake_case)."),
    raw: bool = typer.Option(False, "--raw", help="Skip LLM adaptation, wrap script in minimal run()."),
    provider: Optional[str] = typer.Option(None, "--provider", "-p", help="LLM provider override."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show adapted code."),
) -> None:
    """Teach Alive a skill from an existing Python script."""
    import re as _re

    _setup_logging(verbose)

    # Validate name is valid snake_case
    if not _re.match(r"^[a-z][a-z0-9_]*$", name):
        console.print(Panel(
            f"Invalid skill name: '{name}'\n\n"
            "[dim]Names must be snake_case (lowercase letters, digits, underscores).\n"
            "Example: alive teach script.py --name my_scraper[/]",
            title="[bold red]Invalid Name[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    llm = None
    if not raw:
        llm = _get_provider(provider)

    from alive.core.teach import teach_skill as _teach_skill

    try:
        asyncio.run(_teach_skill(
            source, name, raw=raw, provider=llm, console=console,
        ))
    except FileNotFoundError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Check the path or URL and try again.[/]",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except ConnectionError as e:
        console.print(Panel(
            f"{e}",
            title="[bold red]Connection Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except FileExistsError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Choose a different name or remove the existing skill first.[/]",
            title="[bold red]Already Exists[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except ValueError as e:
        console.print(Panel(
            f"{e}\n\n[dim]Try --raw to skip LLM adaptation.[/]",
            title="[bold red]Adaptation Error[/]",
            border_style="red",
        ))
        raise typer.Exit(1)
    except RuntimeError as e:
        console.print(Panel(
            f"{e}",
            title="[bold red]Test Failed[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


# ── alive chain ────────────────────────────────────────────────────────────


@app.command()
def chain(
    chain_name: str = typer.Argument(..., help="Name for the chain."),
    steps: list[str] = typer.Argument(..., help="Skill names to chain, in order."),
    schedule: Optional[str] = typer.Option(None, "--schedule", help="Cron expression for scheduling."),
    input_json: Optional[str] = typer.Option(None, "--input", "-i", help="Default input JSON for the first skill."),
) -> None:
    """Create a skill chain — a named pipeline of skills that run in sequence."""
    import json

    from alive.core.chain import create_chain, display_chain_created, install_schedule

    default_input = None
    if input_json:
        try:
            default_input = json.loads(input_json)
        except json.JSONDecodeError:
            console.print(Panel(
                f"Invalid JSON for --input: {input_json}",
                title="[bold red]Invalid Input[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    try:
        chain_info = create_chain(chain_name, steps, default_input=default_input, schedule=schedule)
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Invalid Chain[/]", border_style="red"))
        raise typer.Exit(1)
    except FileExistsError as e:
        console.print(Panel(str(e), title="[bold red]Already Exists[/]", border_style="red"))
        raise typer.Exit(1)

    schedule_desc = None
    if schedule:
        try:
            schedule_desc = install_schedule(chain_name, schedule)
        except RuntimeError as e:
            console.print(Panel(str(e), title="[bold yellow]Schedule Warning[/]", border_style="yellow"))
            schedule_desc = f"schedule: {schedule} (not installed)"

    display_chain_created(chain_info, schedule_desc=schedule_desc, console=console)


# ── alive chains ───────────────────────────────────────────────────────────


@app.command()
def chains() -> None:
    """List all saved skill chains."""
    from alive.core.chain import display_chains, list_chains

    chain_list = list_chains()
    display_chains(chain_list, console=console)


# ── alive unchain ──────────────────────────────────────────────────────────


@app.command()
def unchain(
    chain_name: str = typer.Argument(..., help="Name of the chain to delete."),
) -> None:
    """Delete a skill chain."""
    from alive.core.chain import delete_chain

    try:
        delete_chain(chain_name)
    except FileNotFoundError as e:
        console.print(Panel(str(e), title="[bold red]Not Found[/]", border_style="red"))
        raise typer.Exit(1)

    console.print(Panel(
        f"Chain [bold]{chain_name}[/] has been deleted.",
        title="[bold green]Chain Removed[/]",
        border_style="green",
    ))


# ── alive memory ──────────────────────────────────────────────────────────


@app.command()
def memory(
    skill_name: str = typer.Argument(..., help="Name of the skill to inspect."),
    clear: bool = typer.Option(False, "--clear", help="Wipe all stored memory for this skill."),
    key: Optional[str] = typer.Option(None, "--key", "-k", help="Show history for a specific key."),
    verbose: bool = typer.Option(False, "--verbose", "-V", help="Show verbose output."),
) -> None:
    """Inspect or manage a skill's persistent memory."""
    import json as _json

    _setup_logging(verbose)

    from alive.memory.skill_memory import SkillMemory, display_memory

    # Verify the skill exists
    from alive.config import SKILLS_DIR

    skill_dir = SKILLS_DIR / skill_name
    if not skill_dir.exists():
        console.print(Panel(
            f"Skill [bold]{skill_name}[/] not found.\n\n"
            "[dim]List available skills with: alive list[/]",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)

    mem = SkillMemory(skill_name)

    if clear:
        count = mem.clear()
        console.print(Panel(
            f"Cleared [bold]{count}[/] entries from [bold]{skill_name}[/] memory.",
            title="[bold green]Memory Cleared[/]",
            border_style="green",
        ))
        mem.close()
        return

    if key:
        # Show history for a specific key
        history = mem.history(key)
        if not history:
            console.print(Panel(
                f"No entries found for key [bold]{key}[/] in skill [bold]{skill_name}[/].",
                title="[bold yellow]No History[/]",
                border_style="yellow",
            ))
            mem.close()
            return

        from rich.table import Table

        table = Table(
            title=f"History: {skill_name}.{key}",
            border_style="cyan",
            show_lines=True,
        )
        table.add_column("#", style="dim", justify="right")
        table.add_column("Timestamp", style="dim")
        table.add_column("Value", style="white")

        for i, (ts, val) in enumerate(history, 1):
            from alive.memory.skill_memory import _format_timestamp

            val_str = _json.dumps(val, default=str) if not isinstance(val, str) else val
            if len(val_str) > 300:
                val_str = val_str[:300] + "..."
            table.add_row(str(i), _format_timestamp(ts), val_str)

        console.print()
        console.print(table)
        console.print(f"\n[dim]{len(history)} entries (newest first)[/]")
        mem.close()
        return

    # Default: display all keys and current values
    display_memory(skill_name, console=console)
    mem.close()


# ── alive integrations ────────────────────────────────────────────────────


@app.command()
def integrations() -> None:
    """Show the status of all configured integrations."""
    from alive.integrations import display_integrations

    display_integrations(console=console)


# ── alive setup ───────────────────────────────────────────────────────────


@app.command()
def setup(
    integration_name: str = typer.Argument(..., help="Integration to set up (telegram, browser, webhook)."),
) -> None:
    """Set up an integration — interactive wizard."""
    name = integration_name.lower().strip()

    if name == "telegram":
        from alive.integrations.telegram import setup_telegram

        success = setup_telegram(console=console)
        if not success:
            raise typer.Exit(1)

    elif name == "browser":
        from alive.integrations.browser import setup_browser

        success = setup_browser(console=console)
        if not success:
            raise typer.Exit(1)

    elif name == "webhook":
        from alive.integrations.webhook import setup_webhook

        success = setup_webhook(console=console)
        if not success:
            raise typer.Exit(1)

    elif name == "search":
        console.print(Panel(
            "[bold green]Web search requires no setup![/]\n\n"
            "DuckDuckGo works out of the box.\n\n"
            "[dim]Optional: For better results, configure Tavily:[/]\n"
            "  alive config --tavily-key tvly-...\n\n"
            "[dim]Or self-hosted SearXNG:[/]\n"
            "  alive config --searxng-url http://localhost:8888",
            title="[bold cyan]Web Search[/]",
            border_style="cyan",
        ))

    else:
        console.print(Panel(
            f"Unknown integration: '{name}'\n\n"
            "Available integrations:\n"
            "  alive setup telegram\n"
            "  alive setup browser\n"
            "  alive setup webhook\n"
            "  alive setup search  [dim](info only)[/]",
            title="[bold red]Unknown Integration[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


# ── alive search ──────────────────────────────────────────────────────────


@app.command(name="search")
def search_cmd(
    query: str = typer.Argument(..., help="Search query."),
    max_results: int = typer.Option(5, "--max", "-n", help="Maximum number of results."),
) -> None:
    """Search the web for current information."""
    from alive.integrations.web_search import display_search_results, search_web

    with console.status("[bold cyan]  Searching...", spinner="dots"):
        try:
            results = search_web(query, max_results=max_results)
        except Exception as e:
            console.print(Panel(
                f"{e}\n\n[dim]Check your internet connection.[/]",
                title="[bold red]Search Error[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    display_search_results(results, title=f"Results: {query}", console=console)


@app.command()
def news(
    query: str = typer.Argument(..., help="News search query."),
    max_results: int = typer.Option(5, "--max", "-n", help="Maximum number of results."),
) -> None:
    """Search for recent news."""
    from alive.integrations.web_search import display_search_results, search_news

    with console.status("[bold cyan]  Searching news...", spinner="dots"):
        try:
            results = search_news(query, max_results=max_results)
        except Exception as e:
            console.print(Panel(
                f"{e}\n\n[dim]Check your internet connection.[/]",
                title="[bold red]Search Error[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    display_search_results(results, title=f"News: {query}", console=console)


# ── alive test-* ──────────────────────────────────────────────────────────


@app.command(name="test-search")
def test_search_cmd() -> None:
    """Test the web search integration."""
    from alive.integrations.web_search import test_search

    success = test_search(console=console)
    if not success:
        raise typer.Exit(1)


@app.command(name="test-telegram")
def test_telegram_cmd() -> None:
    """Test the Telegram integration by sending a message."""
    from alive.integrations.telegram import test_telegram

    success = test_telegram(console=console)
    if not success:
        raise typer.Exit(1)


@app.command(name="test-browser")
def test_browser_cmd() -> None:
    """Test the browser integration by scraping example.com."""
    from alive.integrations.browser import test_browser

    success = test_browser(console=console)
    if not success:
        raise typer.Exit(1)


@app.command(name="test-webhook")
def test_webhook_cmd(
    webhook_name: Optional[str] = typer.Argument(None, help="Name of a specific webhook to test."),
) -> None:
    """Test a webhook integration by sending a test message."""
    from alive.integrations.webhook import test_webhook

    success = test_webhook(webhook_name=webhook_name, console=console)
    if not success:
        raise typer.Exit(1)


# ── alive schedule ────────────────────────────────────────────────────────


@app.command(name="schedule")
def schedule_cmd(
    request: str = typer.Argument(..., help="Request or skill name to schedule."),
    every: Optional[str] = typer.Option(None, "--every", help="Run interval (e.g., 5m, 30m, 1h, 2h, 1d)."),
    daily: bool = typer.Option(False, "--daily", help="Run once daily."),
    at: Optional[str] = typer.Option(None, "--at", help="Time of day for --daily (e.g., 08:00)."),
    cron: Optional[str] = typer.Option(None, "--cron", help="Cron expression (5-field)."),
    notify: Optional[str] = typer.Option(None, "--notify", help="Notification channel (telegram, webhook:name)."),
    name: Optional[str] = typer.Option(None, "--name", help="Friendly task name."),
) -> None:
    """Schedule a request or skill to run on a recurring basis."""
    from alive.integrations.scheduler import create_schedule, format_interval

    if not any([every, daily, cron]):
        console.print(Panel(
            "Specify a schedule:\n\n"
            "  alive schedule \"check something\" --every 30m\n"
            "  alive schedule \"daily report\" --daily --at 08:00\n"
            "  alive schedule my_skill --cron \"0 9 * * 1-5\"",
            title="[bold yellow]Missing Schedule[/]",
            border_style="yellow",
        ))
        raise typer.Exit(1)

    daily_at = at if daily else None
    if daily and not at:
        daily_at = "09:00"  # default to 9 AM

    try:
        task = create_schedule(
            request,
            name=name,
            interval=every,
            daily_at=daily_at,
            cron=cron,
            notify_via=notify,
        )
    except ValueError as e:
        console.print(Panel(str(e), title="[bold red]Invalid Schedule[/]", border_style="red"))
        raise typer.Exit(1)

    interval_label = every or (f"daily at {daily_at}" if daily_at else cron or "?")
    notify_label = f"\n[dim]Alerts:[/] {notify}" if notify else ""

    console.print(Panel(
        f"[bold green]{task.name}[/]\n\n"
        f"[dim]Request:[/] {request}\n"
        f"[dim]Schedule:[/] every {interval_label}\n"
        f"[dim]Next run:[/] {task.next_run_at}"
        f"{notify_label}\n\n"
        "[dim]Start the daemon to activate: alive daemon start[/]",
        title="[bold green]Task Scheduled[/]",
        border_style="green",
    ))


@app.command()
def schedules() -> None:
    """List all scheduled tasks."""
    from alive.integrations.scheduler import display_schedules

    display_schedules(console=console)


@app.command()
def unschedule(
    task: str = typer.Argument(..., help="Task ID or name to remove."),
) -> None:
    """Remove a scheduled task."""
    from alive.integrations.scheduler import remove_schedule

    if remove_schedule(task):
        console.print(Panel(
            f"Removed scheduled task: [bold]{task}[/]",
            title="[bold green]Task Removed[/]",
            border_style="green",
        ))
    else:
        console.print(Panel(
            f"No task found with ID or name: '{task}'",
            title="[bold red]Not Found[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


@app.command()
def pause(
    task: str = typer.Argument(..., help="Task ID or name to pause."),
) -> None:
    """Pause a scheduled task."""
    from alive.integrations.scheduler import pause_schedule

    if pause_schedule(task):
        console.print(f"[green]✓ Paused: {task}[/]")
    else:
        console.print(f"[red]Task not found: {task}[/]")
        raise typer.Exit(1)


@app.command()
def resume(
    task: str = typer.Argument(..., help="Task ID or name to resume."),
) -> None:
    """Resume a paused scheduled task."""
    from alive.integrations.scheduler import resume_schedule

    if resume_schedule(task):
        console.print(f"[green]✓ Resumed: {task}[/]")
    else:
        console.print(f"[red]Task not found: {task}[/]")
        raise typer.Exit(1)


# ── alive daemon ──────────────────────────────────────────────────────────


@app.command()
def daemon(
    action: str = typer.Argument(..., help="Action: start, stop, status, logs"),
    follow: bool = typer.Option(False, "--follow", "-f", help="Follow log output (for 'logs' action)."),
    lines_count: int = typer.Option(50, "--lines", "-n", help="Number of log lines to show."),
) -> None:
    """Manage the scheduler daemon — start, stop, status, or view logs."""
    action = action.lower().strip()

    if action == "start":
        from alive.integrations.scheduler import daemon_running, start_daemon

        running, pid = daemon_running()
        if running:
            console.print(f"[yellow]Daemon already running (PID {pid})[/]")
            return

        try:
            new_pid = start_daemon()
            console.print(Panel(
                f"[bold green]Daemon started (PID {new_pid})[/]\n\n"
                "[dim]View status: alive daemon status\n"
                "View logs: alive daemon logs\n"
                "Stop: alive daemon stop[/]",
                title="[bold green]Daemon Started[/]",
                border_style="green",
            ))
        except RuntimeError as e:
            console.print(Panel(str(e), title="[bold red]Error[/]", border_style="red"))
            raise typer.Exit(1)

    elif action == "stop":
        from alive.integrations.scheduler import stop_daemon

        if stop_daemon():
            console.print("[green]✓ Daemon stopped.[/]")
        else:
            console.print("[yellow]Daemon was not running.[/]")

    elif action == "status":
        from alive.integrations.scheduler import display_daemon_status

        display_daemon_status(console=console)

    elif action == "logs":
        from alive.integrations.scheduler import _log_file

        log_path = _log_file()
        if not log_path.exists():
            console.print("[dim]No daemon logs yet.[/]")
            return

        if follow:
            import subprocess
            subprocess.run(["tail", "-f", str(log_path)])
        else:
            text = log_path.read_text()
            log_lines = text.strip().split("\n")
            for line in log_lines[-lines_count:]:
                console.print(f"[dim]{line}[/]")

    else:
        console.print(Panel(
            f"Unknown action: '{action}'\n\n"
            "Available actions:\n"
            "  alive daemon start   — start the scheduler daemon\n"
            "  alive daemon stop    — stop the daemon\n"
            "  alive daemon status  — show daemon status\n"
            "  alive daemon logs    — view daemon logs",
            title="[bold red]Unknown Action[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


# ── alive screenshot ──────────────────────────────────────────────────────


@app.command()
def screenshot(
    url: str = typer.Argument(..., help="URL to screenshot."),
    output: Optional[str] = typer.Option(None, "--output", "-o", help="Output file path."),
) -> None:
    """Take a screenshot of a webpage using headless browser."""
    from alive.integrations.browser import BrowserTool

    out_path = output or f"screenshot_{url.split('//')[-1].split('/')[0].replace('.', '_')}.png"

    with console.status(f"[bold cyan]  Capturing {url}...", spinner="dots"):
        try:
            with BrowserTool() as browser:
                saved_path = browser.screenshot(url, out_path)
        except ImportError as e:
            console.print(Panel(
                f"{e}\n\n[dim]Run: alive setup browser[/]",
                title="[bold red]Browser Not Available[/]",
                border_style="red",
            ))
            raise typer.Exit(1)
        except Exception as e:
            console.print(Panel(str(e), title="[bold red]Screenshot Error[/]", border_style="red"))
            raise typer.Exit(1)

    console.print(f"[green]✓ Screenshot saved: {saved_path}[/]")


# ── alive webhooks ────────────────────────────────────────────────────────


@app.command()
def webhooks() -> None:
    """List all configured webhooks."""
    from alive.integrations.webhook import display_webhooks

    display_webhooks(console=console)


# ── alive telegram ────────────────────────────────────────────────────────


@app.command(name="telegram")
def telegram_cmd(
    action: str = typer.Argument(..., help="Action: start, stop, status, test"),
    daemon_mode: bool = typer.Option(False, "--daemon", "-d", help="Start bot as background daemon."),
) -> None:
    """Manage the Telegram bot — start, stop, status, or test."""
    action = action.lower().strip()

    if action == "start":
        from alive.integrations.telegram import start_telegram_bot

        try:
            if daemon_mode:
                start_telegram_bot(daemon=True)
                console.print("[green]✓ Telegram bot started as daemon.[/]")
            else:
                console.print("[bold cyan]Starting Telegram bot (Ctrl+C to stop)...[/]")
                start_telegram_bot(daemon=False)
        except ValueError as e:
            console.print(Panel(str(e), title="[bold red]Error[/]", border_style="red"))
            raise typer.Exit(1)
        except ImportError as e:
            console.print(Panel(
                f"{e}\n\n[dim]Install: pip install 'alive-agent[telegram]'[/]",
                title="[bold red]Missing Package[/]",
                border_style="red",
            ))
            raise typer.Exit(1)

    elif action == "stop":
        from alive.integrations.telegram import stop_telegram_bot

        if stop_telegram_bot():
            console.print("[green]✓ Telegram bot stopped.[/]")
        else:
            console.print("[yellow]Telegram bot was not running.[/]")

    elif action == "status":
        from alive.integrations.telegram import telegram_status

        status = telegram_status()
        if status["configured"]:
            bot = status.get("bot_name") or "unknown"
            running = "[green]running[/]" if status.get("daemon_running") else "[dim]not running[/]"
            console.print(Panel(
                f"Bot: {bot}\n"
                f"Chat ID: [dim]configured[/]\n"
                f"Daemon: {running}",
                title="[bold cyan]Telegram Status[/]",
                border_style="cyan",
            ))
        else:
            console.print("[dim]Not configured. Run: alive setup telegram[/]")

    elif action == "test":
        from alive.integrations.telegram import test_telegram

        success = test_telegram(console=console)
        if not success:
            raise typer.Exit(1)

    else:
        console.print(Panel(
            f"Unknown action: '{action}'\n\n"
            "Available:\n"
            "  alive telegram start [-d]  — start bot\n"
            "  alive telegram stop        — stop bot\n"
            "  alive telegram status      — show status\n"
            "  alive telegram test        — send test message",
            title="[bold red]Unknown Action[/]",
            border_style="red",
        ))
        raise typer.Exit(1)


# ── alive listen ────────────────────────────────────────────────────────────


@app.command()
def listen(
    port: int = typer.Option(8484, "--port", "-p", help="HTTP port to listen on."),
) -> None:
    """Start the ALP listener. Join the agent network."""
    import time as _time

    from rich.live import Live

    from alive.network.identity import get_or_create_identity, get_agent_card
    from alive.network.discovery import start_discovery
    from alive.network.server import start_server

    identity = get_or_create_identity()
    agent_id = identity["agent_id"]
    alias = identity.get("alias") or agent_id[:12]

    # Start HTTP server
    server, _ = start_server(port)

    # Start UDP discovery
    stop_event, registry = start_discovery(get_agent_card, port, agent_id)

    console.print(f"\n[bold green]\U0001f310 Alive[/] (@{alias}) listening on port {port}...")
    console.print("[dim]Press Ctrl+C to stop[/dim]\n")

    try:
        with Live(console=console, refresh_per_second=0.5) as live:
            while True:
                peers = registry.get_active_peers()
                lines = [
                    f"[bold]\U0001f310 Alive Network[/bold]",
                    f"Your agent: {agent_id[:12]} (@{alias})",
                    f"Listening on port {port}",
                    "",
                    f"Peers ({len(peers)}):",
                ]
                if peers:
                    for p in peers:
                        agent = p.get("agent", {})
                        p_alias = agent.get("alias") or "anon"
                        p_skills = agent.get("skill_count", 0)
                        age = int(_time.time() - p["last_seen"])
                        lines.append(
                            f"  \u25cf {agent.get('agent_id', '?')[:12]} "
                            f"(@{p_alias}) \u2014 {p_skills} skills \u2014 {age}s ago"
                        )
                else:
                    lines.append("  [dim]Waiting for peers...[/dim]")

                live.update(Panel(
                    "\n".join(lines),
                    border_style="cyan",
                ))
                _time.sleep(2)
    except KeyboardInterrupt:
        stop_event.set()
        server.shutdown()
        console.print("\n[dim]Network stopped.[/dim]")


# ── network subcommands ────────────────────────────────────────────────────

network_app = typer.Typer(name="network", help="ALP network commands.")
app.add_typer(network_app)


@network_app.command("peers")
def network_peers() -> None:
    """List discovered peers."""
    from alive.network.discovery import get_peer_registry

    registry = get_peer_registry()
    peers = registry.get_active_peers()

    if not peers:
        console.print(Panel(
            "No peers discovered.\n\n"
            "Start the network: alive listen\n"
            "Or connect manually: alive network connect <ip:port>",
            title="[bold cyan]Peers[/]",
            border_style="cyan",
        ))
        return

    table = Table(title="\U0001f310 Network Peers")
    table.add_column("Alias", style="green")
    table.add_column("Agent ID", style="dim")
    table.add_column("Skills", justify="right")
    table.add_column("Last Seen", style="dim")
    table.add_column("Address", style="dim")

    import time as _time
    for p in peers:
        agent = p.get("agent", {})
        age = int(_time.time() - p["last_seen"])
        table.add_row(
            f"@{agent.get('alias') or 'anon'}",
            agent.get("agent_id", "?")[:12],
            str(agent.get("skill_count", 0)),
            f"{age}s ago",
            f"{p['ip']}:{p['port']}",
        )
    console.print(table)


@network_app.command("browse")
def network_browse(
    peer: Optional[str] = typer.Argument(None, help="@alias or agent_id to browse. Omit for all peers."),
) -> None:
    """Browse a peer's skills."""
    import httpx

    from alive.network.discovery import get_peer_registry

    registry = get_peer_registry()

    if peer:
        p = registry.find_peer(peer)
        if not p:
            console.print(f"[red]Peer '{peer}' not found.[/red]")
            raise typer.Exit(1)
        targets = [p]
    else:
        targets = registry.get_active_peers()
        if not targets:
            console.print("[dim]No peers available. Start: alive listen[/dim]")
            raise typer.Exit(1)

    table = Table(title="\U0001f4e6 Skills on Network")
    table.add_column("Skill", style="bold")
    table.add_column("Peer", style="green")
    table.add_column("Confidence", justify="right")
    table.add_column("Gen", justify="right")
    table.add_column("Description")

    for p in targets:
        alias = p.get("agent", {}).get("alias") or "anon"
        url = f"http://{p['ip']}:{p['port']}/alp/skills"
        try:
            resp = httpx.get(url, timeout=10)
            skills = resp.json()
        except Exception:
            continue

        for s in skills:
            table.add_row(
                s["name"],
                f"@{alias}",
                f"{s.get('confidence', 0):.0%}",
                str(s.get("generation", 1)),
                (s.get("description") or "")[:50],
            )

    console.print(table)


@network_app.command("pull")
def network_pull(
    skill_name: str = typer.Argument(..., help="Name of the skill to pull."),
    from_peer: Optional[str] = typer.Option(None, "--from", help="@alias or agent_id of peer."),
) -> None:
    """Pull a skill from the network."""
    import httpx

    from alive.network.discovery import get_peer_registry
    from alive.network.exchange import pull_skill as _pull_skill

    registry = get_peer_registry()

    if from_peer:
        p = registry.find_peer(from_peer)
        if not p:
            console.print(f"[red]Peer '{from_peer}' not found.[/red]")
            raise typer.Exit(1)
    else:
        # Find first peer that has this skill
        peers = registry.get_active_peers()
        p = None
        for candidate in peers:
            url = f"http://{candidate['ip']}:{candidate['port']}/alp/skills"
            try:
                resp = httpx.get(url, timeout=10)
                skills = resp.json()
                if any(s["name"] == skill_name for s in skills):
                    p = candidate
                    break
            except Exception:
                continue
        if not p:
            console.print(f"[red]No peer has skill '{skill_name}'.[/red]")
            raise typer.Exit(1)

    alias = p.get("agent", {}).get("alias") or "anon"
    console.print(f"\nPulling [bold]{skill_name}[/bold] from @{alias}...\n")

    result = asyncio.run(_pull_skill(p["ip"], p["port"], skill_name))
    if not result:
        raise typer.Exit(1)


@network_app.command("push")
def network_push(
    skill_name: str = typer.Argument(..., help="Skill to highlight on network."),
) -> None:
    """Make a skill available on the network (skills are shared by default)."""
    from alive.core.skill_registry import get_registry

    registry = get_registry()
    try:
        info = registry.get_skill(skill_name)
    except KeyError:
        console.print(f"[red]Skill '{skill_name}' not found.[/red]")
        raise typer.Exit(1)

    console.print(Panel(
        f"[bold green]{skill_name}[/bold green] is available on the network.\n\n"
        f"Any peer running [bold]alive listen[/bold] can browse and pull it.",
        title="\U0001f4e1 Skill Available",
        border_style="green",
    ))


@network_app.command("stats")
def network_stats() -> None:
    """Show network evolution statistics."""
    import json as _json

    from alive.core.skill_registry import get_registry

    registry = get_registry()
    skills = registry.list_skills_only()

    network_skills = []
    total_mutations = 0
    max_gen = 0
    max_gen_name = ""

    for s in skills:
        meta_path = registry._skills_dir / s.name / "metadata.json"
        if not meta_path.exists():
            continue
        try:
            meta = _json.loads(meta_path.read_text())
        except (ValueError, OSError):
            continue
        if meta.get("network_evolved"):
            network_skills.append(s.name)
            muts = meta.get("mutations", [])
            total_mutations += len(muts)
            gen = meta.get("generation", 1)
            if gen > max_gen:
                max_gen = gen
                max_gen_name = s.name

    console.print(Panel(
        f"[bold]Skills received:[/bold] {len(network_skills)}\n"
        f"[bold]Total mutations:[/bold] {total_mutations}\n"
        f"[bold]Most evolved:[/bold] {max_gen_name or 'none'} (gen {max_gen})\n"
        f"\n[dim]Network skills: {', '.join(network_skills) or 'none'}[/dim]",
        title="\U0001f4ca Network Stats",
        border_style="cyan",
    ))


@network_app.command("connect")
def network_connect(
    address: str = typer.Argument(..., help="Peer address in ip:port format."),
) -> None:
    """Manually connect to a peer."""
    from alive.network.discovery import connect_to_peer

    console.print(f"[dim]Connecting to {address}...[/dim]")
    card = connect_to_peer(address)
    if card:
        alias = card.get("alias") or "anon"
        console.print(Panel(
            f"Connected to @{alias}\n"
            f"Agent: {card.get('agent_id', '?')[:12]}\n"
            f"Skills: {card.get('skill_count', 0)}",
            title="[bold green]\u2713 Connected[/bold green]",
            border_style="green",
        ))
    else:
        console.print(f"[red]Failed to connect to {address}[/red]")
        raise typer.Exit(1)


@network_app.command("log")
def network_log(
    follow: bool = typer.Option(False, "--follow", "-f", help="Tail the log in real-time."),
    lines_count: int = typer.Option(50, "--lines", "-n", help="Number of lines to show."),
) -> None:
    """Show the network conversation log."""
    import time as _time

    from alive.network.conversation_log import read_log, _log_path

    _EVENT_COLORS = {
        "DISCOVERED": "green",
        "CONNECTED": "green",
        "ANNOUNCE": "dim",
        "SERVER": "dim",
        "BROWSE": "cyan",
        "CARD_REQUEST": "cyan",
        "SKILL_REQUEST": "yellow",
        "SENT": "blue",
        "PULLING": "yellow",
        "RECEIVED": "yellow",
        "MUTATING": "magenta",
        "MUTATED": "magenta",
        "TESTING": "dim",
        "TESTED": "green",
        "TEST_FAILED": "red",
        "INSTALLED": "bold green",
    }

    entries = read_log(lines_count)
    if not entries:
        console.print("[dim]No network activity yet. Start: alive listen[/dim]")
        return

    for entry in entries:
        # Parse event type for coloring
        color = "white"
        for event_key, event_color in _EVENT_COLORS.items():
            if event_key in entry:
                color = event_color
                break
        console.print(f"[{color}]{entry}[/{color}]")

    if follow:
        path = _log_path()
        last_size = path.stat().st_size if path.exists() else 0
        try:
            while True:
                _time.sleep(1)
                if not path.exists():
                    continue
                current_size = path.stat().st_size
                if current_size > last_size:
                    with open(path) as f:
                        f.seek(last_size)
                        new_text = f.read()
                    last_size = current_size
                    for line in new_text.splitlines():
                        color = "white"
                        for event_key, event_color in _EVENT_COLORS.items():
                            if event_key in line:
                                color = event_color
                                break
                        console.print(f"[{color}]{line}[/{color}]")
        except KeyboardInterrupt:
            pass


@network_app.command("alias")
def network_alias(
    name: str = typer.Argument(..., help="Alias for this agent."),
) -> None:
    """Set your agent's alias."""
    from alive.network.identity import set_alias

    set_alias(name)
    console.print(f"[green]\u2713[/green] Alias set to [bold]@{name}[/bold]")
