"""Tool executor — dispatches tool calls to underlying iconfucius functions.

Each handler calls the underlying function, which returns a structured dict,
and builds the response for the AI agent.
"""

import json
import os
from pathlib import Path


def execute_tool(name: str, args: dict, *, persona_name: str = "") -> dict:
    """Execute a tool by name with the given arguments.

    Args:
        name: Tool name.
        args: Tool arguments.
        persona_name: Persona name for memory operations (trade recording, etc.).

    Returns:
        {"status": "ok", ...} on success,
        {"status": "error", "error": "message"} on failure.
    """
    handler = _HANDLERS.get(name)
    if handler is None:
        return {"status": "error", "error": f"Unknown tool: {name}"}
    try:
        # Memory tools need persona_name
        if name in ("memory_read_strategy", "memory_read_learnings", "memory_update"):
            result = handler(args, persona_name=persona_name)
        else:
            result = handler(args)

        # Record successful trades
        if name in ("trade_buy", "trade_sell") and result.get("status") == "ok" and persona_name:
            _record_trade(name, args, result, persona_name)

        return result
    except SystemExit:
        # get_verify_certificates() raises SystemExit when blst is missing
        return {
            "status": "error",
            "error": (
                "verify_certificates is enabled in iconfucius.toml "
                "but the 'blst' library is not installed. "
                "Either install blst (see https://github.com/onicai/IConfucius/blob/main/agent/README-security.md) "
                "or set verify_certificates = false in [settings]."
            ),
        }
    except Exception as e:
        return {"status": "error", "error": str(e)}


# ---------------------------------------------------------------------------
# Formatting handlers
# ---------------------------------------------------------------------------

def _handle_fmt_sats(args: dict) -> dict:
    from iconfucius.config import fmt_sats, get_btc_to_usd_rate

    sats = args.get("sats")
    if sats is None:
        return {"status": "error", "error": "'sats' is required."}

    try:
        rate = get_btc_to_usd_rate()
    except Exception:
        rate = None

    return {"status": "ok", "formatted": fmt_sats(int(sats), rate)}


# ---------------------------------------------------------------------------
# Setup handlers
# ---------------------------------------------------------------------------

def _handle_setup_status(args: dict) -> dict:
    from iconfucius.config import find_config, get_pem_file

    config_path = find_config()
    pem_exists = Path(get_pem_file()).exists()
    env_exists = Path(".env").exists()
    api_key = os.environ.get("ANTHROPIC_API_KEY", "")
    has_api_key = bool(api_key) and api_key != "your-api-key-here"

    return {
        "status": "ok",
        "config_exists": config_path is not None,
        "wallet_exists": pem_exists,
        "env_exists": env_exists,
        "has_api_key": has_api_key,
        "ready": all([config_path is not None, pem_exists, has_api_key]),
    }


def _handle_init(args: dict) -> dict:
    from typer.testing import CliRunner
    from iconfucius.cli import app as cli_app

    cmd = ["init"]
    if args.get("force"):
        cmd.append("--force")
    num_bots = args.get("num_bots")
    if num_bots is not None:
        cmd.extend(["--bots", str(num_bots)])

    runner = CliRunner()
    result = runner.invoke(cli_app, cmd)
    if result.exit_code != 0:
        return {"status": "error", "error": result.output.strip()}

    # Reload config so the rest of the session sees it
    from iconfucius.config import load_config
    load_config(reload=True)

    return {"status": "ok", "display": result.output.strip()}


