import { spawn } from "node:child_process";
import { access } from "node:fs/promises";
import path from "node:path";
import { NextResponse } from "next/server";
import type { PageOptionsState } from "@/types/page-options";
import { pickLeptosTiptapRuntimePageOptions } from "@/lib/documents/page-option-semantics";
import { PAGE_OPTION_SEMANTICS } from "@/lib/documents/page-option-semantics";
import {
  assertOptionsPatch,
  buildDocumentBridgeContext,
  buildDocumentCommandEnvelope,
} from "@/lib/documents/bridge";
import { PAGE_COMMAND_NAMES } from "@/lib/documents/page-command-contract";
import { executePageWriteBridgeCommand } from "@/lib/documents/page-write-command-adapter";

type AgentMessage = { role: "user" | "assistant"; content: string };

export type MnoteCliAgentRunPayload = {
  stream?: boolean;
  maxSteps?: number;
  scope?: string;
  messages: AgentMessage[];
  context?: {
    documentId?: string;
    workspaceId?: string;
    documentBlocks?: unknown;
    pageOptions?: PageOptionsState;
    node?: unknown;
    subtree?: unknown;
    outline?: unknown;
    evidence?: unknown;
    selectedUids?: string[];
  };
  options?: {
    ai?: {
      provider?: string;
      sessionId?: string;
      modelKey?: string;
      profileId?: string;
      model?: string;
    };
  };
};

type CliRunResult = {
  code: number | null;
  stdout: string;
  stderr: string;
};

const CLI_HOST_TIMEOUT_MS = 30_000;
const PAGE_OPTIONS_TOOL_NAME = "page_options_patch";

const toSseFrame = (event: string, data: unknown) => {
  const json = JSON.stringify(data ?? null);
  return `event: ${event}\ndata: ${json}\n\n`;
};

const AI_WRITABLE_BOOLEAN_PAGE_OPTIONS = [
  "wideLayout",
  "smallText",
  "showHeadingNumbers",
  "showToc",
  "showWordCount",
  "collapseBacklinks",
  "hideChildPages",
] as const satisfies ReadonlyArray<keyof PageOptionsState>;

const AI_WRITABLE_PAGE_FONT_VALUES = ["default", "song", "kai"] as const;
const AI_WRITABLE_LAYOUT_DENSITY_VALUES = ["compact", "normal", "spacious"] as const;

function isPlainRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function normalizeAiWritablePageOptionsPatch(value: unknown): Partial<PageOptionsState> | null {
  if (!isPlainRecord(value)) {
    return null;
  }

  const patch: Partial<PageOptionsState> = {};
  for (const key of AI_WRITABLE_BOOLEAN_PAGE_OPTIONS) {
    if (PAGE_OPTION_SEMANTICS[key].runtimeSupport !== "wired") {
      continue;
    }
    const nextValue = value[key];
    if (typeof nextValue === "boolean") {
      patch[key] = nextValue;
    }
  }

  const rawPageFont = value.pageFont;
  if (
    PAGE_OPTION_SEMANTICS.pageFont.runtimeSupport === "wired" &&
    typeof rawPageFont === "string" &&
    (AI_WRITABLE_PAGE_FONT_VALUES as readonly string[]).includes(rawPageFont)
  ) {
    patch.pageFont = rawPageFont;
  }

  const rawLayoutDensity = value.layoutDensity;
  if (
    PAGE_OPTION_SEMANTICS.layoutDensity.runtimeSupport === "wired" &&
    typeof rawLayoutDensity === "string" &&
    (AI_WRITABLE_LAYOUT_DENSITY_VALUES as readonly string[]).includes(rawLayoutDensity)
  ) {
    patch.layoutDensity = rawLayoutDensity;
  }

  const rawEmbedDefaultBlockId = value.embedDefaultBlockId;
  if (PAGE_OPTION_SEMANTICS.embedDefaultBlockId.runtimeSupport === "wired") {
    if (rawEmbedDefaultBlockId === null) {
      patch.embedDefaultBlockId = null;
    } else if (typeof rawEmbedDefaultBlockId === "string" && rawEmbedDefaultBlockId.trim()) {
      patch.embedDefaultBlockId = rawEmbedDefaultBlockId.trim();
    }
  }

  if (Object.keys(patch).length === 0) {
    return null;
  }
  assertOptionsPatch(patch);
  return patch;
}

