/**
 * SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-mdtodo-web-active-editing-hwpod-source;
 * draft-2026-06-26-p1-mdtodo-web-operable.
 * Projects MDTODO Markdown source-of-truth into stable Rxx task projections and mutation helpers.
 */
import { createHash } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";

export type MdtodoTaskStatus = "open" | "in_progress" | "completed";

const taskLinePattern = /^(\s*)(?:[-*+]|\d+[.)])\s+\[([ xX-])\]\s+(.*)$/u;
const rxxHeadingPattern = /^(#{2,})\s+(R\d+(?:\.\d+)*)(?:\s+(.*?))?\s*$/iu;
const rxxIdPattern = /^R\d+(?:\.\d+)*$/iu;

const statusAliases = new Map([
  ["open", "open"],
  ["todo", "open"],
  ["blocked", "open"],
  ["in_progress", "in_progress"],
  ["processing", "in_progress"],
  ["completed", "completed"],
  ["finished", "completed"],
  ["done", "completed"]
]);

const statusMarkers = new Map([
  ["open", null],
  ["in_progress", "in_progress"],
  ["completed", "completed"]
]);

export async function discoverMdtodoDocuments(options = {}) {
  const root = path.resolve(String(options.root || process.cwd()));
  const sourceId = safeToken(options.sourceId, "default-mdtodo");
  const projectId = safeToken(options.projectId, null);
  const maxFiles = positiveInteger(options.maxFiles, 300);
  const files = await listDirectMarkdownFiles(root, maxFiles);
  const documents = [];

  for (const filePath of files) {
    const markdown = await fs.readFile(filePath, "utf8");
    if (!isMdtodoCandidate(filePath, markdown)) continue;
    const relativePath = normalizeRelativePath(path.relative(root, filePath));
    const parsed = parseMdtodoDocument(markdown, { sourceId, relativePath, projectId });
    if (parsed.tasks.length === 0 && !/mdtodo|todo/i.test(path.basename(filePath))) continue;
    documents.push(parsed);
  }

  documents.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
  return documents;
}

export function parseMdtodoDocument(markdown, options = {}) {
  const sourceId = safeToken(options.sourceId, "default-mdtodo");
  const relativePath = normalizeRelativePath(options.relativePath || "MDTODO.md");
  const fileRef = documentFileRef(relativePath);
  const fingerprint = sha256(markdown);
  const projectId = safeToken(options.projectId, projectIdForRelativePath(relativePath));
  const lines = String(markdown ?? "").split(/\r?\n/u);
  const context = { sourceId, relativePath, fileRef, fingerprint, projectId };
  const rxxProjection = parseRxxTasks(lines, context);
  const projection = rxxProjection.tasks.length > 0 ? rxxProjection : parseLegacyCheckboxTasks(lines, context);
  const parserMode = rxxProjection.tasks.length > 0 ? "rxx" : "legacy-checkbox";

  return {
    sourceId,
    fileRef,
    relativePath,
    fingerprint,
    revision: fingerprint,
    projectId,
    title: documentTitle(markdown, relativePath),
    parserMode,
    taskCount: projection.tasks.length,
    tasks: projection.tasks,
    document: {
      source: "markdown-file",
      relativePath,
      firstHeading: documentTitle(markdown, relativePath),
      parserMode,
      revision: fingerprint,
      parseDiagnostics: projection.diagnostics,
      valuesRedacted: true
    }
  };
}

export function readMdtodoTaskDetail(markdown, options = {}) {
  const sourceId = safeToken(options.sourceId, "default-mdtodo");
  const relativePath = normalizeRelativePath(options.relativePath || "MDTODO.md");
  const fileRef = safeToken(options.fileRef, documentFileRef(relativePath));
  const projectId = safeToken(options.projectId, projectIdForRelativePath(relativePath));
  const rxxId = normalizeOptionalRxxId(options.rxxId ?? taskRxxIdFromRef(options.taskRef));
  if (!rxxId) return null;

  const lines = String(markdown ?? "").split(/\r?\n/u);
  const block = collectRxxBlocks(lines).find((candidate) => candidate.rxxId === rxxId);
  if (!block) return null;

  const body = taskBodyFromLines(lines, block);
  const bodyPreview = bodyPreviewFromLines(lines, block.lineIndex + 1, block.endLine);
  const title = cleanTaskTitle(block.title || bodyPreview || block.rxxId);
  return {
    taskRef: taskRefForRxx(sourceId, fileRef, block.rxxId),
    taskId: block.rxxId,
    rxxId: block.rxxId,
    projectId,
    sourceId,
    fileRef,
    relativePath,
    title,
    status: block.status,
    lineNumber: block.lineNumber,
    headingLevel: block.hashes.length,
    statusMarker: block.statusMarker,
    body,
    bodyPreview,
    links: markdownLinksFromBody(body, relativePath),
    valuesRedacted: false
  };
}

export function mutateMdtodoDocument(markdown, operation = {}, options = {}) {
  const originalMarkdown = String(markdown ?? "");
  const previousFingerprint = sha256(originalMarkdown);
  const expectedFingerprint = operation.expectedFingerprint ?? options.expectedFingerprint;
  if (expectedFingerprint && expectedFingerprint !== previousFingerprint) {
    return {
      ok: false,
      conflict: true,
      code: "revision_conflict",
      message: "MDTODO document fingerprint does not match expectedFingerprint",
      expectedFingerprint,
      actualFingerprint: previousFingerprint
    };
  }

  const type = String(operation.type ?? "").trim();
  const lines = originalMarkdown.split(/\r?\n/u);
  const blocks = collectRxxBlocks(lines);
  let nextLines = lines.slice();
  let changedTaskId = null;
  let affectedTaskIds = [];

  if (type === "updateTitle") {
    const block = requireRxxBlock(blocks, operation.rxxId);
    const title = cleanTaskTitle(operation.title);
    nextLines[block.lineIndex] = formatRxxHeading(block.hashes, block.rxxId, block.status, title);
    changedTaskId = block.rxxId;
    affectedTaskIds = [block.rxxId];
  } else if (type === "updateStatus") {
    const block = requireRxxBlock(blocks, operation.rxxId);
    const status = requireStatus(operation.status);
    nextLines[block.lineIndex] = formatRxxHeading(block.hashes, block.rxxId, status, block.title);
    changedTaskId = block.rxxId;
    affectedTaskIds = [block.rxxId];
  } else if (type === "updateBody") {
    const block = requireRxxBlock(blocks, operation.rxxId);
    const bodyLines = bodyReplacementLines(operation.body);
    nextLines.splice(block.lineIndex + 1, Math.max(0, block.endLine - block.lineIndex - 1), ...bodyLines);
    changedTaskId = block.rxxId;
    affectedTaskIds = [block.rxxId];
  } else if (type === "addTask") {
    const add = buildAddTaskMutation(lines, blocks, operation);
    nextLines = insertLines(nextLines, add.insertPos, add.lines);
    changedTaskId = add.rxxId;
    affectedTaskIds = [add.rxxId];
  } else if (type === "deleteTask") {
    const block = requireRxxBlock(blocks, operation.rxxId);
    const endLine = subtreeEndLine(blocks, block.rxxId);
    affectedTaskIds = blocks
      .filter((candidate) => candidate.rxxId === block.rxxId || isDescendantRxxId(candidate.rxxId, block.rxxId))
      .map((candidate) => candidate.rxxId);
    nextLines.splice(block.lineIndex, Math.max(1, endLine - block.lineIndex));
    changedTaskId = block.rxxId;
  } else {
    throw mutationError("unsupported_mutation", `Unsupported MDTODO mutation type: ${type || "<empty>"}`);
  }

  const nextMarkdown = normalizeMarkdownOutput(nextLines);
  const fingerprint = sha256(nextMarkdown);
  const document = parseMdtodoDocument(nextMarkdown, options);
  return {
    ok: true,
    conflict: false,
    markdown: nextMarkdown,
    previousFingerprint,
    fingerprint,
    revision: fingerprint,
    changedTaskId,
    affectedTaskIds,
    taskCount: document.taskCount,
    document
  };
}

export function documentFileRef(relativePath) {
  return `file_${shortHash(normalizeRelativePath(relativePath), 16)}`;
}

function parseRxxTasks(lines, context) {
  const diagnostics = [];
  const blocks = collectRxxBlocks(lines);
  const seen = new Set();
  const uniqueBlocks = [];
  for (const block of blocks) {
    if (seen.has(block.rxxId)) {
      diagnostics.push({ code: "duplicate_rxx_id", rxxId: block.rxxId, lineNumber: block.lineNumber });
      continue;
    }
    seen.add(block.rxxId);
    uniqueBlocks.push(block);
  }

  const taskIds = new Set(uniqueBlocks.map((block) => block.rxxId));
  const tasks = uniqueBlocks.map((block, index) => {
    const parentRxxId = parentRxxIdFor(block.rxxId);
    const parentTaskRef = parentRxxId && taskIds.has(parentRxxId) ? taskRefForRxx(context.sourceId, context.fileRef, parentRxxId) : null;
    if (parentRxxId && !parentTaskRef) diagnostics.push({ code: "missing_parent_rxx_id", rxxId: block.rxxId, parentRxxId, lineNumber: block.lineNumber });
    const bodyText = lines.slice(block.lineIndex + 1, block.endLine).join("\n");
    const bodyPreview = bodyPreviewFromLines(lines, block.lineIndex + 1, block.endLine);
    const links = markdownLinksFromBody(bodyText, context.relativePath);
    const title = cleanTaskTitle(block.title || bodyPreview || block.rxxId);
    return {
      taskRef: taskRefForRxx(context.sourceId, context.fileRef, block.rxxId),
      taskId: block.rxxId,
      rxxId: block.rxxId,
      projectId: context.projectId,
      sourceId: context.sourceId,
      fileRef: context.fileRef,
      relativePath: context.relativePath,
      title,
      status: block.status,
      parentTaskRef,
      parentRxxId: parentTaskRef ? parentRxxId : null,
      depth: rxxDepth(block.rxxId),
      lineNumber: block.lineNumber,
      ordinal: index + 1,
      linkCount: links.length,
      sourceFingerprint: context.fingerprint,
      parserMode: "rxx",
      headingLevel: block.hashes.length,
      statusMarker: block.statusMarker,
      bodyPreview,
      links
    };
  });

  return { tasks, diagnostics };
}

function parseLegacyCheckboxTasks(lines, context) {
  const tasks = [];
  const parentStack = [];
  let ordinal = 0;

  for (let index = 0; index < lines.length; index += 1) {
    const match = taskLinePattern.exec(lines[index]);
    if (!match) continue;
    ordinal += 1;
    const depth = Math.max(0, Math.floor(match[1].replace(/\t/gu, "  ").length / 2));
    const checkbox = match[2];
    const title = cleanTaskTitle(match[3]);
    const taskId = `t${index + 1}_${shortHash(`${context.relativePath}:${index + 1}:${title}`)}`;
    const parentTaskRef = parentStack[depth - 1] ?? null;
    const taskRef = `mdtodo:${context.sourceId}:${context.fileRef}:${taskId}`;
    const task = {
      taskRef,
      taskId,
      projectId: context.projectId,
      sourceId: context.sourceId,
      fileRef: context.fileRef,
      relativePath: context.relativePath,
      title,
      status: checkbox.toLowerCase() === "x" ? "completed" : "open",
      parentTaskRef,
      depth,
      lineNumber: index + 1,
      ordinal,
      linkCount: 0,
      sourceFingerprint: context.fingerprint,
      parserMode: "legacy-checkbox"
    };
    tasks.push(task);
    parentStack[depth] = taskRef;
    parentStack.length = depth + 1;
  }

  return { tasks, diagnostics: [] };
}

function collectRxxBlocks(lines) {
  const headings = [];
  for (let index = 0; index < lines.length; index += 1) {
    const heading = parseRxxHeading(lines[index]);
    if (heading) headings.push({ ...heading, lineIndex: index, lineNumber: index + 1 });
  }
  return headings.map((heading, index) => ({
    ...heading,
    endLine: headings[index + 1]?.lineIndex ?? lines.length
  }));
}

function parseRxxHeading(line) {
  const match = rxxHeadingPattern.exec(String(line ?? ""));
  if (!match) return null;
  const parsed = parseHeadingRest(match[3] ?? "");
  return {
    hashes: match[1],
    rxxId: normalizeRxxId(match[2]),
    status: parsed.status,
    statusMarker: parsed.statusMarker,
    title: parsed.title
  };
}

function parseHeadingRest(value) {
  const rest = String(value ?? "").trim();
  let status = "open";
  let statusMarker = null;
  for (const match of rest.matchAll(/\[([^\]]+)\]/gu)) {
    const candidate = normalizeStatus(match[1]);
    if (!candidate) continue;
    if (statusRank(candidate) >= statusRank(status)) {
      status = candidate;
      statusMarker = match[1];
    }
  }
  return {
    status,
    statusMarker,
    title: stripStatusMarkers(rest)
  };
}

