/**
 * SPEC: PJ2026-010404 项目管理 draft-2026-06-25-p0-mdtodo-web-active-editing-hwpod-source;
 * draft-2026-06-26-p1-mdtodo-web-operable.
 * Owns MDTODO Source normalization and bounded file access for local and HWPOD workspace sources.
 */
import { createHash, randomUUID } from "node:crypto";
import { promises as fs } from "node:fs";
import path from "node:path";

import { HWPOD_NODE_OPS_CONTRACT_VERSION } from "../../tools/src/hwpod-node-ops-contract.ts";
import { discoverMdtodoDocuments, parseMdtodoDocument } from "./mdtodo.ts";

const maxContentBytes = 1024 * 1024;
const hiddenOrSecretPattern = /(^\.|secret|secrets|token|credential|credentials|\.env$)/iu;

export function createMdtodoSourceAdapter(options = {}) {
  const env = options.env ?? process.env;
  const hwpodNodeOpsHandler = options.hwpodNodeOpsHandler ?? createHwpodNodeOpsHttpClient({ env, fetchImpl: options.fetchImpl ?? globalThis.fetch });

  return {
    async probe(source) {
      if (source.sourceKind === "hwpod-workspace") {
        const output = await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.ls", { path: normalizeDirectoryRef(source.mdtodoRootRef ?? ".") });
        return { ok: true, status: "ok", sourceId: source.sourceId, sourceKind: source.sourceKind, entries: Array.isArray(output.entries) ? output.entries.length : 0, valuesRedacted: true };
      }
      const root = localRootForSource(source);
      const entries = await fs.readdir(root, { withFileTypes: true });
      return { ok: true, status: "ok", sourceId: source.sourceId, sourceKind: source.sourceKind, entries: entries.length, valuesRedacted: true };
    },

    async discover(source) {
      if (source.sourceKind === "hwpod-workspace") return discoverHwpodDocuments(hwpodNodeOpsHandler, source);
      return discoverMdtodoDocuments({ root: localRootForSource(source), sourceId: source.sourceId, projectId: source.projectId, maxFiles: source.maxFiles });
    },

    async readFile(source, relativePath) {
      const safePath = normalizeMdtodoRelativePath(relativePath);
      const content = source.sourceKind === "hwpod-workspace"
        ? (await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.cat", { path: hwpodWorkspacePath(source, safePath), maxBytes: maxContentBytes })).content
        : await fs.readFile(localFilePathForSource(source, safePath), "utf8");
      return fileContentPayload(source, safePath, String(content ?? ""));
    },

    async readLinkedMarkdown(source, documentRelativePath, href) {
      const safePath = normalizeLinkedMarkdownPath(documentRelativePath, href);
      const content = source.sourceKind === "hwpod-workspace"
        ? (await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.cat", { path: hwpodWorkspacePath(source, safePath), maxBytes: maxContentBytes })).content
        : await readBoundedLocalMarkdown(source, safePath);
      return linkedMarkdownPayload(source, safePath, String(content ?? ""));
    },

    async writeFile(source, relativePath, content, expectedFingerprint) {
      const safePath = normalizeMdtodoRelativePath(relativePath);
      const nextContent = String(content ?? "");
      if (Buffer.byteLength(nextContent, "utf8") > maxContentBytes) throw sourceError("mdtodo_content_too_large", "MDTODO content is too large", 413);
      const before = await this.readFile(source, safePath);
      if (expectedFingerprint && before.fingerprint !== expectedFingerprint) {
        throw sourceError("revision_conflict", "MDTODO document fingerprint does not match expectedFingerprint", 409, { expectedFingerprint, actualFingerprint: before.fingerprint });
      }
      if (source.sourceKind === "hwpod-workspace") {
        await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.write", { path: hwpodWorkspacePath(source, safePath), content: nextContent, expectedSha: expectedFingerprint ?? before.fingerprint });
      } else {
        const target = localFilePathForSource(source, safePath);
        await fs.mkdir(path.dirname(target), { recursive: true });
        await fs.writeFile(target, nextContent, "utf8");
      }
      return fileContentPayload(source, safePath, nextContent);
    }
  };
}

export function normalizeProjectSourceInput(input, defaults = {}) {
  try {
    const sourceKind = normalizeSourceKind(input.sourceKind ?? input.kind ?? defaults.sourceKind ?? "local-file-tree");
    const projectId = safeToken(input.projectId, defaults.projectId ?? "project_hwlab_v03");
    const displayName = shortText(input.displayName ?? input.name ?? defaults.displayName ?? "HWLAB MDTODO", 180) ?? "HWLAB MDTODO";
    const status = safeToken(input.status, "active");
    const maxFiles = positiveInteger(input.maxFiles ?? defaults.maxFiles, 300);
    const mdtodoRootRef = normalizeDirectoryRef(input.mdtodoRootRef ?? defaults.mdtodoRootRef ?? ".");
    const hwpodId = sourceKind === "hwpod-workspace" ? requiredOpaque(input.hwpodId ?? defaults.hwpodId, "hwpodId") : null;
    const nodeId = sourceKind === "hwpod-workspace" ? requiredOpaque(input.nodeId ?? defaults.nodeId, "nodeId") : null;
    const workspaceRootRef = sourceKind === "hwpod-workspace" ? requiredWorkspaceRoot(input.workspaceRootRef ?? defaults.workspaceRootRef) : null;
    const rootRef = sourceKind === "hwpod-workspace"
      ? `${hwpodId}:${mdtodoRootRef}`
      : path.resolve(String(input.rootRef ?? defaults.rootRef ?? process.cwd()), mdtodoRootRef === "." ? "" : mdtodoRootRef);
    const sourceId = safeToken(input.sourceId, stableSourceId({ sourceKind, projectId, rootRef, hwpodId, nodeId, mdtodoRootRef }));
    const capabilities = normalizeCapabilities(input.capabilities, sourceKind);
    return {
      ok: true,
      source: {
        sourceId,
        kind: sourceKind === "hwpod-workspace" ? "hwpod-workspace" : "mdtodo-file-tree",
        sourceKind,
        displayName,
        projectId,
        rootRef,
        status,
        hwpodId,
        nodeId,
        workspaceRootRef,
        mdtodoRootRef,
        maxFiles,
        capabilities,
        valuesRedacted: true
      }
    };
  } catch (error) {
    return { ok: false, code: error?.code ?? "invalid_source", message: error?.message ?? "Invalid MDTODO source", status: error?.statusCode ?? 400 };
  }
}

export function sourceProjection(source, documents) {
  return {
    sourceId: source.sourceId,
    sourceKind: source.sourceKind,
    rootRef: source.rootRef,
    documentCount: documents.length,
    taskCount: documents.reduce((sum, document) => sum + document.taskCount, 0),
    projectedAt: new Date().toISOString(),
    valuesRedacted: true
  };
}

function createHwpodNodeOpsHttpClient({ env, fetchImpl }) {
  const endpoint = String(env.HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_URL ?? env.HWLAB_HWPOD_NODE_OPS_URL ?? "").trim();
  const apiKey = String(env.HWLAB_PROJECT_MANAGEMENT_HWPOD_NODE_OPS_API_KEY ?? "").trim();
  if (!endpoint || typeof fetchImpl !== "function") return null;
  return async (plan) => {
    const headers = { "content-type": "application/json", "x-source-service-id": "hwlab-project-management" };
    if (apiKey) headers.authorization = `Bearer ${apiKey}`;
    let response;
    try {
      response = await fetchImpl(endpoint, {
        method: "POST",
        headers,
        body: JSON.stringify(plan),
        signal: AbortSignal.timeout(30000)
      });
    } catch {
      throw sourceError("hwpod_transport_unavailable", "HWPOD node-ops transport is unavailable", 502, {
        sourceKind: "hwpod-workspace"
      });
    }
    try {
      return await response.json();
    } catch {
      throw sourceError("hwpod_invalid_response", "HWPOD node-ops returned an invalid response", 502, {
        sourceKind: "hwpod-workspace"
      });
    }
  };
}

async function discoverHwpodDocuments(hwpodNodeOpsHandler, source) {
  const files = await listHwpodMarkdownFiles(hwpodNodeOpsHandler, source);
  const documents = [];
  for (const relativePath of files.slice(0, source.maxFiles)) {
    const content = (await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.cat", { path: hwpodWorkspacePath(source, relativePath), maxBytes: maxContentBytes })).content;
    const parsed = parseMdtodoDocument(String(content ?? ""), { sourceId: source.sourceId, relativePath, projectId: source.projectId });
    if (parsed.tasks.length === 0 && !/mdtodo|todo/i.test(path.basename(relativePath))) continue;
    documents.push(parsed);
  }
  documents.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
  return documents;
}

async function listHwpodMarkdownFiles(hwpodNodeOpsHandler, source) {
  const root = normalizeDirectoryRef(source.mdtodoRootRef ?? ".");
  const files = [];
  const output = await runHwpodOp(hwpodNodeOpsHandler, source, "workspace.ls", { path: root });
  const entries = Array.isArray(output.entries) ? output.entries : [];
  entries.sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? "")));
  for (const entry of entries) {
    const name = String(entry.name ?? "");
    if (!name || isHiddenOrSecretSegment(name)) continue;
    if (entry.type === "file" && name.toLowerCase().endsWith(".md")) files.push(normalizeMdtodoRelativePath(name));
    if (files.length >= source.maxFiles) break;
  }
  return files;
}