function readLatestUserMessage(messages: AgentMessage[]): string {
  for (let index = messages.length - 1; index >= 0; index -= 1) {
    const message = messages[index];
    if (message?.role === "user" && String(message.content ?? "").trim()) {
      return String(message.content);
    }
  }
  return "";
}

function extractStructuredPageOptionsPatch(text: string): Partial<PageOptionsState> | null {
  const tagged = text.match(/<update_page_options>([\s\S]*?)<\/update_page_options>/i);
  const candidates = [tagged?.[1] ?? "", text];

  for (const candidate of candidates) {
    const trimmed = candidate.trim();
    if (!trimmed) {
      continue;
    }
    try {
      const parsed = JSON.parse(trimmed) as unknown;
      if (!isPlainRecord(parsed)) {
        continue;
      }
      const payload =
        String(parsed.action ?? "") === "update_page_options" && isPlainRecord(parsed.pageOptionsPatch)
          ? parsed.pageOptionsPatch
          : parsed;
      const normalized = normalizeAiWritablePageOptionsPatch(payload);
      if (normalized) {
        return normalized;
      }
    } catch {
      // ignore
    }
  }

  return null;
}

function extractNaturalLanguagePageOptionsPatch(text: string): Partial<PageOptionsState> | null {
  const normalizedText = text.trim();
  if (!normalizedText) {
    return null;
  }

  const patch: Partial<PageOptionsState> = {};

  if (/(显示|打开|开启).*(目录|标题目录)|显示标题目录/.test(normalizedText)) {
    patch.showToc = true;
  } else if (/(隐藏|关闭).*(目录|标题目录)|关闭标题目录/.test(normalizedText)) {
    patch.showToc = false;
  }

  if (/(开启|打开|启用|改成|切换成).*(宽版|宽布局|自适应宽度)|使用宽版/.test(normalizedText)) {
    patch.wideLayout = true;
  } else if (/(关闭|取消|恢复).*(宽版|宽布局|自适应宽度)|恢复标准宽度/.test(normalizedText)) {
    patch.wideLayout = false;
  }

  if (/(开启|打开|启用|改成|切换成).*(小字体)|使用小字体/.test(normalizedText)) {
    patch.smallText = true;
  } else if (/(关闭|取消|恢复).*(小字体)|恢复正常字体大小/.test(normalizedText)) {
    patch.smallText = false;
  }

  if (/(显示|打开|开启).*(标题编号|标题自动编号)/.test(normalizedText)) {
    patch.showHeadingNumbers = true;
  } else if (/(隐藏|关闭).*(标题编号|标题自动编号)/.test(normalizedText)) {
    patch.showHeadingNumbers = false;
  }

  if (/宋体/.test(normalizedText)) {
    patch.pageFont = "song";
  } else if (/楷体/.test(normalizedText)) {
    patch.pageFont = "kai";
  } else if (/(默认字体|恢复默认字体)/.test(normalizedText)) {
    patch.pageFont = "default";
  }

  if (/紧凑/.test(normalizedText)) {
    patch.layoutDensity = "compact";
  } else if (/(宽松|疏朗)/.test(normalizedText)) {
    patch.layoutDensity = "spacious";
  } else if (/(默认间距|标准间距|正常间距)/.test(normalizedText)) {
    patch.layoutDensity = "normal";
  }

  return Object.keys(patch).length > 0 ? patch : null;
}

export function extractPageOptionsPatchFromAiMessage(input: {
  messages: AgentMessage[];
  currentPageOptions?: PageOptionsState | null;
}): Partial<PageOptionsState> | null {
  void input.currentPageOptions;
  const latestUserMessage = readLatestUserMessage(input.messages);
  return (
    extractStructuredPageOptionsPatch(latestUserMessage) ??
    extractNaturalLanguagePageOptionsPatch(latestUserMessage)
  );
}

