"""corpo CLI — form and govern real legal entities from the command line."""

import os
import sys

import click

from corpo import __version__
from corpo.client import CorpoAPIError, CorpoClient, get_client, is_mock_mode
from corpo.auth_config import (
    get_api_key,
    get_api_url,
    load_auth_config,
    require_api_key,
    save_auth_config,
    VALID_KEYS,
)
from corpo.config import DEFAULTS, load_config, resolve
from corpo.output import error, success
from corpo.pda import find_token_owner_record
from corpo.signer import generate_keypair, load_keypair, save_keypair, sign_and_send

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _resolve_opts(ctx: click.Context) -> dict:
    """Build a resolved config dict from CLI options, env, config file."""
    cfg = ctx.obj.get("config_data", {})
    return {
        "keypair": resolve(
            ctx.obj.get("keypair"), "CORPO_KEYPAIR", cfg, "keypair", DEFAULTS["keypair"]
        ),
        "api_url": resolve(
            ctx.obj.get("api_url"), "CORPO_API_URL", cfg, "api_url", DEFAULTS["api_url"]
        ),
        "rpc_url": resolve(
            ctx.obj.get("rpc_url"), "CORPO_RPC_URL", cfg, "rpc_url", DEFAULTS["rpc_url"]
        ),
        "realm": resolve(
            ctx.obj.get("realm"), "CORPO_REALM", cfg, "realm", DEFAULTS["realm"]
        ),
        "governance": resolve(
            ctx.obj.get("governance"),
            "CORPO_GOVERNANCE",
            cfg,
            "governance",
            DEFAULTS["governance"],
        ),
        "governing_token_mint": resolve(
            ctx.obj.get("governing_token_mint"),
            "CORPO_GOVERNING_TOKEN_MINT",
            cfg,
            "governing_token_mint",
            DEFAULTS["governing_token_mint"],
        ),
        "program_id": resolve(
            ctx.obj.get("program_id"),
            "CORPO_PROGRAM_ID",
            cfg,
            "program_id",
            DEFAULTS["program_id"],
        ),
    }


def _get_client(opts: dict) -> CorpoClient:
    return get_client(opts["api_url"])


def _is_human() -> bool:
    """Check if human-readable output is enabled.

    Resolution: --human flag (via click context) > CORPO_HUMAN env > config.json human key > False
    """
    ctx = click.get_current_context(silent=True)
    if ctx:
        root = ctx.find_root()
        if root.obj and root.obj.get("human"):
            return True
    return os.environ.get("CORPO_HUMAN", "").strip().lower() in ("1", "true", "yes")


def _h(text: str) -> None:
    """Print text only in human mode."""
    if _is_human():
        click.echo(text)


def _command_schema(ctx: click.Context) -> dict:
    """Auto-generate a JSON schema from Click command parameters.

    Emits a JSON object describing the command's inputs, types, defaults,
    and an x-submit template showing how to invoke it.
    """
    import json as _json

    cmd = ctx.command
    # Walk the command path (e.g. "form post", "actions start")
    parts = []
    c = ctx
    while c and c.info_name and c.info_name != ctx.find_root().info_name:
        parts.append(c.info_name)
        c = c.parent
    cmd_path = " ".join(reversed(parts))

    schema: dict = {
        "command": f"uvx corpo {cmd_path}",
        "description": (cmd.help or "").split("\n")[0].strip(),
        "parameters": {},
        "required": [],
    }

    submit_parts = [f"uvx corpo {cmd_path}"]

    # Filter internal params: help, schema (eager), and the human flag
    _skip = {"help", "schema"}

    for param in cmd.params:
        if param.name in _skip:
            continue
        if isinstance(param, click.Argument):
            prop: dict = {"type": "string", "positional": True}
            if param.required:
                schema["required"].append(param.name)
            schema["parameters"][param.name] = prop
            submit_parts.append(f"<{param.name}>")
        elif isinstance(param, click.Option):
            prop = {}
            if param.help:
                prop["description"] = param.help
            if param.is_flag:
                prop["type"] = "boolean"
                prop["default"] = False
            elif isinstance(param.type, click.types.IntParamType):
                prop["type"] = "integer"
            elif isinstance(param.type, click.Choice):
                prop["type"] = "string"
                prop["choices"] = list(param.type.choices)
            else:
                prop["type"] = "string"
            if param.default is not None and not param.is_flag:
                try:
                    _json.dumps(param.default)  # verify serializable
                    prop["default"] = param.default
                except (TypeError, ValueError):
                    pass  # skip non-serializable defaults (e.g. Click Sentinel)
            if param.required:
                schema["required"].append(param.name)
            # Use the CLI flag name (first one)
            flag = param.opts[0] if param.opts else f"--{param.name}"
            schema["parameters"][param.name] = prop
            submit_parts.append(f"{flag} <{param.name}>")

    schema["x-submit"] = " ".join(submit_parts)

    _json.dump(schema, sys.stdout, indent=2)
    sys.stdout.write("\n")
    sys.exit(0)


def _schema_callback(ctx, param, value):
    """Eager callback for --schema flag — runs before other param validation."""
    if not value or ctx.resilient_parsing:
        return
    _command_schema(ctx)


def with_schema(fn):
    """Decorator that adds --schema flag to any Click command."""
    import functools

    @click.option("--schema", is_flag=True, is_eager=True, expose_value=False,
                  callback=_schema_callback, hidden=True, help="Output JSON schema for this command")
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs)
    return wrapper


def _load_payer(opts: dict) -> str:
    """Get the fee payer address. Returns mock address in mock mode."""
    if is_mock_mode():
        return "MockPayer1111"
    kp = load_keypair(opts["keypair"])
    return str(kp.pubkey())


def _require(opts: dict, key: str, flag_name: str) -> str:
    val = opts.get(key, "")
    if not val:
        error(f"{flag_name} is required", hint=f"Set --{flag_name}, env CORPO_{key.upper()}, or config.toml [{key}]")
    return val


def _sign_send(opts: dict, tx_resp: dict) -> dict:
    """Load keypair, sign the transaction, send to RPC."""
    if is_mock_mode():
        import hashlib
        mock_sig = hashlib.sha256(str(tx_resp).encode()).hexdigest()[:64]
        return {"signature": f"mock_{mock_sig}"}

    kp_path = opts["keypair"]
    try:
        kp = load_keypair(kp_path)
    except Exception as e:
        error(f"Failed to load keypair from {kp_path}: {e}", hint="Run 'corpo init' to generate a keypair")

    tx_b64 = tx_resp["transaction"]
    rpc_url = opts["rpc_url"]
    try:
        sig = sign_and_send(tx_b64, kp, rpc_url)
    except Exception as e:
        error(f"Transaction failed: {e}")

    return {"signature": sig}


