import { execFile, execFileSync } from "node:child_process";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";

function execFileAsync(file, args = [], options = {}) {
  return new Promise((resolve, reject) => {
    execFile(file, args, { timeout: 15000, maxBuffer: 1024 * 1024, ...options }, (error, stdout, stderr) => {
      if (error) {
        error.stdout = stdout;
        error.stderr = stderr;
        reject(error);
        return;
      }

      resolve({ stdout, stderr });
    });
  });
}

function execFileWithInput(file, args = [], input = "", options = {}) {
  return new Promise((resolve, reject) => {
    const child = execFile(file, args, { timeout: 15000, maxBuffer: 1024 * 1024, ...options }, (error, stdout, stderr) => {
      if (error) {
        error.stdout = stdout;
        error.stderr = stderr;
        reject(error);
        return;
      }

      resolve({ stdout, stderr });
    });
    child.stdin?.end(input);
  });
}

const inputHelperPath = new URL("./macos-input.swift", import.meta.url).pathname;
const screenshotHelperPath = new URL("./macos-screenshot.swift", import.meta.url).pathname;
const helperBinaryDir = process.env.HER_AGENT_HELPER_BIN_DIR || path.join(os.homedir(), ".her-desktop-agent", "bin");
const inputHelperBinaryPath = path.join(helperBinaryDir, "macos-input");
const screenshotHelperBinaryPath = path.join(helperBinaryDir, "macos-screenshot");

function assertText(value, name, maxLength = 256) {
  if (typeof value !== "string" || value.trim().length === 0) {
    const error = new Error(`${name} is required.`);
    error.statusCode = 400;
    throw error;
  }

  if (value.length > maxLength) {
    const error = new Error(`${name} is too long.`);
    error.statusCode = 400;
    throw error;
  }

  return value.trim();
}

export async function openApp({ name }) {
  const appName = normalizeAppName(assertText(name, "App name"));
  await execFileAsync("open", ["-a", appName]);
  return { ok: true, message: `Opened ${appName}.` };
}

function normalizeAppName(name) {
  const compact = name.trim().replace(/\.app$/i, "");
  const key = compact.toLowerCase().replace(/\s+/g, "");
  const aliases = new Map([
    ["safari浏览器", "Safari"],
    ["safari", "Safari"],
    ["finder", "Finder"],
    ["访达", "Finder"],
    ["terminal", "Terminal"],
    ["终端", "Terminal"],
    ["chrome", "Google Chrome"],
    ["googlechrome", "Google Chrome"],
    ["谷歌浏览器", "Google Chrome"],
    ["vscode", "Visual Studio Code"],
    ["visualstudiocode", "Visual Studio Code"],
  ]);
  return aliases.get(key) || compact;
}

export async function notify({ title = "Her", body }) {
  const safeTitle = assertText(title, "Title", 80);
  const safeBody = assertText(body, "Body", 512);
  const script = `display notification ${JSON.stringify(safeBody)} with title ${JSON.stringify(safeTitle)}`;

  await execFileAsync("osascript", ["-e", script]);
  return { ok: true, message: "Notification sent." };
}

export async function screenshot({ format = "png", width, quality, fast } = {}) {
  const result = await captureScreenshotBytes({ format, width, quality, fast });
  return {
    ok: true,
    mime: result.mime,
    base64: result.bytes.toString("base64"),
  };
}

export async function screenshotBuffer(options = {}) {
  return captureScreenshotBytes(options);
}

function resolveScreenshotHelperCommand() {
  return resolveCompiledSwiftHelperCommand(screenshotHelperPath, screenshotHelperBinaryPath);
}

function resolveInputHelperCommand() {
  return resolveCompiledSwiftHelperCommand(inputHelperPath, inputHelperBinaryPath);
}

