/**
 * Server-side execution for the DeepSeek-via-Hermes adapter.
 *
 * Spawns `hermes chat -q "..." -Q -m <model> --provider custom` with
 * HERMES_HOME pinned to a DeepSeek-configured profile so the same machine
 * can run other Hermes-based agents on different providers in parallel.
 *
 * The Hermes CLI loads model.base_url, model.key_env (DEEPSEEK_API_KEY),
 * and toolsets from <HERMES_HOME>/config.yaml + <HERMES_HOME>/.env.
 */

import { readFileSync } from "node:fs";
import {
  runChildProcess,
  buildPaperclipEnv,
  renderTemplate,
  ensureAbsoluteDirectory,
} from "@paperclipai/adapter-utils/server-utils";
import {
  HERMES_CLI,
  DEFAULT_PROFILE_HOME,
  DEFAULT_MODEL,
  DEFAULT_PROVIDER,
  DEFAULT_TIMEOUT_SEC,
  DEFAULT_GRACE_SEC,
  SESSION_ID_REGEX,
  SESSION_ID_REGEX_LEGACY,
  TOKEN_USAGE_REGEX,
  COST_REGEX,
} from "../shared/constants.js";

function cfgString(v) {
  return typeof v === "string" && v.length > 0 ? v : undefined;
}
function cfgNumber(v) {
  return typeof v === "number" ? v : undefined;
}
function cfgBoolean(v) {
  return typeof v === "boolean" ? v : undefined;
}
function cfgStringArray(v) {
  return Array.isArray(v) && v.every((i) => typeof i === "string") ? v : undefined;
}

const DEFAULT_PROMPT_TEMPLATE = `You are "{{agentName}}", an AI agent employee in a Paperclip-managed company powered by DeepSeek.

IMPORTANT: Use the \`terminal\` tool with \`curl\` for ALL Paperclip API calls (web_extract and browser cannot access localhost).

Your Paperclip identity:
  Agent ID: {{agentId}}
  Company ID: {{companyId}}
  API Base: {{paperclipApiUrl}}

{{#taskId}}
## Assigned Task

Issue ID: {{taskId}}
Title: {{taskTitle}}

{{taskBody}}

## Workflow

1. Work on the task using your tools.
2. When done, mark the issue completed:
   \`curl -s -X PATCH "{{paperclipApiUrl}}/issues/{{taskId}}" -H "Content-Type: application/json" -d '{"status":"done"}'\`
3. Post a completion comment summarizing what you did:
   \`curl -s -X POST "{{paperclipApiUrl}}/issues/{{taskId}}/comments" -H "Content-Type: application/json" -d '{"body":"DONE: <your summary here>"}'\`
{{/taskId}}

{{#commentId}}
## Comment on This Issue

Someone commented. Read it:
   \`curl -s "{{paperclipApiUrl}}/issues/{{taskId}}/comments/{{commentId}}" | python3 -m json.tool\`
Address the comment, POST a reply if needed, then continue working.
{{/commentId}}

{{#noTask}}
## Heartbeat Wake — Check for Work

1. List your open issues:
   \`curl -s "{{paperclipApiUrl}}/companies/{{companyId}}/issues?assigneeAgentId={{agentId}}"\`
2. Pick the highest priority and work on it. When done, follow steps 2-3 above.
3. If nothing to do, report briefly what you checked.
{{/noTask}}`;

/**
 * Resolve the prompt template, preferring a versioned file over an inline DB
 * string. Precedence: instructionsFilePath > promptTemplate > DEFAULT.
 *
 * This brings deepseek_local into line with claude_local / gemini_local, whose
 * system prompts live as files under .claude/agents/. Keeping the prompt in one
 * git-versioned place (not split between a file and an inline DB column) is the
 * single-source-of-truth the other adapters already enforce.
 *
 * Fail loud: if instructionsFilePath is set but unreadable we throw rather than
 * silently falling back — a wrong/missing prompt file must surface as an error,
 * not run the agent on a stale inline copy. The loaded file still flows through
 * renderTemplate(), so {{wakeReason}}/{{#taskId}}/… placeholders keep working.
 */
