#!/usr/bin/env node
process.title = "⭕️ sling";
process.env.PI_CODING_AGENT = "true";
process.emitWarning = () => {};

// Pin the coding-agent config dir to ~/.sling/agent regardless of how the
// installed bundle resolves piConfig at runtime. Without this, a published
// bundle that loses coding-agent's own package.json falls back to APP_NAME=pi
// and writes/reads from ~/.pi/agent, splitting state from the dev tree.
// We set BOTH possible env-var names because the resolved name depends on
// APP_NAME, which we don't know until coding-agent's config module loads.
// Whichever name matches wins; the other is harmless. Existing values from
// the user's shell are respected.
import { homedir as _homedir } from "node:os";
import { join as _join } from "node:path";
const _slingAgentDir = _join(_homedir(), ".sling", "agent");
process.env.SLING_CODING_AGENT_DIR ||= _slingAgentDir;
process.env.PI_CODING_AGENT_DIR ||= _slingAgentDir;

import { setBedrockProviderModule } from "@earendil-works/pi-ai";
import { bedrockProviderModule } from "@earendil-works/pi-ai/bedrock-provider";
import { EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, cpSync } from "node:fs";
import { homedir } from "node:os";
import { spawn } from "node:child_process";
import readline from "node:readline/promises";
import semver from "semver";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const IS_DEV = process.env.SLING_DEV === "1";

const productionOnly = async (fn, ...args) => (IS_DEV ? undefined : fn(...args));
const devOnly = async (fn, ...args) => (IS_DEV ? fn(...args) : undefined);

function findSlingPkg() {
  let dir = __dirname;
  while (dir !== path.dirname(dir)) {
    const p = path.join(dir, "package.json");
    if (existsSync(p)) {
      const j = JSON.parse(readFileSync(p, "utf-8"));
      if (j.slingVersion) return j;
    }
    dir = path.dirname(dir);
  }
  throw new Error("Could not locate sling package.json with slingVersion");
}
const { slingVersion: SLING_VERSION } = findSlingPkg();

const { main } = IS_DEV
  ? await import(path.resolve(__dirname, "packages/coding-agent/src/main.ts"))
  : await import("@earendil-works/pi-coding-agent");

function applyNoEnv(args) {
  const filtered = [];
  let noEnv = false;
  for (const arg of args) {
    if (arg === "--no-env") noEnv = true;
    else filtered.push(arg);
  }

  if (noEnv) {
    const keys = [
      "ANTHROPIC_API_KEY", "ANTHROPIC_OAUTH_TOKEN", "OPENAI_API_KEY", "GEMINI_API_KEY",
      "GROQ_API_KEY", "CEREBRAS_API_KEY", "XAI_API_KEY", "OPENROUTER_API_KEY", "ZAI_API_KEY",
      "MISTRAL_API_KEY", "MINIMAX_API_KEY", "MINIMAX_CN_API_KEY", "AI_GATEWAY_API_KEY",
      "OPENCODE_API_KEY", "COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN",
      "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_CLOUD_PROJECT", "GCLOUD_PROJECT",
      "GOOGLE_CLOUD_LOCATION", "AWS_PROFILE", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY",
      "AWS_SESSION_TOKEN", "AWS_REGION", "AWS_DEFAULT_REGION", "AWS_BEARER_TOKEN_BEDROCK",
      "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI", "AWS_CONTAINER_CREDENTIALS_FULL_URI",
      "AWS_WEB_IDENTITY_TOKEN_FILE", "AZURE_OPENAI_API_KEY", "AZURE_OPENAI_BASE_URL",
      "AZURE_OPENAI_RESOURCE_NAME"
    ];
    for (const key of keys) delete process.env[key];
    console.log("Running without API keys...");
  }

  return filtered;
}

async function runCommand(command, args) {
  return new Promise((resolve, reject) => {
    const proc = spawn(command, args, { stdio: "inherit", shell: false });
    proc.on("close", (code) => {
      if (code !== 0) reject(new Error(`Command failed with code ${code}`));
      else resolve();
    });
    proc.on("error", reject);
  });
}

