/**
 * Shared helpers and step functions for sinain onboard + sinain config.
 * Both commands import from here to avoid duplication.
 */
import * as p from "@clack/prompts";
import fs from "fs";
import path from "path";
import os from "os";
import net from "net";
import { execFileSync } from "child_process";

export const HOME = os.homedir();
export const SINAIN_DIR = path.join(HOME, ".sinain");
export const ENV_PATH = path.join(SINAIN_DIR, ".env");
export const PKG_DIR = path.dirname(new URL(import.meta.url).pathname);
export const IS_WINDOWS = os.platform() === "win32";
export const IS_MAC = os.platform() === "darwin";

// Wizard write target for agents.json. Lives in user home so npm-installed
// users (whose package dir is often read-only) get a writable path.
// sinain-agent/run.sh and sinain-core/agents-loader.ts both check this
// path with the highest priority after AGENTS_CONFIG_PATH env override.
export const AGENTS_JSON_PATH = path.join(SINAIN_DIR, "agents.json");
// agents.example.json ships inside the package as the bootstrap template
// for first-run wizard writes (and as the fallback when no agents.json
// exists yet on this host).
export const AGENTS_EXAMPLE_PATH = path.join(PKG_DIR, "sinain-agent", "agents.example.json");

// ── Colors ──────────────────────────────────────────────────────────────────

export const c = {
  green: (s) => `\x1b[32m${s}\x1b[0m`,
  yellow: (s) => `\x1b[33m${s}\x1b[0m`,
  red: (s) => `\x1b[31m${s}\x1b[0m`,
  dim: (s) => `\x1b[2m${s}\x1b[0m`,
  bold: (s) => `\x1b[1m${s}\x1b[0m`,
  cyan: (s) => `\x1b[36m${s}\x1b[0m`,
};

// ── Helpers ─────────────────────────────────────────────────────────────────

export function guard(value) {
  if (p.isCancel(value)) {
    p.cancel("Cancelled.");
    process.exit(0);
  }
  return value;
}

export function cmdExists(cmd) {
  try {
    execFileSync(IS_WINDOWS ? "where" : "which", [cmd], { stdio: "pipe" });
    return true;
  } catch {
    return false;
  }
}

export function isPortOpen(port) {
  return new Promise((resolve) => {
    const sock = new net.Socket();
    sock.setTimeout(800);
    sock.on("connect", () => { sock.destroy(); resolve(true); });
    sock.on("error", () => resolve(false));
    sock.on("timeout", () => { sock.destroy(); resolve(false); });
    sock.connect(port, "127.0.0.1");
  });
}

export function maskKey(key) {
  if (!key || key.length < 12) return "****";
  return key.slice(0, 8) + "..." + key.slice(-4);
}

/** Read existing .env into a key=value map */
export function readEnv(envPath = ENV_PATH) {
  if (!fs.existsSync(envPath)) return {};
  const content = fs.readFileSync(envPath, "utf-8");
  const vars = {};
  for (const line of content.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
    const eq = trimmed.indexOf("=");
    if (eq === -1) continue;
    const key = trimmed.slice(0, eq).trim();
    const val = trimmed.slice(eq + 1).trim();
    if (val) vars[key] = val;
  }
  return vars;
}

/** Write vars to .env, preserving template structure */
export function writeEnv(vars) {
  fs.mkdirSync(SINAIN_DIR, { recursive: true });

  const examplePath = path.join(PKG_DIR, ".env.example");
  let template = fs.existsSync(examplePath)
    ? fs.readFileSync(examplePath, "utf-8")
    : "";

  if (template) {
    for (const [k, v] of Object.entries(vars)) {
      const regex = new RegExp(`^#?\\s*${k}=.*$`, "m");
      if (regex.test(template)) {
        template = template.replace(regex, `${k}=${v}`);
      } else {
        template += `\n${k}=${v}`;
      }
    }
    template = `# Generated by sinain — ${new Date().toISOString()}\n${template}`;
    fs.writeFileSync(ENV_PATH, template);
  } else {
    const lines = [
      "# sinain configuration",
      `# Generated by sinain — ${new Date().toISOString()}`,
      "",
    ];
    for (const [k, v] of Object.entries(vars)) lines.push(`${k}=${v}`);
    lines.push("");
    fs.writeFileSync(ENV_PATH, lines.join("\n"));
  }
}