function resolveCompiledSwiftHelperCommand(sourcePath, binaryPath) {
  try {
    const sourceStat = fsSync.statSync(sourcePath);
    const binaryStat = fsSync.existsSync(binaryPath)
      ? fsSync.statSync(binaryPath)
      : null;
    if (!binaryStat || binaryStat.mtimeMs < sourceStat.mtimeMs) {
      fsSync.mkdirSync(path.dirname(binaryPath), { recursive: true });
      execFileSync("swiftc", [sourcePath, "-O", "-o", binaryPath], {
        timeout: 30_000,
        stdio: ["ignore", "pipe", "pipe"],
      });
    }
    return { file: binaryPath, args: [] };
  } catch (error) {
    console.error(`[${new Date().toISOString()}] screen helper binary unavailable, falling back to swift: ${error.message}`);
    return { file: "swift", args: [sourcePath] };
  }
}

async function captureScreenshotBytes({ format = "png", width, quality, fast = false } = {}) {
  const requestedFormat = String(format || "png").trim().toLowerCase();
  const outputFormat = requestedFormat === "jpeg" || requestedFormat === "jpg" ? "jpeg" : "png";
  const targetWidth = Math.max(320, Math.min(2560, Math.round(Number(width) || 960)));
  const targetQuality = Math.max(25, Math.min(90, Math.round(Number(quality) || 62)));
  const useFastCapture = Boolean(fast);

  if (process.env.HER_AGENT_SCREENSHOT_URL) {
    return captureScreenshotFromMainApp({
      baseURL: process.env.HER_AGENT_SCREENSHOT_URL,
      format: outputFormat,
      width: targetWidth,
      quality: targetQuality,
      fast: useFastCapture,
    });
  }

  try {
    const command = resolveScreenshotHelperCommand();
    const result = await execFileAsync(command.file, [
      ...command.args,
      outputFormat,
      String(targetWidth),
      String(targetQuality),
      useFastCapture ? "1" : "0",
    ], {
      timeout: useFastCapture ? 7000 : 12000,
      maxBuffer: 12 * 1024 * 1024,
      encoding: "buffer",
    });
    const bytes = Buffer.isBuffer(result.stdout)
      ? result.stdout
      : Buffer.from(result.stdout, "binary");
    if (bytes.length <= 0) {
      throw new Error("ScreenCaptureKit returned an empty screenshot.");
    }
    return {
      mime: outputFormat === "jpeg" ? "image/jpeg" : "image/png",
      bytes,
    };
  } catch (error) {
    console.error(`[${new Date().toISOString()}] native screenshot helper failed: ${error.message} ${error.stderr || ""}`);
    const wrapped = new Error(`ScreenCaptureKit screenshot failed: ${error.stderr || error.message}`);
    wrapped.statusCode = 500;
    throw wrapped;
  }
}

async function captureScreenshotFromMainApp({ baseURL, format, width, quality, fast }) {
  const url = new URL(baseURL);
  url.searchParams.set("format", format);
  url.searchParams.set("width", String(width));
  url.searchParams.set("quality", String(quality));
  if (fast) url.searchParams.set("fast", "1");

  const response = await fetch(url, { signal: AbortSignal.timeout(fast ? 7_000 : 12_000) });
  if (!response.ok) {
    const text = await response.text().catch(() => "");
    const error = new Error(text || `Screenshot service returned HTTP ${response.status}.`);
    error.statusCode = 500;
    throw error;
  }

  const bytes = Buffer.from(await response.arrayBuffer());
  if (bytes.length <= 0) {
    const error = new Error("Screenshot service returned an empty screenshot.");
    error.statusCode = 500;
    throw error;
  }
  return {
    mime: response.headers.get("content-type")?.split(";")[0] || (format === "jpeg" ? "image/jpeg" : "image/png"),
    bytes,
  };
}

