#!/usr/bin/env bash
set -euo pipefail

RUN_ENROLL=0
RUN_ONBOARD=0
for arg in "$@"; do
  case "$arg" in
    --enroll) RUN_ENROLL=1 ;;
    --onboard) RUN_ONBOARD=1 ;;
    -h|--help)
      printf 'usage: install.sh [--enroll] [--onboard]\n'
      exit 0
      ;;
    *)
      printf 'unknown install option: %s\n' "$arg" >&2
      exit 2
      ;;
  esac
done

VEP_URL="${VEP_URL:-https://vep.live}"
DEFAULT_HOME="${HOME:-.}"
INSTALL_DIR="${CLAUDE_SKILLS_DIR:-${DEFAULT_HOME}/.claude/skills}/twin-brain"
CONFIG_DIR="${TWIN_BRAIN_CONFIG_DIR:-${DEFAULT_HOME}/.config/vep}"
CONFIG_FILE="${TWIN_BRAIN_CONFIG_FILE:-${CONFIG_DIR}/twin-brain.env}"
SETTINGS_FILE="${TWIN_BRAIN_CLAUDE_SETTINGS_FILE:-${DEFAULT_HOME}/.claude/settings.json}"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT

PINNED_TWIN_BRAIN_CONNECTOR_KEYS='{"tb-ed25519-v1":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAbCv/Tv/qVizuxxW3iqcgYzEqmPaYY2usogbR1EEL70A=\n-----END PUBLIC KEY-----"}'

need_cmd() {
  if ! command -v "$1" >/dev/null 2>&1; then
    printf 'missing required command: %s\n' "$1" >&2
    exit 2
  fi
}

need_cmd curl
need_cmd node
need_cmd tar
need_cmd awk
need_cmd sort
need_cmd diff

redeem_enrollment() {
  if [ -z "${TWIN_BRAIN_ENROLLMENT_TOKEN:-}" ]; then
    printf 'TWIN_BRAIN_ENROLLMENT_TOKEN is required for --enroll\n' >&2
    exit 2
  fi
  case "$TWIN_BRAIN_ENROLLMENT_TOKEN" in
    vte_*) ;;
    *)
      printf 'TWIN_BRAIN_ENROLLMENT_TOKEN must use the vte_ prefix\n' >&2
      exit 64
      ;;
  esac

  payload="$(TWIN_BRAIN_ENROLLMENT_TOKEN="$TWIN_BRAIN_ENROLLMENT_TOKEN" node -e 'process.stdout.write(JSON.stringify({ enrollment_token: process.env.TWIN_BRAIN_ENROLLMENT_TOKEN }))')"
  response="$(curl -fsSL \
    --connect-timeout "${TWIN_BRAIN_CONNECT_TIMEOUT:-3}" \
    --max-time "${TWIN_BRAIN_MAX_TIME:-30}" \
    --retry "${TWIN_BRAIN_RETRY_COUNT:-1}" \
    --retry-delay 1 \
    --retry-connrefused \
    -H "Content-Type: application/json" \
    --data "$payload" \
    "${VEP_URL%/}/api/twin-brain/v1/enrollments/redeem")"
  api_key="$(printf '%s' "$response" | node -e 'let d=""; process.stdin.on("data", c => d += c); process.stdin.on("end", () => { const o = JSON.parse(d); process.stdout.write(String(o.api_key || "")); });')"
  case "$api_key" in
    vtk_*) ;;
    *)
      printf 'enrollment redemption did not return a vtk_ API key\n' >&2
      exit 65
      ;;
  esac

  mkdir -p "$CONFIG_DIR"
  old_umask="$(umask)"
  umask 077
  {
    printf 'VEP_URL=%s\n' "$VEP_URL"
    printf 'TWIN_BRAIN_API_KEY=%s\n' "$api_key"
  } > "$CONFIG_FILE"
  umask "$old_umask"
  printf 'Enrollment redeemed; wrote local Twin Brain config to %s\n' "$CONFIG_FILE"
}