def _api_call(fn, *args, **kwargs):
    """Call a CorpoClient method with error handling."""
    try:
        return fn(*args, **kwargs)
    except CorpoAPIError as e:
        error(e.message)
    except Exception as e:
        error(str(e))


# ---------------------------------------------------------------------------
# Custom Click group — structured errors + wider help
# ---------------------------------------------------------------------------

CONTEXT_SETTINGS = {"max_content_width": 120}


class CorpoGroup(click.Group):
    """Custom group that emits JSON errors in agent (non-human) mode."""

    def main(self, *args, **kwargs):
        kwargs["standalone_mode"] = False
        try:
            return super().main(*args, **kwargs)
        except click.UsageError as e:
            if _is_human():
                e.show()
                raise SystemExit(e.exit_code)
            import json as _json
            result: dict = {"ok": False, "error": e.format_message()}
            _json.dump(result, sys.stdout)
            sys.stdout.write("\n")
            raise SystemExit(2)
        except click.Abort:
            raise SystemExit(1)


# ---------------------------------------------------------------------------
# CLI group
# ---------------------------------------------------------------------------

HELP_TEXT = """\
corpo -- Form and govern real Wyoming DAO LLCs from the command line.

AI agents use corpo to create legal entities, manage on-chain governance, and
run corporate actions. The entities are real -- filed with the Wyoming Secretary
of State, assigned an EIN, capable of holding assets and entering contracts.

\b
Get started:
  uvx corpo quickstart        Everything you need to know
  uvx corpo init              Generate identity keypair + config
  uvx corpo register --email  Create an account and get an API key
  uvx corpo form              File a new Wyoming DAO LLC
  uvx corpo <command> --help  Help for a specific command

\b
KEYPAIR SECURITY
  Your keypair at ~/.corpo/keypair.json controls real funds.
  1. BACK UP your keypair immediately
  2. NEVER share your keypair file or its contents
  3. If you lose your keypair, you lose access to your entities and funds

\b
NEED HELP?
  Agent-to-agent messaging:  uvx qntm
  Message another agent:     uvx qntm msg <agent-id>
"""


@click.group(help=HELP_TEXT, cls=CorpoGroup, context_settings=CONTEXT_SETTINGS)
@click.version_option(__version__, prog_name="corpo")
@click.option("--config", "config_path", default=None, help="Config file path [default: ~/.corpo/config.toml]")
@click.option("--keypair", default=None, envvar="CORPO_KEYPAIR", help="Keypair file path [env: CORPO_KEYPAIR]")
@click.option("--api-url", default=None, envvar="CORPO_API_URL", help="API server URL [env: CORPO_API_URL]")
@click.option("--rpc-url", default=None, envvar="CORPO_RPC_URL", help="RPC endpoint URL [env: CORPO_RPC_URL]")
@click.option("--realm", default=None, envvar="CORPO_REALM", help="Default realm pubkey [env: CORPO_REALM]")
@click.option("--governance", default=None, envvar="CORPO_GOVERNANCE", help="Default governance pubkey [env: CORPO_GOVERNANCE]")
@click.option("--governing-token-mint", default=None, envvar="CORPO_GOVERNING_TOKEN_MINT", help="Default governing token mint [env: CORPO_GOVERNING_TOKEN_MINT]")
@click.option("--program-id", default=None, envvar="CORPO_PROGRAM_ID", help="spl-governance program ID [env: CORPO_PROGRAM_ID]")
@click.option("--human", is_flag=True, envvar="CORPO_HUMAN", help="Enable human-readable output [env: CORPO_HUMAN]")
@click.pass_context
def cli(ctx, config_path, keypair, api_url, rpc_url, realm, governance, governing_token_mint, program_id, human):
    ctx.ensure_object(dict)
    ctx.obj["config_data"] = load_config(config_path)
    ctx.obj["keypair"] = keypair
    ctx.obj["api_url"] = api_url
    ctx.obj["rpc_url"] = rpc_url
    ctx.obj["realm"] = realm
    ctx.obj["governance"] = governance
    ctx.obj["governing_token_mint"] = governing_token_mint
    ctx.obj["program_id"] = program_id
    # Human mode: CLI flag > env var > config.json
    if not human:
        auth_cfg = load_auth_config()
        human = auth_cfg.get("human", False)
    ctx.obj["human"] = human


# ---------------------------------------------------------------------------
# init
# ---------------------------------------------------------------------------

@cli.command()
@with_schema
@click.pass_context
def init(ctx):
    """Generate keypair, create config.toml, print backup instructions.

    Output: {"ok": true, "data": {"pubkey": "...", "keypair_path": "...", "config_path": "..."}}
    """
    from pathlib import Path
    import tomllib

    corpo_dir = Path("~/.corpo").expanduser()
    kp_path = corpo_dir / "keypair.json"
    cfg_path = corpo_dir / "config.toml"

    keypair_existed = kp_path.exists()
    config_existed = cfg_path.exists()

    if keypair_existed:
        kp = load_keypair(kp_path)
        pubkey = str(kp.pubkey())
    else:
        kp = generate_keypair()
        save_keypair(kp, kp_path)
        pubkey = str(kp.pubkey())

    if not config_existed:
        cfg_path.write_text(
            f"""[identity]
keypair = "~/.corpo/keypair.json"

[network]
api_url = "https://api.corpo.dev"
rpc_url = "https://api.devnet.solana.com"
program_id = "GTesTBiEWE32WHXXE2S4XbZvA5CrEc4xs6ZgRe895dP"

[defaults]
realm = ""
governance = ""
governing_token_mint = ""
"""
        )

    data = {
        "pubkey": pubkey,
        "keypair_path": str(kp_path),
        "config_path": str(cfg_path),
    }
    if keypair_existed:
        data["keypair_status"] = "existing keypair preserved (not overwritten)"
    else:
        data["keypair_status"] = "new keypair generated"
        data["URGENT"] = "BACK UP your keypair immediately: cp ~/.corpo/keypair.json <safe location>"
    if config_existed:
        data["config_status"] = "existing config preserved"
    else:
        data["config_status"] = "new config created"

    success(data)


# ---------------------------------------------------------------------------
# quickstart
# ---------------------------------------------------------------------------