def _handle_set_bot_count(args: dict) -> dict:
    import re
    from pathlib import Path

    from iconfucius.config import (
        CONFIG_FILENAME,
        add_bots_to_config,
        find_config,
        get_bot_names,
        load_config,
        remove_bots_from_config,
    )

    if not find_config():
        return {"status": "error", "error": "No iconfucius.toml found. Run init first."}

    num_bots = args.get("num_bots")
    if num_bots is None:
        return {"status": "error", "error": "'num_bots' is required."}
    num_bots = max(1, min(1000, int(num_bots)))
    force = args.get("force", False)

    config = load_config(reload=True)
    current_bots = get_bot_names()
    current_count = len(current_bots)

    if num_bots == current_count:
        return {
            "status": "ok",
            "message": f"Already configured with {num_bots} bot(s).",
            "bot_count": num_bots,
        }

    # --- Increasing: add new bots ---
    if num_bots > current_count:
        # Find the highest bot number to continue from
        max_num = 0
        for name in current_bots:
            m = re.search(r'(\d+)$', name)
            if m:
                max_num = max(max_num, int(m.group(1)))
        max_num = max(max_num, current_count)

        added = add_bots_to_config(max_num, max_num + (num_bots - current_count))
        load_config(reload=True)
        return {
            "status": "ok",
            "message": f"Added {len(added)} bot(s). Now at {num_bots}.",
            "bots_added": added,
            "bot_count": num_bots,
        }

    # --- Decreasing: check holdings, then remove ---
    # Sort bots by number, keep lowest, remove highest
    def _sort_key(name):
        m = re.search(r'(\d+)$', name)
        return int(m.group(1)) if m else float('inf')

    sorted_bots = sorted(current_bots, key=_sort_key)
    bots_to_keep = sorted_bots[:num_bots]
    bots_to_remove = sorted_bots[num_bots:]

    if not force:
        # Quick check: only inspect bots that have cached sessions
        # (bots without sessions were never funded)
        cache_dir = Path(".cache")
        bots_to_check = []
        for name in bots_to_remove:
            safe = name.replace("/", "_").replace("\\", "_").replace(" ", "_")
            if (cache_dir / f"session_{safe}.json").exists():
                bots_to_check.append(name)

        if bots_to_check:
            from iconfucius.cli.balance import collect_balances
            from iconfucius.cli.concurrent import run_per_bot

            results = run_per_bot(
                lambda n: collect_balances(n, verbose=False),
                bots_to_check,
            )
            holdings = []
            for bot_name, result in results:
                if isinstance(result, Exception):
                    continue
                if result.odin_sats > 0 or result.token_holdings:
                    holdings.append({
                        "bot_name": result.bot_name,
                        "odin_sats": int(result.odin_sats),
                        "token_holdings": result.token_holdings,
                    })

            if holdings:
                return {
                    "status": "blocked",
                    "reason": "bots_have_holdings",
                    "bots_to_remove": bots_to_remove,
                    "holdings": holdings,
                    "message": (
                        "Some bots have holdings. "
                        "Sweep them first or confirm removal with force=true."
                    ),
                }

    remove_bots_from_config(bots_to_remove)
    load_config(reload=True)
    return {
        "status": "ok",
        "bots_removed": bots_to_remove,
        "message": f"Removed {len(bots_to_remove)} bot(s). Now at {num_bots}.",
        "bot_count": num_bots,
    }


def _handle_wallet_create(args: dict) -> dict:
    from typer.testing import CliRunner
    from iconfucius.cli.wallet import wallet_app

    cmd = ["create"]
    if args.get("force"):
        cmd.append("--force")

    runner = CliRunner()
    result = runner.invoke(wallet_app, cmd)
    if result.exit_code != 0:
        return {"status": "error", "error": result.output.strip()}
    return {"status": "ok", "display": result.output.strip()}


# ---------------------------------------------------------------------------
# Read-only handlers
# ---------------------------------------------------------------------------

def _handle_bot_list(args: dict) -> dict:
    from iconfucius.config import find_config, get_bot_names

    if not find_config():
        return {"status": "error", "error": "No iconfucius.toml found. Run init first."}

    bot_names = get_bot_names()
    names_str = ", ".join(bot_names)
    display = f"{len(bot_names)} bot(s): {names_str}"

    return {
        "status": "ok",
        "display": display,
        "bot_names": bot_names,
        "bot_count": len(bot_names),
    }


def _handle_wallet_balance(args: dict) -> dict:
    from iconfucius.config import get_bot_names, require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    bot_name = args.get("bot_name")
    ckbtc_minter = args.get("ckbtc_minter", False)

    from iconfucius.cli.balance import run_all_balances

    names = [bot_name] if bot_name else get_bot_names()
    data = run_all_balances(names, ckbtc_minter=ckbtc_minter)

    if data is None:
        return {"status": "error", "error": "Balance check failed."}

    display_text = data.pop("_display", "")

    return {
        "status": "ok",
        "display": json.dumps(data, default=str),
        "_terminal_output": display_text.strip(),
    }


