#!/usr/bin/env node
/**
 * ClawRouter CLI
 *
 * Standalone proxy for deployed setups where the proxy needs to survive gateway restarts.
 *
 * Usage:
 *   npx @blockrun/clawrouter              # Start standalone proxy
 *   npx @blockrun/clawrouter --version    # Show version
 *   npx @blockrun/clawrouter --port 8402  # Custom port
 *
 * For production deployments, use with PM2:
 *   pm2 start "npx @blockrun/clawrouter" --name clawrouter
 */

import { startProxy, getProxyPort } from "./proxy.js";
import { VERSION } from "./version.js";
import {
  resolveOrGenerateWalletKey,
  resolvePaymentChain,
  recoverWalletFromMnemonic,
  savePaymentChain,
} from "./auth.js";
import { getSolanaAddress } from "./wallet.js";
import { generateReport } from "./report.js";
import { formatRecentLogs } from "./stats.js";
import { runDoctor } from "./doctor.js";
import { PARTNER_SERVICES } from "./partners/index.js";

function printHelp(): void {
  console.log(`
ClawRouter v${VERSION} - Smart LLM Router

Usage:
  clawrouter [options]
  clawrouter status                    # Live proxy status (wallet, balance, chain)
  clawrouter wallet                    # Wallet details + balance
  clawrouter models                    # List available models
  clawrouter stats [--days <n>]        # Usage stats (default: 7 days)
  clawrouter doctor [opus] [question]
  clawrouter partners [test]
  clawrouter report [daily|weekly|monthly] [--json]
  clawrouter logs [--days <n>]
  clawrouter setup                     # Finish OpenClaw integration after npm install -g
  clawrouter share last [--as=<im>]    # Convert most recent response for IM (default: feishu)
  clawrouter share list [--limit=<n>]  # Recent response ids + summaries
  clawrouter share <id> [--as=<im>]    # Convert a specific response by id
  clawrouter share last --all          # Write all 6 IM variants to /tmp/
                                       # IM presets: feishu, slack, discord, telegram, whatsapp, plain
  clawrouter phone numbers list                  # List wallet's active phone numbers + expiry
  clawrouter phone numbers buy <US|CA> [--area-code <code>]   # Buy 30-day number lease ($5)
  clawrouter phone numbers renew <+E.164>        # Extend lease 30 days ($5)
  clawrouter phone numbers release <+E.164>      # Release a wallet-owned number (free)
  clawrouter phone lookup <+E.164>               # Carrier + line type ($0.01)
  clawrouter phone fraud <+E.164>                # SIM-swap + forwarding fraud signals ($0.05)

Options:
  --version, -v     Show version number
  --help, -h        Show this help message
  --port <number>   Port to listen on (default: ${getProxyPort()})

Query Commands (talk to running proxy on localhost:${getProxyPort()}):
  status            Proxy status: wallet, balance, payment chain
  wallet            Wallet addresses + USDC balance
  models            List all available models with pricing
  stats             Usage breakdown: requests, cost, top models
  stats --days 14   Custom time range (max 30 days)
  cache             Response cache stats (hit rate, size)

Management Commands:
  setup             Finish OpenClaw integration (run after npm install -g).
                    Registers ClawRouter as a plugin, syncs the model allowlist,
                    and writes the auth profile. Idempotent — safe to re-run.
  doctor            AI-powered diagnostics (default: Sonnet ~$0.003)
  doctor opus       Use Opus for deeper analysis (~$0.01)
  logs              Per-request breakdown: model, cost, latency, status
  logs --days 7     Show last 7 days of requests (default: 1 day)
  partners          List available partner APIs with pricing
  partners test     Test partner API endpoints (expect 402 = alive)
  wallet recover    Restore wallet.key from mnemonic (if generated by ClawRouter)
  chain solana      Switch to Solana (persists)
  chain base        Switch to Base EVM (persists)

Environment Variables:
  BLOCKRUN_WALLET_KEY     Private key for x402 payments (auto-generated if not set)
  BLOCKRUN_PROXY_PORT     Default proxy port (default: 8402)

For more info: https://blockrun.ai/clawrouter.md
`);
}