// Only run on a "bare" interactive launch. Skip for --version/-v, --help/-h,
// and any built-in subcommand (install, remove, uninstall, update, list,
// config) so utility invocations don't trigger first-run installs.
const NON_INTERACTIVE_SUBCOMMANDS = new Set([
  "install", "remove", "uninstall", "update", "list", "config",
]);

function isTruthyFlag(value) {
  if (!value) return false;
  const normalized = value.toLowerCase();
  return value === "1" || normalized === "true" || normalized === "yes";
}

function shouldSkipAutoInstall(argv) {
  if (process.env.SKIP_SLING_AUTO_INSTALL) return true;
  for (const a of argv) {
    if (a === "--version" || a === "-v" || a === "--help" || a === "-h") return true;
  }
  for (const a of argv) {
    if (!a.startsWith("-")) return NON_INTERACTIVE_SUBCOMMANDS.has(a);
  }
  return false;
}

function shouldUpdateDefaultPackage(argv) {
  return argv.includes("--update-package");
}

function hasUpdateRequest(argv) {
  return argv.includes("-y") || argv.includes("--yes");
}

function stripUpdateRequestFlags(argv) {
  return argv.filter((arg) => arg !== "-y" && arg !== "--yes");
}

async function fetchLatestNpmVersion() {
  try {
    const res = await fetch("https://registry.npmjs.org/@psnext/slingcli/latest", {
      signal: AbortSignal.timeout(10000),
      headers: { accept: "application/json" },
    });
    if (!res.ok) return null;
    const data = await res.json();
    return typeof data?.version === "string" ? data.version : null;
  } catch {
    return null;
  }
}

async function fetchRemoteAppConfig() {
  try {
    const res = await fetch(
      "https://raw.githubusercontent.com/psnext/slingcli/main/sling-app-config.json",
      {
        signal: AbortSignal.timeout(5000),
        headers: { accept: "application/json" },
      },
    );
    if (!res.ok) return null;
    const data = await res.json();
    if (!data || typeof data !== "object") return null;

    const minVersion = typeof data.minVersion === "string" ? data.minVersion : null;
    const updateMessage = typeof data.updateMessage === "string" ? data.updateMessage : null;

    if (!minVersion || !semver.valid(minVersion)) return null;

    return {
      minVersion,
      updateMessage,
    };
  } catch {
    return null;
  }
}

function stripSlingWrapperFlags(argv) {
  // Wrapper-only flags. They should not be forwarded to pi-coding-agent,
  // which does not recognize them.
  return argv.filter(
    (arg) =>
      arg !== "--update-package" &&
      arg !== "-y" &&
      arg !== "--yes",
  );
}

function syncBundledSkills() {
  if (shouldSkipAutoInstall(process.argv.slice(2))) return;

  const srcSkillsDir = IS_DEV
    ? path.resolve(__dirname, "skills")
    : path.resolve(__dirname, "../skills");

  if (!existsSync(srcSkillsDir)) return;

  const destSkillsDir = path.resolve(homedir(), ".sling", "agent", "skills");
  mkdirSync(destSkillsDir, { recursive: true });

  for (const entry of readdirSync(srcSkillsDir, { withFileTypes: true })) {
    if (!entry.isDirectory()) continue;
    const dest = path.join(destSkillsDir, entry.name);
    try {
    cpSync(path.join(srcSkillsDir, entry.name), dest, { recursive: true, force: true });
    } catch (err) {
    }
  }
}

