import { createHash, randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import path from "node:path";

import { mutateMdtodoDocument, readMdtodoTaskDetail } from "./mdtodo.ts";
import { createMdtodoSourceAdapter, normalizeProjectSourceInput, sourceProjection } from "./source-adapter.ts";

const contractVersion = "project-management-v1";
const serviceId = "hwlab-project-management";

export function createProjectManagementApp(options = {}) {
  const env = options.env ?? process.env;
  const store = options.store;
  const logger = options.logger ?? console;
  const sourceRoot = path.resolve(String(options.sourceRoot ?? env.HWLAB_PROJECT_MANAGEMENT_SOURCE_ROOT ?? process.cwd()));
  const sourceId = safeToken(options.sourceId ?? env.HWLAB_PROJECT_MANAGEMENT_SOURCE_ID, "default-mdtodo");
  const projectId = safeToken(options.projectId ?? env.HWLAB_PROJECT_MANAGEMENT_PROJECT_ID, "project_hwlab_v03");
  const displayName = String(options.displayName ?? env.HWLAB_PROJECT_MANAGEMENT_SOURCE_NAME ?? "HWLAB MDTODO").trim() || "HWLAB MDTODO";
  const maxFiles = positiveInteger(options.maxFiles ?? env.HWLAB_PROJECT_MANAGEMENT_MDTODO_MAX_FILES, 300);
  const taskMaxLimit = positiveInteger(options.taskMaxLimit ?? env.HWLAB_PROJECT_MANAGEMENT_MDTODO_TASK_MAX_LIMIT, 500);
  const taskDefaultLimit = Math.min(taskMaxLimit, positiveInteger(options.taskDefaultLimit ?? env.HWLAB_PROJECT_MANAGEMENT_MDTODO_TASK_DEFAULT_LIMIT, 200));
  const sourceAdapter = options.sourceAdapter ?? createMdtodoSourceAdapter({ env, hwpodNodeOpsHandler: options.hwpodNodeOpsHandler, fetchImpl: options.fetchImpl });
  const defaultSource = normalizeProjectSourceInput({
    sourceId,
    sourceKind: "local-file-tree",
    displayName,
    projectId,
    rootRef: sourceRoot,
    mdtodoRootRef: ".",
    maxFiles
  }, { projectId, displayName, rootRef: sourceRoot, maxFiles }).source;
  let initialized = false;
  let lastProjection = null;
  let lastError = null;
  let initializing = null;

  async function initialize() {
    if (initialized) return lastProjection;
    if (initializing) return initializing;
    initializing = (async () => {
      const startedAt = new Date().toISOString();
      await store.ensureSchema();
      const bootstrapSources = await projectManagementBootstrapSources({ env, projectId, displayName, sourceRoot, maxFiles });
      const sources = dedupeProjectSources([defaultSource, ...bootstrapSources]);
      const projections = [];
      for (const source of sources) {
        await store.upsertSource(source);
        try {
          const documents = await sourceAdapter.discover(source);
          await store.replaceProjection(source, documents);
          projections.push({ ...sourceProjection(source, documents), status: "ready" });
        } catch (error) {
          if (source.sourceId === defaultSource.sourceId) throw error;
          const blocker = { code: error?.code ?? "project_source_projection_failed", message: error?.message ?? String(error) };
          projections.push({ ...sourceProjection(source, []), status: "blocked", blocker });
          logger.warn?.(JSON.stringify({ event: "project-management-bootstrap-source-projection-failed", serviceId, sourceId: source.sourceId, sourceKind: source.sourceKind, ...blocker }));
        }
      }
      lastProjection = summarizeProjectManagementProjection(projections, startedAt);
      initialized = true;
      lastError = null;
      logger.info?.(JSON.stringify({ event: "project-management-projection-ready", serviceId, ...lastProjection }));
      return lastProjection;
    })();
    try {
      return await initializing;
    } catch (error) {
      lastError = error;
      initialized = false;
      throw error;
    } finally {
      initializing = null;
    }
  }

  async function fetchHandler(request) {
    const url = new URL(request.url);
    try {
      if (url.pathname === "/health/live" || url.pathname === "/live") {
        return json(200, healthPayload("live", { initialized, lastProjection, lastError }));
      }
      if (url.pathname === "/health/ready" || url.pathname === "/health") {
        try {
          const projection = await initialize();
          return json(200, healthPayload("ready", { initialized: true, lastProjection: projection, lastError: null }));
        } catch (error) {
          return json(503, healthPayload("ready", { initialized: false, lastProjection, lastError: error }));
        }
      }

      if (!url.pathname.startsWith("/v1/project-management")) return jsonError(404, "not_found", "Project management route is not implemented");
      if (url.pathname === "/v1/project-management/workbench-links" && request.method === "POST") {
        await initialize();
        return await handleCreateWorkbenchLink(request, store);
      }
      if (url.pathname === "/v1/project-management/mdtodo/sources" && request.method === "POST") {
        await initialize();
        return await handleUpsertMdtodoSource(request, store, { mode: "create" });
      }
      const sourceAction = matchSourceAction(url.pathname);
      if (sourceAction && request.method === "PATCH" && !sourceAction.action) {
        await initialize();
        return await handleUpsertMdtodoSource(request, store, { mode: "update", sourceId: sourceAction.sourceId });
      }
      if (sourceAction?.action === "probe" && request.method === "POST") {
        await initialize();
        return await handleProbeMdtodoSource(store, sourceAdapter, sourceAction.sourceId, actorFromRequest(request));
      }
      if (sourceAction?.action === "reindex" && request.method === "POST") {
        await initialize();
        return await handleReindexMdtodoSource(store, sourceAdapter, sourceAction.sourceId, actorFromRequest(request));
      }
      const fileContent = matchFileContent(url.pathname);
      if (fileContent && request.method === "PATCH") {
        await initialize();
        return await handleWriteMdtodoFile(request, url, store, sourceAdapter, fileContent.fileRef, actorFromRequest(request));
      }
      const taskAction = matchTaskAction(url.pathname);
      if (url.pathname === "/v1/project-management/mdtodo/tasks" && request.method === "POST") {
        await initialize();
        return await handleCreateMdtodoTask(request, store, sourceAdapter, actorFromRequest(request));
      }
      if (taskAction && request.method === "PATCH") {
        await initialize();
        return await handlePatchMdtodoTask(request, store, sourceAdapter, taskAction.taskRef, actorFromRequest(request));
      }
      if (taskAction && request.method === "DELETE") {
        await initialize();
        return await handleDeleteMdtodoTask(request, store, sourceAdapter, taskAction.taskRef, actorFromRequest(request));
      }
      if (request.method !== "GET" && request.method !== "HEAD") return jsonError(405, "method_not_allowed", "Project management API only accepts read requests plus Workbench link writes");

      await initialize();
      if (url.pathname === "/v1/project-management" || url.pathname === "/v1/project-management/navigation") {
        return json(200, envelope({ navigation: navigationPayload(actorFromRequest(request)), projection: lastProjection }));
      }
      if (url.pathname === "/v1/project-management/projects") {
        const sources = await store.listSources();
        return json(200, envelope({ projects: projectsFromSources(sources) }));
      }
      if (url.pathname === "/v1/project-management/mdtodo/sources") {
        return json(200, envelope({ sources: await store.listSources() }));
      }
      if (url.pathname === "/v1/project-management/mdtodo/files") {
        return json(200, envelope({ files: await store.listDocuments({ sourceId: queryToken(url, "sourceId") }) }));
      }
      if (fileContent) {
        return await handleReadMdtodoFile(url, store, sourceAdapter, fileContent.fileRef);
      }
      if (url.pathname === "/v1/project-management/mdtodo/launch-context") {
        return await handleReadMdtodoLaunchContext(url, store);
      }
      if (url.pathname === "/v1/project-management/mdtodo/task-detail") {
        return await handleReadMdtodoTaskDetail(url, store, sourceAdapter);
      }
      if (url.pathname === "/v1/project-management/mdtodo/report-preview") {
        return await handleReadMdtodoReportPreview(url, store, sourceAdapter);
      }
      if (url.pathname === "/v1/project-management/mdtodo/tasks") {
        const taskFilters = {
          taskRef: queryOpaque(url, "taskRef"),
          sourceId: queryToken(url, "sourceId"),
          fileRef: queryToken(url, "fileRef"),
          projectId: queryToken(url, "projectId"),
          parentTaskRef: queryOpaque(url, "parentTaskRef"),
          status: queryToken(url, "status"),
          search: querySearch(url, "search")
        };
        const page = taskListPage(url, { defaultLimit: taskDefaultLimit, maxLimit: taskMaxLimit });
        const [total, tasks] = await Promise.all([
          store.countTasks(taskFilters),
          store.listTasks({ ...taskFilters, limit: page.limit, offset: page.offset })
        ]);
        return json(200, envelope({
          tasks,
          page: { ...page, total, hasMore: page.offset + tasks.length < total }
        }));
      }
      if (url.pathname === "/v1/project-management/workbench-links") {
        return json(200, envelope({
          links: await store.listWorkbenchLinks({
            taskRef: queryOpaque(url, "taskRef"),
            projectId: queryToken(url, "projectId"),
            sessionId: queryOpaque(url, "sessionId")
          })
        }));
      }
      return jsonError(404, "not_found", "Project management route is not implemented");
    } catch (error) {
      logger.error?.(JSON.stringify({ event: "project-management-request-failed", serviceId, path: url.pathname, code: error?.code ?? "project_management_error", message: error?.message ?? String(error) }));
      return jsonError(error?.statusCode ?? 500, error?.code ?? "project_management_error", error?.message ?? "Project management request failed");
    }
  }

  return {
    initialize,
    fetch: fetchHandler,
    async close() {
      if (typeof store?.close === "function") await store.close();
    }
  };
}

async function projectManagementBootstrapSources({ env, projectId, displayName, sourceRoot, maxFiles }) {
  const inputs = await readProjectManagementBootstrapSourceInputs(env);
  return inputs.map((input, index) => {
    const normalized = normalizeProjectSourceInput(input, { projectId, displayName, rootRef: sourceRoot, maxFiles });
    if (!normalized.ok) {
      const error = Object.assign(new Error(`bootstrap source[${index}] invalid: ${normalized.message}`), {
        code: normalized.code,
        statusCode: normalized.status
      });
      throw error;
    }
    return normalized.source;
  });
}

async function readProjectManagementBootstrapSourceInputs(env) {
  const pathValue = String(env.HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_PATH ?? "").trim();
  const inlineValue = String(env.HWLAB_PROJECT_MANAGEMENT_BOOTSTRAP_SOURCES_JSON ?? "").trim();
  if (!pathValue && !inlineValue) return [];
  const text = pathValue ? await readFile(pathValue, "utf8") : inlineValue;
  const parsed = JSON.parse(text);
  if (Array.isArray(parsed)) return parsed;
  if (Array.isArray(parsed?.sources)) return parsed.sources;
  const error = Object.assign(new Error("Project management bootstrap sources must be a JSON array or an object with a sources array"), {
    code: "invalid_project_management_bootstrap_sources",
    statusCode: 400
  });
  throw error;
}

function dedupeProjectSources(sources) {
  const byId = new Map();
  for (const source of sources) byId.set(source.sourceId, source);
  return [...byId.values()];
}

function summarizeProjectManagementProjection(projections, startedAt) {
  if (projections.length === 1) return { ...projections[0], startedAt };
  return {
    sourceId: projections[0]?.sourceId ?? "project-management",
    sourceCount: projections.length,
    sources: projections,
    documentCount: projections.reduce((sum, item) => sum + Number(item.documentCount ?? 0), 0),
    taskCount: projections.reduce((sum, item) => sum + Number(item.taskCount ?? 0), 0),
    projectedAt: new Date().toISOString(),
    startedAt,
    valuesRedacted: true
  };
}

function healthPayload(mode, state) {
  const blockers = state.lastError ? [{ code: state.lastError.code ?? "project_management_not_ready", message: state.lastError.message ?? String(state.lastError) }] : [];
  return {
    contractVersion,
    serviceId,
    status: blockers.length ? "blocked" : "ok",
    ready: mode === "live" ? true : blockers.length === 0 && state.initialized === true,
    mode,
    projection: state.lastProjection,
    blockers,
    valuesRedacted: true
  };
}

function envelope(payload) {
  return {
    contractVersion,
    serviceId,
    ok: true,
    ...payload,
    valuesRedacted: true
  };
}

function navigationPayload(actor) {
  return {
    id: "project-management",
    label: "Project Management",
    actor,
    items: [
      { id: "projects", label: "Projects", apiRoute: "/v1/project-management/projects" },
      { id: "mdtodo", label: "MDTODO", apiRoute: "/v1/project-management/mdtodo/tasks" }
    ],
    capabilities: {
      mdtodoProjection: true,
      workbenchLaunch: true,
      workbenchLinks: true,
      sourceOfTruth: "markdown-files"
    }
  };
}

function projectsFromSources(sources) {
  const projects = new Map();
  for (const source of sources) {
    const projectId = source.projectId || "project_hwlab_v03";
    if (!projects.has(projectId)) {
      projects.set(projectId, {
        projectId,
        name: source.displayName || projectId,
        sourceIds: [],
        sourceOfTruth: "markdown-files",
        status: source.status || "active"
      });
    }
    projects.get(projectId).sourceIds.push(source.sourceId);
  }
  return [...projects.values()];
}

function actorFromRequest(request) {
  const actorId = request.headers.get("x-hwlab-actor-id") || null;
  if (!actorId) return null;
  return {
    id: actorId,
    role: request.headers.get("x-hwlab-actor-role") || null,
    authMethod: request.headers.get("x-hwlab-auth-method") || null
  };
}

async function readJsonObject(request, maxBytes = 32768) {
  const text = await request.text();
  if (text.length > maxBytes) return { ok: false, code: "body_too_large", message: "Project management request body is too large" };
  try {
    const value = text ? JSON.parse(text) : {};
    if (!value || typeof value !== "object" || Array.isArray(value)) return { ok: false, code: "invalid_params", message: "body must be a JSON object" };
    return { ok: true, value };
  } catch (error) {
    return { ok: false, code: "parse_error", message: "Invalid JSON body", reason: error?.message ?? String(error) };
  }
}

async function handleCreateWorkbenchLink(request, store) {
  const body = await readJsonObject(request);
  if (!body.ok) return jsonError(400, body.code, body.message);
  const normalized = normalizeWorkbenchLinkInput(body.value, actorFromRequest(request));
  if (!normalized.ok) return jsonError(400, normalized.code, normalized.message);
  const link = await store.upsertWorkbenchLink(normalized.link);
  return json(201, envelope({ link }));
}

async function handleUpsertMdtodoSource(request, store, options = {}) {
  const body = await readJsonObject(request);
  if (!body.ok) return jsonError(400, body.code, body.message);
  const existing = options.mode === "update" ? await getSource(store, options.sourceId) : null;
  if (options.mode === "update" && !existing) return jsonError(404, "source_not_found", "MDTODO source was not found");
  const normalized = normalizeProjectSourceInput({ ...(existing ?? {}), ...body.value, sourceId: options.sourceId ?? body.value.sourceId }, existing ?? {});
  if (!normalized.ok) return jsonError(normalized.status ?? 400, normalized.code, normalized.message);
  await store.upsertSource(normalized.source);
  await recordAuditEvent(store, request, {
    action: options.mode === "update" ? "mdtodo.source.update" : "mdtodo.source.create",
    targetType: "mdtodo_source",
    targetId: normalized.source.sourceId,
    outcome: "accepted",
    sourceKind: normalized.source.sourceKind
  });
  return json(options.mode === "update" ? 200 : 201, envelope({ source: normalized.source }));
}

async function handleProbeMdtodoSource(store, sourceAdapter, sourceId, actor) {
  const source = await getSource(store, sourceId);
  if (!source) return jsonError(404, "source_not_found", "MDTODO source was not found");
  const probe = await sourceAdapter.probe(source);
  await recordAuditEvent(store, { actor }, { action: "mdtodo.source.probe", targetType: "mdtodo_source", targetId: source.sourceId, outcome: "succeeded", sourceKind: source.sourceKind });
  return json(200, envelope({ probe }));
}

async function handleReindexMdtodoSource(store, sourceAdapter, sourceId, actor) {
  const source = await getSource(store, sourceId);
  if (!source) return jsonError(404, "source_not_found", "MDTODO source was not found");
  const documents = await sourceAdapter.discover(source);
  await store.replaceProjection(source, documents);
  const projection = sourceProjection(source, documents);
  await recordAuditEvent(store, { actor }, { action: "mdtodo.source.reindex", targetType: "mdtodo_source", targetId: source.sourceId, outcome: "succeeded", documentCount: projection.documentCount, taskCount: projection.taskCount });
  return json(200, envelope({ projection }));
}

async function handleReadMdtodoFile(url, store, sourceAdapter, fileRef) {
  const sourceId = queryToken(url, "sourceId");
  if (!sourceId) return jsonError(400, "source_id_required", "sourceId is required");
  const { source, document, error } = await resolveMdtodoDocument(store, sourceId, fileRef);
  if (error) return error;
  const file = await sourceAdapter.readFile(source, document.relativePath);
  return json(200, envelope({ file }));
}

async function handleReadMdtodoTaskDetail(url, store, sourceAdapter) {
  const taskRef = queryOpaque(url, "taskRef");
  if (!taskRef) return jsonError(400, "task_ref_required", "taskRef is required");
  const detail = await loadTaskDetail({ store, sourceAdapter, taskRef, allowProjectedFallback: true });
  if (detail.error) return detail.error;
  if (detail.availability?.state === "unavailable") {
    return json(200, envelope({
      status: "partial",
      task: detail.task,
      detail: null,
      file: fileSummary(detail.document),
      detailAuthority: detail.availability
    }));
  }
  return json(200, envelope({
    status: "ready",
    task: { ...detail.task, bodyPreview: detail.detail.bodyPreview, linkCount: detail.detail.links.length },
    detail: detail.detail,
    file: fileSummary(detail.file),
    detailAuthority: taskDetailAuthority("ready", detail.source)
  }));
}

async function handleReadMdtodoLaunchContext(url, store) {
  const taskRef = queryOpaque(url, "taskRef");
  if (!taskRef) return jsonError(400, "task_ref_required", "taskRef is required");
  const task = await getTaskByRef(store, taskRef);
  if (!task) return jsonError(404, "task_not_found", "MDTODO task was not found");
  const source = await getSource(store, task.sourceId);
  if (!source) return jsonError(404, "source_not_found", "MDTODO source was not found");
  const executionContext = buildMdtodoExecutionContext({ task, source });
  if (!executionContext.ok) return jsonError(executionContext.status, executionContext.code, executionContext.message);
  const launchContext = buildMdtodoLaunchContext({ task, source, executionContext: executionContext.context });
  return json(200, envelope({ task, source: sourceSummary(source), executionContext: executionContext.context, launchContext }));
}

async function handleReadMdtodoReportPreview(url, store, sourceAdapter) {
  const taskRef = queryOpaque(url, "taskRef");
  const linkId = queryToken(url, "linkId");
  if (!taskRef) return jsonError(400, "task_ref_required", "taskRef is required");
  if (!linkId) return jsonError(400, "link_id_required", "linkId is required");
  let detail;
  try {
    detail = await loadTaskDetail({ store, sourceAdapter, taskRef });
  } catch (error) {
    if (isHwpodAvailabilityError(error)) return hwpodAvailabilityError(error);
    throw error;
  }
  if (detail.error) return detail.error;
  const link = detail.detail.links.find((item) => item.linkId === linkId);
  if (!link) return jsonError(404, "report_link_not_found", "MDTODO report link was not found");
  if (link.kind !== "markdown-report" || !link.relativePath) return jsonError(400, "unsupported_report_link", "Only safe relative Markdown report links can be previewed");
  let report;
  try {
    report = await sourceAdapter.readLinkedMarkdown(detail.source, detail.document.relativePath, link.href);
  } catch (error) {
    if (isHwpodAvailabilityError(error)) return hwpodAvailabilityError(error);
    throw error;
  }
  return json(200, envelope({
    task: detail.task,
    link,
    report: {
      ...report,
      linkId: link.linkId,
      label: link.label,
      href: link.href,
      valuesRedacted: false
    },
    file: fileSummary(detail.file)
  }));
}

async function loadTaskDetail({ store, sourceAdapter, taskRef, allowProjectedFallback = false }) {
  const task = await getTaskByRef(store, taskRef);
  if (!task) return { error: jsonError(404, "task_not_found", "MDTODO task was not found") };
  const { source, document, error } = await resolveMdtodoDocument(store, task.sourceId, task.fileRef);
  if (error) return { error };
  let file;
  try {
    file = await sourceAdapter.readFile(source, document.relativePath);
  } catch (readError) {
    if (allowProjectedFallback && source.sourceKind === "hwpod-workspace" && isHwpodAvailabilityError(readError)) {
      return {
        source,
        document,
        file: null,
        task,
        detail: null,
        availability: taskDetailAuthority("unavailable", source, readError)
      };
    }
    throw readError;
  }
  const detail = readMdtodoTaskDetail(file.content, {
    sourceId: source.sourceId,
    fileRef: document.fileRef,
    relativePath: document.relativePath,
    projectId: document.projectId,
    taskRef: task.taskRef,
    rxxId: task.taskId
  });
  if (!detail) return { error: jsonError(404, "task_detail_not_found", "MDTODO task detail was not found in source Markdown") };
  return { source, document, file, task, detail };
}

function isHwpodAvailabilityError(error) {
  const statusCode = Number(error?.statusCode);
  return error?.sourceKind === "hwpod-workspace" && [502, 503, 504].includes(statusCode);
}

function hwpodAvailabilityError(error) {
  return jsonError(
    Number(error?.statusCode),
    safeToken(error?.code, "hwpod_source_unavailable"),
    "HWPOD 源当前不可读；报告预览需等待 Node 恢复。"
  );
}

function taskDetailAuthority(state, source, error = null) {
  const unavailable = state === "unavailable";
  return {
    state,
    authority: unavailable ? "project-management-projection" : "source-markdown",
    sourceKind: source?.sourceKind ?? null,
    code: unavailable ? safeToken(error?.code, "hwpod_source_unavailable") : null,
    message: unavailable
      ? "HWPOD 源当前不可读；页面继续展示最近一次项目投影，正文编辑和报告预览需等待 Node 恢复。"
      : null,
    retryable: unavailable,
    valuesRedacted: true
  };
}

async function handleWriteMdtodoFile(request, url, store, sourceAdapter, fileRef, actor) {
  const body = await readJsonObject(request, 1024 * 1024 + 4096);
  if (!body.ok) return jsonError(body.code === "body_too_large" ? 413 : 400, body.code, body.message);
  const sourceId = safeToken(body.value.sourceId, queryToken(url, "sourceId"));
  if (!sourceId) return jsonError(400, "source_id_required", "sourceId is required");
  if (typeof body.value.content !== "string") return jsonError(400, "content_required", "content must be a string");
  const expectedFingerprint = safeOpaqueValue(body.value.expectedFingerprint ?? body.value.revision);
  if (!expectedFingerprint) return jsonError(400, "expected_fingerprint_required", "expectedFingerprint is required for MDTODO writes");
  const { source, document, error } = await resolveMdtodoDocument(store, sourceId, fileRef);
  if (error) return error;
  const file = await sourceAdapter.writeFile(source, document.relativePath, body.value.content, expectedFingerprint);
  const documents = await sourceAdapter.discover(source);
  await store.replaceProjection(source, documents);
  const projection = sourceProjection(source, documents);
  await recordAuditEvent(store, { actor }, { action: "mdtodo.file.write", targetType: "mdtodo_file", targetId: `${source.sourceId}:${fileRef}`, outcome: "succeeded", sourceKind: source.sourceKind, fileRef, revision: file.revision });
  return json(200, envelope({ file, projection }));
}

async function handlePatchMdtodoTask(request, store, sourceAdapter, taskRef, actor) {
  const body = await readJsonObject(request, 256 * 1024);
  if (!body.ok) return jsonError(body.code === "body_too_large" ? 413 : 400, body.code, body.message);
  const task = await getTaskByRef(store, taskRef);
  if (!task) return jsonError(404, "task_not_found", "MDTODO task was not found");
  const expectedFingerprint = safeOpaqueValue(body.value.expectedFingerprint ?? body.value.revision ?? body.value.expectedRevision);
  if (!expectedFingerprint) return jsonError(400, "expected_fingerprint_required", "expectedFingerprint is required for MDTODO task writes");
  const operations = [];
  if (typeof body.value.title === "string") operations.push({ type: "updateTitle", rxxId: task.taskId, title: body.value.title });
  if (typeof body.value.status === "string") operations.push({ type: "updateStatus", rxxId: task.taskId, status: body.value.status });
  if (typeof body.value.body === "string") operations.push({ type: "updateBody", rxxId: task.taskId, body: body.value.body });
  if (operations.length === 0) return jsonError(400, "mutation_required", "At least one MDTODO task field is required");
  return mutateTaskDocument({ store, sourceAdapter, sourceId: task.sourceId, fileRef: task.fileRef, expectedFingerprint, operations, auditAction: "mdtodo.task.update", targetTaskRef: taskRef, actor, status: 200 });
}

async function handleCreateMdtodoTask(request, store, sourceAdapter, actor) {
  const body = await readJsonObject(request, 256 * 1024);
  if (!body.ok) return jsonError(body.code === "body_too_large" ? 413 : 400, body.code, body.message);
  const expectedFingerprint = safeOpaqueValue(body.value.expectedFingerprint ?? body.value.revision ?? body.value.expectedRevision);
  if (!expectedFingerprint) return jsonError(400, "expected_fingerprint_required", "expectedFingerprint is required for MDTODO task writes");
  const title = shortText(body.value.title, 500);
  if (!title) return jsonError(400, "title_required", "title is required for MDTODO task creation");
  const parentTask = body.value.parentTaskRef ? await getTaskByRef(store, safeOpaqueValue(body.value.parentTaskRef)) : null;
  const afterTask = body.value.afterTaskRef ? await getTaskByRef(store, safeOpaqueValue(body.value.afterTaskRef)) : null;
  if (body.value.parentTaskRef && !parentTask) return jsonError(404, "parent_task_not_found", "Parent MDTODO task was not found");
  if (body.value.afterTaskRef && !afterTask) return jsonError(404, "after_task_not_found", "Continuation MDTODO task was not found");
  const sourceId = parentTask?.sourceId ?? afterTask?.sourceId ?? safeToken(body.value.sourceId, null);
  const fileRef = parentTask?.fileRef ?? afterTask?.fileRef ?? safeToken(body.value.fileRef, null);
  if (!sourceId || !fileRef) return jsonError(400, "task_target_required", "sourceId and fileRef are required for root MDTODO task creation");
  const operation = {
    type: "addTask",
    title,
    status: body.value.status ?? "open",
    body: typeof body.value.body === "string" ? body.value.body : "",
    parentRxxId: parentTask?.taskId ?? null,
    afterRxxId: afterTask?.taskId ?? null
  };
  return mutateTaskDocument({ store, sourceAdapter, sourceId, fileRef, expectedFingerprint, operations: [operation], auditAction: "mdtodo.task.create", targetTaskRef: parentTask?.taskRef ?? afterTask?.taskRef ?? `${sourceId}:${fileRef}`, actor, status: 201 });
}

async function handleDeleteMdtodoTask(request, store, sourceAdapter, taskRef, actor) {
  const body = await readJsonObject(request, 32768);
  if (!body.ok) return jsonError(400, body.code, body.message);
  const task = await getTaskByRef(store, taskRef);
  if (!task) return jsonError(404, "task_not_found", "MDTODO task was not found");
  const expectedFingerprint = safeOpaqueValue(body.value.expectedFingerprint ?? body.value.revision ?? body.value.expectedRevision);
  if (!expectedFingerprint) return jsonError(400, "expected_fingerprint_required", "expectedFingerprint is required for MDTODO task writes");
  return mutateTaskDocument({ store, sourceAdapter, sourceId: task.sourceId, fileRef: task.fileRef, expectedFingerprint, operations: [{ type: "deleteTask", rxxId: task.taskId }], auditAction: "mdtodo.task.delete", targetTaskRef: taskRef, actor, status: 200 });
}

async function mutateTaskDocument({ store, sourceAdapter, sourceId, fileRef, expectedFingerprint, operations, auditAction, targetTaskRef, actor, status }) {
  const { source, document, error } = await resolveMdtodoDocument(store, sourceId, fileRef);
  if (error) return error;
  const current = await sourceAdapter.readFile(source, document.relativePath);
  let markdown = current.content;
  let mutation = null;
  for (const operation of operations) {
    mutation = mutateMdtodoDocument(markdown, { ...operation, expectedFingerprint: mutation ? null : expectedFingerprint }, { sourceId: source.sourceId, relativePath: document.relativePath, projectId: document.projectId });
    if (!mutation.ok) return jsonError(mutation.conflict ? 409 : 400, mutation.code ?? "mdtodo_mutation_failed", mutation.message ?? "MDTODO task mutation failed");
    markdown = mutation.markdown;
  }
  const file = await sourceAdapter.writeFile(source, document.relativePath, markdown, expectedFingerprint);
  const documents = await sourceAdapter.discover(source);
  await store.replaceProjection(source, documents);
  const projection = sourceProjection(source, documents);
  const tasks = await store.listTasks({ sourceId: source.sourceId, fileRef });
  const changedTaskRef = mutation?.changedTaskId ? `mdtodo:${source.sourceId}:${fileRef}:${mutation.changedTaskId}` : null;
  const task = changedTaskRef ? tasks.find((item) => item.taskRef === changedTaskRef) ?? null : null;
  await recordAuditEvent(store, { actor }, { action: auditAction, targetType: "mdtodo_task", targetId: targetTaskRef, outcome: "succeeded", sourceKind: source.sourceKind, fileRef, revision: file.revision, changedTaskId: mutation?.changedTaskId ?? null, affectedTaskIds: mutation?.affectedTaskIds ?? [] });
  return json(status, envelope({ task, affectedTaskIds: mutation?.affectedTaskIds ?? [], changedTaskId: mutation?.changedTaskId ?? null, file: fileSummary(file), projection }));
}

async function getTaskByRef(store, taskRef) {
  const safeTaskRef = safeOpaqueValue(taskRef);
  if (!safeTaskRef) return null;
  if (typeof store.getTask === "function") return store.getTask(safeTaskRef);
  return (await store.listTasks({ taskRef: safeTaskRef, limit: 1 }))[0] ?? null;
}

function fileSummary(file) {
  return {
    sourceId: file.sourceId,
    fileRef: file.fileRef,
    relativePath: file.relativePath,
    fingerprint: file.fingerprint,
    revision: file.revision,
    parserMode: file.parserMode,
    taskCount: file.taskCount,
    valuesRedacted: true
  };
}

async function resolveMdtodoDocument(store, sourceId, fileRef) {
  const source = await getSource(store, sourceId);
  if (!source) return { error: jsonError(404, "source_not_found", "MDTODO source was not found") };
  const documents = await store.listDocuments({ sourceId });
  const document = documents.find((item) => item.fileRef === fileRef);
  if (!document) return { error: jsonError(404, "file_not_found", "MDTODO file was not found") };
  return { source, document };
}

async function getSource(store, sourceId) {
  if (!sourceId) return null;
  if (typeof store.getSource === "function") return store.getSource(sourceId);
  return (await store.listSources()).find((source) => source.sourceId === sourceId) ?? null;
}

function normalizeWorkbenchLinkInput(input, actor) {
  const projectId = safeToken(input.projectId, null);
  const taskRef = safeOpaqueValue(input.taskRef);
  const sessionId = safeOpaqueValue(input.sessionId);
  if (!projectId) return { ok: false, code: "project_id_required", message: "ProjectWorkbenchLink requires projectId" };
  if (!taskRef) return { ok: false, code: "task_ref_required", message: "ProjectWorkbenchLink requires taskRef" };
  if (!sessionId) return { ok: false, code: "session_id_required", message: "ProjectWorkbenchLink requires sessionId" };
  const traceId = safeOpaqueValue(input.traceId);
  const role = safeToken(input.role, "launch");
  const link = sanitizeLinkJson(input.link);
  return {
    ok: true,
    link: {
      linkId: safeToken(input.linkId, stableLinkId({ projectId, taskRef, sessionId })),
      projectId,
      taskRef,
      sessionId,
      traceId,
      createdBy: safeOpaqueValue(input.createdBy) ?? actor?.id ?? null,
      role,
      link
    }
  };
}

function sanitizeLinkJson(value) {
  const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
  const context = input.launchContext && typeof input.launchContext === "object" && !Array.isArray(input.launchContext) ? input.launchContext : {};
  const execution = sanitizeExecutionContext(context.executionContext ?? context.execution);
  return {
    launchContext: {
      source: "project-management",
      projectId: safeToken(context.projectId, null),
      taskRef: safeOpaqueValue(context.taskRef),
      sourceId: safeToken(context.sourceId, null),
      sourceKind: safeToken(context.sourceKind, null),
      fileRef: safeToken(context.fileRef, null),
      relativePath: safeRelativePath(context.relativePath),
      taskId: safeToken(context.taskId, null),
      title: shortText(context.title, 220),
      status: safeToken(context.status, null),
      hwpodId: execution?.hwpodId ?? safeOpaqueValue(context.hwpodId),
      nodeId: execution?.nodeId ?? safeOpaqueValue(context.nodeId),
      mdtodoRootRef: execution?.mdtodoRootRef ?? safeRelativePath(context.mdtodoRootRef),
      workspaceRootRef: execution?.workspaceRootRef ?? safeWorkspaceRef(context.workspaceRootRef),
      workspaceRootHash: execution?.workspaceRootHash ?? safeOpaqueValue(context.workspaceRootHash),
      workspaceRootLabel: execution?.workspaceRootLabel ?? shortText(context.workspaceRootLabel, 180),
      hwpodWorkspaceArgs: execution?.hwpodWorkspaceArgs ?? shortText(context.hwpodWorkspaceArgs, 500),
      contextFingerprint: execution?.contextFingerprint ?? safeOpaqueValue(context.contextFingerprint),
      capabilities: sanitizeCapabilities(context.capabilities),
      executionContext: execution,
      route: "/projects/mdtodo",
      valuesRedacted: true
    },
    workbenchUrl: safeWorkbenchUrl(input.workbenchUrl),
    valuesRedacted: true
  };
}

function buildMdtodoLaunchContext({ task, source, executionContext }) {
  return {
    source: "project-management",
    projectId: task.projectId,
    taskRef: task.taskRef,
    sourceId: task.sourceId,
    sourceKind: source.sourceKind ?? source.kind ?? null,
    fileRef: task.fileRef,
    relativePath: safeRelativePath(task.relativePath),
    taskId: task.taskId,
    title: shortText(task.title, 220),
    status: safeToken(task.status, null),
    hwpodId: executionContext.hwpodId,
    nodeId: executionContext.nodeId,
    workspaceRootRef: executionContext.workspaceRootRef,
    workspaceRootHash: executionContext.workspaceRootHash,
    workspaceRootLabel: executionContext.workspaceRootLabel,
    mdtodoRootRef: executionContext.mdtodoRootRef,
    hwpodWorkspaceArgs: executionContext.hwpodWorkspaceArgs,
    contextFingerprint: executionContext.contextFingerprint,
    capabilities: executionContext.capabilities,
    executionContext,
    route: "/projects/mdtodo",
    valuesRedacted: true
  };
}

function buildMdtodoExecutionContext({ task, source }) {
  const sourceKind = safeToken(source.sourceKind ?? source.kind, null);
  const projectId = safeToken(task.projectId ?? source.projectId, null);
  const taskRef = safeOpaqueValue(task.taskRef);
  const sourceId = safeToken(task.sourceId ?? source.sourceId, null);
  const fileRef = safeToken(task.fileRef, null);
  if (!projectId || !taskRef || !sourceId || !fileRef) return { ok: false, status: 409, code: "mdtodo_launch_context_incomplete", message: "MDTODO launch context is missing task identity." };
  const base = {
    sourceId,
    sourceKind,
    projectId,
    taskRef,
    fileRef,
    relativePath: safeRelativePath(task.relativePath),
    taskId: safeToken(task.taskId, null),
    valuesRedacted: true
  };
  if (sourceKind !== "hwpod-workspace") {
    const context = { ...base, capabilities: sanitizeCapabilities(source.capabilities), contextFingerprint: contextFingerprint(base) };
    return { ok: true, context };
  }
  const hwpodId = safeOpaqueValue(source.hwpodId);
  const nodeId = safeOpaqueValue(source.nodeId);
  const workspaceRootRef = safeWorkspaceRef(source.workspaceRootRef);
  const mdtodoRootRef = safeRelativePath(source.mdtodoRootRef) ?? ".";
  if (!hwpodId || !nodeId || !workspaceRootRef) {
    return { ok: false, status: 409, code: "hwpod_launch_context_missing", message: "HWPOD MDTODO source is missing hwpodId, nodeId or workspaceRootRef." };
  }
  const hwpodWorkspaceArgs = `--hwpod-id ${hwpodId} --workspace-path ${promptShellArg(workspaceRootRef)}`;
  const contextBase = {
    ...base,
    hwpodId,
    nodeId,
    workspaceRootRef,
    workspaceRootHash: hashText(workspaceRootRef),
    workspaceRootLabel: workspaceRootLabel(workspaceRootRef),
    mdtodoRootRef,
    hwpodWorkspaceArgs,
    capabilities: sanitizeCapabilities(source.capabilities),
    valuesRedacted: true
  };
  return { ok: true, context: { ...contextBase, contextFingerprint: contextFingerprint(contextBase) } };
}

function sourceSummary(source) {
  return {
    sourceId: source.sourceId,
    sourceKind: source.sourceKind ?? source.kind ?? null,
    projectId: source.projectId ?? null,
    hwpodId: safeOpaqueValue(source.hwpodId),
    nodeId: safeOpaqueValue(source.nodeId),
    mdtodoRootRef: safeRelativePath(source.mdtodoRootRef),
    workspaceRootHash: source.workspaceRootRef ? hashText(source.workspaceRootRef) : null,
    workspaceRootLabel: source.workspaceRootRef ? workspaceRootLabel(source.workspaceRootRef) : null,
    valuesRedacted: true
  };
}

function sanitizeExecutionContext(value) {
  const input = value && typeof value === "object" && !Array.isArray(value) ? value : null;
  if (!input) return null;
  const sourceId = safeToken(input.sourceId, null);
  const taskRef = safeOpaqueValue(input.taskRef);
  return {
    sourceId,
    sourceKind: safeToken(input.sourceKind, null),
    projectId: safeToken(input.projectId, null),
    taskRef,
    fileRef: safeToken(input.fileRef, null),
    relativePath: safeRelativePath(input.relativePath),
    taskId: safeToken(input.taskId, null),
    hwpodId: safeOpaqueValue(input.hwpodId),
    nodeId: safeOpaqueValue(input.nodeId),
    workspaceRootRef: safeWorkspaceRef(input.workspaceRootRef),
    workspaceRootHash: safeOpaqueValue(input.workspaceRootHash),
    workspaceRootLabel: shortText(input.workspaceRootLabel, 180),
    mdtodoRootRef: safeRelativePath(input.mdtodoRootRef),
    hwpodWorkspaceArgs: shortText(input.hwpodWorkspaceArgs, 500),
    contextFingerprint: safeOpaqueValue(input.contextFingerprint),
    capabilities: sanitizeCapabilities(input.capabilities),
    valuesRedacted: true
  };
}

function sanitizeCapabilities(value) {
  const input = value && typeof value === "object" && !Array.isArray(value) ? value : {};
  const out = {};
  for (const [key, item] of Object.entries(input)) {
    if (!/^[A-Za-z0-9_.:-]{1,80}$/u.test(key)) continue;
    if (["string", "number", "boolean"].includes(typeof item) || item === null) out[key] = item;
  }
  return out;
}

function contextFingerprint(value) {
  return `ctx_${hashText(JSON.stringify(value)).slice(0, 24)}`;
}

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

function promptShellArg(value) {
  return `'${String(value ?? "").replace(/'/gu, `'\\''`)}'`;
}

function workspaceRootLabel(value) {
  const normalized = String(value ?? "").replace(/\\/gu, "/").replace(/\/+$/u, "");
  return normalized.split("/").filter(Boolean).pop() || normalized || null;
}

function stableLinkId({ projectId, taskRef, sessionId }) {
  return `lnk_${createHash("sha256").update(`${projectId}\n${taskRef}\n${sessionId}`).digest("hex").slice(0, 24)}`;
}

async function recordAuditEvent(store, requestOrContext, event) {
  if (typeof store.recordAuditEvent !== "function") return;
  const actor = requestOrContext instanceof Request ? actorFromRequest(requestOrContext) : requestOrContext?.actor ?? null;
  await store.recordAuditEvent({
    eventId: `aud_${randomUUID()}`,
    actorId: actor?.id ?? null,
    action: event.action,
    targetType: event.targetType,
    targetId: event.targetId,
    outcome: event.outcome,
    event,
    valuesRedacted: true
  });
}

function matchSourceAction(pathname) {
  const match = /^\/v1\/project-management\/mdtodo\/sources\/([A-Za-z0-9_.:-]{1,160})(?:\/(probe|reindex))?$/u.exec(pathname);
  return match ? { sourceId: match[1], action: match[2] ?? null } : null;
}

function matchFileContent(pathname) {
  const match = /^\/v1\/project-management\/mdtodo\/files\/([A-Za-z0-9_.:-]{1,160})\/content$/u.exec(pathname);
  return match ? { fileRef: match[1] } : null;
}

function matchTaskAction(pathname) {
  const match = /^\/v1\/project-management\/mdtodo\/tasks\/(.+)$/u.exec(pathname);
  return match ? { taskRef: decodeURIComponent(match[1]) } : null;
}

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

function safeWorkbenchUrl(value) {
  const text = String(value ?? "").trim();
  return /^\/workbench\/sessions\/[^/?#]+$/u.test(text) ? text : null;
}

function safeRelativePath(value) {
  const text = String(value ?? "").replace(/\\/gu, "/").trim().replace(/^\/+|\/+$/gu, "");
  if (!text) return null;
  if (text === ".") return ".";
  if (text.includes("..") || /(^|\/)\./u.test(text)) return null;
  return /^[\p{L}\p{N}_./ -]{1,360}$/u.test(text) ? text : null;
}

function safeWorkspaceRef(value) {
  const text = String(value ?? "").trim();
  if (!text || text.includes("\0") || text.length > 500) return null;
  return /^[\p{L}\p{N}_:./\\ -]+$/u.test(text) ? text : null;
}

function shortText(value, max) {
  const text = String(value ?? "").replace(/\s+/gu, " ").trim();
  return text ? text.slice(0, max) : null;
}

function json(status, payload) {
  return new Response(`${JSON.stringify(payload)}\n`, {
    status,
    headers: {
      "content-type": "application/json; charset=utf-8",
      "cache-control": "no-store"
    }
  });
}

function jsonError(status, code, message) {
  return json(status, { ok: false, contractVersion, serviceId, error: { code, message, status }, valuesRedacted: true });
}

function queryToken(url, name) {
  const value = url.searchParams.get(name);
  return /^[A-Za-z0-9_.:-]{1,160}$/u.test(String(value ?? "")) ? value : null;
}

function queryOpaque(url, name) {
  const value = url.searchParams.get(name);
  return /^[A-Za-z0-9_.:/#_-]{1,320}$/u.test(String(value ?? "")) ? value : null;
}

function querySearch(url, name) {
  const value = url.searchParams.get(name);
  if (value === null) return null;
  const text = shortText(value, 120);
  return text && /^[\p{L}\p{N}\s_.:/#-]{1,120}$/u.test(text) ? text : null;
}

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 nonNegativeInteger(value, fallback) {
  const number = Number(value);
  return Number.isInteger(number) && number >= 0 ? number : fallback;
}

function taskListPage(url, options) {
  const limit = Math.min(options.maxLimit, positiveInteger(url.searchParams.get("limit"), options.defaultLimit));
  return {
    offset: nonNegativeInteger(url.searchParams.get("offset"), 0),
    limit,
    maxLimit: options.maxLimit
  };
}