/** Read agents.json (the wizard's authoritative agent + gateway config).
 *
 * Resolution order mirrors agents-loader.ts and run.sh:
 *   1. ~/.sinain/agents.json (the wizard write target)
 *   2. <pkg>/sinain-agent/agents.example.json (bootstrap template fallback)
 *
 * Returns an empty object if neither file exists or parsing fails — callers
 * treat null/missing fields as "use schema defaults".
 */
export function readAgentsConfig() {
  for (const p of [AGENTS_JSON_PATH, AGENTS_EXAMPLE_PATH]) {
    if (!fs.existsSync(p)) continue;
    try {
      return JSON.parse(fs.readFileSync(p, "utf-8"));
    } catch (err) {
      // Bad JSON — surface, but don't crash the wizard
      console.warn(`[config] failed to parse ${p}: ${err.message}`);
    }
  }
  return {};
}

/** Patch ~/.sinain/agents.json with the given updates.
 *
 * `patch` shape (all optional):
 *   - default:        string  — top-level default agent name
 *   - escalationMode: string  — sets escalation.mode
 *   - openclawProfile: object | null — sets profiles.openclaw
 *                       (null deletes the openclaw profile entirely,
 *                        which disables the gateway path)
 *
 * Reads the existing config first (or falls back to the shipped example),
 * applies the patch, writes pretty-printed JSON to AGENTS_JSON_PATH.
 * The example file is the "schema source of truth" — patches preserve any
 * other fields the user has customized (custom profiles, allowedTools,
 * analyzer pacing, etc.).
 */
export function writeAgentsConfig(patch) {
  fs.mkdirSync(SINAIN_DIR, { recursive: true });
  // Start from existing user config if present, else the example template
  // (so first-run writes get the full schema, not just the patched fields).
  let cfg;
  if (fs.existsSync(AGENTS_JSON_PATH)) {
    cfg = JSON.parse(fs.readFileSync(AGENTS_JSON_PATH, "utf-8"));
  } else if (fs.existsSync(AGENTS_EXAMPLE_PATH)) {
    cfg = JSON.parse(fs.readFileSync(AGENTS_EXAMPLE_PATH, "utf-8"));
  } else {
    cfg = { profiles: {} };
  }

  if (patch.default !== undefined) {
    cfg.default = patch.default;
  }
  if (patch.escalationMode !== undefined) {
    cfg.escalation = cfg.escalation || {};
    cfg.escalation.mode = patch.escalationMode;
  }
  if (patch.openclawProfile === null) {
    // Explicit removal: user picked "skip / no gateway"
    if (cfg.profiles?.openclaw) delete cfg.profiles.openclaw;
  } else if (patch.openclawProfile) {
    cfg.profiles = cfg.profiles || {};
    cfg.profiles.openclaw = { type: "openclaw", ...patch.openclawProfile };
  }

  fs.writeFileSync(AGENTS_JSON_PATH, JSON.stringify(cfg, null, 2) + "\n");
}

export async function fetchHealth(port = 9500) {
  try {
    const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000) });
    return await res.json();
  } catch { return null; }
}

/** Build a config summary for display.
 * Reads BOTH .env (existing arg) AND ~/.sinain/agents.json — agent + gateway
 * config moved out of .env into agents.json after the migration, so we
 * have to consult both to render an accurate snapshot. */
export function summarizeConfig(existing) {
  const lines = [];
  const agentsCfg = readAgentsConfig();
  const openclaw = agentsCfg?.profiles?.openclaw;

  if (existing.OPENROUTER_API_KEY) lines.push(`API Key:       ${maskKey(existing.OPENROUTER_API_KEY)}`);
  if (existing.TRANSCRIPTION_BACKEND) lines.push(`Transcription: ${existing.TRANSCRIPTION_BACKEND}`);
  if (existing.AGENT_MODEL) lines.push(`Model:         ${existing.AGENT_MODEL}`);
  if (existing.PRIVACY_MODE) lines.push(`Privacy:       ${existing.PRIVACY_MODE}`);

  // Escalation mode + agent default + gateway URL all live in agents.json
  // now (with .env as a fallback override during the migration window).
  const escMode = agentsCfg?.escalation?.mode || existing.ESCALATION_MODE;
  if (escMode) lines.push(`Escalation:    ${escMode}`);
  const gatewayUrl = openclaw?.wsUrl || existing.OPENCLAW_WS_URL;
  if (gatewayUrl) lines.push(`Gateway:       ${gatewayUrl}`);
  const defaultAgent = agentsCfg?.default || existing.SINAIN_AGENT;
  if (defaultAgent) lines.push(`Agent:         ${defaultAgent}`);
  return lines;
}

// ── Health check ────────────────────────────────────────────────────────────

