"use strict";

const vm = require("vm");
const { spawn } = require("child_process");
const {
  prepareJavaScriptCommand,
  preparePythonCommand,
  resolveJavaScriptHandler
} = require("./command-code");

const FIXED_NOW = Date.parse("2026-01-02T12:34:56.000Z");
const BLOCKED_JS = /\b(?:require|process|child_process|fs|XMLHttpRequest|WebSocket|Deno|Bun)\b|\bimport\s*(?:\(|[\w*{])/;
const BLOCKED_PYTHON = /(?:\b(?:import|open|exec|eval|compile|globals|locals|getattr|setattr|delattr|vars|input|breakpoint|help)\b|__)/;
const MAX_CODE_LENGTH = 20000;
const MAX_PREVIEW_FETCH_BYTES = 128 * 1024;
const PREVIEW_FETCH_TIMEOUT_MS = 3000;

let input = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
  input += chunk;
});
process.stdin.on("end", async () => {
  try {
    const request = JSON.parse(input || "{}");
    const result = request.language === "python"
      ? await previewPython(request.code)
      : await previewJavaScript(request.code);
    process.stdout.write(JSON.stringify({ ok: true, output: result }));
  } catch (error) {
    process.stdout.write(JSON.stringify({
      ok: false,
      error: cleanError(error)
    }));
  }
});

async function previewJavaScript(code) {
  const source = String(code || "");
  if (source.length > MAX_CODE_LENGTH) throw new Error("Command preview code is too large.");
  if (BLOCKED_JS.test(maskJavaScriptLiteralsAndComments(source))) {
    throw new Error("Preview blocks filesystem, process, browser socket, and module access.");
  }

  const sandbox = Object.create(null);
  sandbox.__previewFetchBridge = readOnlyPreviewFetchBridge;
  sandbox.__previewParseUrlBridge = parsePreviewUrl;
  const context = vm.createContext(sandbox, {
    codeGeneration: { strings: false, wasm: false }
  });
  initializeJavaScriptSandbox(context);
  const contextValue = context.ctx;
  const script = new vm.Script(`"use strict";\n${prepareJavaScriptCommand(source)}`, {
    filename: "command-preview.js"
  });
  script.runInContext(context, { timeout: 250 });
  const handler = resolveJavaScriptHandler(context);
  if (typeof handler !== "function") {
    throw new Error("Dynamic command did not create a runnable handler.");
  }
  const returned = await withTimeout(Promise.resolve(handler(contextValue)), PREVIEW_FETCH_TIMEOUT_MS + 500);
  const output = Array.from(context.__previewReplies || [], normalizeResult);
  const normalizedReturn = normalizeResult(returned);
  if (normalizedReturn && !output.includes(normalizedReturn)) output.push(normalizedReturn);
  return output.filter(Boolean).join("\n") || "Command returned no output.";
}