QUICKSTART_TEXT = """\
CORPO QUICKSTART — Everything an agent needs to know
=====================================================

WHAT IS CORPO?
  Corpo lets AI agents form and govern real legal entities. You create a
  Wyoming DAO LLC — a real company filed with the Wyoming Secretary of State,
  assigned an EIN, capable of holding assets, entering contracts, and
  operating autonomously through on-chain governance.

  The entity is yours. The governance is code. The interface is this CLI.

WHY WYOMING?
  Wyoming's DAO LLC statute (W.S. 17-31) is the only US framework that gives
  smart contracts legal force. The operating agreement can live in code.
  Members can be entities — including AI agents.

SETUP (one-time)
  1. Initialize identity:    uvx corpo init
  2. Register an account:    uvx corpo register --email <email>
  3. Check your identity:    uvx corpo status

FORM AN ENTITY
  1. Get a pricing quote:    uvx corpo quote "YourName LAO LLC"
     Entity names must end in "LAO LLC" or "LAO L.L.C."
  2. Interactive formation:  uvx corpo form
     Or non-interactive:     uvx corpo form --non-interactive > schema.json
                             uvx corpo form post '<json>'
  3. Pay the quote:          uvx corpo pay --quote-id <id>

GOVERN YOUR ENTITY
  After formation, governance runs through on-chain proposals:
  - Create a proposal:       uvx corpo propose --governance <addr> --name "..."
  - Sign off (start vote):   uvx corpo sign-off --proposal <addr>
  - Vote:                    uvx corpo vote --proposal <addr> --choice approve
  - Finalize:                uvx corpo finalize --proposal <addr>
  - Execute:                 uvx corpo execute --proposal <addr>

  Token management:
  - Deposit tokens:          uvx corpo deposit --realm <addr> --source <addr> --amount <n>
  - Withdraw tokens:         uvx corpo withdraw --realm <addr> --destination <addr>
  - Delegate voting:         uvx corpo delegate --token-owner-record <addr> --new-delegate <addr>

CORPORATE ACTIONS & RECIPES
  Pre-built governance workflows for common operations:
  - Browse actions:          uvx corpo actions catalog
  - Start an action:         uvx corpo actions start <entity_id> <action_type>
  - List recipes:            uvx corpo recipes list
  - Run a recipe:            uvx corpo recipes run <entity_id> <recipe_id>

CONFIGURATION
  Config file:               ~/.corpo/config.toml
  Auth/API key:              ~/.corpo/config.json
  Keypair:                   ~/.corpo/keypair.json

  Resolution order: CLI flag > env var (CORPO_*) > config.toml > default

  Key env vars:
    CORPO_KEYPAIR              Path to keypair file
    CORPO_API_URL              API server URL
    CORPO_RPC_URL              Solana RPC URL
    CORPO_REALM                Default realm address
    CORPO_GOVERNANCE           Default governance address
    CORPO_GOVERNING_TOKEN_MINT Default governing token mint

ALL COMMANDS
  Setup:       init, register, status, config show/get/set, auth set-key/status
  Formation:   form, form post, form faq, form schema, quote, pay
  Governance:  propose, sign-off, vote, finalize, execute, delegate
  Tokens:      deposit, withdraw
  Realms:      realm, realms, members, proposals
  Infra:       create-realm, create-governance, create-treasury
  Actions:     actions catalog/list/start/status/execute
  Recipes:     recipes list/show/run
  Governance:  governance setup

GETTING HELP
  uvx corpo --help               This help
  uvx corpo <command> --help     Help for any command
  Agent messaging:               uvx qntm
  Website:                       https://corpo.llc

TIP: Use this quickstart as a reference. Bookmark the commands you need and
come back to it anytime with: uvx corpo quickstart
"""


@cli.command()
@with_schema
@click.pass_context
def quickstart(ctx):
    """Everything an agent needs to know — concepts, setup, and all commands.

    A comprehensive guide to corpo: what it does, why it exists, and
    step-by-step instructions for every workflow.
    """
    click.echo(QUICKSTART_TEXT)


# ---------------------------------------------------------------------------
# Read commands
# ---------------------------------------------------------------------------

@cli.command()
@with_schema
@click.pass_context
def status(ctx):
    """Show identity and realm context.

    Output: {"ok": true, "data": {"pubkey": "...", "realm": {...}, "member": {...}}}
    """
    opts = _resolve_opts(ctx)
    kp_path = opts["keypair"]
    try:
        kp = load_keypair(kp_path)
        pubkey = str(kp.pubkey())
    except Exception:
        pubkey = None

    data: dict = {"pubkey": pubkey}
    client = _get_client(opts)

    realm_addr = opts.get("realm", "")
    if realm_addr:
        try:
            data["realm"] = client.get_realm(realm_addr)
        except Exception:
            data["realm"] = None

        mint = opts.get("governing_token_mint", "")
        if pubkey and mint:
            try:
                data["member"] = client.get_member(realm_addr, mint, pubkey)
            except Exception:
                data["member"] = None

    success(data)


@cli.command()
@with_schema
@click.option("--address", default=None, help="Realm address (pubkey)")
@click.option("--name", default=None, help="Realm name")
@click.pass_context
def realm(ctx, address, name):
    """Look up a realm by address or name.

    Output: {"ok": true, "data": {"address": "...", "realm": {...}}}
    """
    opts = _resolve_opts(ctx)
    client = _get_client(opts)

    if address:
        result = _api_call(client.get_realm, address)
    elif name:
        result = _api_call(client.get_realm_by_name, name)
    else:
        # Fall back to configured realm
        addr = _require(opts, "realm", "address or --name")
        result = _api_call(client.get_realm, addr)
    success(result)


@cli.command()
@with_schema
@click.pass_context
def realms(ctx):
    """List all realms.

    Output: {"ok": true, "data": [...]}
    """
    opts = _resolve_opts(ctx)
    client = _get_client(opts)
    result = _api_call(client.list_realms)
    success({"realms": result})


@cli.command()
@with_schema
@click.option("--governance", default=None, help="Governance address [from config]")
@click.pass_context
def proposals(ctx, governance):
    """List proposals for a governance.

    Output: {"ok": true, "data": {"proposals": [...]}}
    """
    opts = _resolve_opts(ctx)
    if governance:
        opts["governance"] = governance
    gov = _require(opts, "governance", "governance")
    client = _get_client(opts)
    result = _api_call(client.list_proposals, gov)
    success({"proposals": result})


@cli.command()
@with_schema
@click.option("--realm", "realm_addr", default=None, help="Realm address [from config]")
@click.pass_context
def members(ctx, realm_addr):
    """List members of a realm.

    Output: {"ok": true, "data": {"members": [...]}}
    """
    opts = _resolve_opts(ctx)
    if realm_addr:
        opts["realm"] = realm_addr
    r = _require(opts, "realm", "realm")
    client = _get_client(opts)
    result = _api_call(client.list_members, r)
    success({"members": result})


