import { createHash, randomUUID } from "node:crypto";
import { lstat, mkdir, readFile, readdir, readlink, rm, symlink, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import type { ProjectResolvedAttachment } from "@qjclaw/shared-types";

interface RunnerInput {
  vendorPackageDir: string;
  instrumentationDir?: string;
  projectRoot: string;
  sessionId: string;
  prompt: string;
  attachments?: ProjectResolvedAttachment[];
  runId?: string;
}

interface AgentCommandModule {
  t?: (options: Record<string, unknown>, runtime?: Record<string, unknown>) => Promise<unknown>;
}

interface InstrumentedModelSelectionModule {
  __qjcOnAgentEvent?: (listener: (event: InstrumentedAgentEvent) => void) => (() => boolean) | (() => void);
}

interface InstrumentedAgentEvent {
  runId?: string;
  stream?: string;
  data?: {
    text?: unknown;
    delta?: unknown;
  };
}

const EVENT_PREFIX = "QJC_WORKSPACE_EVENT\t";

function emit(payload: Record<string, unknown>): void {
  process.stdout.write(`${EVENT_PREFIX}${JSON.stringify(payload)}\n`);
}

function toErrorMessage(error: unknown): string {
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

async function readStdin(): Promise<string> {
  const chunks: Buffer[] = [];
  for await (const chunk of process.stdin) {
    chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString("utf8");
}

async function resolveAgentModulePath(vendorPackageDir: string): Promise<string> {
  const distDir = path.join(vendorPackageDir, "dist");
  const entries = await readdir(distDir);
  const agentModuleFile = [...entries]
    .filter((entry) => /^agent-[A-Za-z0-9_-]+\.js$/.test(entry))
    .sort()[0];
  if (!agentModuleFile) {
    throw new Error(`Unable to locate bundled OpenClaw agent module under ${distDir}.`);
  }

  return path.join(distDir, agentModuleFile);
}

function buildInstrumentationKey(agentModulePath: string, modelSelectionSpecifier: string): string {
  return createHash("sha1")
    .update(agentModulePath)
    .update("\n")
    .update(modelSelectionSpecifier)
    .digest("hex")
    .slice(0, 12);
}

function resolveInstrumentationDir(input: RunnerInput): string {
  const explicitDir = input.instrumentationDir?.trim();
  if (explicitDir) {
    return explicitDir;
  }

  const stateDir = process.env.OPENCLAW_STATE_DIR?.trim();
  if (stateDir) {
    return path.join(stateDir, "workspace-runner", "instrumented-modules");
  }

  return path.join(input.projectRoot, "memory", "workspace-runner", "instrumented-modules");
}

function rewriteModuleSpecifiers(
  source: string,
  baseDir: string,
  overrides: Record<string, string> = {}
): string {
  const toModuleUrl = (specifier: string): string => {
    const override = overrides[specifier];
    if (override) {
      return override;
    }
    if (specifier.startsWith(".")) {
      return pathToFileURL(path.resolve(baseDir, specifier)).href;
    }
    return specifier;
  };

  return source
    .replace(/(^|[\r\n])(\s*(?:import|export)\s+[^"'\r\n;]+?\s+from\s*)(["'])([^"'\r\n]+)\3/g, (
      _match,
      linePrefix: string,
      prefix: string,
      quote: string,
      specifier: string
    ) => {
      return `${linePrefix}${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
    })
    .replace(/(^|[\r\n])(\s*import\s*)(["'])([^"'\r\n]+)\3/g, (
      _match,
      linePrefix: string,
      prefix: string,
      quote: string,
      specifier: string
    ) => {
      return `${linePrefix}${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
    })
    .replace(/\b(import\s*\(\s*)(["'])([^"'\r\n]+)\2/g, (_match, prefix: string, quote: string, specifier: string) => {
      return `${prefix}${quote}${toModuleUrl(specifier)}${quote}`;
    });
}

async function ensureInstrumentationNodeModulesLink(instrumentationDir: string, vendorPackageDir: string): Promise<void> {
  const vendorNodeModulesPath = path.join(vendorPackageDir, "node_modules");
  try {
    const vendorNodeModules = await lstat(vendorNodeModulesPath);
    if (!vendorNodeModules.isDirectory()) {
      throw new Error(`${vendorNodeModulesPath} is not a directory.`);
    }
  } catch (error) {
    throw new Error(`Unable to locate OpenClaw node_modules at ${vendorNodeModulesPath}: ${toErrorMessage(error)}`);
  }

  const linkPath = path.join(instrumentationDir, "node_modules");
  const normalizeForCompare = (value: string): string => path.resolve(value).toLowerCase();
  const expectedTarget = normalizeForCompare(vendorNodeModulesPath);
  const existing = await lstat(linkPath).catch(() => undefined);

  if (existing) {
    if (existing.isSymbolicLink()) {
      const currentTarget = await readlink(linkPath);
      const resolvedTarget = normalizeForCompare(path.resolve(path.dirname(linkPath), currentTarget));
      if (resolvedTarget === expectedTarget) {
        return;
      }
    }
    await rm(linkPath, { recursive: true, force: true });
  }

  await symlink(vendorNodeModulesPath, linkPath, "junction");
}

async function ensureInstrumentedWorkspaceModules(agentModulePath: string, instrumentationDir: string): Promise<{
  agentModuleUrl: string;
  modelSelectionModuleUrl: string;
}> {
  const agentSource = await readFile(agentModulePath, "utf8");
  const modelSelectionImportMatch = agentSource.match(/from\s+["'](\.\/model-selection-[^"']+\.js)["']/);
  if (!modelSelectionImportMatch) {
    throw new Error(`Unable to locate model-selection import in ${agentModulePath}.`);
  }

  const modelSelectionSpecifier = modelSelectionImportMatch[1];
  const distDir = path.dirname(agentModulePath);
  const vendorPackageDir = path.dirname(distDir);
  const modelSelectionPath = path.resolve(distDir, modelSelectionSpecifier);
  const instrumentationKey = buildInstrumentationKey(agentModulePath, modelSelectionSpecifier);
  const instrumentedAgentFileName = `.qjc-agent-${instrumentationKey}.js`;
  const instrumentedModelSelectionFileName = `.qjc-model-selection-${instrumentationKey}.js`;
  const instrumentedAgentPath = path.join(instrumentationDir, instrumentedAgentFileName);
  const instrumentedModelSelectionPath = path.join(instrumentationDir, instrumentedModelSelectionFileName);
  const instrumentedModelSelectionUrl = pathToFileURL(instrumentedModelSelectionPath).href;

  await mkdir(instrumentationDir, { recursive: true });
  await ensureInstrumentationNodeModulesLink(instrumentationDir, vendorPackageDir);

  const modelSelectionSource = await readFile(modelSelectionPath, "utf8");
  const instrumentedModelSelectionSource = `${rewriteModuleSpecifiers(modelSelectionSource, distDir)}\nexport { onAgentEvent as __qjcOnAgentEvent };\n`;
  await writeFile(instrumentedModelSelectionPath, instrumentedModelSelectionSource, "utf8");

  const instrumentedAgentSource = rewriteModuleSpecifiers(agentSource, distDir, {
    [modelSelectionSpecifier]: instrumentedModelSelectionUrl
  });
  await writeFile(instrumentedAgentPath, instrumentedAgentSource, "utf8");

  return {
    agentModuleUrl: pathToFileURL(instrumentedAgentPath).href,
    modelSelectionModuleUrl: pathToFileURL(instrumentedModelSelectionPath).href
  };
}

function extractReplyText(result: unknown): string {
  const payloads = Array.isArray((result as { payloads?: unknown[] } | null)?.payloads)
    ? ((result as { payloads: unknown[] }).payloads)
    : [];
  const parts: string[] = [];

  for (const payload of payloads) {
    if (typeof payload === "string") {
      parts.push(payload);
      continue;
    }
    if (!payload || typeof payload !== "object") {
      continue;
    }

    const typed = payload as {
      text?: unknown;
      markdown?: unknown;
      content?: unknown;
      caption?: unknown;
    };
    for (const candidate of [typed.text, typed.markdown, typed.content, typed.caption]) {
      if (typeof candidate === "string" && candidate.trim()) {
        parts.push(candidate);
        break;
      }
    }
  }

  return parts.join("\n\n").trim();
}

function createRuntimeSessionIdentity(sessionId: string): { sessionId: string; sessionKey: string } {
  const digest = createHash("sha1").update(sessionId).digest("hex");
  const runtimeSessionId = `desktop-${digest.slice(0, 32)}`;
  return {
    sessionId: runtimeSessionId,
    sessionKey: `${runtimeSessionId}-key`
  };
}

function renderAttachmentPrelude(projectRoot: string, attachments: ProjectResolvedAttachment[] | undefined): string {
  if (!attachments?.length) {
    return "";
  }

  const lines = [
    "",
    "Project attachments available for this request:",
    ...attachments.map((attachment, index) => {
      const relativePath = attachment.relativeProjectPath || path.relative(projectRoot, attachment.projectPath);
      return `${index + 1}. ${attachment.kind} | ${attachment.name} | ${relativePath}`;
    }),
    ""
  ];
  return lines.join("\n");
}

async function main(): Promise<void> {
  const inputRaw = await readStdin();
  const input = JSON.parse(inputRaw) as RunnerInput;
  const runId = input.runId?.trim() || randomUUID();

  process.env.OPENCLAW_HIDE_BANNER = "1";
  process.env.OPENCLAW_SUPPRESS_NOTES = "1";
  process.env.NODE_NO_WARNINGS = process.env.NODE_NO_WARNINGS || "1";
  process.chdir(input.projectRoot);

  emit({
    type: "started",
    runId
  });
  emit({
    type: "status",
    runId,
    stage: "workspace-agent",
    label: "Running project workspace agent"
  });

  const agentModulePath = await resolveAgentModulePath(input.vendorPackageDir);
  const instrumentedModules = await ensureInstrumentedWorkspaceModules(agentModulePath, resolveInstrumentationDir(input));
  const agentModule = await import(instrumentedModules.agentModuleUrl) as AgentCommandModule;
  const modelSelectionModule = await import(instrumentedModules.modelSelectionModuleUrl) as InstrumentedModelSelectionModule;
  if (typeof agentModule.t !== "function") {
    throw new Error("Bundled OpenClaw agent module does not expose agentCommand.");
  }

  let streamedText = "";
  const unsubscribe = typeof modelSelectionModule.__qjcOnAgentEvent === "function"
    ? modelSelectionModule.__qjcOnAgentEvent((event) => {
      if (event.runId !== runId || event.stream !== "assistant") {
        return;
      }

      const fullText = typeof event.data?.text === "string" ? event.data.text : "";
      const deltaFromEvent = typeof event.data?.delta === "string" ? event.data.delta : "";
      const nextFullText = fullText && fullText.length >= streamedText.length
        ? fullText
        : deltaFromEvent
          ? streamedText + deltaFromEvent
          : streamedText;
      const textDelta = nextFullText.startsWith(streamedText)
        ? nextFullText.slice(streamedText.length)
        : deltaFromEvent || nextFullText;
      if (!textDelta) {
        return;
      }

      streamedText = nextFullText;
      emit({
        type: "delta",
        runId,
        textDelta,
        fullText: nextFullText
      });
    })
    : undefined;

  const silentRuntime = {
    log: () => undefined,
    info: () => undefined,
    warn: () => undefined,
    error: () => undefined,
    debug: () => undefined
  };

  const runtimeSession = createRuntimeSessionIdentity(input.sessionId);
  const message = `${input.prompt}${renderAttachmentPrelude(input.projectRoot, input.attachments)}`.trim();
  try {
    const result = await agentModule.t({
      message,
      sessionId: runtimeSession.sessionId,
      sessionKey: runtimeSession.sessionKey,
      workspaceDir: input.projectRoot,
      runId
    }, silentRuntime);

    emit({
      type: "completed",
      runId,
      content: extractReplyText(result),
      result
    });
  } finally {
    unsubscribe?.();
  }
}

main().catch((error) => {
  emit({
    type: "error",
    message: toErrorMessage(error)
  });
  process.exitCode = 1;
});