async function runHwpodOp(hwpodNodeOpsHandler, source, op, args) {
  if (!hwpodNodeOpsHandler) {
    throw sourceError("hwpod_adapter_unconfigured", "HWPOD node-ops adapter is not configured", 503, {
      sourceKind: "hwpod-workspace"
    });
  }
  const plan = {
    contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
    planId: `hwpod_plan_${randomUUID()}`,
    hwpodId: source.hwpodId,
    nodeId: source.nodeId,
    intent: op,
    ops: [{ opId: "op_01", op, args: { workspacePath: source.workspaceRootRef, ...args } }]
  };
  const payload = await hwpodNodeOpsHandler(plan, { source });
  const result = Array.isArray(payload?.results) ? payload.results[0] : null;
  if (!payload?.ok || !result?.ok) {
    const blocker = result?.blocker ?? payload?.blocker ?? payload?.error ?? {};
    const otelTraceId = hwpodNodeOpsPayloadOtelTraceId(payload, blocker);
    const summary = String(blocker.summary ?? blocker.message ?? "HWPOD node-ops request failed");
    const traceLine = otelTraceId ? `OTel traceId: ${otelTraceId}` : null;
    throw sourceError(blocker.code ?? "hwpod_node_ops_failed", traceLine ? `${summary}\n${traceLine}` : summary, payload?.status === "blocked" ? 503 : 502, {
      sourceKind: "hwpod-workspace",
      hwpodStatus: payload?.status ?? null,
      otelTraceId: otelTraceId ?? null,
      traceLine,
      diagnostic: blocker.diagnostic ?? payload?.diagnostic ?? null
    });
  }
  return result.output ?? {};
}