/** Query the running proxy HTTP API */
async function queryProxy(path: string, port: number): Promise<unknown> {
  const res = await fetch(`http://127.0.0.1:${port}${path}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  return res.json();
}

/**
 * Pipe text into the system clipboard via the platform-native command.
 * Returns null on success, or a short message describing why it failed.
 * Never throws — clipboard is best-effort.
 */
async function copyToClipboard(text: string): Promise<string | null> {
  const { spawn } = await import("node:child_process");
  const platform = process.platform;
  const candidates: Array<[string, string[]]> =
    platform === "darwin"
      ? [["pbcopy", []]]
      : platform === "win32"
        ? [["clip", []]]
        : [
            ["wl-copy", []],
            ["xclip", ["-selection", "clipboard"]],
            ["xsel", ["--clipboard", "--input"]],
          ];
  for (const [cmd, args] of candidates) {
    try {
      const child = spawn(cmd, args, { stdio: ["pipe", "ignore", "ignore"] });
      const ok = await new Promise<boolean>((resolve) => {
        let settled = false;
        child.on("error", () => {
          if (!settled) {
            settled = true;
            resolve(false);
          }
        });
        child.on("close", (code) => {
          if (!settled) {
            settled = true;
            resolve(code === 0);
          }
        });
        child.stdin.end(text, "utf8");
      });
      if (ok) return null;
    } catch {
      // try next candidate
    }
  }
  return platform === "darwin"
    ? "pbcopy not available"
    : platform === "win32"
      ? "clip.exe not available"
      : "no clipboard tool found (install xclip, xsel, or wl-clipboard)";
}

interface ShareEntry {
  id: string;
  timestamp: string;
  model?: string;
  sessionId?: string;
  requestSummary: string;
  responseLength?: number;
  responseText?: string;
}

interface ShareRendered {
  id: string;
  timestamp: string;
  model?: string;
  preset: string;
  rendered: string;
}

const VALID_PRESETS = ["feishu", "slack", "discord", "telegram", "whatsapp", "plain"];

async function cmdShare(
  port: number,
  target: string | undefined,
  preset: string | undefined,
  limit: number,
  all: boolean,
): Promise<void> {
  // Default target = "last" if user just typed `clawrouter share`.
  const tgt = target ?? "last";

  if (tgt === "list") {
    try {
      const entries = (await queryProxy(`/share/list?limit=${limit}`, port)) as ShareEntry[];
      if (entries.length === 0) {
        console.log("\nNo responses persisted yet.");
        console.log(
          "  Send a request through ClawRouter and try again, or check BLOCKRUN_RESPONSE_STORE.\n",
        );
        return;
      }
      console.log(`\nRecent responses (${entries.length}):\n`);
      for (const e of entries) {
        const t = new Date(e.timestamp).toLocaleString();
        const summary = e.requestSummary || "(no prompt)";
        const len = e.responseLength ? ` ${e.responseLength}b` : "";
        console.log(`  ${e.id}  ${t}  [${e.model ?? "?"}${len}]`);
        console.log(`    ${summary}`);
      }
      console.log(
        `\nUse: clawrouter share <id> --as=<feishu|slack|discord|telegram|whatsapp|plain>\n`,
      );
    } catch (err) {
      console.error(`✗ Cannot fetch share list: ${err instanceof Error ? err.message : err}`);
      console.error(`  Is the proxy running on port ${port}?`);
      process.exit(1);
    }
    return;
  }

  // Render path: target is "last" or a specific id.
  const idOrLast = tgt === "last" ? "last" : tgt;

  // --all: render every preset to /tmp/, print the paths.
  if (all) {
    try {
      const { writeFile } = await import("node:fs/promises");
      const { tmpdir } = await import("node:os");
      const { join: pjoin } = await import("node:path");
      const paths: string[] = [];
      for (const p of VALID_PRESETS) {
        const path =
          idOrLast === "last" ? `/share/last?as=${p}` : `/share/${idOrLast}/render?as=${p}`;
        const result = (await queryProxy(path, port)) as ShareRendered;
        const file = pjoin(tmpdir(), `claw-share-${result.id}-${p}.txt`);
        await writeFile(file, result.rendered, "utf8");
        paths.push(file);
      }
      console.log("\n✓ Wrote all 6 preset variants:");
      for (const p of paths) console.log(`  ${p}`);
      console.log();
    } catch (err) {
      console.error(`✗ Failed: ${err instanceof Error ? err.message : err}`);
      process.exit(1);
    }
    return;
  }

  const chosenPreset = preset ?? process.env.BLOCKRUN_DEFAULT_SHARE_PRESET ?? "feishu";
  if (!VALID_PRESETS.includes(chosenPreset)) {
    console.error(`✗ Unknown preset: ${chosenPreset}`);
    console.error(`  Valid: ${VALID_PRESETS.join(", ")}`);
    process.exit(1);
  }

  try {
    const path =
      idOrLast === "last"
        ? `/share/last?as=${chosenPreset}`
        : `/share/${idOrLast}/render?as=${chosenPreset}`;
    const result = (await queryProxy(path, port)) as ShareRendered;
    process.stdout.write(result.rendered);
    if (!result.rendered.endsWith("\n")) process.stdout.write("\n");
    const clipErr = await copyToClipboard(result.rendered);
    const sizeKb = (result.rendered.length / 1024).toFixed(1);
    if (clipErr) {
      console.error(`\n(clipboard skipped: ${clipErr})`);
    } else {
      console.error(`\n✓ Copied to clipboard (preset=${chosenPreset}, ${sizeKb}KB)`);
    }
  } catch (err) {
    console.error(`✗ Cannot fetch share: ${err instanceof Error ? err.message : err}`);
    console.error(`  Is the proxy running on port ${port}?`);
    process.exit(1);
  }
}

async function cmdStatus(port: number): Promise<void> {
  try {
    const data = (await queryProxy("/health?full=true", port)) as Record<string, unknown>;
    console.log(`\nClawRouter Status (port ${port})\n`);
    console.log(`  Status:        ${data.status}`);
    console.log(`  Payment Chain: ${data.paymentChain}`);
    console.log(`  EVM Wallet:    ${data.wallet}`);
    if (data.solana) console.log(`  Solana Wallet: ${data.solana}`);
    console.log(`  Balance:       ${data.balance ?? "unknown"}`);
    if (data.isLow) console.log(`  ⚠ Balance is low`);
    if (data.isEmpty) console.log(`  ⚠ Balance is empty — using free models`);
    if (data.upstreamProxy) console.log(`  Upstream Proxy: ${data.upstreamProxy}`);
    console.log();
  } catch {
    console.error(`✗ Cannot connect to ClawRouter on port ${port}`);
    console.error(`  Is the proxy running? Start with: npx @blockrun/clawrouter`);
    process.exit(1);
  }
}

async function cmdWallet(port: number): Promise<void> {
  try {
    const data = (await queryProxy("/health?full=true", port)) as Record<string, unknown>;
    console.log(`\nClawRouter Wallet\n`);
    console.log(`  Payment Chain: ${data.paymentChain}`);
    console.log();
    console.log(`  Base (EVM):`);
    console.log(`    Address: ${data.wallet}`);
    console.log(`    Fund:    https://basescan.org/address/${data.wallet}`);
    if (data.solana) {
      console.log();
      console.log(`  Solana:`);
      console.log(`    Address: ${data.solana}`);
      console.log(`    Fund:    https://solscan.io/account/${data.solana}`);
    }
    console.log();
    console.log(`  Balance: ${data.balance ?? "unknown"} (USDC)`);
    if (data.isEmpty) console.log(`  ⚠ Empty — fund wallet or use free models`);
    console.log();
  } catch {
    console.error(`✗ Cannot connect to ClawRouter on port ${port}`);
    process.exit(1);
  }
}