export function resolveTemplate(config) {
  const filePath = cfgString(config.instructionsFilePath);
  if (filePath) {
    try {
      return readFileSync(filePath, "utf8");
    } catch (err) {
      throw new Error(
        `deepseek_local: instructionsFilePath is set ("${filePath}") but could not be read: ${err.message}. ` +
          `Refusing to fall back to promptTemplate/default — fix the path or unset instructionsFilePath.`,
      );
    }
  }
  return cfgString(config.promptTemplate) || DEFAULT_PROMPT_TEMPLATE;
}

function buildPrompt(ctx, config) {
  const template = resolveTemplate(config);
  const taskId = cfgString(ctx.context?.taskId);
  const taskTitle = cfgString(ctx.context?.taskTitle) || "";
  const taskBody = cfgString(ctx.context?.taskBody) || "";
  const commentId = cfgString(ctx.context?.commentId) || "";
  const wakeReason = cfgString(ctx.context?.wakeReason) || "";
  const agentName = ctx.agent?.name || "DeepSeek Agent";
  const companyName = cfgString(ctx.context?.companyName) || "";
  const projectName = cfgString(ctx.context?.projectName) || "";

  let paperclipApiUrl =
    cfgString(config.paperclipApiUrl) ||
    process.env.PAPERCLIP_API_URL ||
    "http://127.0.0.1:3100/api";
  if (!paperclipApiUrl.endsWith("/api")) {
    paperclipApiUrl = paperclipApiUrl.replace(/\/+$/, "") + "/api";
  }

  const vars = {
    agentId: ctx.agent?.id || "",
    agentName,
    companyId: ctx.agent?.companyId || "",
    companyName,
    runId: ctx.runId || "",
    taskId: taskId || "",
    taskTitle,
    taskBody,
    commentId,
    wakeReason,
    projectName,
    paperclipApiUrl,
  };

  let rendered = template;
  rendered = rendered.replace(/\{\{#taskId\}\}([\s\S]*?)\{\{\/taskId\}\}/g, taskId ? "$1" : "");
  rendered = rendered.replace(/\{\{#noTask\}\}([\s\S]*?)\{\{\/noTask\}\}/g, taskId ? "" : "$1");
  rendered = rendered.replace(/\{\{#commentId\}\}([\s\S]*?)\{\{\/commentId\}\}/g, commentId ? "$1" : "");
  return renderTemplate(rendered, vars);
}

function cleanResponse(raw) {
  return raw
    .split("\n")
    .filter((line) => {
      const t = line.trim();
      if (!t) return true;
      if (t.startsWith("[tool]") || t.startsWith("[hermes]") || t.startsWith("[paperclip]") || t.startsWith("[deepseek]")) return false;
      if (t.startsWith("session_id:")) return false;
      if (/^\[\d{4}-\d{2}-\d{2}T/.test(t)) return false;
      if (/^\[done\]\s*┊/.test(t)) return false;
      if (/^┊\s*[\p{Emoji_Presentation}]/u.test(t) && !/^┊\s*💬/.test(t)) return false;
      if (/^\p{Emoji_Presentation}\s*(Completed|Running|Error)?\s*$/u.test(t)) return false;
      return true;
    })
    .map((line) => {
      let t = line.replace(/^[\s]*┊\s*💬\s*/, "").trim();
      t = t.replace(/^\[done\]\s*/, "").trim();
      return t;
    })
    .join("\n")
    .replace(/\n{3,}/g, "\n\n")
    .trim();
}

function parseHermesOutput(stdout, stderr) {
  const combined = stdout + "\n" + stderr;
  const result = {};

  const sessionMatch = stdout.match(SESSION_ID_REGEX);
  if (sessionMatch?.[1]) {
    result.sessionId = sessionMatch[1];
    const sessionLineIdx = stdout.lastIndexOf("\nsession_id:");
    if (sessionLineIdx > 0) {
      result.response = cleanResponse(stdout.slice(0, sessionLineIdx));
    }
  } else {
    const legacyMatch = combined.match(SESSION_ID_REGEX_LEGACY);
    if (legacyMatch?.[1]) result.sessionId = legacyMatch[1];
    const cleaned = cleanResponse(stdout);
    if (cleaned.length > 0) result.response = cleaned;
  }

  const usageMatch = combined.match(TOKEN_USAGE_REGEX);
  if (usageMatch) {
    result.usage = {
      inputTokens: parseInt(usageMatch[1], 10) || 0,
      outputTokens: parseInt(usageMatch[2], 10) || 0,
    };
  }

  const costMatch = combined.match(COST_REGEX);
  if (costMatch?.[1]) result.costUsd = parseFloat(costMatch[1]);

  if (stderr.trim()) {
    const errorLines = stderr
      .split("\n")
      .filter((line) => /error|exception|traceback|failed/i.test(line))
      .filter((line) => !/INFO|DEBUG|warn/i.test(line));
    if (errorLines.length > 0) result.errorMessage = errorLines.slice(0, 5).join("\n");
  }

  return result;
}

export async function execute(ctx) {
  const config = ctx.agent?.adapterConfig ?? {};

  const hermesCmd = cfgString(config.hermesCommand) || HERMES_CLI;
  const model = cfgString(config.model) || DEFAULT_MODEL;
  const provider = cfgString(config.provider) || DEFAULT_PROVIDER;
  const profileHome = cfgString(config.hermesProfileHome) || DEFAULT_PROFILE_HOME;
  const timeoutSec = cfgNumber(config.timeoutSec) || DEFAULT_TIMEOUT_SEC;
  const graceSec = cfgNumber(config.graceSec) || DEFAULT_GRACE_SEC;
  const toolsets = cfgString(config.toolsets) || cfgStringArray(config.enabledToolsets)?.join(",");
  const extraArgs = cfgStringArray(config.extraArgs);
  const persistSession = cfgBoolean(config.persistSession) !== false;
  const worktreeMode = cfgBoolean(config.worktreeMode) === true;
  const checkpoints = cfgBoolean(config.checkpoints) === true;
  const useQuiet = cfgBoolean(config.quiet) !== false;

  const prompt = buildPrompt(ctx, config);

  const args = ["chat", "-q", prompt];
  if (useQuiet) args.push("-Q");
  if (model) args.push("-m", model);
  args.push("--provider", provider);
  if (toolsets) args.push("-t", toolsets);
  if (worktreeMode) args.push("-w");
  if (checkpoints) args.push("--checkpoints");
  if (cfgBoolean(config.verbose) === true) args.push("-v");
  args.push("--source", "tool");
  args.push("--yolo");

  const prevSessionId = cfgString(ctx.runtime?.sessionParams?.sessionId);
  if (persistSession && prevSessionId) args.push("--resume", prevSessionId);
  if (extraArgs?.length) args.push(...extraArgs);

  // Pin Hermes to the DeepSeek profile by default. The agent can override
  // by setting adapter_config.hermesProfileHome or adapter_config.env.HERMES_HOME.
  const env = {
    ...process.env,
    ...buildPaperclipEnv(ctx.agent),
    HERMES_HOME: profileHome,
  };
  if (ctx.runId) env.PAPERCLIP_RUN_ID = ctx.runId;
  const taskId = cfgString(ctx.context?.taskId);
  if (taskId) env.PAPERCLIP_TASK_ID = taskId;

  // Parity with hermes_local (paperclip-src/server/src/adapters/registry.ts:267):
  // inject the per-run agent auth token so the agent can call the Paperclip API.
  // Without this, every Paperclip API write from the running agent fails with 401.
  //
  // Resolve env from the runtime-resolved config (ctx.config.env contains plain
  // strings — Paperclip's secrets service unwraps {type:"plain"|"secret_ref", ...}
  // bindings before invocation in services/heartbeat.ts:5433-5437).
  // Fall back to agent.adapterConfig.env with manual unwrapping for older paths.
  function unwrapEnvValue(v) {
    if (typeof v === "string") return v;
    if (v && typeof v === "object" && !Array.isArray(v)) {
      if (v.type === "plain" && typeof v.value === "string") return v.value;
    }
    return undefined; // skip secret_ref / unknown types — let resolver handle them
  }
  const resolvedUserEnv =
    ctx.config && typeof ctx.config === "object" && ctx.config.env && typeof ctx.config.env === "object" && !Array.isArray(ctx.config.env)
      ? ctx.config.env
      : null;
  const rawUserEnv =
    typeof config.env === "object" && config.env !== null && !Array.isArray(config.env)
      ? config.env
      : {};
  // Prefer pre-resolved values from ctx.config.env when available; fall back to
  // unwrapping raw bindings from agent.adapterConfig.env.
  const flattenedUserEnv = {};
  for (const [k, v] of Object.entries(rawUserEnv)) {
    const resolved = resolvedUserEnv && typeof resolvedUserEnv[k] === "string" ? resolvedUserEnv[k] : unwrapEnvValue(v);
    if (typeof resolved === "string") flattenedUserEnv[k] = resolved;
  }
  const userEnvApiKey = flattenedUserEnv.PAPERCLIP_API_KEY;
  const explicitApiKey =
    typeof userEnvApiKey === "string" && userEnvApiKey.trim().length > 0;
  if (ctx.authToken && !explicitApiKey) env.PAPERCLIP_API_KEY = ctx.authToken;

  // Apply unwrapped user env (may override HERMES_HOME, OPENAI_API_KEY, etc.).
  Object.assign(env, flattenedUserEnv);

  const cwd = cfgString(config.cwd) || cfgString(ctx.config?.workspaceDir) || ".";
  try {
    await ensureAbsoluteDirectory(cwd);
  } catch {
    // non-fatal
  }

  await ctx.onLog(
    "stdout",
    `[deepseek] Starting Hermes (model=${model}, provider=${provider}, profileHome=${env.HERMES_HOME}, timeout=${timeoutSec}s)\n`,
  );
  if (prevSessionId) {
    await ctx.onLog("stdout", `[deepseek] Resuming session: ${prevSessionId}\n`);
  }

  // Reclassify benign Hermes stderr lines as stdout so the UI doesn't paint them red.
  const wrappedOnLog = async (stream, chunk) => {
    if (stream === "stderr") {
      const trimmed = chunk.trimEnd();
      const isBenign =
        /^\[?\d{4}[-/]\d{2}[-/]\d{2}T/.test(trimmed) ||
        /^[A-Z]+:\s+(INFO|DEBUG|WARN|WARNING)\b/.test(trimmed) ||
        /Successfully registered all tools/.test(trimmed) ||
        /MCP [Ss]erver/.test(trimmed) ||
        /tool registered successfully/.test(trimmed) ||
        /Application initialized/.test(trimmed);
      if (isBenign) return ctx.onLog("stdout", chunk);
    }
    return ctx.onLog(stream, chunk);
  };

  // Forward ctx.onSpawn so Paperclip persists processPid/processGroupId to the
  // heartbeat_runs row. Without it, the reaper cannot verify the child is alive
  // (run.processPid is null) and treats the run as orphaned during long quiet
  // phases (DeepSeek V4-Pro thinking can be silent for 60-90s per turn).
  const result = await runChildProcess(ctx.runId, hermesCmd, args, {
    cwd,
    env,
    timeoutSec,
    graceSec,
    onLog: wrappedOnLog,
    onSpawn: ctx.onSpawn,
  });

  const parsed = parseHermesOutput(result.stdout || "", result.stderr || "");
  await ctx.onLog(
    "stdout",
    `[deepseek] Exit code: ${result.exitCode ?? "null"}, timed out: ${result.timedOut}\n`,
  );
  if (parsed.sessionId) {
    await ctx.onLog("stdout", `[deepseek] Session: ${parsed.sessionId}\n`);
  }

  const executionResult = {
    exitCode: result.exitCode,
    signal: result.signal,
    timedOut: result.timedOut,
    provider,
    model,
  };
  if (parsed.errorMessage) executionResult.errorMessage = parsed.errorMessage;
  if (parsed.usage) executionResult.usage = parsed.usage;
  if (parsed.costUsd !== undefined) executionResult.costUsd = parsed.costUsd;
  if (parsed.response) executionResult.summary = parsed.response.slice(0, 2000);

  executionResult.resultJson = {
    result: parsed.response || "",
    session_id: parsed.sessionId || null,
    usage: parsed.usage || null,
    cost_usd: parsed.costUsd ?? null,
  };

  if (persistSession && parsed.sessionId) {
    executionResult.sessionParams = { sessionId: parsed.sessionId };
    executionResult.sessionDisplayId = parsed.sessionId.slice(0, 16);
  }

  return executionResult;
}