function buildAddTaskMutation(lines, blocks, operation) {
  const title = cleanTaskTitle(operation.title);
  const status = requireStatus(operation.status ?? "open");
  const parentRxxId = normalizeOptionalRxxId(operation.parentRxxId);
  const afterRxxId = normalizeOptionalRxxId(operation.afterRxxId);
  let rxxId;
  let headingLevel;
  let insertPos;

  if (parentRxxId) {
    const parent = requireRxxBlock(blocks, parentRxxId);
    rxxId = nextChildRxxId(blocks, parent.rxxId);
    headingLevel = headingLevelForRxx(rxxId);
    insertPos = subtreeEndLine(blocks, parent.rxxId);
  } else if (afterRxxId) {
    const after = requireRxxBlock(blocks, afterRxxId);
    const siblingParent = parentRxxIdFor(after.rxxId);
    rxxId = siblingParent ? nextChildRxxId(blocks, siblingParent) : nextRootRxxId(blocks);
    headingLevel = headingLevelForRxx(rxxId);
    insertPos = subtreeEndLine(blocks, after.rxxId);
  } else {
    rxxId = nextRootRxxId(blocks);
    headingLevel = headingLevelForRxx(rxxId);
    insertPos = logicalDocumentEnd(lines);
  }

  const bodyLines = normalizeBodyLines(operation.body);
  const taskLines = [formatRxxHeading("#".repeat(headingLevel), rxxId, status, title)];
  if (bodyLines.length > 0) taskLines.push("", ...bodyLines);
  return { rxxId, insertPos, lines: taskLines };
}