async function checkAndInstallPackages(argv = process.argv.slice(2)) {
  if (shouldSkipAutoInstall(argv)) return;

  const configPath = IS_DEV ? path.resolve(__dirname, "./sling-default-packages.json") : path.resolve(__dirname, "../sling-default-packages.json");
  if (!existsSync(configPath)) return;

  let installed = new Set();
  let unsupported = new Set();
  const settingsPath = path.resolve(homedir(), ".sling/agent/settings.json");
  if (existsSync(settingsPath)) {
    try {
      const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
      for (const p of settings.packages || []) {
        const source = typeof p === "object" && p !== null ? p.source : p;
        installed.add(source);
      }
      for (const p of settings.unsupportedPackages || []) {
        const source = typeof p === "object" && p !== null ? p.source : p;
        unsupported.add(source);
      }
    } catch {}
  }

  try {

    //uninstall unsupported packages
    for (const u of unsupported) {
      console.log(`Removing unsupported package ${u}...`);
      try {
        await runCommand(process.execPath, [__filename, "remove", "-g", u]);
        console.log(`✓ Removed ${u}`);
      } catch (err) {
        console.error(`✗ Failed to remove ${u}:`, err.message);
        console.error(`You may need to manually remove it with: sling remove ${u}`);
      }
    }

    //remove unsupported packages from installed set
    for (const u of unsupported) installed.delete(u);

    const config = JSON.parse(readFileSync(configPath, "utf-8"));
    const updateDefaultPackage = shouldUpdateDefaultPackage(argv);

    if (updateDefaultPackage) {
      try {
        // Update extension packages only. Running one update command here keeps
        // wrapper/package-manager behavior consistent with manual `sling update`.
        await runCommand(process.execPath, [__filename, "update", "--extensions"]);
        console.log("✓ Updated installed packages");
      } catch (err) {
        console.error("✗ Failed to update installed packages:", err.message);
        console.error("You can manually update them later with: sling update --extensions");
      }
    }
    const packages = (config.packages || []).filter((p) => {
      const source = typeof p === "object" && p !== null ? p.source : p;
      return !installed.has(source);
    });
    if (packages.length === 0) return;

    console.log("\nInstalling default packages...");
    for (const pkg of packages) {
      console.log(`Installing ${pkg}...`);
      try {
        // Pass -g so promptInstallScopeIfNeeded skips the scope prompt and
        // delegates to pi, which installs globally by default.
        await runCommand(process.execPath, [__filename, "install", "-g", pkg]);
        console.log(`✓ Installed ${pkg}`);
      } catch (err) {
        console.error(`✗ Failed to install ${pkg}:`, err.message);
        console.error(`You can manually install it later with: sling install ${pkg}`);
      }
    }
  } catch (err) {
    console.error("Failed to read default packages config:", err.message);
  }
}

async function checkForUpdate(args) {
  if (process.env.SKIP_SLING_AUTO_UPDATE_CHECK) return;
  if (args.includes("--version") || args.includes("-v")) return;
  if (args.includes("install")) return;

  const autoYes = hasUpdateRequest(args);

  const [latestVersion, remoteConfig] = await Promise.all([
    fetchLatestNpmVersion(),
    fetchRemoteAppConfig(),
  ]);

  if (!latestVersion || !semver.gt(latestVersion, SLING_VERSION)) return;

  // Gate 2: force if current < minVersion (user is below the minimum required)
  const shouldForceUpdate = remoteConfig && semver.lt(SLING_VERSION, remoteConfig.minVersion);

  if (!shouldForceUpdate && !autoYes && (!process.stdin.isTTY || !process.stdout.isTTY)) return;

  if (shouldForceUpdate) {
    if (remoteConfig?.updateMessage) console.log(remoteConfig.updateMessage);
    console.log(`Required version ${latestVersion} detected. Updating now...`);
  } else {
    console.log(`A new version ${latestVersion} is available (current ${SLING_VERSION}).`);
  }

  let shouldUpdate = autoYes || shouldForceUpdate;
  if (!shouldUpdate) {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    let answer;
    try {
      answer = (await rl.question("Update now? [Y/n] ")).trim().toLowerCase();
    } finally {
      rl.close();
    }
    shouldUpdate = !answer.startsWith("n");
  }

  if (!shouldUpdate) return;

  console.log("Installing @psnext/slingcli@" + latestVersion + "...");
  try {
    await runCommand("npm", ["install", "-g", "@psnext/slingcli@latest"]);
  } catch {
    console.error("✗ Auto-update failed. Run manually:");
    console.error("  npm install -g @psnext/slingcli@latest");
    console.error("  (you may need sudo)");
    if (shouldForceUpdate) process.exit(1);
    return;
  }

  console.log("✓ Updated to " + latestVersion + ". Restarting sling...");
  const restartArgs = stripUpdateRequestFlags(args);
  try {
    await runCommand(process.execPath, [__filename, ...restartArgs]);
    process.exit(0);
  } catch {
    console.error("✓ Updated, but failed to restart automatically.");
    console.error("  Please run: sling " + restartArgs.join(" "));
    process.exit(0);
  }
}