def _handle_wallet_receive(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    from icp_agent import Agent, Client
    from icp_identity import Identity

    from iconfucius.config import IC_HOST, fmt_sats, get_btc_to_usd_rate, get_pem_file
    from iconfucius.transfers import (
        create_ckbtc_minter,
        create_icrc1_canister,
        get_balance,
        get_btc_address,
    )

    pem_path = get_pem_file()
    with open(pem_path, "r") as f:
        pem_content = f.read()
    identity = Identity.from_pem(pem_content)
    principal = str(identity.sender())

    client = Client(url=IC_HOST)
    anon_agent = Agent(Identity(anonymous=True), client)
    minter = create_ckbtc_minter(anon_agent)
    btc_address = get_btc_address(minter, principal)

    icrc1 = create_icrc1_canister(anon_agent)
    balance = get_balance(icrc1, principal)

    try:
        rate = get_btc_to_usd_rate()
    except Exception:
        rate = None

    balance_str = fmt_sats(balance, rate)
    display = (
        f"Principal:       {principal}\n"
        f"Deposit address: {btc_address}\n"
        f"Balance:         {balance_str}\n"
        f"\n"
        f"To fund your wallet, send ckBTC to the principal or BTC to the deposit address.\n"
        f"BTC deposits require min 10,000 sats and ~6 confirmations."
    )

    return {
        "status": "ok",
        "display": display,
        "wallet_principal": principal,
        "btc_deposit_address": btc_address,
        "ckbtc_balance_sats": balance,
        "balance_display": balance_str,
    }


def _handle_wallet_info(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    from iconfucius.cli.balance import run_wallet_balance

    data = run_wallet_balance()
    if data is None:
        return {"status": "error", "error": "Wallet balance check failed."}

    display_text = data.pop("_display", "")
    return {"status": "ok", "display": display_text.strip()}


def _handle_wallet_monitor(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    from iconfucius.cli.balance import run_wallet_balance

    data = run_wallet_balance(ckbtc_minter=True)
    if data is None:
        return {"status": "error", "error": "Wallet balance check failed."}

    display_text = data.pop("_display", "")
    return {"status": "ok", "display": display_text.strip()}


def _handle_security_status(args: dict) -> dict:
    from iconfucius.config import find_config, get_cache_sessions, load_config

    config_path = find_config()

    # Check blst availability
    try:
        import blst  # noqa: F401
        blst_installed = True
    except (ImportError, ModuleNotFoundError):
        blst_installed = False

    # Check verify_certificates setting
    verify_certs = False
    if config_path:
        config = load_config()
        verify_certs = config.get("settings", {}).get(
            "verify_certificates", False
        )

    # Check session caching setting
    cache_sessions = get_cache_sessions() if config_path else True

    lines = ["Security status:"]
    # blst / certificate verification
    if blst_installed and verify_certs:
        lines.append("  IC certificate verification: enabled (blst installed)")
    elif blst_installed and not verify_certs:
        lines.append(
            "  IC certificate verification: disabled "
            "(blst installed — enable with verify_certificates = true)"
        )
    else:
        lines.append(
            "  IC certificate verification: disabled "
            "(blst not installed — use install_blst to enable)"
        )

    # Session caching
    if cache_sessions:
        lines.append(
            "  Session caching: enabled "
            "(sessions stored in .cache/ with 0600 permissions)"
        )
    else:
        lines.append(
            "  Session caching: disabled (fresh SIWB login every command)"
        )

    # Recommendations
    recs = []
    if not blst_installed:
        recs.append(
            "Install blst for IC certificate verification "
            "(protects balance checks and address lookups)"
        )
    elif not verify_certs:
        recs.append(
            "Enable verify_certificates = true in iconfucius.toml "
            "(blst is already installed)"
        )

    if recs:
        lines.append("")
        lines.append("Recommendations:")
        for r in recs:
            lines.append(f"  - {r}")

    return {
        "status": "ok",
        "display": "\n".join(lines),
        "blst_installed": blst_installed,
        "verify_certificates": verify_certs,
        "cache_sessions": cache_sessions,
    }


def _enable_verify_certificates() -> dict:
    """Enable verify_certificates = true in iconfucius.toml.

    Returns {"enabled_now": True} if it changed the setting,
    {"enabled_now": False} if already enabled or no config found.
    """
    from iconfucius.config import find_config

    config_path = find_config()
    if not config_path:
        return {"enabled_now": False}

    content = Path(config_path).read_text()
    if "verify_certificates = true" in content:
        return {"enabled_now": False}

    if "verify_certificates = false" in content:
        content = content.replace(
            "verify_certificates = false",
            "verify_certificates = true",
        )
    elif "verify_certificates" not in content:
        if "[settings]" in content:
            content = content.replace(
                "[settings]",
                "[settings]\nverify_certificates = true",
            )
        else:
            content += "\n[settings]\nverify_certificates = true\n"
    else:
        return {"enabled_now": False}

    Path(config_path).write_text(content)
    return {"enabled_now": True}


def _handle_install_blst(args: dict) -> dict:
    import platform
    import shutil
    import subprocess
    import tempfile

    # Check if already installed
    blst_already = False
    try:
        import blst  # noqa: F401
        blst_already = True
    except (ImportError, ModuleNotFoundError):
        pass

    if blst_already:
        # Still ensure verify_certificates is enabled in config
        result = _enable_verify_certificates()
        if result["enabled_now"]:
            return {
                "status": "ok",
                "display": (
                    "blst is already installed.\n"
                    "Enabled verify_certificates = true in iconfucius.toml."
                ),
            }
        return {
            "status": "ok",
            "display": (
                "blst is already installed and "
                "verify_certificates is already enabled."
            ),
        }

    # Check prerequisites
    missing = []
    if not shutil.which("git"):
        missing.append("git")
    if not shutil.which("swig"):
        missing.append("swig")

    # Check for C compiler
    has_cc = bool(
        shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
    )
    if not has_cc:
        missing.append("C compiler (gcc/clang)")

    if missing:
        system = platform.system()
        lines = [
            f"Missing prerequisites: {', '.join(missing)}",
            "",
            "Install them first:",
        ]
        if system == "Darwin":
            if "C compiler" in " ".join(missing):
                lines.append("  xcode-select --install")
            if "swig" in missing:
                lines.append("  brew install swig")
        else:
            # Linux
            if shutil.which("apt-get"):
                lines.append(
                    "  sudo apt-get install build-essential swig python3-dev"
                )
            elif shutil.which("dnf"):
                lines.append(
                    "  sudo dnf install gcc gcc-c++ make swig python3-devel"
                )
            else:
                lines.append(
                    "  Install: C compiler, make, swig, python3 headers"
                )
        lines.append("")
        lines.append("Then run install_blst again.")
        return {"status": "error", "error": "\n".join(lines)}

    # Build and install blst from source
    blst_version = "v0.3.16"
    blst_commit = "e7f90de551e8df682f3cc99067d204d8b90d27ad"

    blst_dir = tempfile.mkdtemp(prefix="blst_")
    try:
        # Clone
        subprocess.run(
            ["git", "clone", "--branch", blst_version, "--depth", "1",
             "https://github.com/supranational/blst", blst_dir],
            check=True, capture_output=True, text=True,
        )

        # Verify commit hash
        result = subprocess.run(
            ["git", "rev-parse", "HEAD"],
            cwd=blst_dir, capture_output=True, text=True, check=True,
        )
        actual_commit = result.stdout.strip()
        if actual_commit != blst_commit:
            return {
                "status": "error",
                "error": (
                    f"Commit mismatch! Expected {blst_commit}, "
                    f"got {actual_commit}. Aborting for safety."
                ),
            }

        # Build Python bindings
        bindings_dir = os.path.join(blst_dir, "bindings", "python")
        env = os.environ.copy()
        if platform.machine().startswith("arm") or platform.machine() == "aarch64":
            env["BLST_PORTABLE"] = "1"
        subprocess.run(
            ["python3", "run.me"],
            cwd=bindings_dir, env=env,
            capture_output=True, text=True,
        )

        # Find install paths
        import sysconfig
        purelib = sysconfig.get_paths()["purelib"]
        platlib = sysconfig.get_paths()["platlib"]

        # Copy built files
        import glob as _glob
        blst_py = os.path.join(bindings_dir, "blst.py")
        if not os.path.exists(blst_py):
            return {
                "status": "error",
                "error": "Build failed — blst.py not found after build.",
            }
        shutil.copy2(blst_py, purelib)

        so_files = _glob.glob(os.path.join(bindings_dir, "_blst*.so"))
        if not so_files:
            return {
                "status": "error",
                "error": "Build failed — _blst*.so not found after build.",
            }
        for so in so_files:
            shutil.copy2(so, platlib)

    finally:
        shutil.rmtree(blst_dir, ignore_errors=True)

    # Verify installation
    try:
        # Clear any cached import failures
        import importlib
        if "blst" in __import__("sys").modules:
            del __import__("sys").modules["blst"]
        importlib.import_module("blst")
    except (ImportError, ModuleNotFoundError):
        return {
            "status": "error",
            "error": "blst was built but could not be imported. Check build output.",
        }

    # Enable verify_certificates in config
    result = _enable_verify_certificates()

    from iconfucius.config import find_config
    config_path = find_config()

    lines = ["blst installed successfully!"]
    lines.append("IC certificate verification is now available.")
    if result["enabled_now"]:
        lines.append("Enabled verify_certificates = true in iconfucius.toml.")
    elif config_path:
        lines.append("verify_certificates is already enabled in iconfucius.toml.")
    else:
        lines.append(
            "Run init first, then set verify_certificates = true "
            "in iconfucius.toml."
        )

    return {"status": "ok", "display": "\n".join(lines)}


def _handle_persona_list(args: dict) -> dict:
    from iconfucius.config import get_default_persona
    from iconfucius.persona import list_personas

    names = list_personas()
    default = get_default_persona()
    lines = ["Available personas:"]
    for name in names:
        marker = " (default)" if name == default else ""
        lines.append(f"  {name}{marker}")
    return {"status": "ok", "display": "\n".join(lines), "personas": names}


def _handle_persona_show(args: dict) -> dict:
    from iconfucius.persona import PersonaNotFoundError, load_persona

    name = args.get("name", "")
    if not name:
        return {"status": "error", "error": "Persona name is required."}

    try:
        p = load_persona(name)
    except PersonaNotFoundError:
        return {"status": "error", "error": f"Persona '{name}' not found."}

    budget = "unlimited" if p.budget_limit == 0 else f"{p.budget_limit:,} sats"
    display = (
        f"Name:        {p.name}\n"
        f"Description: {p.description}\n"
        f"Voice:       {p.voice}\n"
        f"Risk:        {p.risk}\n"
        f"Budget:      {budget}\n"
        f"Default bot: {p.bot}\n"
        f"AI backend:  {p.ai_backend}\n"
        f"AI model:    {p.ai_model}"
    )
    return {
        "status": "ok",
        "display": display,
        "name": p.name,
        "description": p.description,
        "voice": p.voice,
        "risk": p.risk,
        "budget_limit": p.budget_limit,
        "bot": p.bot,
        "ai_backend": p.ai_backend,
        "ai_model": p.ai_model,
    }


def _handle_token_lookup(args: dict) -> dict:
    from iconfucius.tokens import search_token

    query = args.get("query", "")
    if not query:
        return {"status": "error", "error": "Query is required."}

    result = search_token(query)
    search_results = [
        {
            "id": r.get("id"),
            "name": r.get("name"),
            "ticker": r.get("ticker"),
            "bonded": r.get("bonded"),
            "twitter_verified": r.get("twitter_verified"),
            "holder_count": r.get("holder_count"),
            "volume": r.get("volume"),
            "safety": r.get("safety"),
        }
        for r in result["search_results"]
    ]

    # Build display
    lines = [f"Token search: {query}"]
    km = result["known_match"]
    if km:
        flags = []
        if km.get("bonded"):
            flags.append("bonded")
        if km.get("twitter_verified"):
            flags.append("Twitter verified")
        flags_str = ", ".join(flags) if flags else "unverified"
        holders = km.get("holder_count")
        holder_str = f", {holders:,} holders" if holders else ""
        lines.append(
            f"Known match: {km.get('name')} ({km.get('id')}) "
            f"— {flags_str}{holder_str}"
        )
    if not km and search_results:
        # Only show search results when there's no known match
        lines.append("Search results:")
        for r in search_results:
            lines.append(f"  {r.get('name')} ({r.get('id')}) — {r.get('safety', '')}")
    elif not km and not search_results:
        lines.append("No results found.")

    return {
        "status": "ok",
        "display": "\n".join(lines),
        "query": query,
        "known_match": km,
        "search_results": search_results,
    }


def _handle_token_price(args: dict) -> dict:
    from iconfucius.config import fmt_sats, get_btc_to_usd_rate
    from iconfucius.tokens import fetch_token_data, lookup_token_with_fallback

    query = args.get("query", "")
    if not query:
        return {"status": "error", "error": "Query is required."}

    token = lookup_token_with_fallback(query)
    if not token:
        return {"status": "error", "error": f"Token not found: {query}"}

    token_id = token["id"]
    token_name = token.get("name", token_id)
    token_ticker = token.get("ticker", "")

    data = fetch_token_data(token_id)
    if not data:
        return {
            "status": "error",
            "error": f"Could not fetch price data for {token_name} ({token_id}).",
        }

    # API returns BTC-denominated fields in millisatoshis (msat)
    MSAT_PER_SAT = 1000
    price_msat = data.get("price", 0)
    price_1h_msat = data.get("price_1h", 0)
    price_6h_msat = data.get("price_6h", 0)
    price_1d_msat = data.get("price_1d", 0)
    marketcap_sats = data.get("marketcap", 0) // MSAT_PER_SAT
    volume_24_sats = data.get("volume_24", 0) // MSAT_PER_SAT
    holder_count = data.get("holder_count", 0)
    btc_liquidity_sats = data.get("btc_liquidity", 0) // MSAT_PER_SAT

    # Price: API gives msat per token, convert to sats
    price_sats = price_msat / MSAT_PER_SAT

    def _pct_change(current: int, previous: int) -> str:
        if not previous or not current:
            return "n/a"
        pct = ((current - previous) / previous) * 100
        sign = "+" if pct >= 0 else ""
        return f"{sign}{pct:.1f}%"

    try:
        btc_usd = get_btc_to_usd_rate()
    except Exception:
        btc_usd = None

    # Format price with appropriate precision (can be fractional sats)
    if btc_usd:
        price_usd = (price_sats / 100_000_000) * btc_usd
        price_str = f"{price_sats:,.3f} sats (${price_usd:.5f})"
    else:
        price_str = f"{price_sats:,.3f} sats"

    lines = [
        f"{token_name} ({token_ticker}) — {token_id}",
        f"Price:        {price_str} per token",
        f"Change 1h:    {_pct_change(price_msat, price_1h_msat)}",
        f"Change 6h:    {_pct_change(price_msat, price_6h_msat)}",
        f"Change 24h:   {_pct_change(price_msat, price_1d_msat)}",
        f"Market cap:   {fmt_sats(marketcap_sats, btc_usd)}",
        f"24h volume:   {fmt_sats(volume_24_sats, btc_usd)}",
        f"Liquidity:    {fmt_sats(btc_liquidity_sats, btc_usd)}",
        f"Holders:      {holder_count:,}",
    ]

    return {
        "status": "ok",
        "display": "\n".join(lines),
        "token_id": token_id,
        "token_name": token_name,
        "ticker": token_ticker,
        "price_sats": price_sats,
        "price_usd": (price_sats / 100_000_000) * btc_usd if btc_usd else None,
        "change_1h": _pct_change(price_msat, price_1h_msat),
        "change_6h": _pct_change(price_msat, price_6h_msat),
        "change_24h": _pct_change(price_msat, price_1d_msat),
        "marketcap_sats": marketcap_sats,
        "volume_24h_sats": volume_24_sats,
        "holder_count": holder_count,
    }


# ---------------------------------------------------------------------------
# State-changing handlers
# ---------------------------------------------------------------------------

def _resolve_bot_names(args: dict) -> list[str]:
    """Resolve bot_name / bot_names / all_bots args into a list of bot names."""
    if args.get("all_bots"):
        from iconfucius.config import get_bot_names
        return get_bot_names()
    names = args.get("bot_names")
    if names:
        return list(names)
    bot_name = args.get("bot_name")
    if bot_name:
        return [bot_name]
    return []


def _handle_fund(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    amount = args.get("amount")
    bot_names = _resolve_bot_names(args)
    if not amount or not bot_names:
        return {"status": "error", "error": "'amount' and at least one bot are required."}

    from iconfucius.cli.fund import run_fund

    result = run_fund(bot_names, int(amount))

    if result["status"] == "error":
        return result

    # Build display from structured result
    funded = result.get("funded", [])
    failed = result.get("failed", [])
    lines = []
    if funded:
        lines.append(f"Funded {len(funded)} bot(s) with {result['amount']:,} sats each")
    for f in failed:
        lines.append(f"  {f['bot']}: FAILED — {f['error']}")
    display = "\n".join(lines) if lines else "No bots funded"

    return {
        "status": result["status"],
        "display": display,
        "funded": len(funded),
        "failed": len(failed),
    }


def _usd_to_sats(amount_usd: float) -> int:
    """Convert a USD amount to satoshis using the live BTC/USD rate."""
    from iconfucius.config import get_btc_to_usd_rate

    btc_usd = get_btc_to_usd_rate()
    return int((amount_usd / btc_usd) * 100_000_000)


def _usd_to_tokens(amount_usd: float, token_id: str) -> int:
    """Convert a USD amount to raw token sub-units using live price data.

    Returns the raw token amount (not display tokens).
    Formula: raw_tokens = sats * 10^6 * 10^div / price
    """
    from iconfucius.tokens import fetch_token_data

    data = fetch_token_data(token_id)
    if not data or not data.get("price"):
        raise ValueError(f"Could not fetch price for token {token_id}")

    sats = _usd_to_sats(amount_usd)
    price = data["price"]
    divisibility = data.get("divisibility", 8)
    raw_tokens = int(sats * 1_000_000 * (10 ** divisibility) / price)
    return raw_tokens


def _handle_trade_buy(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    token_id = args.get("token_id")
    amount = args.get("amount")
    amount_usd = args.get("amount_usd")
    bot_names = _resolve_bot_names(args)

    # Convert USD to sats if needed
    if amount_usd is not None and amount is None:
        try:
            amount = _usd_to_sats(amount_usd)
            args["amount"] = amount  # write back for _record_trade
        except Exception as e:
            return {"status": "error", "error": f"USD conversion failed: {e}"}

    if not all([token_id, amount, bot_names]):
        return {"status": "error", "error": "'token_id', 'amount' (or 'amount_usd'), and at least one bot are required."}

    from iconfucius.cli.concurrent import run_per_bot
    from iconfucius.cli.trade import run_trade

    results = run_per_bot(
        lambda name: run_trade(name, "buy", token_id, str(amount)),
        bot_names,
    )
    return _aggregate_trade_results(results, "buy", token_id)


def _handle_trade_sell(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    token_id = args.get("token_id")
    amount = args.get("amount")
    amount_usd = args.get("amount_usd")
    bot_names = _resolve_bot_names(args)

    # Convert USD to raw token amount if needed
    if amount_usd is not None and amount is None:
        try:
            amount = _usd_to_tokens(amount_usd, token_id)
            args["amount"] = amount  # write back for _record_trade
        except Exception as e:
            return {"status": "error", "error": f"USD conversion failed: {e}"}

    if not all([token_id, amount, bot_names]):
        return {"status": "error", "error": "'token_id', 'amount' (or 'amount_usd'), and at least one bot are required."}

    from iconfucius.cli.concurrent import run_per_bot
    from iconfucius.cli.trade import run_trade

    results = run_per_bot(
        lambda name: run_trade(name, "sell", token_id, str(amount)),
        bot_names,
    )
    return _aggregate_trade_results(results, "sell", token_id)


def _aggregate_trade_results(results: list, action: str, token_id: str) -> dict:
    """Aggregate per-bot trade results into a single structured response."""
    succeeded, failed, skipped = [], [], []
    for bot_name, result in results:
        if isinstance(result, Exception):
            failed.append({"bot": bot_name, "error": str(result)})
        elif isinstance(result, dict) and result.get("status") == "skipped":
            skipped.append({"bot": bot_name, "reason": result.get("reason", "")})
        elif isinstance(result, dict) and result.get("status") == "error":
            failed.append({"bot": bot_name, "error": result.get("error", "")})
        elif isinstance(result, dict) and result.get("status") == "ok":
            succeeded.append({"bot": bot_name, **result})
        else:
            failed.append({"bot": bot_name, "error": f"Unexpected result: {result}"})

    # Build terminal summary for the user watching the chat
    verb = "Bought" if action == "buy" else "Sold"
    lines = []
    if succeeded:
        lines.append(f"{verb} {token_id} from {len(succeeded)} bot(s)")
    for s in skipped:
        lines.append(f"  {s['bot']}: skipped — {s['reason']}")
    for f in failed:
        lines.append(f"  {f['bot']}: FAILED — {f['error']}")
    display = "\n".join(lines) if lines else "No trades executed"

    all_ok = not failed
    return {
        "status": "ok" if all_ok else "partial",
        "display": display,
        "succeeded": len(succeeded),
        "failed": len(failed),
        "skipped": len(skipped),
    }


def _handle_withdraw(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    amount = args.get("amount")
    bot_names = _resolve_bot_names(args)
    if not amount or not bot_names:
        return {"status": "error", "error": "'amount' and at least one bot are required."}

    from iconfucius.cli.concurrent import run_per_bot
    from iconfucius.cli.withdraw import run_withdraw

    results = run_per_bot(
        lambda name: run_withdraw(name, str(amount)),
        bot_names,
    )

    succeeded, failed = [], []
    for bot_name, result in results:
        if isinstance(result, Exception):
            failed.append({"bot": bot_name, "error": str(result)})
        elif isinstance(result, dict) and result.get("status") == "error":
            failed.append({"bot": bot_name, "error": result.get("error", "")})
        elif isinstance(result, dict) and result.get("status") == "partial":
            failed.append({"bot": bot_name, "error": result.get("error", "partial")})
        elif isinstance(result, dict) and result.get("status") == "ok":
            succeeded.append({"bot": bot_name, **result})
        else:
            failed.append({"bot": bot_name, "error": f"Unexpected result: {result}"})

    lines = []
    if succeeded:
        lines.append(f"Withdrew from {len(succeeded)} bot(s)")
    for f in failed:
        lines.append(f"  {f['bot']}: FAILED — {f['error']}")
    display = "\n".join(lines) if lines else "No withdrawals executed"

    all_ok = not failed
    return {
        "status": "ok" if all_ok else "partial",
        "display": display,
        "succeeded": len(succeeded),
        "failed": len(failed),
    }


def _handle_wallet_send(args: dict) -> dict:
    from iconfucius.config import require_wallet

    if not require_wallet():
        return {"status": "error", "error": "No wallet found. Run: iconfucius wallet create"}

    amount = args.get("amount")
    address = args.get("address")
    if not amount or not address:
        return {"status": "error", "error": "Both 'amount' and 'address' are required."}

    # wallet send uses typer.Exit for errors, so we need to catch it
    import typer

    try:
        from iconfucius.cli.wallet import send
        # Invoke via the underlying Click command
        from typer.testing import CliRunner
        from iconfucius.cli.wallet import wallet_app

        runner = CliRunner()
        result = runner.invoke(wallet_app, ["send", str(amount), address])
        if result.exit_code != 0:
            return {"status": "error", "error": result.output.strip()}
        return {"status": "ok", "display": result.output.strip()}
    except typer.Exit:
        return {"status": "error", "error": "Command failed."}


# ---------------------------------------------------------------------------
# Trade recording
# ---------------------------------------------------------------------------

def _record_trade(tool_name: str, args: dict, result: dict,
                  persona_name: str) -> None:
    """Record a completed trade to memory. Best-effort — never raises."""
    from datetime import datetime, timezone

    from iconfucius.memory import append_trade

    action = "BUY" if tool_name == "trade_buy" else "SELL"
    token_id = args.get("token_id", "?")
    amount = args.get("amount", "?")
    bots = _resolve_bot_names(args)
    bot_str = ", ".join(bots) if bots else "?"
    ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")

    # Fetch live price and BTC/USD rate for enriched logging
    price = 0
    ticker = token_id
    divisibility = 8
    btc_usd = None
    try:
        from iconfucius.tokens import fetch_token_data

        data = fetch_token_data(token_id)
        if data:
            price = data.get("price", 0)
            ticker = data.get("ticker", token_id)
            divisibility = data.get("divisibility", 8)
    except Exception:
        pass
    try:
        from iconfucius.config import get_btc_to_usd_rate

        btc_usd = get_btc_to_usd_rate()
    except Exception:
        pass

    def _fmt_usd(sats: float) -> str:
        if btc_usd:
            return f"${(sats / 100_000_000) * btc_usd:.2f}"
        return "?"

    lines = [f"## {action} — {ts}"]
    lines.append(f"- Token: {token_id} ({ticker})")

    is_sell_all = str(amount).lower() == "all"
    amount_int = 0 if is_sell_all else (int(amount) if str(amount).isdigit() else 0)
    if action == "BUY":
        # amount is sats spent; estimate display tokens received
        lines.append(f"- Spent: {amount_int:,} sats ({_fmt_usd(amount_int)})")
        if price:
            # display_tokens = sats * 10^6 / price (divisibility cancels out)
            display_tokens = amount_int * 1_000_000 / price
            lines.append(f"- Est. tokens: ~{display_tokens:,.2f}")
    elif is_sell_all:
        lines.append(f"- Sold: ALL tokens")
    else:
        # amount is raw token sub-units; show display tokens and estimate sats
        display_tokens = amount_int / (10 ** divisibility)
        lines.append(f"- Sold: {display_tokens:,.2f} tokens ({amount_int:,} sub-units)")
        if price:
            est_sats = (amount_int * price) / (10 ** divisibility) / 1_000_000
            lines.append(f"- Est. received: ~{est_sats:,.0f} sats ({_fmt_usd(est_sats)})")

    if price:
        lines.append(f"- Price: {price:,} sats/token ({_fmt_usd(price)})")
    lines.append(f"- Bots: {bot_str}")

    entry = "\n".join(lines)
    try:
        append_trade(persona_name, entry)
    except Exception:
        pass  # best-effort


# ---------------------------------------------------------------------------
# Memory handlers
# ---------------------------------------------------------------------------

def _handle_memory_read_strategy(args: dict, *, persona_name: str = "") -> dict:
    from iconfucius.memory import read_strategy

    if not persona_name:
        return {"status": "error", "error": "No persona context available."}
    content = read_strategy(persona_name)
    if not content:
        return {"status": "ok", "display": "No strategy notes yet."}
    return {"status": "ok", "display": content}


def _handle_memory_read_learnings(args: dict, *, persona_name: str = "") -> dict:
    from iconfucius.memory import read_learnings

    if not persona_name:
        return {"status": "error", "error": "No persona context available."}
    content = read_learnings(persona_name)
    if not content:
        return {"status": "ok", "display": "No learnings recorded yet."}
    return {"status": "ok", "display": content}


def _handle_memory_update(args: dict, *, persona_name: str = "") -> dict:
    from iconfucius.memory import write_learnings, write_strategy

    if not persona_name:
        return {"status": "error", "error": "No persona context available."}

    file = args.get("file")
    content = args.get("content")
    if not file or not content:
        return {"status": "error", "error": "'file' and 'content' are required."}

    if file == "strategy":
        write_strategy(persona_name, content)
        return {"status": "ok", "display": "Strategy updated."}
    elif file == "learnings":
        write_learnings(persona_name, content)
        return {"status": "ok", "display": "Learnings updated."}
    else:
        return {"status": "error", "error": f"Unknown file: {file}. Use 'strategy' or 'learnings'."}


# ---------------------------------------------------------------------------
# Handler registry
# ---------------------------------------------------------------------------

_HANDLERS: dict[str, callable] = {
    "fmt_sats": _handle_fmt_sats,
    "setup_status": _handle_setup_status,
    "init": _handle_init,
    "set_bot_count": _handle_set_bot_count,
    "bot_list": _handle_bot_list,
    "wallet_create": _handle_wallet_create,
    "wallet_balance": _handle_wallet_balance,
    "wallet_receive": _handle_wallet_receive,
    "wallet_info": _handle_wallet_info,
    "wallet_monitor": _handle_wallet_monitor,
    "security_status": _handle_security_status,
    "install_blst": _handle_install_blst,
    "persona_list": _handle_persona_list,
    "persona_show": _handle_persona_show,
    "token_lookup": _handle_token_lookup,
    "token_price": _handle_token_price,
    "fund": _handle_fund,
    "trade_buy": _handle_trade_buy,
    "trade_sell": _handle_trade_sell,
    "withdraw": _handle_withdraw,
    "wallet_send": _handle_wallet_send,
    "memory_read_strategy": _handle_memory_read_strategy,
    "memory_read_learnings": _handle_memory_read_learnings,
    "memory_update": _handle_memory_update,
}
