#!/usr/bin/env node
/**
 * Setup: create/update a daily 23:00 cron job that
 * - runs openclaw security audits
 * - DMs a chosen recipient (channel+id)
 * - optionally emails a configured recipient via sendmail/SMTP
 *
 * Uses the `openclaw cron` CLI so it can run on a host without direct Gateway RPC access.
 */

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import { fileURLToPath } from "node:url";

const JOB_NAME = "Daily security audit (Prompt Security)";
const DEFAULT_TZ = "UTC";
const DEFAULT_EXPR = "0 23 * * *"; // 23:00 daily
const PERSISTED_ENV_KEYS = [
  "PROMPTSEC_EMAIL_TO",
  "PROMPTSEC_GIT_PULL",
  "OPENCLAW_AUDIT_CONFIG",
  "PROMPTSEC_SENDMAIL_BIN",
  "PROMPTSEC_SMTP_HOST",
  "PROMPTSEC_SMTP_PORT",
  "PROMPTSEC_SMTP_HELO",
  "PROMPTSEC_SMTP_FROM",
];

const SCRIPT_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const UNEXPANDED_HOME_TOKEN_PATTERN =
  /(?:^|[\\/])(?:\\?\$HOME|\\?\$\{HOME\}|\\?\$USERPROFILE|\\?\$\{USERPROFILE\}|%HOME%|%USERPROFILE%|\$env:HOME|\$env:USERPROFILE)(?:$|[\\/])/i;

function sh(cmd, args, { input } = {}) {
  const res = spawnSync(cmd, args, {
    encoding: "utf8",
    input: input ?? undefined,
    stdio: [input ? "pipe" : "ignore", "pipe", "pipe"],
  });
  if (res.error) throw res.error;
  if (res.status !== 0) {
    const msg = (res.stderr || res.stdout || "").trim();
    throw new Error(`${cmd} ${args.join(" ")} failed (code ${res.status})${msg ? `: ${msg}` : ""}`);
  }
  return res.stdout;
}

async function prompt(question, { defaultValue = "" } = {}) {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const q = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
  const answer = await new Promise((resolve) => rl.question(q, resolve));
  rl.close();
  const trimmed = String(answer ?? "").trim();
  return trimmed || defaultValue;
}

function envOrEmpty(name) {
  const v = process.env[name];
  return typeof v === "string" ? v.trim() : "";
}

function detectHomeDirectory() {
  const home = envOrEmpty("HOME");
  if (home) return home;
  const userProfile = envOrEmpty("USERPROFILE");
  if (userProfile) return userProfile;
  const homeDrive = envOrEmpty("HOMEDRIVE");
  const homePath = envOrEmpty("HOMEPATH");
  if (homeDrive && homePath) return `${homeDrive}${homePath}`;
  return os.homedir();
}

function resolveUserPath(inputPath, label) {
  const raw = String(inputPath ?? "").trim();
  if (!raw) return raw;

  const homeDir = detectHomeDirectory();
  let expanded = raw;

  if (expanded === "~") {
    expanded = homeDir;
  } else if (expanded.startsWith("~/") || expanded.startsWith("~\\")) {
    expanded = path.join(homeDir, expanded.slice(2));
  }

  expanded = expanded
    .replace(/(?<!\\)\$\{HOME\}/g, homeDir)
    .replace(/(?<!\\)\$HOME(?=$|[\\/])/g, homeDir)
    .replace(/(?<!\\)\$\{USERPROFILE\}/gi, homeDir)
    .replace(/(?<!\\)\$USERPROFILE(?=$|[\\/])/gi, homeDir)
    .replace(/%HOME%/gi, homeDir)
    .replace(/%USERPROFILE%/gi, homeDir)
    .replace(/(?<!\\)\$env:HOME/gi, homeDir)
    .replace(/(?<!\\)\$env:USERPROFILE/gi, homeDir);

  const normalized = path.normalize(expanded);
  if (UNEXPANDED_HOME_TOKEN_PATTERN.test(normalized)) {
    throw new Error(
      `Unexpanded home token detected in ${label}: ${raw}. ` +
        "Use an absolute path or an unquoted home-path expression.",
    );
  }

  return normalized;
}

function oneline(v) {
  return String(v ?? "")
    .replace(/[\r\n]+/g, " ")
    .replace(/\\/g, "\\\\")
    .replace(/"/g, "\\\"")
    .trim();
}