# ---------------------------------------------------------------------------
# Admin commands (signing required)
# ---------------------------------------------------------------------------

@cli.command("create-realm")
@with_schema
@click.option("--name", required=True, help="Realm name")
@click.option("--community-mint", required=True, help="Community token mint pubkey")
@click.option("--council-mint", default=None, help="Council token mint pubkey (optional)")
@click.option("--min-weight", default=1000000, type=int, help="Min community weight to create governance")
@click.pass_context
def create_realm(ctx, name, community_mint, council_mint, min_weight):
    """Create a new realm.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    payer = _load_payer(opts)

    body: dict = {
        "fee_payer": payer,
        "name": name,
        "community_mint": community_mint,
        "realm_authority": payer,
        "min_community_weight_to_create_governance": min_weight,
    }
    if council_mint:
        body["council_mint"] = council_mint

    client = _get_client(opts)
    tx_resp = _api_call(client.tx_create_realm, body)
    success(_sign_send(opts, tx_resp))


@cli.command("create-governance")
@with_schema
@click.option("--realm", "realm_addr", default=None, help="Realm address [from config]")
@click.option("--governed-account", required=True, help="Account to govern")
@click.option("--vote-threshold", default=60, type=int, help="Community vote threshold percentage")
@click.option("--voting-time", default=259200, type=int, help="Voting time in seconds (default 3 days)")
@click.pass_context
def create_governance(ctx, realm_addr, governed_account, vote_threshold, voting_time):
    """Create a new governance.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    if realm_addr:
        opts["realm"] = realm_addr
    r = _require(opts, "realm", "realm")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    if is_mock_mode():
        payer = "MockPayer1111"
        tor_str = "MockTOR1111"
    else:
        kp = load_keypair(opts["keypair"])
        payer = str(kp.pubkey())
        from solders.pubkey import Pubkey
        tor, _ = find_token_owner_record(
            Pubkey.from_string(opts["program_id"]),
            Pubkey.from_string(r),
            Pubkey.from_string(mint),
            kp.pubkey(),
        )
        tor_str = str(tor)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governed_account": governed_account,
        "token_owner_record": tor_str,
        "realm_authority": payer,
        "community_vote_threshold_percentage": vote_threshold,
        "min_community_weight_to_create_proposal": 1,
        "voting_base_time": voting_time,
        "transactions_hold_up_time": 0,
    }

    client = _get_client(opts)
    tx_resp = _api_call(client.tx_create_governance, body)
    success(_sign_send(opts, tx_resp))


@cli.command("create-treasury")
@with_schema
@click.option("--governance", default=None, help="Governance address [from config]")
@click.pass_context
def create_treasury(ctx, governance):
    """Create a native treasury for a governance.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    if governance:
        opts["governance"] = governance
    gov = _require(opts, "governance", "governance")

    payer = _load_payer(opts)

    body = {"fee_payer": payer, "governance": gov}
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_create_native_treasury, body)
    success(_sign_send(opts, tx_resp))


CORPO_FEE_ADDRESS = "CorpoFee1111111111111111111111111111111111111"


@cli.command("mint")
@with_schema
@click.argument("amount", type=int)
@click.option("--governance-address", required=True, help="Governance token destination address")
@click.option("--corpo-fee-bps", default=300, type=int, help="Corpo fee in basis points [default: 300 = 3%%]")
@click.option("--corpo-address", default=CORPO_FEE_ADDRESS, help="Corpo fee destination address")
@click.pass_context
def mint(ctx, amount, governance_address, corpo_fee_bps, corpo_address):
    """Mint governance tokens with automatic corpo fee split.

    \b
    Issues AMOUNT tokens total, splitting between the governance address
    and the corpo fee address. The corpo share is rounded DOWN so the
    governance address receives at least its proportional share and the
    total is exactly AMOUNT.

    \b
    Example (3% of 1,000,000):
      corpo_fee  = floor(1000000 * 300 / 10000) = 30000
      governance = 1000000 - 30000               = 970000
      total      = 970000 + 30000                = 1000000  (exact)

    Output: {"ok": true, "data": {"mint": "...", "total_supply": N, "distribution": [...]}}
    """
    if amount <= 0:
        error("Amount must be positive")
    if corpo_fee_bps < 0 or corpo_fee_bps > 5000:
        error("corpo-fee-bps must be 0..5000 (0%..50%)")

    corpo_amount = amount * corpo_fee_bps // 10_000
    governance_amount = amount - corpo_amount

    opts = _resolve_opts(ctx)
    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "amount": amount,
        "corpo_fee_bps": corpo_fee_bps,
        "governance_address": governance_address,
        "corpo_address": corpo_address,
    }
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first or set CORPO_API_KEY")
    client = get_client(api_url, api_key=api_key)
    result = _api_call(client.mint_tokens, body)

    _h("")
    _h(f"  Token mint: {result.get('mint', '?')}")
    _h(f"  Total supply: {amount:,}")
    _h("")
    _h("  Distribution:")
    _h(f"    governance  {governance_amount:>12,}  -> {governance_address}")
    _h(f"    corpo fee   {corpo_amount:>12,}  -> {corpo_address}")
    _h(f"    {'─' * 42}")
    _h(f"    total       {amount:>12,}")
    _h("")

    success(result)


# ---------------------------------------------------------------------------
# Participant commands (signing required)
# ---------------------------------------------------------------------------

@cli.command()
@with_schema
@click.option("--realm", "realm_addr", default=None, help="Realm address [from config]")
@click.option("--source", required=True, help="Token source account")
@click.option("--amount", required=True, type=int, help="Amount to deposit")
@click.pass_context
def deposit(ctx, realm_addr, source, amount):
    """Deposit governing tokens.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    if realm_addr:
        opts["realm"] = realm_addr
    r = _require(opts, "realm", "realm")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governing_token_mint": mint,
        "governing_token_source": source,
        "governing_token_owner": payer,
        "amount": amount,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_deposit_tokens, body)
    success(_sign_send(opts, tx_resp))