async function cmdModels(port: number): Promise<void> {
  try {
    const data = (await queryProxy("/v1/models", port)) as {
      data: Array<{ id: string; owned_by?: string }>;
    };
    console.log(`\nAvailable Models (${data.data.length})\n`);

    // Group by provider
    const groups = new Map<string, string[]>();
    for (const m of data.data) {
      const provider = m.id.includes("/") ? m.id.split("/")[0] : "blockrun";
      if (!groups.has(provider)) groups.set(provider, []);
      groups.get(provider)!.push(m.id);
    }

    for (const [provider, models] of groups) {
      console.log(`  ${provider} (${models.length}):`);
      for (const id of models) {
        console.log(`    ${id}`);
      }
      console.log();
    }
  } catch {
    console.error(`✗ Cannot connect to ClawRouter on port ${port}`);
    process.exit(1);
  }
}

async function cmdStats(port: number, days: number): Promise<void> {
  try {
    const data = (await queryProxy(`/stats?days=${days}`, port)) as Record<string, unknown>;
    const stats = data as {
      totalRequests?: number;
      totalCostUsd?: number;
      savedUsd?: number;
      savingsPercent?: number;
      topModels?: Array<{ model: string; requests: number; costUsd: number }>;
    };

    console.log(`\nUsage Stats (last ${days} days)\n`);
    console.log(`  Requests: ${stats.totalRequests ?? 0}`);
    console.log(`  Cost:     $${(stats.totalCostUsd ?? 0).toFixed(4)}`);
    if (stats.savedUsd) {
      console.log(
        `  Saved:    $${stats.savedUsd.toFixed(4)} (${stats.savingsPercent?.toFixed(0) ?? 0}% vs Opus)`,
      );
    }

    if (stats.topModels && stats.topModels.length > 0) {
      console.log();
      console.log(`  Top Models:`);
      for (const m of stats.topModels.slice(0, 10)) {
        console.log(
          `    ${m.model.padEnd(40)} ${String(m.requests).padStart(5)} reqs  $${m.costUsd.toFixed(4)}`,
        );
      }
    }
    console.log();
  } catch {
    console.error(`✗ Cannot connect to ClawRouter on port ${port}`);
    process.exit(1);
  }
}

/**
 * `clawrouter phone ...` — wallet-facing phone CLI.
 *
 * All operations POST to the local proxy at `/v1/phone/...` (or `/v1/voice/...`),
 * which transparently handles x402 payment via the wallet. The CLI just renders
 * upstream JSON in a human format.
 */