manifest="$TMP_DIR/manifest.json"
package="$TMP_DIR/twin-brain-connector.tar.gz"
allowlist="$TMP_DIR/allowlist.txt"
tar_entries="$TMP_DIR/tar-entries.txt"

curl -fsSL "${VEP_URL%/}/api/twin-brain/v1/skill/manifest" -o "$manifest"
curl -fsSL "${VEP_URL%/}/api/twin-brain/v1/skill/package" -o "$package"

PINNED_TWIN_BRAIN_CONNECTOR_KEYS="$PINNED_TWIN_BRAIN_CONNECTOR_KEYS" node - "$manifest" "$package" "$allowlist" <<'NODE'
const crypto = require('node:crypto');
const fs = require('node:fs');

const [manifestPath, packagePath, allowlistPath] = process.argv.slice(2);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
const packageBuffer = fs.readFileSync(packagePath);
const pinnedKeys = JSON.parse(process.env.PINNED_TWIN_BRAIN_CONNECTOR_KEYS || '{}');

function sha256(data) {
  return crypto.createHash('sha256').update(data).digest('hex');
}

function canonicalize(value) {
  if (value === null) return 'null';
  if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`;
  if (typeof value === 'object') {
    return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalize(value[key])}`).join(',')}}`;
  }
  if (typeof value === 'string') return JSON.stringify(value);
  if (typeof value === 'number') {
    if (!Number.isFinite(value)) throw new Error('non-finite number in manifest');
    return JSON.stringify(value);
  }
  if (typeof value === 'boolean') return value ? 'true' : 'false';
  throw new Error(`unsupported manifest value type: ${typeof value}`);
}

function unsignedManifest(input) {
  const out = { ...input };
  delete out.integrity_sha256;
  delete out.signature;
  return out;
}

if (!manifest.key_id) throw new Error('connector manifest missing key_id');
const publicKeyPem = pinnedKeys[manifest.key_id];
if (!publicKeyPem) throw new Error(`connector manifest key_id is not pinned: ${manifest.key_id}`);
if (manifest.signature_algorithm !== 'Ed25519') throw new Error('connector manifest signature_algorithm mismatch');
if (manifest.signature_payload !== 'canonical_manifest_v1') throw new Error('connector manifest signature_payload mismatch');
if (!manifest.signature) throw new Error('connector manifest missing signature');
if (!Array.isArray(manifest.package_allowlist) || !manifest.package_allowlist.length) {
  throw new Error('connector manifest package_allowlist missing');
}
if (manifest.package_sha256 !== sha256(packageBuffer)) throw new Error('connector package sha256 mismatch');

const unsigned = unsignedManifest(manifest);
const integrity = sha256(Buffer.from(canonicalize(unsigned), 'utf8'));
if (manifest.integrity_sha256 !== integrity) throw new Error('connector manifest integrity mismatch');

const verified = crypto.verify(
  null,
  Buffer.from(canonicalize(unsigned), 'utf8'),
  crypto.createPublicKey(publicKeyPem.replace(/\\n/g, '\n')),
  Buffer.from(String(manifest.signature), 'base64'),
);
if (!verified) throw new Error('connector manifest signature mismatch');

fs.writeFileSync(allowlistPath, `${manifest.package_allowlist.slice().sort().join('\n')}\n`);
NODE

tar -tzf "$package" | sort > "$tar_entries"
if ! diff -u "$allowlist" "$tar_entries" >/dev/null; then
  printf 'connector package tar entries do not match signed allowlist\n' >&2
  diff -u "$allowlist" "$tar_entries" >&2 || true
  exit 1
fi

tar -xzf "$package" -C "$TMP_DIR"
PINNED_TWIN_BRAIN_CONNECTOR_KEYS="$PINNED_TWIN_BRAIN_CONNECTOR_KEYS" node - "$manifest" "$TMP_DIR" <<'NODE'
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');

const [manifestPath, root] = process.argv.slice(2);
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));

function sha256(data) {
  return crypto.createHash('sha256').update(data).digest('hex');
}

const required = new Set([
  'twin-brain/SKILL.md',
  'twin-brain/manifest.json',
  'twin-brain/twin-brain.ps1',
  'twin-brain/twin-brain.sh',
  'twin-brain/user-prompt-submit.mjs',
]);
for (const entry of required) {
  if (!fs.existsSync(path.join(root, entry))) throw new Error(`connector package missing ${entry}`);
}
for (const file of manifest.files || []) {
  const fullPath = path.join(root, file.path);
  const body = fs.readFileSync(fullPath);
  if (sha256(body) !== file.sha256) throw new Error(`connector file sha256 mismatch: ${file.path}`);
  if (body.length !== file.size) throw new Error(`connector file size mismatch: ${file.path}`);
  const mode = fs.statSync(fullPath).mode;
  if (file.executable && (mode & 0o111) === 0) {
    throw new Error(`connector file not executable: ${file.path}`);
  }
}
NODE

HOOK_PATH="$INSTALL_DIR/user-prompt-submit.mjs"
node "$TMP_DIR/twin-brain/user-prompt-submit.mjs" --validate-settings "$SETTINGS_FILE" "$HOOK_PATH"

install_parent="$(dirname "$INSTALL_DIR")"
mkdir -p "$install_parent"
stage_dir="$(mktemp -d "$install_parent/.twin-brain.stage.XXXXXX")"
cp "$TMP_DIR/twin-brain/SKILL.md" "$stage_dir/SKILL.md"
cp "$TMP_DIR/twin-brain/twin-brain.ps1" "$stage_dir/twin-brain.ps1"
cp "$TMP_DIR/twin-brain/twin-brain.sh" "$stage_dir/twin-brain.sh"
cp "$TMP_DIR/twin-brain/user-prompt-submit.mjs" "$stage_dir/user-prompt-submit.mjs"
cp "$TMP_DIR/twin-brain/manifest.json" "$stage_dir/manifest.json"
chmod 700 "$stage_dir/twin-brain.sh" "$stage_dir/user-prompt-submit.mjs"

backup_root=""
if [ -e "$INSTALL_DIR" ] || [ -L "$INSTALL_DIR" ]; then
  backup_root="$(mktemp -d "$install_parent/.twin-brain.backup.XXXXXX")"
  mv "$INSTALL_DIR" "$backup_root/original"
fi

if ! mv "$stage_dir" "$INSTALL_DIR"; then
  if [ -n "$backup_root" ]; then
    mv "$backup_root/original" "$INSTALL_DIR"
    rmdir "$backup_root"
  fi
  exit 1
fi

if ! node "$INSTALL_DIR/user-prompt-submit.mjs" --install-settings "$SETTINGS_FILE" "$HOOK_PATH"; then
  rm -rf "$INSTALL_DIR"
  if [ -n "$backup_root" ]; then
    mv "$backup_root/original" "$INSTALL_DIR"
    rmdir "$backup_root"
  fi
  printf 'Claude settings were not changed; restored the previous Twin Brain connector\n' >&2
  exit 1
fi

if [ -n "$backup_root" ]; then
  rm -rf "$backup_root"
fi

printf 'Installed signed VEP Twin Brain connector to %s\n' "$INSTALL_DIR"
printf 'Installed the fail-open UserPromptSubmit hook without replacing existing Claude settings or hooks\n'

if [ "$RUN_ENROLL" = "1" ] || [ -n "${TWIN_BRAIN_ENROLLMENT_TOKEN:-}" ]; then
  redeem_enrollment
  printf 'Configured read-only key locally; run: %s/twin-brain.sh doctor\n' "$INSTALL_DIR"
elif [ "$RUN_ONBOARD" = "1" ]; then
  printf 'No enrollment token provided; set TWIN_BRAIN_API_KEY=vtk_... or run the setup command from VEP, then run: %s/twin-brain.sh doctor\n' "$INSTALL_DIR"
else
  printf 'Set TWIN_BRAIN_API_KEY=vtk_... separately or run the setup command from VEP, then run: %s/twin-brain.sh doctor\n' "$INSTALL_DIR"
fi

if [ "$RUN_ONBOARD" = "1" ]; then
  "$INSTALL_DIR/twin-brain.sh" onboard || true
fi