function describePageOptionsPatch(patch: Partial<PageOptionsState>): string {
  const labels: string[] = [];
  if ("showToc" in patch) labels.push(patch.showToc ? "显示目录" : "隐藏目录");
  if ("wideLayout" in patch) labels.push(patch.wideLayout ? "启用宽版" : "关闭宽版");
  if ("smallText" in patch) labels.push(patch.smallText ? "启用小字体" : "关闭小字体");
  if ("showHeadingNumbers" in patch) {
    labels.push(patch.showHeadingNumbers ? "显示标题编号" : "隐藏标题编号");
  }
  if ("pageFont" in patch) {
    labels.push(
      patch.pageFont === "song" ? "切换为宋体" : patch.pageFont === "kai" ? "切换为楷体" : "恢复默认字体",
    );
  }
  if ("layoutDensity" in patch) {
    labels.push(
      patch.layoutDensity === "compact"
        ? "切换为紧凑排版"
        : patch.layoutDensity === "spacious"
          ? "切换为宽松排版"
          : "恢复标准排版",
    );
  }
  if ("embedDefaultBlockId" in patch) {
    labels.push(patch.embedDefaultBlockId ? "更新嵌入默认位置" : "清除嵌入默认位置");
  }
  if ("showWordCount" in patch) labels.push(patch.showWordCount ? "显示字数统计" : "隐藏字数统计");
  if ("collapseBacklinks" in patch) {
    labels.push(patch.collapseBacklinks ? "折叠反向引用" : "展开反向引用");
  }
  if ("hideChildPages" in patch) labels.push(patch.hideChildPages ? "隐藏子页面" : "显示子页面");
  return labels.length > 0 ? `已更新页面设置：${labels.join("，")}。` : "已更新页面设置。";
}

async function pathExists(targetPath: string) {
  try {
    await access(targetPath);
    return true;
  } catch {
    return false;
  }
}