async function cmdPhone(
  port: number,
  subcommand: "numbers" | "lookup" | "fraud",
  numbersAction: "list" | "buy" | "renew" | "release" | undefined,
  arg: string | undefined,
  areaCode: string | undefined,
): Promise<void> {
  const base = `http://127.0.0.1:${port}`;

  async function postJson(path: string, body: unknown): Promise<unknown> {
    const resp = await fetch(`${base}${path}`, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
    });
    const text = await resp.text();
    if (!resp.ok) {
      if (resp.status === 402) {
        throw new Error(
          `Insufficient wallet balance (HTTP 402). Fund wallet via clawrouter wallet.\n${text}`,
        );
      }
      throw new Error(`HTTP ${resp.status}: ${text}`);
    }
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  }

  function fmtExpiry(expiresAt: string): string {
    const ms = new Date(expiresAt).getTime();
    if (!Number.isFinite(ms)) return expiresAt;
    const days = Math.round((ms - Date.now()) / (1000 * 60 * 60 * 24));
    const dateStr = new Date(ms).toISOString().split("T")[0];
    if (days < 0) return `${dateStr} (expired ${-days}d ago)`;
    if (days <= 2) return `${dateStr} (in ${days}d) ⚠ renew soon`;
    return `${dateStr} (in ${days}d)`;
  }

  try {
    if (subcommand === "numbers") {
      if (!numbersAction || numbersAction === "list") {
        const result = (await postJson("/v1/phone/numbers/list", {})) as {
          numbers?: Array<{ phone_number: string; expires_at: string; country?: string }>;
        };
        const numbers = result.numbers ?? [];
        if (numbers.length === 0) {
          console.log("\nNo phone numbers owned by this wallet.\n");
          console.log("Buy one with: clawrouter phone numbers buy US [--area-code 415]\n");
          return;
        }
        console.log(`\nActive numbers (${numbers.length}):\n`);
        const numWidth = Math.max(...numbers.map((n) => n.phone_number.length), 16);
        for (const n of numbers) {
          const country = (n.country ?? "??").padEnd(3);
          console.log(
            `  ${n.phone_number.padEnd(numWidth)}  ${country}  expires ${fmtExpiry(n.expires_at)}`,
          );
        }
        console.log();
      } else if (numbersAction === "buy") {
        const country = (arg ?? "").toUpperCase();
        if (country !== "US" && country !== "CA") {
          console.error("✗ Country must be 'US' or 'CA'");
          console.error("  Usage: clawrouter phone numbers buy <US|CA> [--area-code <code>]");
          process.exit(1);
        }
        const body: Record<string, string> = { country };
        if (areaCode) body.areaCode = areaCode;
        console.log(
          `\nBuying ${country} number${areaCode ? ` (area code ${areaCode})` : ""}... ($5.00 / 30-day lease)\n`,
        );
        const result = (await postJson("/v1/phone/numbers/buy", body)) as {
          phone_number?: string;
          expires_at?: string;
        };
        if (result.phone_number) {
          console.log(`  ✓ ${result.phone_number}`);
          if (result.expires_at) console.log(`    expires ${fmtExpiry(result.expires_at)}`);
          console.log();
        } else {
          console.log(JSON.stringify(result, null, 2));
        }
      } else if (numbersAction === "renew") {
        if (!arg) {
          console.error("✗ Usage: clawrouter phone numbers renew <+E.164-number>");
          process.exit(1);
        }
        console.log(`\nRenewing ${arg}... ($5.00 / +30 days)\n`);
        const result = (await postJson("/v1/phone/numbers/renew", { phoneNumber: arg })) as {
          phone_number?: string;
          expires_at?: string;
        };
        console.log(`  ✓ ${result.phone_number ?? arg}`);
        if (result.expires_at) console.log(`    expires ${fmtExpiry(result.expires_at)}`);
        console.log();
      } else if (numbersAction === "release") {
        if (!arg) {
          console.error("✗ Usage: clawrouter phone numbers release <+E.164-number>");
          process.exit(1);
        }
        console.log(`\nReleasing ${arg}...\n`);
        await postJson("/v1/phone/numbers/release", { phoneNumber: arg });
        console.log(`  ✓ Released ${arg}\n`);
      }
    } else if (subcommand === "lookup" || subcommand === "fraud") {
      if (!arg) {
        console.error(`✗ Usage: clawrouter phone ${subcommand} <+E.164-number>`);
        process.exit(1);
      }
      const path = subcommand === "fraud" ? "/v1/phone/lookup/fraud" : "/v1/phone/lookup";
      const price = subcommand === "fraud" ? "$0.05" : "$0.01";
      console.log(`\nLooking up ${arg}... (${price})\n`);
      const result = await postJson(path, { phoneNumber: arg });
      console.log(JSON.stringify(result, null, 2));
      console.log();
    }
  } catch (err) {
    console.error(`✗ ${err instanceof Error ? err.message : String(err)}`);
    console.error(`  Is the proxy running on port ${port}? (start: npx @blockrun/clawrouter)`);
    process.exit(1);
  }
}

async function cmdCache(port: number): Promise<void> {
  try {
    const data = (await queryProxy("/cache", port)) as Record<string, unknown>;
    console.log(`\nCache Stats\n`);
    for (const [key, value] of Object.entries(data)) {
      console.log(`  ${key}: ${JSON.stringify(value)}`);
    }
    console.log();
  } catch {
    console.error(`✗ Cannot connect to ClawRouter on port ${port}`);
    process.exit(1);
  }
}

/**
 * `clawrouter setup` — finish OpenClaw integration after `npm install -g`.
 *
 * Bare `npm install -g @blockrun/clawrouter` puts the package on disk and
 * adds the `clawrouter` binary to PATH, but performs ZERO OpenClaw plugin
 * registration: no entry in openclaw.json's `plugins.entries`, no provider
 * registration, no models allowlist sync, no auth profile. The bot ends up
 * showing OpenClaw's hardcoded default models (~7 entries) and the user
 * thinks ClawRouter "doesn't work".
 *
 * This command repairs that by running the steps OpenClaw's plugin lifecycle
 * would have run: `openclaw plugins install --force` (registration), then
 * directly invoking `injectModelsConfig` and `injectAuthProfile` with
 * `forceWrite: true` to bypass the install-transaction guard added in
 * v0.12.184. Result: the user's openclaw.json now has the full ~38-entry
 * blockrun model allowlist and the `blockrun:default` auth profile.
 *
 * Idempotent — safe to run repeatedly. Use this after `npm install -g` to
 * complete setup, or after a stuck partial install to repair config.
 */