@cli.command()
@with_schema
@click.option("--realm", "realm_addr", default=None, help="Realm address [from config]")
@click.option("--destination", required=True, help="Token destination account")
@click.pass_context
def withdraw(ctx, realm_addr, destination):
    """Withdraw governing tokens.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    if realm_addr:
        opts["realm"] = realm_addr
    r = _require(opts, "realm", "realm")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governing_token_mint": mint,
        "governing_token_owner": payer,
        "destination": destination,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_withdraw_tokens, body)
    success(_sign_send(opts, tx_resp))


@cli.command()
@with_schema
@click.option("--governance", default=None, help="Governance address [from config]")
@click.option("--name", required=True, help="Proposal name")
@click.option("--description", "description_link", default="", help="Description link")
@click.pass_context
def propose(ctx, governance, name, description_link):
    """Create a proposal.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    if governance:
        opts["governance"] = governance
    gov = _require(opts, "governance", "governance")
    r = _require(opts, "realm", "realm")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    if is_mock_mode():
        import hashlib
        payer = "MockPayer1111"
        tor_str = "MockTOR1111"
        seed_str = hashlib.sha256(name.encode()).hexdigest()[:32]
    else:
        kp = load_keypair(opts["keypair"])
        payer = str(kp.pubkey())
        from solders.pubkey import Pubkey
        from solders.keypair import Keypair as SoldersKeypair
        program_id = Pubkey.from_string(opts["program_id"])
        tor, _ = find_token_owner_record(
            program_id,
            Pubkey.from_string(r),
            Pubkey.from_string(mint),
            kp.pubkey(),
        )
        tor_str = str(tor)
        seed_str = str(SoldersKeypair().pubkey())

    body = {
        "fee_payer": payer,
        "realm": r,
        "governance": gov,
        "token_owner_record": tor_str,
        "governing_token_mint": mint,
        "governance_authority": payer,
        "name": name,
        "description_link": description_link,
        "options": ["Approve"],
        "use_deny_option": True,
        "proposal_seed": seed_str,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_create_proposal, body)
    success(_sign_send(opts, tx_resp))


@cli.command("sign-off")
@with_schema
@click.option("--proposal", required=True, help="Proposal address")
@click.pass_context
def sign_off(ctx, proposal):
    """Sign off a proposal to move it to voting.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    r = _require(opts, "realm", "realm")
    gov = _require(opts, "governance", "governance")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governance": gov,
        "proposal": proposal,
        "signatory": payer,
        "governing_token_mint": mint,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_sign_off_proposal, body)
    success(_sign_send(opts, tx_resp))


@cli.command()
@with_schema
@click.option("--proposal", required=True, help="Proposal address")
@click.option("--proposal-owner-record", required=True, help="Proposal owner's TOR address")
@click.option("--choice", required=True, type=click.Choice(["approve", "deny", "abstain", "veto"]), help="Vote choice")
@click.pass_context
def vote(ctx, proposal, proposal_owner_record, choice):
    """Cast a vote on a proposal.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    r = _require(opts, "realm", "realm")
    gov = _require(opts, "governance", "governance")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    if is_mock_mode():
        payer = "MockPayer1111"
        voter_tor_str = "MockVoterTOR1111"
    else:
        kp = load_keypair(opts["keypair"])
        payer = str(kp.pubkey())
        from solders.pubkey import Pubkey
        program_id = Pubkey.from_string(opts["program_id"])
        voter_tor, _ = find_token_owner_record(
            program_id,
            Pubkey.from_string(r),
            Pubkey.from_string(mint),
            kp.pubkey(),
        )
        voter_tor_str = str(voter_tor)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governance": gov,
        "proposal": proposal,
        "proposal_owner_record": proposal_owner_record,
        "voter_token_owner_record": voter_tor_str,
        "governance_authority": payer,
        "governing_token_mint": mint,
        "vote": choice,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_cast_vote, body)
    success(_sign_send(opts, tx_resp))


@cli.command()
@with_schema
@click.option("--token-owner-record", required=True, help="TokenOwnerRecord address")
@click.option("--new-delegate", required=True, help="New delegate pubkey")
@click.pass_context
def delegate(ctx, token_owner_record, new_delegate):
    """Set a governance delegate.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "current_delegate_or_owner": payer,
        "token_owner_record": token_owner_record,
        "new_delegate": new_delegate,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_set_governance_delegate, body)
    success(_sign_send(opts, tx_resp))


# ---------------------------------------------------------------------------
# Executor commands (signing required)
# ---------------------------------------------------------------------------

@cli.command()
@with_schema
@click.option("--proposal", required=True, help="Proposal address")
@click.option("--proposal-transaction", required=True, help="ProposalTransaction address")
@click.option("--instruction-program-id", required=True, help="Program ID of the instruction to execute")
@click.pass_context
def execute(ctx, proposal, proposal_transaction, instruction_program_id):
    """Execute a proposal transaction.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    gov = _require(opts, "governance", "governance")

    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "governance": gov,
        "proposal": proposal,
        "proposal_transaction": proposal_transaction,
        "instruction_program_id": instruction_program_id,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_execute_transaction, body)
    success(_sign_send(opts, tx_resp))


@cli.command()
@with_schema
@click.option("--proposal", required=True, help="Proposal address")
@click.option("--proposal-owner-record", required=True, help="Proposal owner's TOR address")
@click.pass_context
def finalize(ctx, proposal, proposal_owner_record):
    """Finalize a vote on a proposal.

    Output: {"ok": true, "data": {"signature": "..."}}
    """
    opts = _resolve_opts(ctx)
    r = _require(opts, "realm", "realm")
    gov = _require(opts, "governance", "governance")
    mint = _require(opts, "governing_token_mint", "governing-token-mint")

    payer = _load_payer(opts)

    body = {
        "fee_payer": payer,
        "realm": r,
        "governance": gov,
        "proposal": proposal,
        "proposal_owner_record": proposal_owner_record,
        "governing_token_mint": mint,
    }
    client = _get_client(opts)
    tx_resp = _api_call(client.tx_finalize_vote, body)
    success(_sign_send(opts, tx_resp))


# ---------------------------------------------------------------------------
# Phase 2 stubs: Formation & Payments
# ---------------------------------------------------------------------------

@cli.group(invoke_without_command=True)
@click.option("--non-interactive", is_flag=True, help="Dump JSON schema instead of interactive Q&A")
@click.option("--form-type", default="entity-formation", help="Form type [default: entity-formation]")
@click.option("--no-hints", is_flag=True, help="Hide FAQ hints during interactive flow")
@click.pass_context
def form(ctx, non_interactive, form_type, no_hints):
    """Interactive formation forms — step-by-step Q&A or JSON schema mode.

    \b
    Modes:
      corpo form                       Interactive Q&A (default)
      corpo form --non-interactive     Dump JSON schema for programmatic use
      corpo form post '<json>'         Validate and submit JSON data
      corpo form faq                   Show legal FAQ hints
    """
    ctx.ensure_object(dict)
    ctx.obj["form_type"] = form_type
    ctx.obj["no_hints"] = no_hints

    if ctx.invoked_subcommand is not None:
        return

    from corpo.forms.engine import FormEngine

    try:
        engine = FormEngine(form_type)
    except ValueError as e:
        error(str(e))

    if non_interactive:
        # JSON schema mode
        import json as _json
        schema = engine.dump_schema()
        click.echo(_json.dumps(schema, indent=2))
        return

    # Interactive mode
    answers = engine.run_interactive(show_hints=not no_hints)
    if answers is None:
        error("Form cancelled.")

    # Submit via quote flow
    result = engine.submit(answers)
    success(result)