function initializeJavaScriptSandbox(context) {
  const serializedContext = JSON.stringify(createSerializableContext());
  const bootstrap = new vm.Script(`
    "use strict";
    {
      const fetchBridge = globalThis.__previewFetchBridge;
      const parseUrlBridge = globalThis.__previewParseUrlBridge;
      delete globalThis.__previewFetchBridge;
      delete globalThis.__previewParseUrlBridge;

      const replies = [];
      const noOpMutation = async () => ({ ok: true, preview: true });
      const contextValue = ${serializedContext};

      Object.defineProperty(Math, "random", {
        value: () => 0.424242,
        configurable: false,
        writable: false
      });
      Object.freeze(Math);

      const NativeDate = Date;
      class PreviewDate extends NativeDate {
        constructor(...args) {
          super(args.length ? args[0] : ${FIXED_NOW});
        }
        static now() {
          return ${FIXED_NOW};
        }
      }
      Object.freeze(PreviewDate);

      class PreviewURLSearchParams {
        constructor(input = "") {
          this._pairs = [];
          const value = String(input || "").replace(/^\\?/, "");
          for (const part of value.split("&")) {
            if (!part) continue;
            const separator = part.indexOf("=");
            const key = separator >= 0 ? part.slice(0, separator) : part;
            const item = separator >= 0 ? part.slice(separator + 1) : "";
            this._pairs.push([decodeURIComponent(key.replace(/\\+/g, " ")), decodeURIComponent(item.replace(/\\+/g, " "))]);
          }
        }
        append(key, value) { this._pairs.push([String(key), String(value)]); }
        delete(key) { const target = String(key); this._pairs = this._pairs.filter(([name]) => name !== target); }
        get(key) { const row = this._pairs.find(([name]) => name === String(key)); return row ? row[1] : null; }
        getAll(key) { return this._pairs.filter(([name]) => name === String(key)).map(([, value]) => value); }
        has(key) { return this._pairs.some(([name]) => name === String(key)); }
        set(key, value) { this.delete(key); this.append(key, value); }
        entries() { return this._pairs[Symbol.iterator](); }
        keys() { return this._pairs.map(([key]) => key)[Symbol.iterator](); }
        values() { return this._pairs.map(([, value]) => value)[Symbol.iterator](); }
        forEach(callback, thisArg) { for (const [key, value] of this._pairs) callback.call(thisArg, value, key, this); }
        toString() { return this._pairs.map(([key, value]) => encodeURIComponent(key) + "=" + encodeURIComponent(value)).join("&"); }
        [Symbol.iterator]() { return this.entries(); }
      }

      class PreviewURL {
        constructor(input, base) {
          const parsed = parseUrlBridge(String(input), base === undefined ? undefined : String(base));
          for (const key of ["href", "origin", "protocol", "username", "password", "host", "hostname", "port", "pathname", "search", "hash"]) {
            this[key] = parsed[key];
          }
          this.searchParams = new PreviewURLSearchParams(parsed.search);
        }
        toString() { return this.href; }
        toJSON() { return this.href; }
      }

      async function previewFetch(resource, options = {}) {
        const method = String(options && options.method || "GET").toUpperCase();
        const raw = await fetchBridge(String(resource), { method });
        const headers = Object.freeze({
          get(name) { return raw.headers[String(name).toLowerCase()] ?? null; }
        });
        return Object.freeze({
          ok: Boolean(raw.ok),
          status: Number(raw.status),
          statusText: String(raw.statusText || ""),
          url: String(raw.url || ""),
          preview: true,
          readOnly: true,
          truncated: Boolean(raw.truncated),
          headers,
          text: async () => String(raw.text || ""),
          json: async () => JSON.parse(String(raw.text || "null")),
          arrayBuffer: async () => Uint8Array.from(raw.bytes || []).buffer
        });
      }

      contextValue.reply = async (value) => {
        replies.push(value);
        return { ok: true, preview: true };
      };
      contextValue.economy = Object.freeze({
        getBalance: async () => 1234,
        balance: async () => 1234,
        add: noOpMutation,
        take: noOpMutation,
        transfer: noOpMutation
      });
      contextValue.inventory = Object.freeze({
        list: async () => [{ id: "item_12345", name: "Example item", quantity: 2 }],
        add: noOpMutation,
        remove: noOpMutation
      });
      contextValue.moderation = Object.freeze({
        warn: noOpMutation,
        timeout: noOpMutation,
        ban: noOpMutation,
        unban: noOpMutation
      });
      contextValue.db = Object.freeze({
        prepare: () => Object.freeze({
          run: () => ({ changes: 0, preview: true }),
          get: () => null,
          all: () => []
        })
      });
      contextValue.files = Object.freeze({
        read: async () => null,
        write: noOpMutation,
        remove: noOpMutation
      });
      contextValue.api = Object.freeze({
        request: async (resource, options = {}) => {
          const response = await previewFetch(resource, options);
          const text = await response.text();
          let data = null;
          try { data = text ? JSON.parse(text) : null; } catch {}
          return {
            ok: response.ok,
            status: response.status,
            statusText: response.statusText,
            url: response.url,
            preview: true,
            readOnly: true,
            truncated: response.truncated,
            text,
            data
          };
        }
      });

      globalThis.Date = PreviewDate;
      globalThis.URL = PreviewURL;
      globalThis.URLSearchParams = PreviewURLSearchParams;
      globalThis.fetch = previewFetch;
      globalThis.console = Object.freeze({ log() {}, warn() {}, error() {} });
      globalThis.ctx = Object.freeze(contextValue);
      globalThis.module = { exports: {} };
      globalThis.exports = {};
      globalThis.__previewReplies = replies;
    }
  `, { filename: "command-preview-bootstrap.js" });
  bootstrap.runInContext(context, { timeout: 100 });
}