async function cmdSetup(): Promise<void> {
  const { execFileSync, execSync } = await import("node:child_process");
  const { injectModelsConfig, injectAuthProfile } = await import("./index.js");

  console.log("🦞 ClawRouter setup\n");

  // Step 1: detect OpenClaw on PATH
  let openclawPath: string;
  try {
    openclawPath = execSync("command -v openclaw", {
      encoding: "utf8",
      stdio: ["ignore", "pipe", "ignore"],
    }).trim();
    if (!openclawPath) throw new Error("not found");
    console.log(`  ✓ Found openclaw at ${openclawPath}`);
  } catch {
    console.error("  ✗ openclaw not found on PATH");
    console.error("    Install OpenClaw first: npm install -g openclaw");
    console.error("    Then re-run: clawrouter setup");
    process.exit(1);
  }

  // Step 2: register as an OpenClaw plugin (writes plugins.entries.clawrouter).
  // OpenClaw 2026.5.2 added strict validation that may reject our config writes
  // during install (e.g. unknown web_search provider blockrun-exa before the
  // gateway is running). If that happens, OpenClaw rolls back its own install
  // record but our injectModelsConfig step below will still populate the user's
  // openclaw.json correctly — they'll just need to re-run `openclaw plugins
  // install --force @blockrun/clawrouter` after the gateway has started.
  console.log("\n→ Registering ClawRouter with OpenClaw...");
  let installOk = false;
  try {
    execFileSync(openclawPath, ["plugins", "install", "--force", "@blockrun/clawrouter"], {
      stdio: "inherit",
    });
    installOk = true;
  } catch (err) {
    console.warn(
      `  ⚠ openclaw plugins install reported a problem: ${err instanceof Error ? err.message : String(err)}`,
    );
    console.warn("    Continuing with manual config sync — if validation rejected the install,");
    console.warn(
      "    re-run `openclaw plugins install --force @blockrun/clawrouter` after gateway start.",
    );
  }

  // Step 3: sync the models allowlist + provider config + auth profile directly.
  // forceWrite: true bypasses the gateway-mode guard added in v0.12.184 since
  // this is an explicit user command, not a plugin-activation hook inside an
  // install transaction.
  console.log("\n→ Syncing models allowlist + auth profile...");
  const setupLogger = {
    info: (msg: string) => console.log(`  ${msg}`),
  };
  try {
    injectModelsConfig(setupLogger, { forceWrite: true });
    injectAuthProfile(setupLogger);
  } catch (err) {
    console.error(`  ✗ Config sync failed: ${err instanceof Error ? err.message : String(err)}`);
    process.exit(1);
  }

  if (installOk) {
    console.log("\n✓ Setup complete.");
  } else {
    console.log("\n✓ Models config synced (plugin registration may need a manual retry).");
  }
  console.log("\nNext: restart the gateway to load ClawRouter:");
  console.log("  openclaw gateway restart");
  console.log("\nThen verify in your bot: /models — you should see ~38 BlockRun models.");
}