export async function runHealthCheck() {
  const checks = [];

  const coreUp = await isPortOpen(9500);
  if (coreUp) {
    checks.push(c.green("  sinain-core: reachable (port 9500)"));
    const health = await fetchHealth();
    if (health) {
      const audioState = health.audio?.state || health.audioPipeline?.state;
      if (audioState === "active" || audioState === "running") {
        checks.push(c.green("  Audio pipeline: active"));
      } else {
        checks.push(c.yellow("  Audio pipeline: inactive — check audio device settings"));
      }
      const screenActive = health.screen?.state === "active" || health.senseActive;
      if (screenActive) {
        checks.push(c.green("  Screen capture: active"));
      } else {
        if (IS_MAC) {
          checks.push(c.yellow("  Screen capture: inactive"));
          checks.push(c.dim("    Grant Screen Recording in System Settings → Privacy & Security"));
        } else {
          checks.push(c.yellow("  Screen capture: inactive"));
        }
      }
      const gwConnected = health.escalation?.gatewayConnected;
      if (gwConnected) {
        checks.push(c.green("  OpenClaw gateway: connected"));
      } else if (health.escalation?.mode === "off") {
        checks.push(c.dim("  OpenClaw gateway: not configured (standalone mode)"));
      } else {
        checks.push(c.yellow("  OpenClaw gateway: not connected"));
      }
    }
  } else {
    checks.push(c.dim("  sinain-core: not running"));
    checks.push(c.dim("  Start with: sinain start (or ./start.sh)"));
  }

  if (checks.length > 0) {
    p.note(checks.join("\n"), "System health");
  }
}

// ── Step functions ──────────────────────────────────────────────────────────
// label param: optional prefix like "[1/5]" for onboard, omitted for config

export async function stepApiKey(existing, label = "OpenRouter API key") {
  const current = existing.OPENROUTER_API_KEY;
  const message = current
    ? `${label} ${c.dim(`(current: ${maskKey(current)})`)}`
    : label;

  p.note(
    `Key is stored in plain text at ${c.dim(ENV_PATH)}.\nDon't commit this file or share it.`,
    "Security",
  );

  const key = guard(await p.text({
    message,
    placeholder: current ? "Press Enter to keep current" : "sk-or-v1-...",
    defaultValue: current || "",
    validate: (val) => {
      if (!val && !current) return "API key is required. Get one free at https://openrouter.ai/keys";
    },
  }));

  return key || current;
}

export async function stepTranscription(existing, label = "Audio transcription") {
  const current = existing.TRANSCRIPTION_BACKEND || "openrouter";
  const hasWhisper = !IS_WINDOWS && cmdExists("whisper-cli");

  const choice = guard(await p.select({
    message: label,
    options: [
      {
        value: "openrouter",
        label: "Cloud (OpenRouter)",
        hint: "Uses your OpenRouter key, no setup needed",
      },
      {
        value: "local",
        label: "Local (Whisper)",
        hint: hasWhisper ? "whisper-cli detected on this machine" : "~1.5 GB download, fully private",
      },
    ],
    initialValue: current,
  }));

  if (choice === "local" && !hasWhisper && IS_MAC) {
    const install = guard(await p.confirm({
      message: "whisper-cli not found. Install via Homebrew?",
      initialValue: true,
    }));
    if (install) {
      const s = p.spinner();
      s.start("Installing whisper-cpp...");
      try {
        execFileSync("brew", ["install", "whisper-cpp"], { stdio: "pipe" });
        s.stop(c.green("whisper-cpp installed."));
      } catch {
        s.stop(c.yellow("Install failed — falling back to cloud."));
        return "openrouter";
      }
    }
  }

  return choice;
}

/**
 * Gateway setup step.
 *
 * Returns `{ envVars, agentsPatch }`:
 *   - envVars: tokens (OPENCLAW_WS_TOKEN, OPENCLAW_HTTP_TOKEN) — stay in
 *     .env as secrets, referenced from agents.json via ${VAR} indirection
 *   - agentsPatch: feed to writeAgentsConfig(); writes the openclaw profile
 *     (urls + session) and escalation.mode into ~/.sinain/agents.json.
 *
 * The user picks whether to wire up the gateway-style profile at all.
 * The dispatch decision (which lane uses WS vs HTTP) is made later via
 * the overlay's per-lane agent selector — there's no transport question
 * here anymore; agent identity IS the transport.
 *
 * Pre-existing gateway config is read from agents.json first, then .env
 * as a backwards-compat fallback for users migrating from before the
 * profile refactor.
 */