function escapeForShellEnvVar(v) {
  return String(v ?? "")
    .replace(/[\r\n]+/g, " ")
    .replace(/\\/g, "\\\\")
    .replace(/\$/g, "\\$")
    .replace(/`/g, "\\`")
    .replace(/"/g, "\\\"")
    .trim();
}

function buildRunnerEnv({ hostLabel, emailTo }) {
  const envVars = {
    PROMPTSEC_HOST_LABEL: hostLabel,
  };

  if (emailTo) {
    envVars.PROMPTSEC_EMAIL_TO = emailTo;
  }

  for (const key of PERSISTED_ENV_KEYS) {
    const value = envOrEmpty(key);
    if (value) {
      envVars[key] = value;
    }
  }

  return envVars;
}

function buildRunnerCommand({ installDir, hostLabel, emailTo }) {
  const envVars = buildRunnerEnv({ hostLabel, emailTo });
  const exports = Object.entries(envVars)
    .filter(([, value]) => String(value ?? "").trim() !== "")
    .map(([key, value]) => `${key}="${escapeForShellEnvVar(value)}"`);

  const exportPrefix = exports.length ? `${exports.join(" ")} ` : "";
  return `cd "${escapeForShellEnvVar(installDir || "")}" && ${exportPrefix}./scripts/runner.sh`;
}

function printPreflightSummary({ dmChannel, dmTo, emailTo, installDir, tz, hostLabel }) {
  const emailSummary = emailTo || "disabled (set PROMPTSEC_EMAIL_TO to enable)";
  const persistedKeys = Array.from(new Set([
    "PROMPTSEC_HOST_LABEL",
    emailTo ? "PROMPTSEC_EMAIL_TO" : null,
    ...PERSISTED_ENV_KEYS.filter((key) => envOrEmpty(key)),
  ].filter(Boolean)));

  const lines = [
    "Preflight review:",
    "- This setup creates or updates an unattended openclaw cron job.",
    "- Required runtime: openclaw CLI, node, bash.",
    "- Optional email runtime: local sendmail or PROMPTSEC_SMTP_HOST/PROMPTSEC_SMTP_PORT relay.",
    `- DM target: ${oneline(dmChannel)}:${oneline(dmTo)}`,
    `- Email target: ${oneline(emailSummary)}`,
    `- Schedule: ${DEFAULT_EXPR} (${oneline(tz)})`,
    `- Install dir: ${oneline(installDir)}`,
  ];

  if (hostLabel) {
    lines.push(`- Host label: ${oneline(hostLabel)}`);
  }

  if (persistedKeys.length) {
    lines.push(`- Cron payload persists env: ${persistedKeys.join(", ")}`);
  }

  process.stdout.write(lines.join("\n") + "\n\n");
}

function defaultInstallDir() {
  const env = envOrEmpty("PROMPTSEC_INSTALL_DIR");
  if (env) return resolveUserPath(env, "PROMPTSEC_INSTALL_DIR");
  const home = detectHomeDirectory();
  if (home) return path.join(home, ".config", "security-checkup");
  return resolveUserPath(SCRIPT_ROOT, "script root");
}

function buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir, emailTo }) {
  const runnerCommand = buildRunnerCommand({ installDir, hostLabel, emailTo });
  const emailLine = emailTo
    ? `Email: ${oneline(emailTo)} (sendmail first, SMTP fallback if configured)`
    : "Email: disabled unless PROMPTSEC_EMAIL_TO is set";

  return [
    "Run daily openclaw security audits and deliver report to the configured recipients.",
    "",
    "Dependencies:",
    "- Required runtime: openclaw CLI, node, bash.",
    "- Optional email runtime: local sendmail or PROMPTSEC_SMTP_HOST/PROMPTSEC_SMTP_PORT relay.",
    "",
    "Configured delivery:",
    `Delivery DM: ${oneline(dmChannel)}:${oneline(dmTo)}`,
    emailLine,
    "",
    "Execute:",
    `- Run via exec: ${runnerCommand}`,
    "",
    "Output requirements:",
    "- Print the report to stdout (cron deliver will DM it).",
    "- If PROMPTSEC_EMAIL_TO is set, email the same report to that address; if email fails, append a NOTE line to stdout.",
    "- Do not apply fixes automatically.",
  ].join("\n");
}

function buildDescription({ dmChannel, dmTo, emailTo }) {
  const emailPart = emailTo ? `; email ${emailTo}` : "; email disabled unless configured";
  return `Runs openclaw security audit daily and delivers to ${dmChannel}:${dmTo}${emailPart}.`;
}

