#!/bin/sh

set -eu
umask 077

PI_MIN_VERSION="0.81.1"
PI_NPM_PACKAGE="${PI_NPM_PACKAGE:-@earendil-works/pi-coding-agent@0.81.1}"
INSTALLER_VERSION="0.1.14"
AGENT_SOURCE="${PI_FRONTEND_AGENT_SOURCE:-npm:pi-frontend-kpc-agent}"
OBSERVABLE_AGENT_SOURCE="${PI_OBSERVABLE_AGENT_SOURCE:-npm:pi-observable-coding-agent}"
HOME_DIR="${HOME:-}"
AGENT_DIR="${PI_CODING_AGENT_DIR:-${HOME_DIR}/.pi/agent}"
MODELS_PATH="${AGENT_DIR}/models.json"
OVERWRITE_MODELS="${PI_MODELS_OVERWRITE:-0}"

log() {
  printf '[pi-frontend-kpc-agent] %s\n' "$*"
}

die() {
  printf '[pi-frontend-kpc-agent] ERROR: %s\n' "$*" >&2
  exit 1
}

[ -n "$HOME_DIR" ] || die "HOME is unavailable. Set HOME to your user directory and retry."

case "$AGENT_DIR" in
  /*) ;;
  *) die "PI_CODING_AGENT_DIR must be an absolute path: ${AGENT_DIR}" ;;
esac

case "$OVERWRITE_MODELS" in
  0|1) ;;
  *) die "PI_MODELS_OVERWRITE must be 0 or 1" ;;
esac

command -v node >/dev/null 2>&1 || die "Node.js is required. Install Node.js >= 22.19.0 first."
command -v npm >/dev/null 2>&1 || die "npm is required. Install Node.js >= 22.19.0 first."

if ! node -e '
const actual = process.versions.node.split(".").map(Number);
const required = [22, 19, 0];
const supported = actual.some((value, index) => value > required[index] && actual.slice(0, index).every((part, partIndex) => part === required[partIndex])) || actual.every((value, index) => value === required[index]);
process.exit(supported ? 0 : 1);
'; then
  die "Node.js $(node --version) is too old. Run: nvm install 22.19.0 && nvm use 22.19.0"
fi

read_pi_version() {
  PI_VERSION="$(pi --version 2>/dev/null)" || die "The pi executable exists but could not run. Check your Pi installation."
  if ! node -e '
const parse = (value) => {
  const match = value.match(/(?:^|[^0-9])(\d+)\.(\d+)\.(\d+)(?:[^0-9]|$)/);
  return match ? match.slice(1).map(Number) : undefined;
};
const actual = parse(process.argv[1]);
const required = parse(process.argv[2]);
if (!actual || !required) process.exit(1);
for (let index = 0; index < required.length; index += 1) {
  if (actual[index] !== required[index]) process.exit(actual[index] > required[index] ? 0 : 1);
}
process.exit(0);
' "$PI_VERSION" "$PI_MIN_VERSION"; then
    die "Pi ${PI_MIN_VERSION} or newer is required, but found: ${PI_VERSION}. Update with: npm install -g --ignore-scripts ${PI_NPM_PACKAGE}"
  fi
}

NPM_CACHE_DIR=""
LOCK_DIR=""
cleanup() {
  if [ -n "$LOCK_DIR" ]; then
    rmdir -- "$LOCK_DIR" 2>/dev/null || true
  fi
  if [ -n "$NPM_CACHE_DIR" ]; then
    rm -rf -- "$NPM_CACHE_DIR" || true
  fi
}
on_signal() {
  trap - EXIT
  cleanup
  exit 130
}
trap cleanup EXIT
trap on_signal HUP INT TERM

NPM_CACHE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/pi-frontend-kpc-agent-npm.XXXXXX")"

if [ -e "$AGENT_DIR" ]; then
  [ -d "$AGENT_DIR" ] || die "Agent target exists but is not a directory: ${AGENT_DIR}"
else
  mkdir -p "$AGENT_DIR"
  chmod 700 "$AGENT_DIR"
fi

LOCK_PATH="${AGENT_DIR}/.pi-frontend-kpc-agent-install.lock"
if ! mkdir "$LOCK_PATH" 2>/dev/null; then
  die "Another installer appears to be using ${AGENT_DIR}. Remove ${LOCK_PATH} only if no installer is running."
fi
LOCK_DIR="$LOCK_PATH"

if command -v pi >/dev/null 2>&1; then
  read_pi_version
  log "Pi already installed: ${PI_VERSION}"
else
  log "Installing Pi..."
  npm_config_cache="$NPM_CACHE_DIR" npm install --global --ignore-scripts "$PI_NPM_PACKAGE"
  hash -r 2>/dev/null || true
  command -v pi >/dev/null 2>&1 || die "Pi was installed but is not on PATH. Reopen the terminal and run this script again."
  read_pi_version
  log "Pi installed: ${PI_VERSION}"
fi

log "Installing Agent Package from ${AGENT_SOURCE}..."
PI_CODING_AGENT_DIR="$AGENT_DIR" npm_config_cache="$NPM_CACHE_DIR" npm_config_ignore_scripts=true pi install "$AGENT_SOURCE"

log "Installing Observatory Package from ${OBSERVABLE_AGENT_SOURCE}..."
PI_CODING_AGENT_DIR="$AGENT_DIR" npm_config_cache="$NPM_CACHE_DIR" npm_config_ignore_scripts=true pi install "$OBSERVABLE_AGENT_SOURCE"

MODELS_PATH="$MODELS_PATH" PI_MODELS_OVERWRITE="$OVERWRITE_MODELS" node <<'NODE'
const { constants } = require("node:fs");
const { chmod, copyFile, lstat, open, readFile, rename, rm } = require("node:fs/promises");

const providerId = "company-openai";
const providerTemplate = {
  name: "星流",
  baseUrl: process.env.PI_COMPANY_OPENAI_BASE_URL || "http://kspmas.ksyun.com/v1",
  api: "openai-completions",
  apiKey: "$COMPANY_LLM_API_KEY",
  models: [
    {
      id: "qwen3.6-plus",
      name: "qwen3.6-plus",
      reasoning: false,
      input: ["text", "image"],
      contextWindow: 128000,
      maxTokens: 16384,
    },
    {
      id: "glm-5.2",
      name: "glm-5.2",
      reasoning: false,
      input: ["text"],
      contextWindow: 128000,
      maxTokens: 16384,
    },
  ],
};

function stripJsonComments(source) {
  return source
    .replace(/"(?:\\.|[^"\\])*"|\/\/[^\n]*/g, (match) => (match[0] === '"' ? match : ""))
    .replace(/"(?:\\.|[^"\\])*"|,(\s*[}\]])/g, (match, tail) => tail ?? (match[0] === '"' ? match : ""));
}

