import { createHash, randomUUID } from "node:crypto";
import { spawn } from "node:child_process";
import { existsSync } from "node:fs";
import type { Dirent } from "node:fs";
import { readdir, readFile, stat } from "node:fs/promises";
import path from "node:path";
import type { RuntimeManager } from "@qjclaw/runtime-manager";
import type { ChatMessage, ProjectResolvedAttachment, TaskPanelArtifact } 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;
  artifacts?: TaskPanelArtifact[];
  handoff: {
    target: "chat-fallback";
    content: string;
  };
}

export interface ProjectWorkspaceReplyResult {
  runId: string;
  artifacts?: TaskPanelArtifact[];
  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";
const WORKSPACE_ARTIFACT_EXTENSIONS = new Set([
  ".md",
  ".txt",
  ".pdf",
  ".docx",
  ".xlsx",
  ".csv",
  ".json",
  ".mp4",
  ".mov",
  ".png",
  ".jpg",
  ".jpeg",
  ".webp"
]);
const TEXT_SUMMARY_EXTENSIONS = new Set([".md", ".txt"]);

export interface WorkspaceArtifactSnapshotEntry {
  path: string;
  relativePath: string;
  size: number;
  mtimeMs: number;
}

export type WorkspaceArtifactSnapshot = Map<string, WorkspaceArtifactSnapshotEntry>;

export interface CollectWorkspaceExecutionArtifactsInput {
  projectRoot: string;
  beforeSnapshot: WorkspaceArtifactSnapshot;
  assistantSummary?: string;
}

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 toLocalDateTimeValue(date: Date): string {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  const hour = String(date.getHours()).padStart(2, "0");
  const minute = String(date.getMinutes()).padStart(2, "0");
  const second = String(date.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}

function createWorkspaceArtifactId(relativePath: string): string {
  return "artifact-" + createHash("sha256").update(relativePath).digest("hex").slice(0, 32);
}

function normalizeSnapshotPath(value: string): string {
  return value.replace(/\\/g, "/");
}

function shouldSkipWorkspaceArtifactPath(relativePath: string): boolean {
  const segments = normalizeSnapshotPath(relativePath).split("/").filter(Boolean);
  if (segments.some((segment) => segment === ".git" || segment === "node_modules" || segment.startsWith(".qjc-"))) {
    return true;
  }
  if (segments[0] === "memory" && segments[1] === "workspace-runner") {
    return true;
  }
  if (segments[0] === "inputs") {
    return true;
  }
  return false;
}

function inferWorkspaceArtifactKind(filePath: string): string {
  const extension = path.extname(filePath).toLowerCase();
  if (extension === ".mp4" || extension === ".mov") {
    return "视频";
  }
  if (extension === ".png" || extension === ".jpg" || extension === ".jpeg" || extension === ".webp") {
    return "图片";
  }
  if (extension === ".xlsx" || extension === ".csv") {
    return "表格";
  }
  return "文档";
}

async function readWorkspaceArtifactSummary(filePath: string, fallbackSummary?: string): Promise<string | undefined> {
  if (!TEXT_SUMMARY_EXTENSIONS.has(path.extname(filePath).toLowerCase())) {
    return fallbackSummary?.trim().slice(0, 120) || undefined;
  }

  try {
    const content = await readFile(filePath, "utf8");
    return content.replace(/\s+/g, " ").trim().slice(0, 120) || fallbackSummary?.trim().slice(0, 120) || undefined;
  } catch {
    return fallbackSummary?.trim().slice(0, 120) || undefined;
  }
}

async function scanWorkspaceArtifacts(
  projectRoot: string,
  currentDirectory: string,
  snapshot: WorkspaceArtifactSnapshot
): Promise<void> {
  let entries: Dirent[];
  try {
    entries = await readdir(currentDirectory, { withFileTypes: true });
  } catch {
    return;
  }

  for (const entry of entries) {
    const absolutePath = path.join(currentDirectory, entry.name);
    const relativePath = normalizeSnapshotPath(path.relative(projectRoot, absolutePath));
    if (!relativePath || shouldSkipWorkspaceArtifactPath(relativePath)) {
      continue;
    }

    if (entry.isDirectory()) {
      await scanWorkspaceArtifacts(projectRoot, absolutePath, snapshot);
      continue;
    }
    if (!entry.isFile() || !WORKSPACE_ARTIFACT_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) {
      continue;
    }

    try {
      const stats = await stat(absolutePath);
      snapshot.set(relativePath, {
        path: absolutePath,
        relativePath,
        size: stats.size,
        mtimeMs: stats.mtimeMs
      });
    } catch {
      // Files may be moved by the runtime while we are scanning.
    }
  }
}

export async function createWorkspaceArtifactSnapshot(projectRoot: string): Promise<WorkspaceArtifactSnapshot> {
  const snapshot: WorkspaceArtifactSnapshot = new Map();
  await scanWorkspaceArtifacts(projectRoot, projectRoot, snapshot);
  return snapshot;
}

export async function collectWorkspaceExecutionArtifacts(
  input: CollectWorkspaceExecutionArtifactsInput
): Promise<TaskPanelArtifact[]> {
  const afterSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot);
  const changedEntries = [...afterSnapshot.values()]
    .filter((entry) => {
      const before = input.beforeSnapshot.get(entry.relativePath);
      return !before || before.size !== entry.size || before.mtimeMs !== entry.mtimeMs;
    })
    .sort((left, right) => left.relativePath.localeCompare(right.relativePath));

  const artifacts: TaskPanelArtifact[] = [];
  for (const entry of changedEntries) {
    artifacts.push({
      id: createWorkspaceArtifactId(entry.relativePath),
      name: path.basename(entry.path),
      kind: inferWorkspaceArtifactKind(entry.path),
      summary: await readWorkspaceArtifactSummary(entry.path, input.assistantSummary),
      url: entry.path,
      path: entry.path,
      producedAt: toLocalDateTimeValue(new Date(entry.mtimeMs))
    });
  }

  return artifacts;
}

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 beforeArtifactSnapshot = await createWorkspaceArtifactSnapshot(input.projectRoot);

    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));
      };
      const collectArtifacts = (assistantSummary?: string) => collectWorkspaceExecutionArtifacts({
        projectRoot: input.projectRoot,
        beforeSnapshot: beforeArtifactSnapshot,
        assistantSummary
      }).catch(() => []);

      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);
            void collectArtifacts(content).then((artifacts) => {
              resolve({
                runId: activeRunId,
                artifacts,
                reply: {
                  id: randomUUID(),
                  role: "assistant",
                  content,
                  createdAt: new Date().toISOString()
                }
              });
            });
            return;
          }

          if (event.type === "handoff" && event.target === "chat-fallback") {
            if (settled) {
              return;
            }
            settled = true;
            const content = typeof event.content === "string" && event.content.trim()
              ? event.content
              : input.userPrompt?.trim() || input.prompt;
            void collectArtifacts(content).then((artifacts) => {
              resolve({
                runId: activeRunId,
                artifacts,
                handoff: {
                  target: "chat-fallback",
                  content
                }
              });
            });
            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();
    });
  }
}