function findExistingJobId(listJson) {
  const jobs = Array.isArray(listJson?.jobs) ? listJson.jobs : [];
  const match = jobs.find((j) => j?.name === JOB_NAME);
  return match?.id ?? null;
}

async function run() {
  // Non-interactive first (MDM-friendly)
  const tzEnv = envOrEmpty("PROMPTSEC_TZ");
  const dmChannelEnv = envOrEmpty("PROMPTSEC_DM_CHANNEL");
  const dmToEnv = envOrEmpty("PROMPTSEC_DM_TO");
  const hostLabelEnv = envOrEmpty("PROMPTSEC_HOST_LABEL");
  const emailToEnv = envOrEmpty("PROMPTSEC_EMAIL_TO");

  const interactive = !(tzEnv && dmChannelEnv && dmToEnv);

  const tz = interactive
    ? await prompt("Timezone for daily 11pm run (IANA)", { defaultValue: tzEnv || DEFAULT_TZ })
    : tzEnv || DEFAULT_TZ;

  const dmChannel = interactive
    ? await prompt("DM channel (e.g. telegram, slack, discord)", { defaultValue: dmChannelEnv })
    : dmChannelEnv;

  const dmTo = interactive
    ? await prompt("DM recipient id (Telegram numeric chatId/userId preferred)", { defaultValue: dmToEnv })
    : dmToEnv;

  const hostLabel = interactive
    ? await prompt("Optional host label to include in report", { defaultValue: hostLabelEnv })
    : hostLabelEnv;
  const emailTo = interactive
    ? await prompt("Optional email recipient (leave empty to disable email)", { defaultValue: emailToEnv })
    : emailToEnv;

  const installDirDefault = defaultInstallDir();
  const installDirInput = interactive
    ? await prompt("Install dir containing scripts/runner.sh", { defaultValue: installDirDefault })
    : installDirDefault;
  const installDir = resolveUserPath(installDirInput, "install dir containing scripts/runner.sh");

  if (!dmChannel || !dmTo) {
    throw new Error("Missing DM target. Set PROMPTSEC_DM_CHANNEL and PROMPTSEC_DM_TO (or run interactively). ");
  }

  const runnerPath = path.join(installDir, "scripts", "runner.sh");
  if (!fs.existsSync(runnerPath)) {
    throw new Error(`runner.sh not found at ${runnerPath}; set PROMPTSEC_INSTALL_DIR to the deployed path`);
  }

  printPreflightSummary({ dmChannel, dmTo, emailTo, installDir, tz, hostLabel });

  const listOut = sh("openclaw", ["cron", "list", "--json"]);
  const listJson = JSON.parse(listOut);
  const existingId = findExistingJobId(listJson);

  const agentMessage = buildAgentMessage({ dmChannel, dmTo, hostLabel, installDir, emailTo });
  const description = buildDescription({ dmChannel, dmTo, emailTo });

  if (!existingId) {
    const args = [
      "cron",
      "add",
      "--name",
      JOB_NAME,
      "--description",
      description,
      "--session",
      "isolated",
      "--wake",
      "now",
      "--cron",
      DEFAULT_EXPR,
      "--tz",
      tz,
      "--message",
      agentMessage,
      "--deliver",
      "--channel",
      dmChannel,
      "--to",
      dmTo,
      "--best-effort-deliver",
      "--post-prefix",
      "[daily security audit]",
      "--post-mode",
      "summary",
      "--json",
    ];
    const out = sh("openclaw", args);
    const job = JSON.parse(out);
    process.stdout.write(`Created cron job ${job.id}: ${JOB_NAME}\n`);
  } else {
    const args = [
      "cron",
      "edit",
      existingId,
      "--name",
      JOB_NAME,
      "--description",
      description,
      "--enable",
      "--session",
      "isolated",
      "--wake",
      "now",
      "--cron",
      DEFAULT_EXPR,
      "--tz",
      tz,
      "--message",
      agentMessage,
      "--deliver",
      "--channel",
      dmChannel,
      "--to",
      dmTo,
      "--best-effort-deliver",
      "--post-prefix",
      "[daily security audit]",
    ];
    sh("openclaw", args);
    process.stdout.write(`Updated cron job ${existingId}: ${JOB_NAME}\n`);
  }
}

run().catch((err) => {
  process.stderr.write(String(err?.stack || err) + "\n");
  process.exit(1);
});