export async function computerAction(input = {}) {
  const action = normalizeComputerAction(input.action);
  if (!action) {
    const error = new Error("Computer action is required.");
    error.statusCode = 400;
    throw error;
  }

  if (action === "type") {
    const text = assertText(input.text || input.content || input.value, "Text", 4000);
    await execFileWithInput("pbcopy", [], text, { timeout: 15000 });
    await runInputAction({ action: "paste" });
    return { ok: true, action, method: "pasteboard", characters: Array.from(text).length };
  }

  if (action === "key") {
    const key = assertText(input.key || input.text, "Key", 64);
    await sendKey(key);
    return { ok: true, action, key };
  }

  if (action === "scroll") {
    const direction = String(input.direction || "").trim().toLowerCase();
    const amount = Number(input.amount || input.delta || 6);
    return runInputAction({
      action,
      direction: direction === "up" ? "up" : "down",
      amount: Math.max(1, Math.min(20, Math.round(Math.abs(amount)))),
    });
  }

  if (action === "drag") {
    const from = normalizedPoint(input.from || input.start || input.source || input.coordinate);
    const to = normalizedPoint(input.to || input.end || input.target || input.destination);
    return runInputAction({ action, from: [from.x, from.y], to: [to.x, to.y] });
  }

  const point = normalizedPoint(input.coordinate || input.point || [input.x, input.y]);
  if (action === "move") {
    return runPointerAction(action, point);
  }
  if (action === "left_click" || action === "click") {
    return runPointerAction(action, point);
  } else if (action === "double_click") {
    return runPointerAction(action, point);
  } else if (action === "right_click") {
    return runPointerAction(action, point);
  } else if (
    action === "left_down" ||
    action === "left_drag" ||
    action === "left_up" ||
    action === "right_down" ||
    action === "right_drag" ||
    action === "right_up"
  ) {
    return runPointerAction(action, point);
  } else {
    const error = new Error(`Unsupported computer action: ${action}`);
    error.statusCode = 400;
    throw error;
  }
}

function normalizeComputerAction(raw) {
  const key = String(raw || "").trim().toLowerCase().replace(/-/g, "_");
  const aliases = new Map([
    ["tap", "left_click"],
    ["press", "left_click"],
    ["input", "type"],
    ["text", "type"],
    ["input_text", "type"],
    ["write_text", "type"],
    ["keyboard_input", "type"],
  ]);
  const normalized = aliases.get(key) || key;
  if ([
    "left_click",
    "click",
    "double_click",
    "right_click",
    "left_down",
    "left_drag",
    "left_up",
    "right_down",
    "right_drag",
    "right_up",
    "type",
    "key",
    "scroll",
    "drag",
    "move",
  ].includes(normalized)) return normalized;
  return "";
}

async function runPointerAction(action, point) {
  return runInputAction({ action, coordinate: [point.x, point.y] });
}

async function runInputAction(body) {
  if (process.env.HER_AGENT_INPUT_URL) {
    return callMainAppInput(body);
  }

  const command = resolveInputHelperCommand();
  const action = body.action;
  if (action === "scroll") {
    const result = await execFileAsync(command.file, [
      ...command.args,
      "scroll",
      body.direction === "up" ? "up" : "down",
      String(body.amount || 6),
    ], { timeout: 30000 });
    return parseHelperResult(result.stdout, { ok: true, action, direction: body.direction || "down" });
  }
  if (action === "drag") {
    const from = body.from || body.coordinate;
    const to = body.to;
    const result = await execFileAsync(command.file, [
      ...command.args,
      "drag",
      String(from?.[0] ?? 500),
      String(from?.[1] ?? 500),
      String(to?.[0] ?? 500),
      String(to?.[1] ?? 500),
    ], { timeout: 30000 });
    return parseHelperResult(result.stdout, { ok: true, action, from, to });
  }
  if (action === "paste") {
    const result = await execFileAsync(command.file, [...command.args, "paste"], { timeout: 30000 });
    return parseHelperResult(result.stdout, { ok: true, action });
  }
  if (action === "key") {
    const result = await execFileAsync(command.file, [...command.args, "key", body.key], { timeout: 30000 });
    return parseHelperResult(result.stdout, { ok: true, action, key: body.key });
  }
  const point = body.coordinate || [500, 500];
  const result = await execFileAsync(command.file, [
    ...command.args,
    action,
    String(point[0]),
    String(point[1]),
  ], { timeout: 30000 });
  return parseHelperResult(result.stdout, { ok: true, action, point });
}

async function sendKey(raw) {
  const key = String(raw).trim().toLowerCase();
  await runInputAction({ action: "key", key });
}