function hwpodNodeOpsPayloadOtelTraceId(payload, blocker) {
  const resultBlocker = Array.isArray(payload?.results) ? payload.results.find((result) => result?.blocker?.otelTraceId || result?.blocker?.diagnostic?.otelTraceId)?.blocker : null;
  const value = payload?.otelTraceId
    ?? payload?.diagnostic?.otelTraceId
    ?? payload?.requestMeta?.otelTraceId
    ?? blocker?.otelTraceId
    ?? blocker?.diagnostic?.otelTraceId
    ?? resultBlocker?.otelTraceId
    ?? resultBlocker?.diagnostic?.otelTraceId;
  const text = String(value ?? "").trim();
  return text || null;
}

function fileContentPayload(source, relativePath, content) {
  const parsed = parseMdtodoDocument(content, { sourceId: source.sourceId, relativePath, projectId: source.projectId });
  return {
    sourceId: source.sourceId,
    fileRef: parsed.fileRef,
    relativePath,
    content,
    fingerprint: parsed.fingerprint,
    revision: parsed.fingerprint,
    parserMode: parsed.parserMode,
    taskCount: parsed.taskCount,
    document: parsed.document,
    valuesRedacted: false
  };
}

function linkedMarkdownPayload(source, relativePath, content) {
  const fingerprint = sha256(content);
  return {
    sourceId: source.sourceId,
    relativePath,
    content,
    fingerprint,
    revision: fingerprint,
    byteCount: Buffer.byteLength(content, "utf8"),
    render: "markdown",
    valuesRedacted: false
  };
}

function localRootForSource(source) {
  return path.resolve(String(source.rootRef ?? process.cwd()));
}