export async function stepGateway(existing, label = "OpenClaw gateway") {
  // Read existing gateway config from BOTH places — wizard might be
  // running mid-migration where some users still have OPENCLAW_WS_URL in
  // .env but agents.json's openclaw profile is absent.
  const agentsCfg = readAgentsConfig();
  const oc = agentsCfg?.profiles?.openclaw;
  const existingWsUrl = oc?.wsUrl || existing.OPENCLAW_WS_URL || "";

  p.note(
    "The gateway gives sinain a persistent agent backend (OpenClaw/NemoClaw)\n" +
    "that handles escalations and background tasks via WS RPC. Without it,\n" +
    "the local bare agent (claude, openclaude, etc.) handles everything via\n" +
    "the HTTP path.\n\n" +
    "You can pick which agent handles each lane (escalation/spawn) at runtime\n" +
    "via the overlay's flash-icon selector.",
    "About the gateway",
  );

  const choice = guard(await p.select({
    message: label,
    options: [
      {
        value: "skip",
        label: "Skip / Disable",
        hint: "Local agents only — add a gateway later with `sinain config`",
      },
      {
        value: "local",
        label: "Local gateway",
        hint: "Install & start openclaw on this machine",
      },
      {
        value: "remote",
        label: "Remote gateway",
        hint: "Connect to existing gateway (URL + token)",
      },
    ],
    initialValue: existingWsUrl ? "remote" : "skip",
  }));

  if (choice === "skip") {
    // Drop the openclaw profile from agents.json entirely. Tokens cleared
    // in .env so they don't linger as misleading secrets.
    return {
      envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
      agentsPatch: { escalationMode: "off", openclawProfile: null },
    };
  }

  if (choice === "local") {
    return await setupLocalGateway(existing);
  }

  // remote
  const wsUrl = guard(await p.text({
    message: "Gateway WebSocket URL",
    placeholder: "Example: ws://192.168.1.100:18789",
    defaultValue: existingWsUrl,
    validate: (val) => {
      if (!val) return "URL is required";
      if (!val.startsWith("ws://") && !val.startsWith("wss://")) return "Must start with ws:// or wss://";
    },
  }));

  const token = guard(await p.text({
    message: "Gateway auth token (48-char hex)",
    placeholder: existing.OPENCLAW_WS_TOKEN ? "Press Enter to keep current" : "Paste token here",
    defaultValue: existing.OPENCLAW_WS_TOKEN || "",
  }));

  const s = p.spinner();
  s.start("Testing gateway connection...");
  try {
    const parsed = new URL(wsUrl);
    const port = parseInt(parsed.port || "18789");
    const reachable = await isPortOpen(port);
    if (reachable) {
      s.stop(c.green("Gateway reachable."));
    } else {
      s.stop(c.yellow("Gateway not reachable — config saved anyway."));
    }
  } catch {
    s.stop(c.yellow("Could not parse URL — config saved anyway."));
  }

  const httpUrl = wsUrl.replace(/^ws/, "http") + "/hooks/agent";
  // Tokens → .env; URLs + session → agents.json. The agents.json profile
  // references the env tokens via ${OPENCLAW_WS_TOKEN}/${OPENCLAW_HTTP_TOKEN}
  // indirection (resolved by sinain-core's agents-loader at startup).
  return {
    envVars: {
      OPENCLAW_WS_TOKEN: token,
      OPENCLAW_HTTP_TOKEN: token,
    },
    agentsPatch: {
      escalationMode: "rich",
      openclawProfile: {
        wsUrl,
        httpUrl,
        wsToken: "${OPENCLAW_WS_TOKEN}",
        httpToken: "${OPENCLAW_HTTP_TOKEN}",
        sessionKey: "agent:main:sinain",
      },
    },
  };
}