async function callMainAppInput(body) {
  const response = await fetch(process.env.HER_AGENT_INPUT_URL, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(12_000),
  });
  const text = await response.text();
  if (!response.ok) {
    const error = new Error(text || `Input service returned HTTP ${response.status}.`);
    error.statusCode = 500;
    throw error;
  }
  return JSON.parse(text || JSON.stringify({ ok: true, action: body.action }));
}

function normalizedPoint(value) {
  if (!Array.isArray(value) || value.length < 2) {
    const error = new Error("coordinate must be [x, y] in Qwen 1000x1000 space.");
    error.statusCode = 400;
    throw error;
  }
  const x = Number(value[0]);
  const y = Number(value[1]);
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
    const error = new Error("coordinate values must be numbers.");
    error.statusCode = 400;
    throw error;
  }
  return {
    x: Math.max(0, Math.min(1000, x)),
    y: Math.max(0, Math.min(1000, y)),
  };
}

function parseHelperResult(stdout, fallback) {
  return JSON.parse(stdout.trim() || JSON.stringify(fallback));
}

export async function terminalRun({ command, cwd, mode = "exec" }) {
  if (process.env.HER_AGENT_ENABLE_TERMINAL !== "1") {
    const error = new Error("Terminal control is disabled. Start the agent with HER_AGENT_ENABLE_TERMINAL=1.");
    error.statusCode = 403;
    throw error;
  }

  const safeCommand = assertText(command, "Command", 4000);
  const safeMode = String(mode || "exec").trim();
  const workingDirectory = typeof cwd === "string" && cwd.trim().length > 0 ? cwd.trim() : undefined;

  if (safeMode === "terminal") {
    const escaped = workingDirectory
      ? `cd ${shellQuote(workingDirectory)} && ${safeCommand}`
      : safeCommand;
    const script = [
      'tell application "Terminal"',
      "activate",
      `do script ${JSON.stringify(escaped)}`,
      "end tell",
    ].join("\n");
    await execFileAsync("osascript", ["-e", script], { timeout: 30000 });
    return { ok: true, mode: "terminal", message: "Command sent to Terminal." };
  }

  const result = await execFileAsync("/bin/zsh", ["-lc", safeCommand], {
    cwd: workingDirectory,
    timeout: 120000,
    maxBuffer: 2 * 1024 * 1024,
  });
  return {
    ok: true,
    mode: "exec",
    stdout: result.stdout,
    stderr: result.stderr,
  };
}

function shellQuote(value) {
  return `'${String(value).replace(/'/g, `'\\''`)}'`;
}

const runAllowlist = {
  pwd: () => execFileAsync("pwd"),
  whoami: () => execFileAsync("whoami"),
  uptime: () => execFileAsync("uptime"),
  sw_vers: () => execFileAsync("sw_vers"),
  "ls-home": () => execFileAsync("ls", ["-la", os.homedir()]),
  "ls-desktop": () => execFileAsync("ls", ["-la", path.join(os.homedir(), "Desktop")]),
  "ls-downloads": () => execFileAsync("ls", ["-la", path.join(os.homedir(), "Downloads")]),
  "ls-documents": () => execFileAsync("ls", ["-la", path.join(os.homedir(), "Documents")]),
  "lock-screen": () => execFileAsync("pmset", ["displaysleepnow"]),
};

export async function runCommand({ command }) {
  const safeCommand = assertText(command, "Command", 64);
  const runner = runAllowlist[safeCommand];

  if (!runner) {
    const error = new Error(`Command is not allowed: ${safeCommand}`);
    error.statusCode = 403;
    error.allowed = Object.keys(runAllowlist);
    throw error;
  }

  const result = await runner();
  return {
    ok: true,
    command: safeCommand,
    stdout: result.stdout,
    stderr: result.stderr,
  };
}

export function getAdapterInfo() {
  return {
    platform: "macos",
    capabilities: ["open-app", "notify", "screenshot", "computer-action", "run", "terminal-run"],
    allowedCommands: Object.keys(runAllowlist),
    terminalEnabled: process.env.HER_AGENT_ENABLE_TERMINAL === "1",
  };
}