function localFilePathForSource(source, relativePath) {
  const root = localRootForSource(source);
  const target = path.resolve(root, normalizeMdtodoRelativePath(relativePath));
  if (target !== root && !target.startsWith(`${root}${path.sep}`)) throw sourceError("invalid_mdtodo_path", "MDTODO path escapes source root", 400);
  return target;
}

async function readBoundedLocalMarkdown(source, relativePath) {
  const target = localFilePathForSource(source, relativePath);
  const stats = await fs.stat(target);
  if (stats.size > maxContentBytes) throw sourceError("mdtodo_content_too_large", "Markdown report content is too large", 413);
  return fs.readFile(target, "utf8");
}

function hwpodWorkspacePath(source, relativePath) {
  return joinRelative(normalizeDirectoryRef(source.mdtodoRootRef ?? "."), normalizeMdtodoRelativePath(relativePath));
}

function normalizeMdtodoRelativePath(value) {
  const normalized = normalizeRelativeSegments(value, { allowDirectory: false });
  if (!normalized.toLowerCase().endsWith(".md")) throw sourceError("invalid_mdtodo_path", "MDTODO file path must end with .md", 400);
  return normalized;
}

function normalizeLinkedMarkdownPath(documentRelativePath, 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)) failPath();
  const withoutFragment = raw.split("#")[0]?.trim() ?? "";
  const directory = path.posix.dirname(normalizeMdtodoRelativePath(documentRelativePath));
  const normalized = path.posix.normalize(path.posix.join(directory === "." ? "" : directory, withoutFragment));
  return normalizeMdtodoRelativePath(normalized);
}

function normalizeDirectoryRef(value) {
  return normalizeRelativeSegments(value || ".", { allowDirectory: true });
}

function normalizeRelativeSegments(value, options = {}) {
  const text = String(value ?? "").replace(/\\/gu, "/").trim();
  if (!text || text === ".") return options.allowDirectory ? "." : failPath();
  if (text.startsWith("/") || /^[A-Za-z]:\//u.test(text)) failPath();
  const segments = text.split("/").filter((segment) => segment && segment !== ".");
  if (segments.length === 0) return options.allowDirectory ? "." : failPath();
  for (const segment of segments) {
    if (segment === ".." || segment.includes("\0") || isHiddenOrSecretSegment(segment)) failPath();
  }
  return segments.join("/");
}

function joinRelative(root, relativePath, options = {}) {
  const rootRef = normalizeDirectoryRef(root);
  const child = normalizeRelativeSegments(relativePath, { allowDirectory: options.allowDirectory === true });
  if (rootRef === ".") return child;
  if (child === ".") return rootRef;
  return `${rootRef}/${child}`;
}

function failPath() {
  throw sourceError("invalid_mdtodo_path", "MDTODO path must be a safe relative path inside the configured root", 400);
}

function isHiddenOrSecretSegment(segment) {
  return hiddenOrSecretPattern.test(String(segment ?? ""));
}

function normalizeSourceKind(value) {
  const text = String(value ?? "").trim().toLowerCase();
  if (["hwpod-workspace", "hwpod"].includes(text)) return "hwpod-workspace";
  if (["local-file-tree", "mdtodo-file-tree", "file-tree", "local"].includes(text)) return "local-file-tree";
  throw sourceError("invalid_source_kind", "sourceKind must be local-file-tree or hwpod-workspace", 400);
}

function normalizeCapabilities(value, sourceKind) {
  const defaults = sourceKind === "hwpod-workspace" ? ["probe", "list", "read", "write", "reindex"] : ["list", "read", "write", "reindex"];
  const input = Array.isArray(value) ? value : defaults;
  return input.map((item) => safeToken(item, null)).filter(Boolean).slice(0, 20);
}

function requiredOpaque(value, name) {
  const text = String(value ?? "").trim();
  if (/^[A-Za-z0-9_.:-]{1,160}$/u.test(text)) return text;
  throw sourceError("invalid_source_binding", `${name} is required`, 400);
}

function requiredWorkspaceRoot(value) {
  const text = String(value ?? "").trim();
  if (!text || text.length > 512 || /[\n\r\0]/u.test(text)) throw sourceError("invalid_workspace_root", "workspaceRootRef is required", 400);
  return text;
}

function stableSourceId(value) {
  return `src_${sha256(JSON.stringify(value)).slice(0, 16)}`;
}

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

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

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

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

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