async function resolveRepoRoot() {
  let dir = path.resolve(process.cwd());
  for (let i = 0; i < 8; i += 1) {
    if (await pathExists(path.join(dir, "rust", "Cargo.toml"))) {
      return dir;
    }
    const parent = path.dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return path.resolve(process.cwd(), "..");
}

function buildCliArgs(input: { repoRoot: string; userId: string; payload: MnoteCliAgentRunPayload }) {
  const sessionId = String(input.payload.options?.ai?.sessionId ?? "").trim() || `ai-${Date.now()}`;
  const documentId = String(input.payload.context?.documentId ?? "").trim() || "current";
  const workspaceId = String(input.payload.context?.workspaceId ?? "").trim() || null;
  const argsJson = JSON.stringify({
    pageId: documentId,
    documentId,
    workspaceId,
    provider: input.payload.options?.ai?.provider ?? null,
    modelKey: input.payload.options?.ai?.modelKey ?? null,
    profileId: input.payload.options?.ai?.profileId ?? null,
    selectedUids: input.payload.context?.selectedUids ?? null,
    pageOptions: input.payload.context?.pageOptions ?? null,
    editorRuntimePageOptions: input.payload.context?.pageOptions
      ? pickLeptosTiptapRuntimePageOptions(input.payload.context.pageOptions)
      : null,
  });

  return [
    "run",
    "--quiet",
    "--manifest-path",
    path.join(input.repoRoot, "rust", "Cargo.toml"),
    "-p",
    "mnote-cli",
    "--",
    "--json",
    "--validate-only",
    "--dry-run",
    "--actor-id",
    input.userId,
    "--actor-type",
    "user",
    "--session-id",
    sessionId,
    "--reason",
    "ai-agent-run:mnote-cli-host",
    "tool",
    "run",
    "--tool-name",
    "doc_get",
    "--kind",
    "query",
    "--mode",
    "explain-plan",
    "--args-json",
    argsJson,
  ];
}

async function runMnoteCli(input: {
  userId: string;
  userEmail?: string;
  userName?: string;
  payload: MnoteCliAgentRunPayload;
}): Promise<CliRunResult> {
  const repoRoot = await resolveRepoRoot();
  const args = buildCliArgs({ repoRoot, userId: input.userId, payload: input.payload });

  return new Promise<CliRunResult>((resolve, reject) => {
    const child = spawn("cargo", args, {
      cwd: repoRoot,
      env: {
        ...process.env,
        CARGO_TERM_COLOR: "never",
        RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN?.trim() || "1.89.0",
        DEV_USER_ID: input.userId,
        DEV_USER_EMAIL: input.userEmail?.trim() || process.env.DEV_USER_EMAIL || "dev@mnote.local",
        DEV_USER_NAME: input.userName?.trim() || process.env.DEV_USER_NAME || "开发用户",
        MNOTE_CLI_ALLOW_CREATE_PAGE: process.env.MNOTE_CLI_ALLOW_CREATE_PAGE || "1",
        MNOTE_CLI_ALLOW_EDIT: process.env.MNOTE_CLI_ALLOW_EDIT || "1",
      },
      stdio: ["ignore", "pipe", "pipe"],
    });

    let stdout = "";
    let stderr = "";
    let timedOut = false;
    const timeout = setTimeout(() => {
      timedOut = true;
      child.kill("SIGKILL");
    }, CLI_HOST_TIMEOUT_MS);

    child.stdout.setEncoding("utf8");
    child.stderr.setEncoding("utf8");
    child.stdout.on("data", (chunk: string) => {
      stdout += chunk;
    });
    child.stderr.on("data", (chunk: string) => {
      stderr += chunk;
    });
    child.on("error", (error) => {
      clearTimeout(timeout);
      reject(error);
    });
    child.on("close", (code) => {
      clearTimeout(timeout);
      if (timedOut) {
        reject(new Error("mnote-cli host 执行超时"));
        return;
      }
      resolve({ code, stdout, stderr });
    });
  });
}

async function executeAiPageOptionsPatch(input: {
  request: Request;
  payload: MnoteCliAgentRunPayload;
  patch: Partial<PageOptionsState>;
}) {
  const documentId = String(input.payload.context?.documentId ?? "").trim();
  const workspaceId = String(input.payload.context?.workspaceId ?? "").trim() || null;
  const bridgeContext = await buildDocumentBridgeContext({
    request: input.request,
    workspaceId,
  });
  const envelope = buildDocumentCommandEnvelope({
    name: PAGE_COMMAND_NAMES.updateLayout,
    payload: {
      documentId,
      workspaceId,
      options: input.patch,
    },
    context: bridgeContext,
    target: {
      workspaceId,
      pageId: documentId,
    },
    reason: "ai-agent-run:mnote-cli-host:update-page-options",
    refs: ["mnote-cli-host", "page_options_patch"],
  });
  const result = await executePageWriteBridgeCommand({
    context: bridgeContext,
    envelope,
  });
  return {
    documentId,
    patch: input.patch,
    meta: {
      requestId: result.requestId,
      traceId: result.traceId,
      commandId: result.commandId,
      commandName: result.commandName,
    },
    assistantText: describePageOptionsPatch(input.patch),
  };
}

export async function startMnoteCliAgentHostRun(input: {
  request: Request;
  userId: string;
  userEmail?: string;
  userName?: string;
  payload: MnoteCliAgentRunPayload;
}): Promise<Response> {
  const pageOptionsPatch = extractPageOptionsPatchFromAiMessage({
    messages: input.payload.messages,
    currentPageOptions: input.payload.context?.pageOptions ?? null,
  });
  const shouldHandlePageOptionsPatch =
    pageOptionsPatch &&
    String(input.payload.context?.documentId ?? "").trim() &&
    String(input.payload.context?.workspaceId ?? "").trim();

  if (shouldHandlePageOptionsPatch) {
    const toolCallId = `${PAGE_OPTIONS_TOOL_NAME}_${Date.now()}`;
    const toolArgs = {
      documentId: String(input.payload.context?.documentId ?? "").trim(),
      pageOptionsPatch,
    };
    if (!input.payload.stream) {
      const result = await executeAiPageOptionsPatch({
        request: input.request,
        payload: input.payload,
        patch: pageOptionsPatch,
      });
      return NextResponse.json({
        ok: true,
        bridgeOwner: "mnote-cli",
        text: result.assistantText,
        toolResult: {
          action: "update_page_options",
          documentId: result.documentId,
          pageOptionsPatch: result.patch,
          meta: result.meta,
        },
      });
    }

    const body = new ReadableStream<Uint8Array>({
      async start(controller) {
        const encoder = new TextEncoder();
        controller.enqueue(encoder.encode(toSseFrame("ready", { ok: true, bridgeOwner: "mnote-cli" })));
        controller.enqueue(
          encoder.encode(
            toSseFrame("tool_call", {
              id: toolCallId,
              tool: PAGE_OPTIONS_TOOL_NAME,
              args: toolArgs,
            }),
          ),
        );
        const startedAt = Date.now();
        try {
          const result = await executeAiPageOptionsPatch({
            request: input.request,
            payload: input.payload,
            patch: pageOptionsPatch,
          });
          const toolResult = {
            action: "update_page_options",
            documentId: result.documentId,
            pageOptionsPatch: result.patch,
            meta: result.meta,
          };
          controller.enqueue(
            encoder.encode(
              toSseFrame("tool_result", {
                id: toolCallId,
                tool: PAGE_OPTIONS_TOOL_NAME,
                ok: true,
                ms: Math.max(0, Date.now() - startedAt),
                result: toolResult,
              }),
            ),
          );
          controller.enqueue(encoder.encode(toSseFrame("assistant_message", { text: result.assistantText })));
          controller.enqueue(
            encoder.encode(toSseFrame("completion", { ok: true, text: result.assistantText, steps: 1 })),
          );
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          controller.enqueue(
            encoder.encode(
              toSseFrame("tool_result", {
                id: toolCallId,
                tool: PAGE_OPTIONS_TOOL_NAME,
                ok: false,
                ms: Math.max(0, Date.now() - startedAt),
                result: { error: message },
              }),
            ),
          );
          controller.enqueue(encoder.encode(toSseFrame("error", { ok: false, message })));
        } finally {
          controller.close();
        }
      },
    });

    return new Response(body, {
      headers: {
        "Content-Type": "text/event-stream; charset=utf-8",
        "Cache-Control": "no-cache, no-transform",
        Connection: "keep-alive",
        "X-Accel-Buffering": "no",
        "x-mnote-ai-execution-owner": "mnote-cli",
      },
    });
  }

  const run = runMnoteCli({
    userId: input.userId,
    userEmail: input.userEmail,
    userName: input.userName,
    payload: input.payload,
  });

  if (!input.payload.stream) {
    const result = await run;
    if (result.code !== 0) {
      return NextResponse.json({ error: result.stderr || "mnote-cli 执行失败" }, { status: 500 });
    }
    return NextResponse.json({
      ok: true,
      bridgeOwner: "mnote-cli",
      text: result.stdout.trim(),
    });
  }

  const body = new ReadableStream<Uint8Array>({
    async start(controller) {
      const encoder = new TextEncoder();
      controller.enqueue(encoder.encode(toSseFrame("ready", { ok: true, bridgeOwner: "mnote-cli" })));
      try {
        const result = await run;
        if (result.code !== 0) {
          controller.enqueue(
            encoder.encode(toSseFrame("error", { ok: false, message: result.stderr || "mnote-cli 执行失败" })),
          );
        } else {
          const text = result.stdout.trim() || "mnote-cli 无输出";
          controller.enqueue(encoder.encode(toSseFrame("assistant_message", { text })));
          controller.enqueue(encoder.encode(toSseFrame("completion", { ok: true, text, steps: 1 })));
        }
      } catch (error) {
        controller.enqueue(
          encoder.encode(toSseFrame("error", { ok: false, message: error instanceof Error ? error.message : String(error) })),
        );
      } finally {
        controller.close();
      }
    },
  });

  return new Response(body, {
    headers: {
      "Content-Type": "text/event-stream; charset=utf-8",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no",
      "x-mnote-ai-execution-owner": "mnote-cli",
    },
  });
}