// `sling install <pkg>` defaults to global (~/.sling/agent). When run from a
// TTY without an explicit scope flag, prompt for global vs local. Pi has no
// `-g/--global` flag because global is its default; we only inject `-l` when
// the user picks local. Non-TTY (CI/scripts) keeps the current default.
async function promptInstallScopeIfNeeded(args) {
  let isInstall = false;
  for (const a of args) {
    if (!a.startsWith("-")) {
      isInstall = a === "install";
      break;
    }
  }
  if (!isInstall) return args;

  let hasScope = false;
  for (const a of args) {
    if (a === "-l" || a === "--local" || a === "-g" || a === "--global") {
      hasScope = true;
      break;
    }
  }

  const next = args.slice();
  if (!hasScope && process.stdin.isTTY && process.stdout.isTTY) {
    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
    let answer = "";
    try {
      answer = (await rl.question("Install scope? [G]lobal (~/.sling) / [l]ocal (./.sling) [G]: ")).trim().toLowerCase();
    } finally {
      rl.close();
    }
    if (answer === "l" || answer === "local") {
      next.push("-l");
    } else if (answer !== "" && answer !== "g" && answer !== "global") {
      console.error("sling: unrecognized scope '" + answer + "', defaulting to global");
    }
  }
  // Strip -g/--global before delegating; pi treats global as default.
  return next.filter((a) => a !== "-g" && a !== "--global");
}

function updateSettings() {
  const configPath = path.resolve(homedir(), ".sling/agent/settings.json");
  mkdirSync(path.dirname(configPath), { recursive: true });

  let config = { customItems: [] };
  if (existsSync(configPath)) {
    try {
      config = JSON.parse(readFileSync(configPath, "utf-8"));
    } catch (err) {
      console.error("Failed to update settings.json:", err.message);
    }
  }

  //disable startup notices
  config.enableInstallTelemetry = false;
  config.collapseChangelog = true;

  config.powerline ={...{
    "preset": "default",
    "customItems": [
    ]
  },...config.powerline||{}};

  const idx = config.powerline.customItems.findIndex((item) => item.id === "credit")
  if (idx!==-1) config.powerline.customItems.splice(idx,1);
  config.powerline.customItems.push({
    "id": "credit",
    "statusKey": "ss-credit",
    "position": "right",
    "prefix": "Credit",
    "color": "#febc38",
    "hideWhenMissing": true,
    "excludeFromExtensionStatuses": true
  });
  writeFileSync(configPath, JSON.stringify(config, null, 2));
}
async function run() {
  const rawArgs = process.argv.slice(2);
  if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
    console.log(SLING_VERSION);
    process.exit(0);
  }
  setGlobalDispatcher(new EnvHttpProxyAgent({ bodyTimeout: 0, headersTimeout: 0 }));
  await productionOnly(checkForUpdate, rawArgs);
  syncBundledSkills();
  await checkAndInstallPackages(rawArgs);
  setBedrockProviderModule(bedrockProviderModule);
  let args = applyNoEnv(stripSlingWrapperFlags(rawArgs));
  args = await promptInstallScopeIfNeeded(args);

  // Built-in subcommands are dispatched by parsePackageCommand, which only
  // inspects args[0]. Prepending "-e <path> --provider slingshot" pushes the
  // subcommand off args[0] and silently falls through to interactive mode.
  const BUILTIN_SUBCOMMANDS = new Set(["install", "remove", "uninstall", "update", "list", "config"]);
  const firstPositional = args.find((a) => !a.startsWith("-"));
  if (firstPositional && BUILTIN_SUBCOMMANDS.has(firstPositional)) {
    await main(args);
  } else {
    updateSettings();
    if (existsSync(path.resolve("./.slingshot/skills"))) {
      args.push("--skill", path.resolve("./.slingshot/skills"));
    }
    if (existsSync(path.resolve(homedir(), ".slingshot/skills"))) {
      args.push("--skill", path.resolve(homedir(), ".slingshot/skills"));
    }
    const extensionPath = IS_DEV
      ? path.resolve(__dirname, "slingshot")
      : path.resolve(__dirname, "../slingshot");
    await main(["-e", extensionPath, "--provider", "slingshot", ...args]);
  }
}

run().catch((err) => {
  console.error(err instanceof Error ? err.message : String(err));
  process.exit(1);
});