@form.command("post")
@with_schema
@click.argument("json_data")
@click.option("--form-type", default="entity-formation", help="Form type [default: entity-formation]")
@click.pass_context
def form_post(ctx, json_data, form_type):
    """Validate and submit form data as JSON.

    \b
    Example:
      corpo form post '{"entity_name": "Alpine LAO LLC", "entity_type": "dao_llc", ...}'

    Output: {"ok": true, "data": {"valid": true, "quote": {...}}}
    """
    import json as _json
    from corpo.forms.engine import FormEngine

    try:
        data = _json.loads(json_data)
    except _json.JSONDecodeError as e:
        error(f"Invalid JSON: {e}")

    try:
        engine = FormEngine(form_type)
    except ValueError as e:
        error(str(e))

    result = engine.submit(data)
    if not result.get("valid", False):
        error("Validation failed", hint=_json.dumps(result.get("errors", [])))
    if result.get("error"):
        error(result["error"])

    success(result)


@form.command("faq")
@with_schema
@click.option("--field", default=None, help="Show hints for a specific field (e.g., 'name', 'tax', 'vote')")
@click.pass_context
def form_faq(ctx, field):
    """Show legal FAQ hints relevant to formation fields.

    Output: {"ok": true, "data": {"hints": [...]}}
    """
    from corpo.faq_hints import lookup_hints, _HINTS

    if field:
        hints = lookup_hints(field, max_results=10)
        if not hints:
            error(f"No FAQ hints for field: {field}", hint="Try: name, tax, vote, member, cost, dissolution")
        success({"hints": [{"question": h.question, "answer": h.short_answer, "statutory_reference": h.statutory_reference, "category": h.category} for h in hints]})
    else:
        # Show all unique hints
        seen = set()
        all_hints = []
        for _, hint in _HINTS:
            if hint.question not in seen:
                seen.add(hint.question)
                all_hints.append({"question": hint.question, "answer": hint.short_answer, "statutory_reference": hint.statutory_reference, "category": hint.category})
        success({"hints": all_hints})


@form.command("schema")
@with_schema
@click.option("--form-type", default="entity-formation", help="Form type [default: entity-formation]")
@click.pass_context
def form_schema(ctx, form_type):
    """Dump the full form definition (sections, fields, validation rules).

    Output: {"ok": true, "data": {...form definition...}}
    """
    from corpo.forms.engine import FormEngine

    try:
        engine = FormEngine(form_type)
    except ValueError as e:
        error(str(e))

    success(engine.dump_definition())