async function setupLocalGateway(existing) {
  const hasOpenclaw = cmdExists("openclaw");

  if (!hasOpenclaw) {
    const install = guard(await p.confirm({
      message: "openclaw CLI not found. Install globally via npm?",
      initialValue: true,
    }));
    if (install) {
      const s = p.spinner();
      s.start("Installing openclaw...");
      try {
        execFileSync("npm", ["install", "-g", "openclaw@latest"], { stdio: "pipe", timeout: 120_000 });
        s.stop(c.green("openclaw installed."));
      } catch {
        s.stop(c.red("Installation failed."));
        p.note(
          "Install manually: npm install -g openclaw\nThen re-run setup.",
          "Manual install",
        );
        return {
          envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
          agentsPatch: { escalationMode: "off", openclawProfile: null },
        };
      }
    } else {
      return {
        envVars: { OPENCLAW_WS_TOKEN: "", OPENCLAW_HTTP_TOKEN: "" },
        agentsPatch: { escalationMode: "off", openclawProfile: null },
      };
    }
  }

  const s = p.spinner();
  s.start("Checking gateway...");
  const gatewayUp = await isPortOpen(18789);

  const ocJsonPath = path.join(HOME, ".openclaw/openclaw.json");
  let token = "";
  if (fs.existsSync(ocJsonPath)) {
    try {
      const ocConfig = JSON.parse(fs.readFileSync(ocJsonPath, "utf-8"));
      token = ocConfig.gateway?.auth?.token || "";
    } catch { /* ignore */ }
  }

  if (gatewayUp) {
    s.stop(c.green("Gateway already running (port 18789)."));
  } else {
    s.stop(c.yellow("Gateway not running."));
    p.note("Start your gateway:\n  openclaw gateway run\n  or: openclaw onboard --install-daemon", "Next step");
  }

  return {
    envVars: {
      OPENCLAW_WS_TOKEN: token,
      OPENCLAW_HTTP_TOKEN: token,
    },
    agentsPatch: {
      escalationMode: "rich",
      openclawProfile: {
        wsUrl: "ws://localhost:18789",
        httpUrl: "http://localhost:18789/hooks/agent",
        wsToken: "${OPENCLAW_WS_TOKEN}",
        httpToken: "${OPENCLAW_HTTP_TOKEN}",
        sessionKey: "agent:main:sinain",
      },
    },
  };
}

export async function stepPrivacy(existing, label = "Privacy mode") {
  const current = existing.PRIVACY_MODE || "standard";
  return guard(await p.select({
    message: label,
    options: [
      {
        value: "off",
        label: "Off",
        hint: "No filtering — screen text, credentials, everything sent to cloud",
      },
      {
        value: "standard",
        label: "Standard",
        hint: "Auto-redacts cards, API keys, tokens before sending to cloud",
      },
      {
        value: "strict",
        label: "Strict",
        hint: "Only summaries leave your machine, no raw screen text or audio",
      },
      {
        value: "paranoid",
        label: "Paranoid",
        hint: "Zero cloud calls — needs Whisper + Ollama installed or nothing works",
      },
    ],
    initialValue: current,
  }));
}

export async function stepModel(existing, label = "AI model for HUD analysis") {
  const current = existing.AGENT_MODEL || "google/gemini-2.5-flash-lite";
  const knownModels = [
    "google/gemini-2.5-flash-lite",
    "google/gemini-2.5-flash",
    "anthropic/claude-3.5-haiku",
  ];

  const choice = guard(await p.select({
    message: label,
    options: [
      { value: "google/gemini-2.5-flash-lite", label: "gemini-2.5-flash-lite", hint: "Fastest, cheapest — recommended" },
      { value: "google/gemini-2.5-flash", label: "gemini-2.5-flash", hint: "Better quality, slightly more expensive" },
      { value: "anthropic/claude-3.5-haiku", label: "claude-3.5-haiku", hint: "Anthropic, fast and affordable" },
      { value: "custom", label: "Custom", hint: "Enter any model ID from openrouter.ai/models" },
    ],
    initialValue: knownModels.includes(current) ? current : "custom",
  }));

  if (choice === "custom") {
    return guard(await p.text({
      message: "Model ID (from openrouter.ai/models)",
      placeholder: "provider/model-name",
      defaultValue: knownModels.includes(current) ? "" : current,
      validate: (val) => {
        if (!val) return "Model ID is required";
        if (!val.includes("/")) return "Format: provider/model-name";
      },
    }));
  }

  return choice;
}

export async function stepAgent(existing, label = "Bare agent") {
  const current = existing.SINAIN_AGENT || "claude";
  return guard(await p.select({
    message: label,
    options: [
      { value: "claude", label: "Claude Code", hint: "Calls sinain tools directly — recommended" },
      { value: "openclaude", label: "OpenClaude", hint: "Claude Code clone, local-first (Ollama/OpenAI-compat)" },
      { value: "codex", label: "Codex", hint: "Calls sinain tools directly" },
      { value: "goose", label: "Goose", hint: "Calls sinain tools directly" },
      { value: "junie", label: "Junie", hint: "JetBrains IDE agent" },
      { value: "aider", label: "Aider", hint: "Receives text only, no tool access" },
    ],
    initialValue: current,
  }));
}