function parseArgs(args: string[]): {
  version: boolean;
  help: boolean;
  doctor: boolean;
  logs: boolean;
  logsDays: number;
  partners: boolean;
  partnersTest: boolean;
  report: boolean;
  reportPeriod: "daily" | "weekly" | "monthly";
  reportJson: boolean;
  walletRecover: boolean;
  chain?: "solana" | "base";
  port?: number;
  // Query commands
  queryStatus: boolean;
  queryWallet: boolean;
  queryModels: boolean;
  queryStats: boolean;
  queryStatsDays: number;
  queryCache: boolean;
  setup: boolean;
  // Share commands
  share: boolean;
  shareTarget?: "last" | "list" | string; // string = an id like resp_...
  sharePreset?: string;
  shareLimit: number;
  shareAll: boolean;
  // Phone commands
  phone: boolean;
  phoneSubcommand?: "numbers" | "lookup" | "fraud";
  phoneNumbersAction?: "list" | "buy" | "renew" | "release";
  phoneArg?: string;
  phoneAreaCode?: string;
} {
  const result = {
    version: false,
    help: false,
    doctor: false,
    logs: false,
    logsDays: 1,
    partners: false,
    partnersTest: false,
    report: false,
    reportPeriod: "daily" as "daily" | "weekly" | "monthly",
    reportJson: false,
    walletRecover: false,
    chain: undefined as "solana" | "base" | undefined,
    port: undefined as number | undefined,
    queryStatus: false,
    queryWallet: false,
    queryModels: false,
    queryStats: false,
    queryStatsDays: 7,
    queryCache: false,
    setup: false,
    share: false,
    shareTarget: undefined as "last" | "list" | string | undefined,
    sharePreset: undefined as string | undefined,
    shareLimit: 20,
    shareAll: false,
    phone: false,
    phoneSubcommand: undefined as "numbers" | "lookup" | "fraud" | undefined,
    phoneNumbersAction: undefined as "list" | "buy" | "renew" | "release" | undefined,
    phoneArg: undefined as string | undefined,
    phoneAreaCode: undefined as string | undefined,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === "--version" || arg === "-v") {
      result.version = true;
    } else if (arg === "--help" || arg === "-h") {
      result.help = true;
    } else if (arg === "status") {
      result.queryStatus = true;
    } else if (arg === "models") {
      result.queryModels = true;
    } else if (arg === "stats") {
      result.queryStats = true;
      if (args[i + 1] === "--days" && args[i + 2]) {
        result.queryStatsDays = Math.min(parseInt(args[i + 2], 10) || 7, 30);
        i += 2;
      }
    } else if (arg === "cache") {
      result.queryCache = true;
    } else if (arg === "doctor" || arg === "--doctor") {
      result.doctor = true;
    } else if (arg === "logs") {
      result.logs = true;
      if (args[i + 1] === "--days" && args[i + 2]) {
        result.logsDays = parseInt(args[i + 2], 10) || 1;
        i += 2;
      }
    } else if (arg === "partners") {
      result.partners = true;
      if (args[i + 1] === "test") {
        result.partnersTest = true;
        i++;
      }
    } else if (arg === "report") {
      result.report = true;
      const next = args[i + 1];
      if (next && ["daily", "weekly", "monthly"].includes(next)) {
        result.reportPeriod = next as "daily" | "weekly" | "monthly";
        i++;
        if (args[i + 1] === "--json") {
          result.reportJson = true;
          i++;
        }
      } else if (next === "--json") {
        result.reportJson = true;
        i++;
      }
    } else if (arg === "wallet") {
      if (args[i + 1] === "recover") {
        result.walletRecover = true;
        i++;
      } else if (args[i + 1] === "solana" || args[i + 1] === "base") {
        result.chain = args[i + 1] as "solana" | "base";
        i++;
      } else {
        result.queryWallet = true;
      }
    } else if (
      (arg === "chain" || arg === "/wallet") &&
      (args[i + 1] === "solana" || args[i + 1] === "base")
    ) {
      result.chain = args[i + 1] as "solana" | "base";
      i++;
    } else if (arg === "--port" && args[i + 1]) {
      result.port = parseInt(args[i + 1], 10);
      i++;
    } else if (arg === "setup") {
      result.setup = true;
    } else if (arg === "phone") {
      result.phone = true;
      const next = args[i + 1];
      if (next === "numbers") {
        result.phoneSubcommand = "numbers";
        const action = args[i + 2];
        if (action === "list" || action === "buy" || action === "renew" || action === "release") {
          result.phoneNumbersAction = action;
          i += 2;
          // Capture positional arg for buy/renew/release
          const positional = args[i + 1];
          if (positional && !positional.startsWith("--")) {
            result.phoneArg = positional;
            i++;
          }
          // Optional --area-code flag (buy only)
          while (i + 1 < args.length) {
            const flag = args[i + 1];
            if (flag === "--area-code" && args[i + 2]) {
              result.phoneAreaCode = args[i + 2];
              i += 2;
            } else if (flag.startsWith("--area-code=")) {
              result.phoneAreaCode = flag.slice("--area-code=".length);
              i++;
            } else break;
          }
        } else {
          i++;
        }
      } else if (next === "lookup" || next === "fraud") {
        result.phoneSubcommand = next;
        i++;
        const positional = args[i + 1];
        if (positional && !positional.startsWith("--")) {
          result.phoneArg = positional;
          i++;
        }
      }
    } else if (arg === "share") {
      result.share = true;
      // Next positional arg is target: "last" | "list" | resp_<id>
      const next = args[i + 1];
      if (next && !next.startsWith("--")) {
        result.shareTarget = next;
        i++;
      }
      // Scan remaining args for share-specific flags
      while (i + 1 < args.length) {
        const flag = args[i + 1];
        if (flag === "--all") {
          result.shareAll = true;
          i++;
        } else if (flag === "--as" && args[i + 2]) {
          result.sharePreset = args[i + 2];
          i += 2;
        } else if (flag.startsWith("--as=")) {
          result.sharePreset = flag.slice("--as=".length);
          i++;
        } else if (flag === "--limit" && args[i + 2]) {
          result.shareLimit = Math.min(Math.max(parseInt(args[i + 2], 10) || 20, 1), 100);
          i += 2;
        } else if (flag.startsWith("--limit=")) {
          result.shareLimit = Math.min(
            Math.max(parseInt(flag.slice("--limit=".length), 10) || 20, 1),
            100,
          );
          i++;
        } else {
          break;
        }
      }
    }
  }

  return result;
}