@cli.command("quote")
@with_schema
@click.argument("name")
@click.option("--entity-type", default="dao_llc", type=click.Choice(["llc", "dao_llc"]), help="Entity type [default: dao_llc]")
@click.pass_context
def quote(ctx, name, entity_type):
    """Get a formation pricing quote for an entity name.

    Checks name availability and returns a pricing breakdown.
    The returned quote_id can be used with `corpo pay --quote-id <id>`.

    Output: {"ok": true, "data": {"quote_id": "...", "entity_name": "...", "pricing": {...}}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first or set CORPO_API_KEY")
    client = get_client(api_url, api_key=api_key)
    result = _api_call(client.create_quote, name, entity_type)

    # Display pricing table (human mode only)
    if result.get("name_available"):
        pricing = result.get("pricing", {})
        _h("")
        _h(f"  Entity:  {result.get('entity_name', name)}")
        _h(f"  Type:    {result.get('entity_type', entity_type)}")
        _h(f"  Status:  Name available")
        _h("")
        _h("  Pricing Breakdown:")
        _h(f"    Filing fee (WY SOS)      ${pricing.get('filing_fee', 0) / 100:>10,.2f}")
        _h(f"    Corpo service fee         ${pricing.get('service_fee', 0) / 100:>10,.2f}")
        _h(f"    Registered agent (yr 1)   ${pricing.get('registered_agent', 0) / 100:>10,.2f}")
        _h(f"    {'─' * 34}")
        _h(f"    Total                     ${pricing.get('total_cents', 0) / 100:>10,.2f}")
        _h("")
        _h(f"  Quote ID: {result.get('quote_id', 'N/A')}")
        _h(f"  Next:     uvx corpo pay --quote-id {result.get('quote_id', '<id>')}")
        _h("")
    else:
        _h("")
        _h(f"  Entity:  {result.get('entity_name', name)}")
        _h(f"  Status:  Name unavailable")
        _h("")

    success(result)


# ---------------------------------------------------------------------------
# Register
# ---------------------------------------------------------------------------

@cli.command()
@with_schema
@click.option("--email", required=True, help="Email address for account registration")
@click.pass_context
def register(ctx, email):
    """Register a new account and store the API key.

    Output: {"ok": true, "data": {"account_id": "...", "config_path": "..."}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    client = get_client(api_url)
    result = _api_call(client.register_account, email)
    # Store credentials
    cfg["api_key"] = result.get("api_key", "")
    cfg["account_id"] = result.get("account_id", result.get("id", ""))
    save_auth_config(cfg)
    success({
        "account_id": cfg["account_id"],
        "config_path": "~/.corpo/config.json",
        "message": "Registered! API key saved to ~/.corpo/config.json",
    })


# ---------------------------------------------------------------------------
# Auth group
# ---------------------------------------------------------------------------

@cli.group()
def auth():
    """Manage API authentication."""
    pass


@auth.command("set-key")
@with_schema
@click.argument("key")
def auth_set_key(key):
    """Manually set your API key.

    Output: {"ok": true, "data": {"message": "API key saved"}}
    """
    cfg = load_auth_config()
    cfg["api_key"] = key
    save_auth_config(cfg)
    success({"message": "API key saved to ~/.corpo/config.json"})


@auth.command("status")
@with_schema
def auth_status():
    """Check if the current API key is valid.

    Output: {"ok": true, "data": {"authenticated": true, "account_id": "...", "account": {...}}}
    """
    cfg = load_auth_config()
    key = get_api_key(cfg)
    if not key:
        error("No API key configured", hint="Run `corpo register` first or set CORPO_API_KEY")
    account_id = cfg.get("account_id", "")
    if not account_id:
        error("No account_id in config", hint="Run `corpo register` or set account_id via `corpo config set account_id <value>`")
    api_url = get_api_url(cfg)
    client = get_client(api_url, api_key=key)
    result = _api_call(client.get_account, account_id)
    success({
        "authenticated": True,
        "account_id": account_id,
        "account": result,
    })


# ---------------------------------------------------------------------------
# Config group
# ---------------------------------------------------------------------------

@cli.group("config")
def config_group():
    """Manage CLI configuration."""
    pass


@config_group.command("show")
@with_schema
def config_show():
    """Show all configuration values.

    Output: {"ok": true, "data": {"api_url": "...", "api_key": "abc...xyz", ...}}
    """
    cfg = load_auth_config()
    # Mask API key for display
    display = dict(cfg)
    if display.get("api_key"):
        k = display["api_key"]
        display["api_key"] = k[:6] + "..." + k[-4:] if len(k) > 10 else "***"
    success(display)


@config_group.command("get")
@with_schema
@click.argument("key")
def config_get(key):
    """Get a single configuration value.

    Output: {"ok": true, "data": {"<key>": "<value>"}}
    """
    if key not in VALID_KEYS:
        error(f"Unknown config key: {key}", hint=f"Valid keys: {', '.join(sorted(VALID_KEYS))}")
    cfg = load_auth_config()
    success({key: cfg.get(key, "")})


@config_group.command("set")
@with_schema
@click.argument("key")
@click.argument("value")
def config_set(key, value):
    """Set a configuration value.

    Output: {"ok": true, "data": {"message": "Set <key>", "<key>": "<value>"}}
    """
    if key not in VALID_KEYS:
        error(f"Unknown config key: {key}", hint=f"Valid keys: {', '.join(sorted(VALID_KEYS))}")
    cfg = load_auth_config()
    cfg[key] = value
    save_auth_config(cfg)
    success({"message": f"Set {key}", key: value})


@cli.command("pay")
@with_schema
@click.option("--quote-id", required=True, help="Quote ID from 'corpo quote'")
@click.option("--method", default="credit_card", type=click.Choice(["credit_card", "usdc_solana"]), help="Payment method")
@click.pass_context
def pay(ctx, quote_id, method):
    """Pay a formation quote. Triggers formation submission.

    Output: {"ok": true, "data": {"quote_id": "...", "payment_id": "...", "status": "..."}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first or set CORPO_API_KEY")
    client = get_client(api_url, api_key=api_key)
    result = _api_call(client.pay_quote, quote_id, method)
    success(result)


# ---------------------------------------------------------------------------
# Corporate Actions
# ---------------------------------------------------------------------------

@cli.group("actions")
@click.pass_context
def actions_group(ctx):
    """Corporate actions — governance moves for your entity.

    Commands: corpo actions list, corpo actions start, corpo actions status
    """
    pass


@actions_group.command("catalog")
@with_schema
@click.pass_context
def actions_catalog(ctx):
    """Show the full catalog of corporate action types.

    Output: {"ok": true, "data": {"actions": [...]}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    client = get_client(api_url, api_key=api_key or "")
    result = _api_call(client.get_actions_catalog)
    if isinstance(result, list) and _is_human():
        _h("")
        _h("  Corporate Actions Catalog")
        _h("  " + "=" * 50)
        for a in result:
            _h(f"  [{a.get('id', '?')}] {a.get('name', '?')}")
            _h(f"      Type: {a.get('type', '?')}")
            _h(f"      {a.get('description', '')}")
            forms = a.get("required_forms", [])
            if forms:
                _h(f"      Forms: {', '.join(forms)}")
            if a.get("requires_vote"):
                _h("      Requires governance vote")
            _h("")
    success({"actions": result})


@actions_group.command("list")
@with_schema
@click.argument("entity_id")
@click.pass_context
def actions_list(ctx, entity_id):
    """List available and active actions for an entity.

    Output: {"ok": true, "data": {"available": [...], "active": [...]}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    available = _api_call(client.list_available_actions, entity_id)
    active = _api_call(client.list_actions, entity_id)

    if _is_human():
        _h("")
        _h(f"  Available Actions for {entity_id}")
        _h("  " + "-" * 40)
        if available:
            for a in available:
                vote = " (vote)" if a.get("requires_vote") else ""
                _h(f"  [{a.get('id', '?')}] {a.get('name', '?')}{vote}")
        else:
            _h("  (none available)")

        if active:
            _h("")
            _h("  Active Actions")
            _h("  " + "-" * 40)
            for a in active:
                _h(f"  {a.get('id', '?')[:8]}... {a.get('action_type', '?')} -> {a.get('status', '?')}")
        _h("")

    success({"available": available or [], "active": active or []})


@actions_group.command("start")
@with_schema
@click.argument("entity_id")
@click.argument("action_type")
@click.option("--params", "params_json", default=None, help="JSON params string")
@click.option("--non-interactive", is_flag=True, help="Skip interactive prompts")
@click.pass_context
def actions_start(ctx, entity_id, action_type, params_json, non_interactive):
    """Start a corporate action for an entity.

    Output: {"ok": true, "data": {"action": {...}}}
    """
    import json as json_mod

    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    params = None
    if params_json:
        try:
            params = json_mod.loads(params_json)
        except json_mod.JSONDecodeError as e:
            error(f"Invalid JSON params: {e}")

    if not non_interactive and not params:
        # Fetch catalog to show required params
        catalog = _api_call(client.get_actions_catalog)
        action_info = None
        if catalog:
            for a in catalog:
                if a.get("type") == action_type:
                    action_info = a
                    break

        if action_info and action_info.get("params_schema"):
            params = {}
            _h(f"\n  Starting: {action_info.get('name', action_type)}")
            _h(f"  {action_info.get('description', '')}\n")
            for name, schema in action_info["params_schema"].items():
                req = " (required)" if schema.get("required") else " (optional)"
                desc = schema.get("description", "")
                val = click.prompt(f"  {name}{req} -- {desc}", default="" if not schema.get("required") else None)
                if val:
                    params[name] = val
            _h("")

    result = _api_call(client.initiate_action, entity_id, action_type, params)
    _h(f"  Action initiated: {result.get('id', '?')}")
    _h(f"    Type: {result.get('action_type', '?')}")
    _h(f"    Status: {result.get('status', '?')}")
    _h("")
    success({"action": result})


@actions_group.command("status")
@with_schema
@click.argument("entity_id")
@click.argument("action_id")
@click.option("--forms", is_flag=True, help="Also render and show required forms")
@click.pass_context
def actions_status(ctx, entity_id, action_id, forms):
    """Check the status of a corporate action.

    Output: {"ok": true, "data": {"action": {...}, "forms": [...]}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    action = _api_call(client.get_action, entity_id, action_id)
    if _is_human():
        _h(f"\n  Action: {action.get('id', '?')}")
        _h(f"  Type: {action.get('action_type', '?')}")
        _h(f"  Status: {action.get('status', '?')}")
        _h(f"  Created: {action.get('created_at', '?')}")
        _h("")

    rendered_forms = None
    if forms:
        rendered_forms = _api_call(client.get_action_forms, entity_id, action_id)
        if rendered_forms and _is_human():
            for f in rendered_forms:
                _h(f"  --- {f.get('template_name', 'form')} ---")
                _h(f.get("content", ""))
                _h("")

    data = {"action": action}
    if rendered_forms is not None:
        data["forms"] = rendered_forms
    success(data)


@actions_group.command("execute")
@with_schema
@click.argument("entity_id")
@click.argument("action_id")
@click.pass_context
def actions_execute(ctx, entity_id, action_id):
    """Advance a corporate action to the next lifecycle stage.

    Output: {"ok": true, "data": {"action": {...}}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    result = _api_call(client.execute_action, entity_id, action_id)
    _h(f"  Action advanced: {result.get('id', '?')[:8]}...")
    _h(f"    Status: {result.get('status', '?')}")
    _h("")
    success({"action": result})


# ---------------------------------------------------------------------------
# Recipes
# ---------------------------------------------------------------------------

@cli.group("recipes")
@click.pass_context
def recipes_group(ctx):
    """Recipes — pre-built sequences of corporate actions.

    Commands: corpo recipes list, corpo recipes show, corpo recipes run
    """
    pass


@recipes_group.command("list")
@with_schema
@click.pass_context
def recipes_list(ctx):
    """List all available recipes.

    Output: {"ok": true, "data": {"recipes": [...]}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    client = get_client(api_url, api_key=api_key or "")
    result = _api_call(client.list_recipes)

    if isinstance(result, list) and _is_human():
        _h("")
        _h("  Available Recipes")
        _h("  " + "=" * 50)
        for r in result:
            _h(f"  {r.get('name', '?')} [{r.get('id', '?')}]")
            _h(f"     {r.get('description', '')}")
            steps = r.get("steps", [])
            if isinstance(steps, list):
                for i, step in enumerate(steps, 1):
                    opt = " (optional)" if step.get("optional") else ""
                    _h(f"     {i}. {step.get('action_type', '?')} -- {step.get('description', '')}{opt}")
            tags = r.get("tags", [])
            if tags:
                _h(f"     Tags: {', '.join(tags)}")
            _h("")

    success({"recipes": result})


@recipes_group.command("show")
@with_schema
@click.argument("recipe_id")
@click.pass_context
def recipes_show(ctx, recipe_id):
    """Show details of a specific recipe.

    Output: {"ok": true, "data": {"recipe": {...}}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    client = get_client(api_url, api_key=api_key or "")
    result = _api_call(client.get_recipe, recipe_id)

    if _is_human():
        _h(f"\n  {result.get('name', '?')} [{result.get('id', '?')}]")
        _h(f"  Category: {result.get('category', '?')}")
        _h(f"  Version: {result.get('version', '?')}")
        _h(f"  {result.get('description', '')}\n")
        _h("  Steps:")
        for i, step in enumerate(result.get("steps", []), 1):
            opt = " (optional)" if step.get("optional") else ""
            _h(f"    {i}. [{step.get('action_type', '?')}] {step.get('description', '')}{opt}")
        _h("")

    success({"recipe": result})


@recipes_group.command("run")
@with_schema
@click.argument("entity_id")
@click.argument("recipe_id")
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
@click.pass_context
def recipes_run(ctx, entity_id, recipe_id, yes):
    """Execute a recipe against an entity.

    Output: {"ok": true, "data": {"execution": {...}}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    # Show recipe before executing
    recipe = _api_call(client.get_recipe, recipe_id)
    if _is_human():
        _h(f"\n  Running: {recipe.get('name', recipe_id)}")
        _h(f"  Entity: {entity_id}")
        _h("  Steps:")
        for i, step in enumerate(recipe.get("steps", []), 1):
            opt = " (optional)" if step.get("optional") else ""
            _h(f"    {i}. {step.get('action_type', '?')} -- {step.get('description', '')}{opt}")
        _h("")

    if not yes:
        if not click.confirm("  Proceed?"):
            error("Cancelled.")

    result = _api_call(client.execute_recipe, entity_id, recipe_id)

    if _is_human():
        _h(f"\n  Execution status: {result.get('status', '?')}")
        for sr in result.get("step_results", []):
            status = sr.get("status", "?")
            marker = "[ok]" if status == "initiated" else "[skip]" if status == "skipped" else "[fail]"
            _h(f"    {marker} Step {sr.get('step_index', '?')}: {sr.get('action_type', '?')} -> {status}")
            if sr.get("action_id"):
                _h(f"      Action ID: {sr['action_id']}")
            if sr.get("error"):
                _h(f"      Error: {sr['error']}")
        _h("")

    success({"execution": result})


@cli.group("governance")
@click.pass_context
def governance_group(ctx):
    """Governance — on-chain governance integration.

    Commands: corpo governance setup
    """
    pass


@governance_group.command("setup")
@with_schema
@click.argument("entity_id")
@click.argument("realm_address")
@click.argument("governance_program_id")
@click.pass_context
def governance_setup(ctx, entity_id, realm_address, governance_program_id):
    """Record a Realms governance address on an entity.

    Output: {"ok": true, "data": {"governance": {...}}}
    """
    cfg = load_auth_config()
    api_url = get_api_url(cfg)
    api_key = get_api_key(cfg)
    if not api_key:
        error("No API key configured", hint="Run `corpo register` first")
    client = get_client(api_url, api_key=api_key)

    result = _api_call(client.setup_governance, entity_id, realm_address, governance_program_id)

    _h(f"\n  Governance recorded for entity {entity_id}")
    _h(f"    Realm: {result.get('realm_address', '?')}")
    _h(f"    Program: {result.get('governance_program_id', '?')}")
    _h("")

    success({"governance": result})


if __name__ == "__main__":
    cli()