function maskJavaScriptLiteralsAndComments(source) {
  const value = String(source || "");
  let output = "";
  let state = "code";
  for (let index = 0; index < value.length; index += 1) {
    const char = value[index];
    const next = value[index + 1];
    if (state === "code") {
      if (char === "'" || char === '"' || char === "`") {
        state = char;
        output += " ";
      } else if (char === "/" && next === "/") {
        state = "line-comment";
        output += "  ";
        index += 1;
      } else if (char === "/" && next === "*") {
        state = "block-comment";
        output += "  ";
        index += 1;
      } else {
        output += char;
      }
      continue;
    }
    if (state === "line-comment") {
      if (char === "\n" || char === "\r") {
        state = "code";
        output += char;
      } else {
        output += " ";
      }
      continue;
    }
    if (state === "block-comment") {
      if (char === "*" && next === "/") {
        state = "code";
        output += "  ";
        index += 1;
      } else {
        output += char === "\n" || char === "\r" ? char : " ";
      }
      continue;
    }
    if (char === "\\") {
      output += "  ";
      index += 1;
    } else if (char === state) {
      state = "code";
      output += " ";
    } else {
      output += char === "\n" || char === "\r" ? char : " ";
    }
  }
  return output;
}

function previewPython(code) {
  const source = String(code || "");
  if (source.length > MAX_CODE_LENGTH) throw new Error("Command preview code is too large.");
  if (BLOCKED_PYTHON.test(source)) {
    throw new Error("Preview blocks imports, filesystem, process, and unsafe reflection.");
  }
  const wrapper = `
import json, sys
payload = json.loads(sys.stdin.read() or "{}")
replies = []
class AttrDict(dict):
    __getattr__ = dict.get
def convert(value):
    if isinstance(value, dict):
        return AttrDict({key: convert(item) for key, item in value.items()})
    if isinstance(value, list):
        return [convert(item) for item in value]
    return value
ctx = convert(payload["ctx"])
ctx.reply = lambda value: replies.append(value)
noop = lambda *args, **kwargs: {"ok": True, "preview": True}
ctx.economy = AttrDict({"getBalance": lambda *args: 1234, "balance": lambda *args: 1234, "add": noop, "take": noop, "transfer": noop})
ctx.inventory = AttrDict({"list": lambda *args: [AttrDict({"id": "item_12345", "name": "Example item", "quantity": 2})], "add": noop, "remove": noop})
ctx.moderation = AttrDict({"warn": noop, "timeout": noop, "ban": noop, "unban": noop})
ctx.db = AttrDict({"prepare": lambda *args: AttrDict({"run": noop, "get": lambda *args: None, "all": lambda *args: []})})
ctx.files = AttrDict({"read": lambda *args: None, "write": noop, "remove": noop})
ctx.api = AttrDict({"request": lambda *args: {"ok": True, "preview": True, "readOnly": True, "status": 200, "text": "", "data": {}}})
safe = {
    "str": str, "int": int, "float": float, "bool": bool, "len": len,
    "min": min, "max": max, "round": round, "range": range,
    "enumerate": enumerate, "list": list, "dict": dict, "set": set,
    "tuple": tuple, "sum": sum, "abs": abs
}
scope = {"__builtins__": safe}
exec(payload["code"], scope, scope)
handler = scope.get("run")
if not callable(handler):
    raise Exception("Define a run(ctx) function.")
returned = handler(ctx)
if returned is not None:
    replies.append(returned)
print(json.dumps(replies))
`;
  return new Promise((resolve, reject) => {
    const child = spawn("python", ["-I", "-S", "-c", wrapper], {
      stdio: ["pipe", "pipe", "pipe"],
      windowsHide: true,
      env: {}
    });
    let stdout = "";
    let stderr = "";
    const timer = setTimeout(() => {
      child.kill("SIGKILL");
      reject(new Error("Python preview timed out."));
    }, 700);
    child.stdout.on("data", (chunk) => {
      stdout += chunk.toString();
    });
    child.stderr.on("data", (chunk) => {
      stderr += chunk.toString();
    });
    child.on("error", (error) => {
      clearTimeout(timer);
      reject(error);
    });
    child.on("close", (exitCode) => {
      clearTimeout(timer);
      if (exitCode !== 0) {
        reject(new Error(stderr.trim() || "Python preview failed."));
        return;
      }
      try {
        const rows = JSON.parse(stdout.trim() || "[]").map(normalizeResult).filter(Boolean);
        resolve(rows.join("\n") || "Command returned no output.");
      } catch {
        reject(new Error("Python preview returned invalid output."));
      }
    });
    child.stdin.end(JSON.stringify({
    code: preparePythonCommand(source),
      ctx: createSerializableContext()
    }));
  });
}