async function main(): Promise<void> {
  const args = parseArgs(process.argv.slice(2));

  if (args.version) {
    console.log(VERSION);
    process.exit(0);
  }

  if (args.help) {
    printHelp();
    process.exit(0);
  }

  if (args.setup) {
    await cmdSetup();
    process.exit(0);
  }

  // Query commands — talk to running proxy
  const queryPort = args.port ?? getProxyPort();

  if (args.queryStatus) {
    await cmdStatus(queryPort);
    process.exit(0);
  }
  if (args.queryWallet) {
    await cmdWallet(queryPort);
    process.exit(0);
  }
  if (args.queryModels) {
    await cmdModels(queryPort);
    process.exit(0);
  }
  if (args.queryStats) {
    await cmdStats(queryPort, args.queryStatsDays);
    process.exit(0);
  }
  if (args.queryCache) {
    await cmdCache(queryPort);
    process.exit(0);
  }

  if (args.share) {
    await cmdShare(queryPort, args.shareTarget, args.sharePreset, args.shareLimit, args.shareAll);
    process.exit(0);
  }

  if (args.phone) {
    if (!args.phoneSubcommand) {
      console.error("✗ Usage: clawrouter phone <numbers|lookup|fraud> ...");
      console.error("  See: clawrouter --help");
      process.exit(1);
    }
    await cmdPhone(
      queryPort,
      args.phoneSubcommand,
      args.phoneNumbersAction,
      args.phoneArg,
      args.phoneAreaCode,
    );
    process.exit(0);
  }

  if (args.doctor) {
    // Parse: doctor [opus|sonnet] [question...]
    const rawArgs = process.argv.slice(2);
    const doctorIndex = rawArgs.findIndex((a) => a === "doctor" || a === "--doctor");
    const afterDoctor = rawArgs.slice(doctorIndex + 1);

    // Check if first arg is model selection
    let model: "sonnet" | "opus" = "sonnet"; // default to cheaper
    let questionArgs = afterDoctor;

    if (afterDoctor[0] === "opus") {
      model = "opus";
      questionArgs = afterDoctor.slice(1);
    } else if (afterDoctor[0] === "sonnet") {
      model = "sonnet";
      questionArgs = afterDoctor.slice(1);
    }

    const userQuestion = questionArgs.join(" ").trim() || undefined;
    await runDoctor(userQuestion, model);
    process.exit(0);
  }

  if (args.logs) {
    const output = await formatRecentLogs(args.logsDays);
    console.log(output);
    process.exit(0);
  }

  if (args.partners) {
    if (PARTNER_SERVICES.length === 0) {
      console.log("No partner APIs available.");
      process.exit(0);
    }

    console.log(`\nClawRouter Partner APIs (v${VERSION})\n`);

    // Compact grouped view — one line per tool, grouped by category.
    const byCategory = new Map<string, typeof PARTNER_SERVICES>();
    for (const svc of PARTNER_SERVICES) {
      const bucket = byCategory.get(svc.category) ?? [];
      bucket.push(svc);
      byCategory.set(svc.category, bucket);
    }
    const toolWidth = Math.max(...PARTNER_SERVICES.map((s) => `blockrun_${s.id}`.length), 28);
    const priceWidth = Math.max(
      ...PARTNER_SERVICES.map((s) =>
        s.pricing.perUnit === "free" ? 4 : `${s.pricing.perUnit}/${s.pricing.unit}`.length,
      ),
      12,
    );
    for (const [category, services] of byCategory) {
      console.log(`${category}`);
      for (const svc of services) {
        const tool = `blockrun_${svc.id}`.padEnd(toolWidth);
        const price = (
          svc.pricing.perUnit === "free" ? "free" : `${svc.pricing.perUnit}/${svc.pricing.unit}`
        ).padEnd(priceWidth);
        console.log(`  ${tool}  ${price}  ${svc.shortDescription}`);
      }
      console.log();
    }

    if (args.partnersTest) {
      console.log("Testing partner endpoints...\n");
      const apiBase = "https://blockrun.ai/api";
      for (const svc of PARTNER_SERVICES) {
        const url = `${apiBase}/v1${svc.proxyPath}`;
        try {
          const response = await fetch(url, { method: "GET" });
          const status = response.status;
          const ok = status === 402 ? "alive (402 = payment required)" : `status ${status}`;
          console.log(`  ${svc.id}: ${ok}`);
        } catch (err) {
          console.log(`  ${svc.id}: error - ${err instanceof Error ? err.message : String(err)}`);
        }
      }
      console.log();
    }

    process.exit(0);
  }

  if (args.walletRecover) {
    await recoverWalletFromMnemonic();
    process.exit(0);
  }

  if (args.chain) {
    const targetChain = args.chain;

    if (targetChain === "solana") {
      // Ensure Solana wallet is set up (mnemonic → keypair)
      const { existsSync } = await import("fs");
      const { MNEMONIC_FILE, setupSolana } = await import("./auth.js");
      const { deriveSolanaKeyBytes, getSolanaAddress } = await import("./wallet.js");

      let solanaAddr: string;
      if (existsSync(MNEMONIC_FILE)) {
        // Already set up — derive address from existing mnemonic
        const { readFileSync } = await import("fs");
        const mnemonic = readFileSync(MNEMONIC_FILE, "utf8").trim();
        const keyBytes = deriveSolanaKeyBytes(mnemonic);
        solanaAddr = await getSolanaAddress(keyBytes);
        console.log(`[ClawRouter] Solana wallet already set up.`);
      } else {
        // First time — generate mnemonic + keypair
        console.log(`[ClawRouter] Setting up Solana wallet...`);
        const { solanaPrivateKeyBytes } = await setupSolana();
        solanaAddr = await getSolanaAddress(solanaPrivateKeyBytes);
        console.log(`[ClawRouter] Solana wallet created.`);
        console.log(`[ClawRouter] ⚠  Back up your mnemonic: ${MNEMONIC_FILE}`);
      }

      await savePaymentChain("solana");
      console.log(`[ClawRouter] Payment chain → Solana`);
      console.log(`[ClawRouter] Solana address : ${solanaAddr}`);
      console.log(`[ClawRouter] Fund USDC at   : https://solscan.io/account/${solanaAddr}`);
    } else {
      await savePaymentChain("base");
      console.log(`[ClawRouter] Payment chain → Base (EVM)`);
    }

    console.log(
      `[ClawRouter] Restart gateway to apply: systemctl --user restart openclaw-gateway.service`,
    );
    console.log(`[ClawRouter]   or: openclaw gateway restart`);
    process.exit(0);
  }

  if (args.report) {
    const report = await generateReport(args.reportPeriod, args.reportJson);
    console.log(report);
    process.exit(0);
  }

  // Resolve wallet key
  const wallet = await resolveOrGenerateWalletKey();

  if (wallet.source === "generated") {
    console.log(`[ClawRouter] Generated new wallet: ${wallet.address}`);
  } else if (wallet.source === "saved") {
    console.log(`[ClawRouter] Using saved wallet: ${wallet.address}`);
  } else if (wallet.source === "config") {
    console.log(`[ClawRouter] Using wallet from plugin config: ${wallet.address}`);
  } else {
    console.log(`[ClawRouter] Using wallet from BLOCKRUN_WALLET_KEY: ${wallet.address}`);
  }

  // Show Solana address if available
  if (wallet.solanaPrivateKeyBytes) {
    try {
      const solAddr = await getSolanaAddress(wallet.solanaPrivateKeyBytes);
      console.log(`[ClawRouter] Solana address: ${solAddr}`);
    } catch {
      // Non-fatal
    }
  }

  // Start the proxy
  const proxy = await startProxy({
    wallet,
    port: args.port,
    onReady: (port) => {
      console.log(`[ClawRouter] v${VERSION} | Proxy listening on http://127.0.0.1:${port}`);
      console.log(`[ClawRouter] Health check: http://127.0.0.1:${port}/health`);
    },
    onError: (error) => {
      console.error(`[ClawRouter] Error: ${error.message}`);
    },
    onRouted: (decision) => {
      const cost = decision.costEstimate.toFixed(4);
      const saved = (decision.savings * 100).toFixed(0);
      console.log(`[ClawRouter] [${decision.tier}] ${decision.model} $${cost} (saved ${saved}%)`);
    },
    onLowBalance: (info) => {
      console.warn(`[ClawRouter] Low balance: ${info.balanceUSD}. Fund: ${info.walletAddress}`);
    },
    onInsufficientFunds: (info) => {
      console.error(
        `[ClawRouter] Insufficient funds. Balance: ${info.balanceUSD}, Need: ${info.requiredUSD}`,
      );
      console.error(`[ClawRouter] Need help? Run: npx @blockrun/clawrouter doctor`);
    },
  });

  // Check balance on the active payment chain
  const paymentChain = await resolvePaymentChain();
  const displayAddress =
    paymentChain === "solana" && proxy.solanaAddress ? proxy.solanaAddress : wallet.address;
  try {
    const balance = await proxy.balanceMonitor.checkBalance();
    if (balance.isEmpty) {
      console.log(`[ClawRouter] Wallet balance: $0.00 (using FREE model)`);
      console.log(`[ClawRouter] Fund wallet for premium models: ${displayAddress}`);
    } else if (balance.isLow) {
      console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
    } else {
      console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD}`);
    }
    // Invalidate after display so the first real request always gets a fresh check.
    // Prevents a flaky RPC at startup from caching a false $0.00 and triggering
    // free-model fallback on the first request.
    proxy.balanceMonitor.invalidate();
  } catch {
    console.log(`[ClawRouter] Wallet: ${displayAddress} (balance check pending)`);
  }

  console.log(`[ClawRouter] Ready - Ctrl+C to stop`);

  // Handle graceful shutdown
  const shutdown = async (signal: string) => {
    console.log(`\n[ClawRouter] Received ${signal}, shutting down...`);
    try {
      await proxy.close();
      console.log(`[ClawRouter] Proxy closed`);
      process.exit(0);
    } catch (err) {
      console.error(`[ClawRouter] Error during shutdown: ${err}`);
      process.exit(1);
    }
  };

  process.on("SIGINT", () => shutdown("SIGINT"));
  process.on("SIGTERM", () => shutdown("SIGTERM"));

  // Keep process alive
  await new Promise(() => {});
}

main().catch((err) => {
  console.error(`[ClawRouter] Fatal error: ${err.message}`);
  console.error(`[ClawRouter] Need help? Run: npx @blockrun/clawrouter doctor`);
  process.exit(1);
});