function requireRxxBlock(blocks, value) {
  const rxxId = normalizeOptionalRxxId(value);
  if (!rxxId) throw mutationError("rxx_id_required", "A valid Rxx id is required");
  const block = blocks.find((candidate) => candidate.rxxId === rxxId);
  if (!block) throw mutationError("rxx_id_not_found", `MDTODO task ${rxxId} was not found`, { rxxId });
  return block;
}

function insertLines(lines, insertPos, newLines) {
  const next = lines.slice();
  const insertion = [];
  if (insertPos > 0 && String(next[insertPos - 1] ?? "").trim() !== "") insertion.push("");
  insertion.push(...newLines);
  if (insertPos < next.length && String(next[insertPos] ?? "").trim() !== "") insertion.push("");
  next.splice(insertPos, 0, ...insertion);
  return next;
}

function bodyReplacementLines(value) {
  const lines = normalizeBodyLines(value);
  if (lines.length === 0) return [];
  return ["", ...lines, ""];
}

function normalizeBodyLines(value) {
  const text = String(value ?? "").replace(/\r\n/gu, "\n").trim();
  if (!text) return [];
  return text.split("\n");
}

function formatRxxHeading(hashes, rxxId, status, title) {
  const marker = statusMarkers.get(normalizeStatus(status) ?? "open");
  const parts = [hashes, rxxId];
  if (title) parts.push(cleanTaskTitle(title));
  if (marker) parts.push(`[${marker}]`);
  return parts.join(" ");
}