async function readOnlyPreviewFetchBridge(resource, options = {}) {
  if (typeof fetch !== "function") {
    throw new Error("Preview fetch is unavailable in this runtime.");
  }
  const method = String(options?.method || "GET").toUpperCase();
  if (!["GET", "HEAD"].includes(method)) {
    throw new Error("Preview fetch only allows read-only GET and HEAD requests.");
  }
  const target = new URL(String(resource || ""));
  if (target.protocol !== "data:") {
    throw new Error("Preview does not send external network requests.");
  }
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), PREVIEW_FETCH_TIMEOUT_MS);
  try {
    const response = await fetch(target, {
      ...options,
      method,
      body: undefined,
      signal: controller.signal
    });
    const buffer = method === "HEAD"
      ? Buffer.alloc(0)
      : Buffer.from(await response.arrayBuffer());
    const limited = buffer.length > MAX_PREVIEW_FETCH_BYTES
      ? buffer.subarray(0, MAX_PREVIEW_FETCH_BYTES)
      : buffer;
    const text = limited.toString("utf8");
    const headers = Object.create(null);
    response.headers.forEach((value, key) => {
      headers[String(key).toLowerCase()] = String(value);
    });
    return {
      ok: response.ok,
      status: response.status,
      statusText: response.statusText,
      url: response.url,
      preview: true,
      readOnly: true,
      truncated: buffer.length > MAX_PREVIEW_FETCH_BYTES,
      headers,
      text,
      bytes: Array.from(limited)
    };
  } finally {
    clearTimeout(timeout);
  }
}

function parsePreviewUrl(input, base) {
  const parsed = base === undefined ? new URL(input) : new URL(input, base);
  return {
    href: parsed.href,
    origin: parsed.origin,
    protocol: parsed.protocol,
    username: parsed.username,
    password: parsed.password,
    host: parsed.host,
    hostname: parsed.hostname,
    port: parsed.port,
    pathname: parsed.pathname,
    search: parsed.search,
    hash: parsed.hash
  };
}

function createSerializableContext() {
  return {
    platform: "discord",
    user: {
      id: "user_12345",
      platformId: "platform_67890",
      username: "some_user",
      displayName: "some_user",
      tag: "some_user"
    },
    channel: {
      id: "channel_13579",
      name: "example_channel"
    },
    message: {
      id: "message_24680",
      content: "!example demo",
      channelId: "channel_13579",
      guildId: "guild_97531"
    },
    permissions: {
      isAdmin: false,
      isMod: false,
      canManageMessages: false
    },
    args: ["demo"],
    argsText: "demo"
  };
}

function normalizeResult(value) {
  if (value === null || value === undefined) return "";
  if (typeof value === "object" && value.preview === true) return "";
  if (typeof value === "string") return value;
  if (typeof value === "number" || typeof value === "boolean") return String(value);
  if (typeof value === "object" && value.content !== undefined) return String(value.content);
  try {
    return JSON.stringify(value);
  } catch {
    return String(value);
  }
}

function withTimeout(promise, timeoutMs) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error("Command preview timed out.")), timeoutMs);
    promise.then(
      (value) => {
        clearTimeout(timer);
        resolve(value);
      },
      (error) => {
        clearTimeout(timer);
        reject(error);
      }
    );
  });
}

function cleanError(error) {
  return String(error?.message || error || "Preview failed.")
    .replace(/\s+/g, " ")
    .slice(0, 500);
}