function isRecord(value) {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

async function main() {
  const modelsPath = process.env.MODELS_PATH;
  if (!modelsPath) throw new Error("MODELS_PATH is unavailable");
  const overwrite = process.env.PI_MODELS_OVERWRITE === "1";
  const stats = await lstat(modelsPath).catch((error) => {
    if (error?.code === "ENOENT") return undefined;
    throw error;
  });
  if (stats?.isSymbolicLink()) {
    throw new Error(`Refusing to replace symbolic-link models file: ${modelsPath}`);
  }
  if (stats && !stats.isFile()) {
    throw new Error(`Refusing to read non-regular models file: ${modelsPath}`);
  }

  const existingText = stats ? await readFile(modelsPath, "utf8") : "{\"providers\":{}}";
  let existing;
  try {
    existing = JSON.parse(stripJsonComments(existingText));
  } catch (error) {
    throw new Error(`Existing models.json is invalid: ${error instanceof Error ? error.message : error}`);
  }
  if (!isRecord(existing)) throw new Error("Existing models.json root must be an object");
  const providers = existing.providers === undefined ? {} : existing.providers;
  if (!isRecord(providers)) throw new Error("Existing models.json providers must be an object");

  if (Object.hasOwn(providers, providerId) && !overwrite) {
    process.stdout.write(`[pi-frontend-kpc-agent] Preserved existing provider ${providerId} in ${modelsPath}\n`);
    return;
  }

  const next = {
    ...existing,
    providers: { ...providers, [providerId]: providerTemplate },
  };
  let backupPath;
  if (stats) {
    backupPath = `${modelsPath}.bak-${Date.now()}-${process.pid}`;
    await copyFile(modelsPath, backupPath, constants.COPYFILE_EXCL);
    await chmod(backupPath, 0o600);
  }
  const temporaryPath = `${modelsPath}.tmp-${process.pid}`;
  try {
    const handle = await open(temporaryPath, "wx", 0o600);
    try {
      await handle.writeFile(`${JSON.stringify(next, null, 2)}\n`, "utf8");
      await handle.sync();
    } finally {
      await handle.close();
    }
    await rename(temporaryPath, modelsPath);
    await chmod(modelsPath, 0o600);
  } finally {
    await rm(temporaryPath, { force: true });
  }
  process.stdout.write(
    `[pi-frontend-kpc-agent] ${stats ? "Updated" : "Created"} ${modelsPath}${backupPath ? ` (backup: ${backupPath})` : ""}\n`,
  );
}

main().catch((error) => {
  process.stderr.write(`[pi-frontend-kpc-agent] ERROR: ${error instanceof Error ? error.message : error}\n`);
  process.exitCode = 1;
});
NODE

case "${PI_COMPANY_OPENAI_BASE_URL:-http://kspmas.ksyun.com/v1}" in
  http://*) log "Warning: the configured model endpoint uses HTTP. Use it only on a trusted internal network, or set PI_COMPANY_OPENAI_BASE_URL to an HTTPS endpoint." ;;
esac

if [ -z "${COMPANY_LLM_API_KEY:-}" ]; then
  log "Model config installed. Set COMPANY_LLM_API_KEY before starting Pi, or use /login for company-openai."
else
  log "COMPANY_LLM_API_KEY is available in this shell."
fi

log "Installation complete. Enter a project directory and run: pi --model company-openai/qwen3.6-plus"