function nextRootRxxId(blocks) {
  const next = Math.max(0, ...blocks
    .map((block) => rxxSegments(block.rxxId))
    .filter((segments) => segments.length === 1)
    .map((segments) => segments[0])) + 1;
  return `R${next}`;
}

function nextChildRxxId(blocks, parentRxxId) {
  const parentSegments = rxxSegments(parentRxxId);
  const prefix = `${parentRxxId}.`;
  const next = Math.max(0, ...blocks
    .filter((block) => block.rxxId.startsWith(prefix))
    .map((block) => rxxSegments(block.rxxId))
    .filter((segments) => segments.length === parentSegments.length + 1)
    .map((segments) => segments.at(-1))) + 1;
  return `${parentRxxId}.${next}`;
}

function subtreeEndLine(blocks, rxxId) {
  const startIndex = blocks.findIndex((block) => block.rxxId === rxxId);
  if (startIndex < 0) return 0;
  let endLine = blocks[startIndex].endLine;
  for (let index = startIndex + 1; index < blocks.length; index += 1) {
    if (!isDescendantRxxId(blocks[index].rxxId, rxxId)) break;
    endLine = blocks[index].endLine;
  }
  return endLine;
}

function logicalDocumentEnd(lines) {
  let end = lines.length;
  while (end > 0 && String(lines[end - 1] ?? "").trim() === "") end -= 1;
  return end;
}

