import { randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import type { RuntimeManager } from "@qjclaw/runtime-manager";
import type { ChatMessage, ProjectResolvedAttachment } from "@qjclaw/shared-types";

interface ProjectWorkspaceExecutionInput {
  sessionId: string;
  projectRoot: string;
  prompt: string;
  userPrompt?: string;
  attachments?: ProjectResolvedAttachment[];
  runId?: string;
  extraEnv?: Record<string, string>;
}

interface ProjectWorkspaceExecutionCallbacks {
  onStarted?: (runId: string) => void;
  onStatus?: (stage: string, label: string, detail?: string) => void;
  onDelta?: (textDelta: string, fullText: string | undefined, runId: string) => void;
}

interface RunnerEvent {
  type?: string;
  target?: string;
  runId?: string;
  stage?: string;
  label?: string;
  detail?: string;
  message?: string;
  errorCategory?: string;
  content?: string;
  textDelta?: string;
  fullText?: string;
  result?: unknown;
}

export interface ProjectWorkspaceHandoffResult {
  runId: string;
  handoff: {
    target: "chat-fallback";
    content: string;
  };
}

export interface ProjectWorkspaceReplyResult {
  runId: string;
  reply: ChatMessage;
}

export type ProjectWorkspaceExecutionResult = ProjectWorkspaceReplyResult | ProjectWorkspaceHandoffResult;

interface ProjectAutomationCommandConfig {
  runtime?: unknown;
  script?: unknown;
  args?: unknown;
  env?: unknown;
}

interface ResolvedProjectAutomationCommand {
  runtime: "python";
  executablePath: string;
  scriptPath: string;
  args: string[];
  env: Record<string, string>;
}

const EVENT_PREFIX = "QJC_WORKSPACE_EVENT\t";

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

function buildWorkspaceExecutionError(message: string, errorCategory?: string): Error & { errorCategory?: string } {
  const error = new Error(message) as Error & { errorCategory?: string };
  if (errorCategory) {
    error.errorCategory = errorCategory;
  }
  return error;
}

function resolveExecutableOnPath(commandNames: readonly string[]): string | undefined {
  const pathEntries = (process.env.PATH ?? "")
    .split(path.delimiter)
    .map((entry) => entry.trim())
    .filter(Boolean);

  for (const commandName of commandNames) {
    const trimmed = commandName.trim();
    if (!trimmed) {
      continue;
    }

    if (path.isAbsolute(trimmed) && existsSync(trimmed)) {
      return trimmed;
    }

    for (const directory of pathEntries) {
      const candidate = path.join(directory, trimmed);
      if (existsSync(candidate)) {
        return candidate;
      }
    }
  }

  return undefined;
}

function resolveInjectedBinaryPath(
  envName: string,
  bundledPath: string | undefined,
  commandNames: readonly string[]
): string | undefined {
  const configured = process.env[envName]?.trim();
  if (configured) {
    return configured;
  }
  if (bundledPath && existsSync(bundledPath)) {
    return bundledPath;
  }
  return resolveExecutableOnPath(commandNames);
}

async function pathExists(targetPath: string): Promise<boolean> {
  try {
    await stat(targetPath);
    return true;
  } catch {
    return false;
  }
}

function parseRunnerEvent(line: string): RunnerEvent | null {
  if (!line.startsWith(EVENT_PREFIX)) {
    return null;
  }

  try {
    return JSON.parse(line.slice(EVENT_PREFIX.length)) as RunnerEvent;
  } catch {
    return null;
  }
}

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 renderTemplate(
  value: string,
  variables: Record<string, string>
): string {
  return value.replace(/\{([a-zA-Z0-9_]+)\}/g, (_match, key: string) => variables[key] ?? "");
}

function normalizeAutomationEnv(value: unknown): Record<string, string> {
  if (!value || typeof value !== "object" || Array.isArray(value)) {
    return {};
  }

  return Object.fromEntries(
    Object.entries(value)
      .filter((entry): entry is [string, string] => typeof entry[1] === "string" && entry[0].trim().length > 0)
      .map(([key, envValue]) => [key, envValue])
  );
}

async function resolveProjectAutomationCommand(
  projectRoot: string,
  input: ProjectWorkspaceExecutionInput,
  pythonExecutable: string
): Promise<ResolvedProjectAutomationCommand | null> {
  const projectJsonPath = path.join(projectRoot, "project.json");
  let rawProjectJson: string;
  try {
    rawProjectJson = await readFile(projectJsonPath, "utf8");
  } catch {
    return null;
  }

  let parsed: Record<string, unknown>;
  try {
    parsed = JSON.parse(rawProjectJson) as Record<string, unknown>;
  } catch {
    return null;
  }

  const automation = (parsed.workspaceAutomation ?? parsed.workspace_automation) as ProjectAutomationCommandConfig | undefined;
  if (!automation || typeof automation !== "object" || Array.isArray(automation)) {
    return null;
  }

  const runtime = typeof automation.runtime === "string" ? automation.runtime.trim().toLowerCase() : "";
  if (runtime !== "python") {
    return null;
  }

  const script = typeof automation.script === "string" ? automation.script.trim() : "";
  if (!script) {
    return null;
  }

  const args = Array.isArray(automation.args)
    ? automation.args.filter((value): value is string => typeof value === "string")
    : [];
  const env = normalizeAutomationEnv(automation.env);
  const variables = {
    prompt: input.userPrompt?.trim() || input.prompt,
    preparedPrompt: input.prompt,
    projectRoot: input.projectRoot,
    sessionId: input.sessionId,
    firstAttachmentPath: input.attachments?.[0]?.projectPath ?? "",
    attachmentPathsJson: JSON.stringify(input.attachments?.map((attachment) => attachment.projectPath) ?? [])
  };
  const scriptPath = path.resolve(projectRoot, renderTemplate(script, variables));

  if (!(await pathExists(scriptPath))) {
    throw new Error(`Project automation script is missing: ${scriptPath}`);
  }

  return {
    runtime: "python",
    executablePath: pythonExecutable,
    scriptPath,
    args: args.map((value) => renderTemplate(value, variables)),
    env: Object.fromEntries(
      Object.entries(env).map(([key, value]) => [key, renderTemplate(value, variables)])
    )
  };
}

async function resolveRunnerScriptPath(): Promise<string> {
  const directPath = path.resolve(__dirname, "./project-workspace-agent-runner.js");
  const candidates = [directPath];

  if (directPath.includes("app.asar")) {
    candidates.unshift(directPath.replace("app.asar", "app.asar.unpacked"));
  }

  for (const candidate of candidates) {
    if (await pathExists(candidate)) {
      return candidate;
    }
  }

  throw new Error(`Workspace runner script is missing: ${candidates.join(" | ")}`);
}

export class ProjectWorkspaceExecutorService {
  private readonly runtimeManager: RuntimeManager;

  constructor(runtimeManager: RuntimeManager) {
    this.runtimeManager = runtimeManager;
  }

  async execute(
    input: ProjectWorkspaceExecutionInput,
    callbacks: ProjectWorkspaceExecutionCallbacks = {}
  ): Promise<ProjectWorkspaceExecutionResult> {
    const runtimeStatus = await this.runtimeManager.status();
    if (runtimeStatus.payloadState !== "ready") {
      throw new Error("Bundled runtime payload is not ready for project workspace execution.");
    }

    await this.runtimeManager.syncManagedConfig("sync");
    const paths = this.runtimeManager.resolveBundledPaths();
    const automationCommand = await resolveProjectAutomationCommand(input.projectRoot, input, paths.pythonExecutable);
    const runnerScriptPath = automationCommand ? null : await resolveRunnerScriptPath();

    const vendorPackageDir = path.join(paths.runtimeDir, "openclaw", "package");
    const instrumentationDir = path.join(paths.runtimeDataDir, "workspace-runner", "instrumented-modules");
    const runId = input.runId?.trim() || randomUUID();

    callbacks.onStatus?.(
      automationCommand ? "launch-workspace-automation" : "launch-workspace",
      automationCommand ? "Launching project workspace automation" : "Launching project workspace agent"
    );

    return await new Promise<ProjectWorkspaceExecutionResult>((resolve, reject) => {
      let settled = false;
      let stderr = "";
      let stdoutBuffer = "";
      let activeRunId = runId;
      const resolvedFfmpeg = resolveInjectedBinaryPath("FFMPEG_BIN", paths.ffmpegExecutable, ["ffmpeg"]);
      const resolvedFfprobe = resolveInjectedBinaryPath("FFPROBE_BIN", paths.ffprobeExecutable, ["ffprobe"]);

      const childEnv = {
        ...process.env,
        OPENCLAW_HOME: paths.runtimeDataDir,
        OPENCLAW_STATE_DIR: paths.runtimeStateDir,
        OPENCLAW_CONFIG_PATH: paths.managedConfigPath,
        QJCLAW_BUNDLED_RUNTIME_DIR: paths.runtimeDir,
        PLAYWRIGHT_BROWSERS_PATH: paths.playwrightBrowsersPath,
        OPENCLAW_HIDE_BANNER: "1",
        OPENCLAW_SUPPRESS_NOTES: "1",
        NODE_NO_WARNINGS: process.env.NODE_NO_WARNINGS || "1",
        PYTHONUTF8: "1",
        PYTHONIOENCODING: "utf-8",
        PATH: [
          paths.nodeExecutable ? path.dirname(paths.nodeExecutable) : null,
          path.dirname(paths.pythonExecutable),
          paths.ffmpegExecutable ? path.dirname(paths.ffmpegExecutable) : null,
          paths.ffprobeExecutable ? path.dirname(paths.ffprobeExecutable) : null,
          process.env.PATH ?? ""
        ].filter(Boolean).join(path.delimiter),
        QJC_PROJECT_ATTACHMENTS_JSON: JSON.stringify(input.attachments ?? []),
        QJC_PROJECT_MAIN_IMAGE: input.attachments?.find((attachment) => attachment.kind === "image")?.projectPath ?? "",
        ...(resolvedFfmpeg ? { FFMPEG_BIN: resolvedFfmpeg } : {}),
        ...(resolvedFfprobe ? { FFPROBE_BIN: resolvedFfprobe } : {}),
        ...(automationCommand?.env ?? {}),
        ...(input.extraEnv ?? {})
      };
      const spawnOptions = {
        cwd: input.projectRoot,
        stdio: ["pipe", "pipe", "pipe"] as ["pipe", "pipe", "pipe"],
        env: childEnv
      };

      const child = automationCommand
        ? spawn(automationCommand.executablePath, [automationCommand.scriptPath, ...automationCommand.args], spawnOptions)
        : spawn(paths.nodeExecutable, [runnerScriptPath!], spawnOptions);

      const finishWithError = (message: string, errorCategory?: string) => {
        if (settled) {
          return;
        }
        settled = true;
        reject(buildWorkspaceExecutionError(message, errorCategory));
      };

      child.stdout.setEncoding("utf8");
      child.stdout.on("data", (chunk: string) => {
        stdoutBuffer += chunk;
        const lines = stdoutBuffer.split(/\r?\n/);
        stdoutBuffer = lines.pop() ?? "";

        for (const line of lines) {
          const event = parseRunnerEvent(line);
          if (!event) {
            continue;
          }

          if (event.type === "started" && typeof event.runId === "string" && event.runId) {
            activeRunId = event.runId;
            callbacks.onStarted?.(activeRunId);
            continue;
          }

          if (event.type === "status" && typeof event.stage === "string" && typeof event.label === "string") {
            callbacks.onStatus?.(event.stage, event.label, typeof event.detail === "string" ? event.detail : undefined);
            continue;
          }

          if (event.type === "delta" && typeof event.textDelta === "string" && event.textDelta) {
            callbacks.onDelta?.(
              event.textDelta,
              typeof event.fullText === "string" ? event.fullText : undefined,
              event.runId ?? activeRunId
            );
            continue;
          }

          if (event.type === "completed") {
            if (settled) {
              return;
            }
            settled = true;
            const content = typeof event.content === "string" && event.content.trim()
              ? event.content
              : extractReplyText(event.result);
            resolve({
              runId: activeRunId,
              reply: {
                id: randomUUID(),
                role: "assistant",
                content,
                createdAt: new Date().toISOString()
              }
            });
            return;
          }

          if (event.type === "handoff" && event.target === "chat-fallback") {
            if (settled) {
              return;
            }
            settled = true;
            resolve({
              runId: activeRunId,
              handoff: {
                target: "chat-fallback",
                content: typeof event.content === "string" && event.content.trim()
                  ? event.content
                  : input.userPrompt?.trim() || input.prompt
              }
            });
            return;
          }

          if (event.type === "error") {
            finishWithError(
              event.message?.trim() || "Project workspace execution failed.",
              typeof event.errorCategory === "string" ? event.errorCategory.trim() || undefined : undefined
            );
            return;
          }
        }
      });

      child.stderr.setEncoding("utf8");
      child.stderr.on("data", (chunk: string) => {
        stderr += chunk;
      });

      child.on("error", (error) => {
        finishWithError(`Failed to start project workspace runner: ${toErrorMessage(error)}`);
      });

      child.on("close", (code) => {
        if (settled) {
          return;
        }
        const message = stderr.trim() || `Project workspace runner exited with code ${code ?? "unknown"}.`;
        finishWithError(message);
      });

      const payload = JSON.stringify({
        vendorPackageDir,
        instrumentationDir,
        projectRoot: input.projectRoot,
        sessionId: input.sessionId,
        prompt: input.prompt,
        attachments: input.attachments ?? [],
        runId
      });
      if (!automationCommand) {
        child.stdin.write(payload);
      }
      child.stdin.end();
    });
  }
}