function normalizeMarkdownOutput(lines) {
  const text = lines.join("\n").replace(/[\t ]+$/gmu, "").replace(/\n{4,}/gu, "\n\n\n").trimEnd();
  return text ? `${text}\n` : "";
}

function isMdtodoCandidate(filePath, markdown) {
  const basename = path.basename(filePath).toLowerCase();
  return basename.includes("todo") || basename.includes("mdtodo") || String(markdown ?? "").split(/\r?\n/u).some((line) => taskLinePattern.test(line) || rxxHeadingPattern.test(line));
}

async function listDirectMarkdownFiles(root, maxFiles) {
  try {
    const entries = await fs.readdir(root, { withFileTypes: true });
    return entries
      .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md"))
      .sort((a, b) => a.name.localeCompare(b.name))
      .slice(0, maxFiles)
      .map((entry) => path.join(root, entry.name));
  } catch {
    return [];
  }
}

function documentTitle(markdown, relativePath) {
  const heading = String(markdown ?? "").split(/\r?\n/u).find((line) => /^#{1,6}\s+\S/u.test(line));
  if (heading) return heading.replace(/^#{1,6}\s+/u, "").trim().slice(0, 180);
  return path.basename(relativePath, path.extname(relativePath)) || "MDTODO";
}

function projectIdForRelativePath(relativePath) {
  const segments = normalizeRelativePath(relativePath).split("/").filter(Boolean);
  const candidate = segments.find((segment) => /^PJ\d{4}-\d+/iu.test(segment)) ?? segments[0] ?? "default";
  return `project_${safeToken(candidate.toLowerCase().replace(/[^a-z0-9_.:-]+/gu, "_"), "default")}`;
}

function taskRefForRxx(sourceId, fileRef, rxxId) {
  return `mdtodo:${sourceId}:${fileRef}:${rxxId}`;
}

function parentRxxIdFor(rxxId) {
  const segments = String(rxxId ?? "").split(".");
  if (segments.length <= 1) return null;
  return segments.slice(0, -1).join(".");
}

function isDescendantRxxId(candidate, parent) {
  return candidate.startsWith(`${parent}.`);
}

function rxxDepth(rxxId) {
  return Math.max(0, rxxSegments(rxxId).length - 1);
}

function headingLevelForRxx(rxxId) {
  return Math.min(6, rxxDepth(rxxId) + 2);
}

function rxxSegments(rxxId) {
  return String(rxxId ?? "").replace(/^R/iu, "").split(".").map((segment) => Number(segment)).filter((segment) => Number.isInteger(segment) && segment > 0);
}

function normalizeRxxId(value) {
  return String(value ?? "").replace(/^r/iu, "R");
}

function normalizeOptionalRxxId(value) {
  const text = normalizeRxxId(value).trim();
  return rxxIdPattern.test(text) ? text : null;
}

function taskRxxIdFromRef(taskRef) {
  const parts = String(taskRef ?? "").split(":");
  return parts.length >= 4 ? parts.at(-1) : null;
}

function normalizeStatus(value) {
  const key = String(value ?? "open").trim().toLowerCase().replace(/[\s-]+/gu, "_");
  return statusAliases.get(key) ?? null;
}

function requireStatus(value) {
  const status = normalizeStatus(value);
  if (!status) throw mutationError("invalid_status", `Unsupported MDTODO status: ${String(value ?? "")}`);
  return status;
}

function statusRank(status) {
  if (status === "completed") return 2;
  if (status === "in_progress") return 1;
  return 0;
}

function stripStatusMarkers(value) {
  return String(value ?? "").replace(/\s*\[([^\]]+)\]/gu, (match, label) => normalizeStatus(label) ? "" : match).trim();
}

function markdownLinksFromBody(body, relativePath) {
  const text = String(body ?? "");
  const links = [];
  const markdownPattern = /!?\[([^\]]*)\]\(([^)]+)\)/gu;
  const markdownRanges = [];
  let ordinal = 0;
  for (const match of text.matchAll(markdownPattern)) {
    markdownRanges.push([match.index ?? 0, (match.index ?? 0) + String(match[0] ?? "").length]);
    ordinal += 1;
    const label = String(match[1] ?? "").trim();
    const href = cleanMarkdownHref(match[2]);
    const target = markdownReportTarget(relativePath, href);
    links.push({
      linkId: `lnk_${shortHash(`${relativePath}\n${href}\n${ordinal}`, 12)}`,
      label: label || href,
      href,
      kind: target ? "markdown-report" : linkKind(href),
      relativePath: target,
      ordinal,
      valuesRedacted: true
    });
  }

  for (const match of text.matchAll(/https?:\/\/\S+/giu)) {
    const index = match.index ?? 0;
    if (markdownRanges.some(([start, end]) => index >= start && index < end)) continue;
    ordinal += 1;
    const href = String(match[0] ?? "").replace(/[),.;]+$/u, "");
    links.push({
      linkId: `lnk_${shortHash(`${relativePath}\n${href}\n${ordinal}`, 12)}`,
      label: href,
      href,
      kind: "external",
      relativePath: null,
      ordinal,
      valuesRedacted: true
    });
  }

  return links;
}

function cleanMarkdownHref(value) {
  const text = String(value ?? "").trim().replace(/^<|>$/gu, "");
  return text.split(/\s+/u)[0] ?? "";
}

function markdownReportTarget(relativePath, href) {
  const raw = String(href ?? "").trim();
  if (!raw || raw.startsWith("#") || /^[a-z][a-z0-9+.-]*:/iu.test(raw) || raw.startsWith("/") || /^[A-Za-z]:\//u.test(raw)) return null;
  const withoutFragment = raw.split("#")[0]?.trim() ?? "";
  if (!withoutFragment.toLowerCase().endsWith(".md")) return null;
  const directory = path.posix.dirname(normalizeRelativePath(relativePath));
  const normalized = path.posix.normalize(path.posix.join(directory === "." ? "" : directory, withoutFragment));
  if (!normalized || normalized === "." || normalized.startsWith("../") || normalized === ".." || normalized.includes("\0")) return null;
  return normalized;
}

function linkKind(href) {
  if (/^https?:\/\//iu.test(String(href ?? ""))) return "external";
  if (String(href ?? "").startsWith("#")) return "anchor";
  return "file";
}

function taskBodyFromLines(lines, block) {
  return lines.slice(block.lineIndex + 1, block.endLine).join("\n").trim();
}

function bodyPreviewFromLines(lines, start, end) {
  for (const line of lines.slice(start, end)) {
    const text = String(line ?? "").trim();
    if (!text || rxxHeadingPattern.test(text)) continue;
    return text.replace(/^[-*+]\s+/u, "").slice(0, 240);
  }
  return "";
}

function cleanTaskTitle(value) {
  return String(value ?? "").replace(/\s+<!--.*?-->\s*$/u, "").trim().slice(0, 500) || "Untitled task";
}

function normalizeRelativePath(value) {
  return String(value ?? "").replace(/\\/gu, "/").replace(/^\/+|\/+$/gu, "") || "MDTODO.md";
}

function shortHash(value, length = 10) {
  return sha256(value).slice(0, length);
}

function sha256(value) {
  return createHash("sha256").update(String(value ?? "")).digest("hex");
}

function safeToken(value, fallback) {
  const text = String(value ?? "").trim();
  return /^[A-Za-z0-9_.:-]{1,96}$/u.test(text) ? text : fallback;
}

function positiveInteger(value, fallback) {
  const number = Number(value);
  return Number.isInteger(number) && number > 0 ? number : fallback;
}

function mutationError(code, message, details = {}) {
  const error = new Error(message);
  error.code = code;
  Object.assign(error, details);
  return error;
}
