import { createHmac, timingSafeEqual } from "node:crypto";

const MCP_PROTOCOL_VERSION = "2025-06-18";
const MAX_BODY_BYTES = 1024 * 1024;

function sendJson(response, statusCode, payload) {
  response.writeHead(statusCode, {
    "cache-control": "no-store",
    "content-type": "application/json; charset=utf-8",
  });
  response.end(JSON.stringify(payload));
}

async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
  const chunks = [];
  let size = 0;
  for await (const chunk of request) {
    size += chunk.length;
    if (size > maxBytes) throw new Error("payload_too_large");
    chunks.push(chunk);
  }
  return JSON.parse(Buffer.concat(chunks).toString("utf8"));
}

function timingSafeTokenEqual(left, right) {
  const leftBuffer = Buffer.from(String(left || ""));
  const rightBuffer = Buffer.from(String(right || ""));
  return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
}

function bearerToken(request) {
  const value = String(request.headers.authorization || "");
  return value.startsWith("Bearer ") ? value.slice("Bearer ".length).trim() : "";
}

function originAllowed(request, allowedOrigins) {
  const origin = String(request.headers.origin || "").trim();
  if (!origin) return true;
  return allowedOrigins.has(origin);
}

function base64UrlJson(value) {
  return Buffer.from(JSON.stringify(value), "utf8").toString("base64url");
}

function parseBase64UrlJson(value) {
  try {
    return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
  } catch {
    return null;
  }
}

function signCapability(encodedPayload, secret) {
  return createHmac("sha256", secret)
    .update(`nodedc.module-foundry.mcp-capability.v1.${encodedPayload}`)
    .digest("base64url");
}

function mintMcpCapability(identity, config) {
  const now = Date.now();
  const payload = {
    schemaVersion: "nodedc.module-foundry.mcp-capability.v1",
    service: "module-foundry",
    actorId: identity.actorId,
    ownerKey: identity.ownerKey,
    issuedAt: now,
    expiresAt: now + config.mcpCapabilityTtlMs,
  };
  const encodedPayload = base64UrlJson(payload);
  return `fnd1.${encodedPayload}.${signCapability(encodedPayload, config.capabilitySecret)}`;
}

function actorFromMcpCapability(token, config) {
  const [prefix, encodedPayload, signature, ...rest] = String(token || "").split(".");
  if (prefix !== "fnd1" || !encodedPayload || !signature || rest.length || !config.capabilitySecret) return null;
  const expectedSignature = signCapability(encodedPayload, config.capabilitySecret);
  if (!timingSafeTokenEqual(signature, expectedSignature)) return null;
  const payload = parseBase64UrlJson(encodedPayload);
  if (!payload || payload.schemaVersion !== "nodedc.module-foundry.mcp-capability.v1" || payload.service !== "module-foundry") return null;
  const actorId = String(payload.actorId || "").trim();
  const ownerKey = String(payload.ownerKey || "").trim();
  const expiresAt = Number(payload.expiresAt);
  const issuedAt = Number(payload.issuedAt);
  if (!actorId || !ownerKey || !Number.isFinite(expiresAt) || !Number.isFinite(issuedAt)) return null;
  if (issuedAt > Date.now() + 60_000 || expiresAt <= Date.now() || expiresAt - issuedAt > config.mcpCapabilityTtlMs + 60_000) return null;
  return { actorId, ownerKey };
}

function mcpError(id, code, message, data) {
  return {
    jsonrpc: "2.0",
    id: id ?? null,
    error: {
      code,
      message,
      ...(data === undefined ? {} : { data }),
    },
  };
}

function mcpResult(id, result) {
  return { jsonrpc: "2.0", id: id ?? null, result };
}

function toolText(payload, isError = false) {
  return {
    content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
    structuredContent: payload,
    ...(isError ? { isError: true } : {}),
  };
}

function normalizeMcpError(error) {
  const code = String(error?.message || "foundry_operation_failed");
  if (code === "application_not_found") return { code, status: 404 };
  if (/^(?:data_product_|resync_required$|stream_cursor_invalid$)/.test(code) && Number.isInteger(error?.statusCode)) {
    return { code, status: error.statusCode };
  }
  if (code.startsWith("invalid_") || code.startsWith("unknown_") || code.startsWith("map_") || code.endsWith("_required")) return { code, status: 400 };
  if (code.startsWith("duplicate_") || code.includes("conflict") || code.includes("mismatch")) return { code, status: 409 };
  return { code: "foundry_operation_failed", status: 500 };
}

const mapPresentationProfileInputSchema = {
  type: "object",
  additionalProperties: false,
  required: [
    "id", "version", "title", "semanticTypes", "label", "target", "facets",
    "styles", "classes", "defaultClassId", "sort",
  ],
  properties: {
    id: { type: "string", description: "Stable provider-neutral map.style_profile id." },
    version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
    title: { type: "string" },
    semanticTypes: {
      type: "array",
      minItems: 1,
      maxItems: 8,
      uniqueItems: true,
      items: { type: "string" },
    },
    label: {
      type: "object",
      additionalProperties: false,
      required: [
        "mode", "fields", "fontWeight", "sizePx", "color", "outlineColor",
        "outlineWidthPx", "backgroundColor", "backgroundOpacity", "paddingX", "paddingY",
        "maxLength", "offsetX", "offsetY", "hideCameraHeightMeters",
      ],
      properties: {
        mode: { type: "string", enum: ["subject_id", "attributes", "none"] },
        fields: {
          type: "array",
          minItems: 1,
          maxItems: 16,
          uniqueItems: true,
          description: "Provider-neutral fact attributes in fallback order, for example display_name, label, name, title.",
          items: { type: "string" },
        },
        fontWeight: { type: "integer", minimum: 400, maximum: 700 },
        sizePx: { type: "number", minimum: 8, maximum: 32 },
        color: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
        outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
        outlineWidthPx: { type: "number", minimum: 0, maximum: 6 },
        backgroundColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
        backgroundOpacity: { type: "number", minimum: 0, maximum: 1 },
        paddingX: { type: "number", minimum: 0, maximum: 40 },
        paddingY: { type: "number", minimum: 0, maximum: 40 },
        maxLength: { type: "integer", minimum: 8, maximum: 240 },
        offsetX: { type: "number", minimum: -100, maximum: 100 },
        offsetY: { type: "number", minimum: -100, maximum: 100 },
        hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
      },
    },
    target: {
      oneOf: [
        {
          type: "object",
          additionalProperties: false,
          required: [
            "variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor",
            "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters",
          ],
          properties: {
            variant: { type: "string", enum: ["elevated-spike"] },
            stemHeightMeters: { type: "number", minimum: 1, maximum: 100000 },
            headSizePx: { type: "number", minimum: 1, maximum: 64 },
            stemWidthPx: { type: "number", minimum: 0.25, maximum: 16 },
            outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
            outlineOpacity: { type: "number", minimum: 0, maximum: 1 },
            outlineWidthPx: { type: "number", minimum: 0, maximum: 8 },
            hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
          },
        },
        {
          type: "object",
          additionalProperties: false,
          required: ["variant", "outlineColor", "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters"],
          properties: {
            variant: {
              type: "string",
              enum: ["surface-fill"],
              description: "Provider-neutral filled surface interpreted by the active Map adapter.",
            },
            outlineColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
            outlineOpacity: { type: "number", minimum: 0, maximum: 1 },
            outlineWidthPx: { type: "number", minimum: 0, maximum: 8 },
            hideCameraHeightMeters: { type: "number", minimum: 1, maximum: 100000000 },
          },
        },
      ],
    },
    facets: {
      type: "array",
      minItems: 1,
      maxItems: 16,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["id", "field", "label", "filterable", "counter", "values"],
        properties: {
          id: { type: "string" },
          field: { type: "string", description: "Normalized provider-neutral Data Product field." },
          label: { type: "string" },
          filterable: { type: "boolean" },
          counter: { type: "boolean" },
          values: {
            type: "array",
            minItems: 1,
            maxItems: 32,
            items: {
              type: "object",
              additionalProperties: false,
              required: ["value", "label", "order"],
              properties: {
                value: { type: "string" },
                label: { type: "string" },
                order: { type: "integer", minimum: 0, maximum: 1000 },
              },
            },
          },
        },
      },
    },
    styles: {
      type: "array",
      minItems: 1,
      maxItems: 32,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["id", "color", "opacity"],
        properties: {
          id: { type: "string" },
          color: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
          opacity: { type: "number", minimum: 0, maximum: 1 },
        },
      },
    },
    classes: {
      type: "array",
      minItems: 1,
      maxItems: 64,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["id", "label", "priority", "match", "styleId", "renderable"],
        properties: {
          id: { type: "string" },
          label: { type: "string" },
          priority: { type: "integer", minimum: -10000, maximum: 10000 },
          match: {
            type: "array",
            maxItems: 8,
            description: "All conditions use declared normalized facet fields; an empty list is the fallback class.",
            items: {
              type: "object",
              additionalProperties: false,
              required: ["field", "equals"],
              properties: { field: { type: "string" }, equals: { type: "string" } },
            },
          },
          styleId: { type: "string" },
          renderable: { type: "boolean" },
        },
      },
    },
    defaultClassId: { type: "string" },
    sort: {
      type: "array",
      maxItems: 16,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["field", "order"],
        properties: {
          field: { type: "string" },
          order: { type: "array", minItems: 1, uniqueItems: true, items: { type: "string" } },
        },
      },
    },
  },
};

const mapSubjectDetailProfileFieldInputSchema = {
  type: "object",
  additionalProperties: false,
  required: ["id", "source", "field", "label", "format"],
  properties: {
    id: { type: "string", description: "Stable field presentation id." },
    aspectId: { type: "string", description: "Provider-neutral subject aspect id; defaults to primary." },
    dataClass: {
      type: "string",
      enum: ["operational", "restricted"],
      description: "Explicit field presentation class. Restricted identifiers require a restricted joined aspect.",
    },
    source: { type: "string", enum: ["fact", "attribute", "geometry", "context"] },
    field: { type: "string", description: "Exact registered fact, context, geometry or provider-neutral attribute field." },
    label: { type: "string", minLength: 1, maxLength: 100 },
    format: {
      type: "string",
      enum: ["text", "number", "timestamp", "boolean", "coordinate", "signal_state", "movement_state", "string_list", "telemetry_readings"],
    },
    unit: { type: "string", minLength: 1, maxLength: 24 },
    allowedReadingIds: {
      type: "array",
      maxItems: 256,
      uniqueItems: true,
      description: "Explicit classified sensor ids. Empty means no dynamic readings are rendered.",
      items: { type: "string" },
    },
  },
};

const mapSubjectDetailProfileInputSchema = {
  type: "object",
  additionalProperties: false,
  required: ["id", "version", "title", "semanticTypes", "defaultTabId", "tabs"],
  properties: {
    id: { type: "string", description: "Stable provider-neutral subject.detail_profile id." },
    version: { type: "string", pattern: "^\\d+\\.\\d+\\.\\d+$" },
    title: { type: "string", minLength: 1, maxLength: 120 },
    semanticTypes: { type: "array", minItems: 1, maxItems: 8, uniqueItems: true, items: { type: "string" } },
    defaultTabId: { type: "string" },
    tabs: {
      type: "array",
      minItems: 1,
      maxItems: 12,
      items: {
        type: "object",
        additionalProperties: false,
        required: ["id", "label", "emptyMessage", "sections"],
        properties: {
          id: { type: "string" },
          label: { type: "string", minLength: 1, maxLength: 80 },
          emptyMessage: { type: "string", minLength: 1, maxLength: 240 },
          sections: {
            type: "array",
            maxItems: 12,
            items: {
              type: "object",
              additionalProperties: false,
              required: ["id", "label", "fields"],
              properties: {
                id: { type: "string" },
                label: { type: "string", minLength: 1, maxLength: 80 },
                fields: { type: "array", maxItems: 32, items: mapSubjectDetailProfileFieldInputSchema },
              },
            },
          },
        },
      },
    },
  },
};

const mapPageSettingsPatchInputSchema = {
  type: "object",
  additionalProperties: false,
  minProperties: 1,
  properties: {
    imagerySource: { type: "string" },
    imageryVisible: { type: "boolean" },
    cacheEnabled: { type: "boolean" },
    cacheNoOverwrite: { type: "boolean" },
    terrainEnabled: { type: "boolean" },
    terrainExaggeration: { type: "number" },
    monochrome: { type: "boolean" },
    monochromeColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    imageryGamma: { type: "number" },
    imageryHue: { type: "number" },
    imageryAlpha: { type: "number" },
    globeColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    backgroundColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    atmosphereEnabled: { type: "boolean" },
    atmosphereHue: { type: "number" },
    atmosphereSaturation: { type: "number" },
    atmosphereBrightness: { type: "number" },
    fogEnabled: { type: "boolean" },
    fogDensity: { type: "number" },
    sunEnabled: { type: "boolean" },
    sunHour: { type: "number" },
    sunIntensity: { type: "number" },
    shadowsEnabled: { type: "boolean" },
    buildingsVisible: { type: "boolean" },
    buildingsColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    buildingsOpacity: { type: "number" },
    buildingsDetail: { type: "number" },
    imageryBrightness: { type: "number" },
    imageryContrast: { type: "number" },
    imagerySaturation: { type: "number" },
    gridVisible: { type: "boolean" },
    gridLodEnabled: { type: "boolean" },
    gridHeightMeters: { type: "number" },
    gridLod1MaxHeightKm: { type: "number" },
    gridLod1StepKm: { type: "number" },
    gridLod2MaxHeightKm: { type: "number" },
    gridLod2StepKm: { type: "number" },
    gridLod3StepKm: { type: "number" },
    gridRadiusKm: { type: "number" },
    gridLineWidth: { type: "number" },
    gridColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    gridOpacity: { type: "number" },
    gridDotsEnabled: { type: "boolean" },
    gridDotsSize: { type: "number" },
    gridDotsColor: { type: "string", pattern: "^#[0-9A-Fa-f]{6}$" },
    gridDotsOpacity: { type: "number" },
  },
};

const mapSubjectStateInputSchema = {
  type: "object",
  additionalProperties: false,
  required: ["bindingId", "visible", "filters", "window"],
  properties: {
    bindingId: { type: "string", description: "Stable Map Data Product binding id. Editable labels are never state keys." },
    visible: { type: "boolean", description: "False is an explicit empty map state and must not be normalized to all." },
    filters: {
      type: "object",
      description: "Facet selections keyed by normalized field. Missing field is unconstrained; an empty array matches nothing.",
      additionalProperties: { type: "array", uniqueItems: true, items: { type: "string" } },
    },
    window: {
      type: "object",
      additionalProperties: false,
      required: ["open", "rect", "maximized", "zIndex"],
      properties: {
        open: { type: "boolean" },
        rect: {
          type: "object",
          additionalProperties: false,
          required: ["x", "y", "width", "height"],
          properties: {
            x: { type: "number" },
            y: { type: "number" },
            width: { type: "number", minimum: 200 },
            height: { type: "number", minimum: 160 },
          },
        },
        maximized: { type: "boolean" },
        zIndex: { type: "integer", minimum: 1 },
      },
    },
  },
};

const tools = [
  {
    name: "foundry_status",
    title: "NDC Module Foundry status",
    description: "Read the availability and baseline scope of NDC Module Foundry. Canonical Page Library templates are read-only.",
    inputSchema: { type: "object", additionalProperties: false, properties: {} },
  },
  {
    name: "foundry_list_applications",
    title: "List application instances",
    description: "List editable application instances in NDC Module Foundry. This does not mutate Page Library.",
    inputSchema: { type: "object", additionalProperties: false, properties: {} },
  },
  {
    name: "foundry_get_application",
    title: "Get application instance",
    description: "Read one editable application instance, its page instances, features, map presentation profiles and bindings.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId"],
      properties: { applicationId: { type: "string", description: "UUID of an application instance." } },
    },
  },
  {
    name: "foundry_create_application",
    title: "Create application instance",
    description: "Create a draft application instance from a registered page template. The canonical template itself remains unchanged. Application deletion is deliberately unavailable.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["name", "idempotencyKey"],
      properties: {
        name: { type: "string", description: "Human-readable application name." },
        idempotencyKey: { type: "string", description: "Stable key for replay-safe writes." },
        slug: { type: "string" },
        description: { type: "string" },
        templateId: { type: "string", description: "Registered Page Library template id. Defaults to map." },
        templateVersion: { type: "string" },
        theme: { type: "string", enum: ["dark", "light"] },
      },
    },
  },
  {
    name: "foundry_update_application_metadata",
    title: "Update application metadata",
    description: "Rename or describe an application instance. It cannot change canonical templates or delete an application.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "idempotencyKey"],
      properties: {
        applicationId: { type: "string" },
        idempotencyKey: { type: "string" },
        name: { type: "string" },
        slug: { type: "string" },
        description: { type: "string" },
      },
    },
  },
  {
    name: "foundry_add_page_instance",
    title: "Add page instance to application",
    description: "Create one more instance of a registered Page Library template inside an application. A template may be used repeatedly; Page Library remains read-only.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "templateId", "idempotencyKey"],
      properties: {
        applicationId: { type: "string" },
        templateId: { type: "string", description: "Registered Page Library template id." },
        templateVersion: { type: "string" },
        idempotencyKey: { type: "string" },
        title: { type: "string" },
        navigationLabel: { type: "string" },
      },
    },
  },
  {
    name: "foundry_upsert_map_pin_binding",
    title: "Upsert map pin binding",
    description: "Create or update a visual map-pin binding on a Map Page instance. This stores a stable visual binding; it does not call a provider, Engine or an external data source.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "binding"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the application." },
        idempotencyKey: { type: "string" },
        binding: {
          type: "object",
          description: "Provider-neutral map-pin binding. Coordinates and source entity are mandatory.",
          required: ["id", "subjectId", "coordinates", "source"],
          properties: {
            id: { type: "string" },
            subjectId: { type: "string" },
            label: { type: "string" },
            status: { type: "string" },
            coordinates: {
              type: "object",
              required: ["longitude", "latitude"],
              properties: {
                longitude: { type: "number" },
                latitude: { type: "number" },
                heightMeters: { type: "number" },
              },
            },
            source: {
              type: "object",
              required: ["entityId"],
              properties: {
                entityId: { type: "string" },
                streamId: { type: "string" },
                displayFields: { type: "array", items: { type: "string" } },
              },
            },
            attributes: { type: "object" },
          },
        },
      },
    },
  },
  {
    name: "foundry_remove_map_pin_binding",
    title: "Remove map pin binding",
    description: "Remove one obsolete visual map-pin binding from a Map Page instance. The operation does not affect data-product consumers or the canonical Page Library.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId", "idempotencyKey"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the application." },
        bindingId: { type: "string", description: "Stable id of the obsolete visual map-pin binding." },
        idempotencyKey: { type: "string" },
      },
    },
  },
  {
    name: "foundry_upsert_map_presentation_profile",
    title: "Upsert Map presentation profile",
    description: "Create or update a versioned provider-neutral Map presentation profile. The profile owns target geometry, state classes, filters, counters, sort order and labels; it contains no provider transport or credentials.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "profile"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the application." },
        idempotencyKey: { type: "string" },
        profile: {
          ...mapPresentationProfileInputSchema,
          description: "Versioned provider-neutral map.style_profile. Classification uses declared normalized facets, never provider-specific raw status values.",
        },
      },
    },
  },
  {
    name: "foundry_upsert_map_subject_detail_profile",
    title: "Upsert Map subject detail profile",
    description: "Create or update a versioned provider-neutral subject detail profile. Tabs and fields are exact, fail-closed presentation declarations; provider transport and unrestricted payload rendering are forbidden.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "profile"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the application." },
        idempotencyKey: { type: "string" },
        profile: mapSubjectDetailProfileInputSchema,
      },
    },
  },
  {
    name: "foundry_update_map_page_settings",
    title: "Update Application Map settings",
    description: "Update visual environment settings on one Map page instance inside one Application. This operation never modifies the canonical Page Library template.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "settings"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map page instance id inside the editable Application." },
        idempotencyKey: { type: "string" },
        settings: mapPageSettingsPatchInputSchema,
      },
    },
  },
  {
    name: "foundry_save_map_page_view_state",
    title: "Save Application Map view state",
    description: "Persist one complete current Map view through the Application Save path: camera, map geometry, base settings, exact subject visibility/facets and every binding window geometry/z-order. Stable identity is bindingId.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "viewState"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the editable Application." },
        idempotencyKey: { type: "string" },
        viewState: {
          type: "object",
          additionalProperties: false,
          required: ["mapHeight", "camera", "subjectStates"],
          properties: {
            settings: mapPageSettingsPatchInputSchema,
            mapHeight: { type: "integer", minimum: 360, maximum: 5000 },
            camera: {
              type: "object",
              additionalProperties: false,
              required: ["longitude", "latitude", "height", "heading", "pitch", "roll"],
              properties: {
                longitude: { type: "number" },
                latitude: { type: "number" },
                height: { type: "number" },
                heading: { type: "number" },
                pitch: { type: "number" },
                roll: { type: "number" },
              },
            },
            subjectStates: { type: "array", maxItems: 64, items: mapSubjectStateInputSchema },
            inspectorOpenSections: {
              type: "array",
              maxItems: 1,
              uniqueItems: true,
              description: "Exact expanded Map Inspector section ids. Empty means every section is collapsed.",
              items: { type: "string", minLength: 1, maxLength: 256 },
            },
          },
        },
      },
    },
  },
  {
    name: "foundry_upsert_map_data_product_binding",
    title: "Upsert Map data product binding",
    description: "Bind one approved provider-neutral data product to a live-data slot (points or zones) of a Map Page instance. The binding stores no provider transport, endpoint, tenant, connection or credential.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "idempotencyKey", "binding"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string", description: "Map Page instance id inside the application." },
        idempotencyKey: { type: "string" },
        binding: {
          type: "object",
          required: ["id", "dataProductId", "slotId", "semanticTypes"],
          properties: {
            id: { type: "string" },
            displayName: { type: "string", minLength: 1, maxLength: 120, description: "User-defined text label shown in the Objects menu and window header." },
            order: { type: "integer", minimum: 0, maximum: 10000, description: "Application composition order inside the Objects menu." },
            dataProductId: { type: "string", description: "Versioned provider-neutral product, for example fleet.positions.current.v1." },
            slotId: { type: "string", description: "Approved Map Page live-data slot (entity-stream or zone-stream)." },
            semanticTypes: { type: "array", items: { type: "string" } },
            fieldProjection: { type: "array", items: { type: "string" } },
            presentationProfileId: { type: "string", description: "Existing page-owned provider-neutral Map presentation profile id." },
            subjectDetailProfileId: { type: "string", description: "Existing page-owned provider-neutral subject detail profile id." },
            aspectId: { type: "string", description: "Stable provider-neutral aspect id inside the selected subject composition." },
            joinToBindingId: { type: "string", description: "Primary Map binding whose stable sourceId is used to join this subject-details aspect." },
            dataClass: {
              type: "string",
              enum: ["operational", "restricted"],
              description: "Declared data class, verified against the versioned Foundry consumer policy before runtime.",
            },
          },
        },
      },
    },
  },
  {
    name: "foundry_plan_map_data_product_consumer",
    title: "Plan Map data product consumer",
    description: "Validate an approved Map data-product binding, its target-scoped server reader and versioned Foundry consumer policy. Returns a safe deterministic plan; it never returns a capability or endpoint.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string" },
        bindingId: { type: "string" },
      },
    },
  },
  {
    name: "foundry_apply_map_data_product_consumer",
    title: "Apply Map data product consumer",
    description: "Apply an exact consumer plan and commit a scoped snapshot into server-owned Foundry state. Active page viewers then share one upstream durable patch stream.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId", "planId", "idempotencyKey"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string" },
        bindingId: { type: "string" },
        planId: { type: "string" },
        idempotencyKey: { type: "string" },
      },
    },
  },
  {
    name: "foundry_get_map_data_product_consumer_status",
    title: "Get Map data product consumer status",
    description: "Read safe persisted cursor, subject/status counters, reconnect diagnostics and viewer/upstream counts for one approved binding. No raw fact attributes, capability or endpoint are returned.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string" },
        bindingId: { type: "string" },
      },
    },
  },
  {
    name: "foundry_accept_map_data_product_consumer",
    title: "Accept Map data product consumer",
    description: "Run a bounded lifecycle acceptance lease against the shared server consumer and verify persisted snapshot/cursor, subject identity, optional new patches, deduplication and secret boundary.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string" },
        bindingId: { type: "string" },
        timeoutMs: { type: "integer", minimum: 250, maximum: 30000 },
        minSubjectCount: { type: "integer", minimum: 0, maximum: 5000 },
        minPatchCount: { type: "integer", minimum: 0, maximum: 1000 },
      },
    },
  },
  {
    name: "foundry_rollback_map_data_product_consumer",
    title: "Rollback Map data product consumer",
    description: "Stop one server-owned consumer without deleting its last safe snapshot. Re-applying a fresh exact plan resumes it.",
    inputSchema: {
      type: "object",
      additionalProperties: false,
      required: ["applicationId", "pageId", "bindingId", "idempotencyKey"],
      properties: {
        applicationId: { type: "string" },
        pageId: { type: "string" },
        bindingId: { type: "string" },
        idempotencyKey: { type: "string" },
      },
    },
  },
];

function toolMap(operations) {
  return {
    foundry_status: () => operations.status(),
    foundry_list_applications: () => operations.listApplications(),
    foundry_get_application: (input) => operations.getApplication(input.applicationId),
    foundry_create_application: (input, actor) => operations.createApplication(input, actor),
    foundry_update_application_metadata: (input, actor) => operations.updateApplicationMetadata(input, actor),
    foundry_add_page_instance: (input, actor) => operations.addPageInstance(input, actor),
    foundry_upsert_map_pin_binding: (input, actor) => operations.upsertMapPinBinding(input, actor),
    foundry_remove_map_pin_binding: (input, actor) => operations.removeMapPinBinding(input, actor),
    foundry_upsert_map_presentation_profile: (input, actor) => operations.upsertMapPresentationProfile(input, actor),
    foundry_upsert_map_subject_detail_profile: (input, actor) => operations.upsertMapSubjectDetailProfile(input, actor),
    foundry_update_map_page_settings: (input, actor) => operations.updateMapPageSettings(input, actor),
    foundry_save_map_page_view_state: (input, actor) => operations.saveMapPageViewState(input, actor),
    foundry_upsert_map_data_product_binding: (input, actor) => operations.upsertMapDataProductBinding(input, actor),
    foundry_plan_map_data_product_consumer: (input) => operations.planMapDataProductConsumer(input),
    foundry_apply_map_data_product_consumer: (input, actor) => operations.applyMapDataProductConsumer(input, actor),
    foundry_get_map_data_product_consumer_status: (input) => operations.getMapDataProductConsumerStatus(input),
    foundry_accept_map_data_product_consumer: (input) => operations.acceptMapDataProductConsumer(input),
    foundry_rollback_map_data_product_consumer: (input, actor) => operations.rollbackMapDataProductConsumer(input, actor),
  };
}

export async function handleFoundryMcpRequest(request, response, options) {
  const config = options.getConfig();
  const allowedOrigins = new Set(config.mcpAllowedOrigins || []);
  if (request.method !== "POST") {
    response.writeHead(405, { Allow: "POST" });
    response.end();
    return;
  }
  if (!originAllowed(request, allowedOrigins)) return sendJson(response, 403, { error: "mcp_origin_not_allowed" });
  const token = bearerToken(request);
  let actor = config.capabilitySecret ? actorFromMcpCapability(token, config) : null;
  if (!actor && typeof options.authenticateAgentToken === "function") {
    actor = await options.authenticateAgentToken(token, { check: true });
  }
  if (!config.capabilitySecret && typeof options.authenticateAgentToken !== "function") {
    return sendJson(response, 503, { error: "foundry_mcp_not_configured" });
  }
  if (!actor) return sendJson(response, 401, { error: "mcp_unauthorized" });

  let message;
  try {
    message = await readJsonBody(request);
  } catch (error) {
    return sendJson(response, error?.message === "payload_too_large" ? 413 : 400, { error: error?.message === "payload_too_large" ? "payload_too_large" : "invalid_json" });
  }
  if (!message || message.jsonrpc !== "2.0" || typeof message.method !== "string") {
    return sendJson(response, 400, mcpError(message?.id, -32600, "Invalid Request"));
  }
  const id = message.id;
  if (message.method === "initialize") {
    return sendJson(response, 200, mcpResult(id, {
      protocolVersion: MCP_PROTOCOL_VERSION,
      capabilities: { tools: { listChanged: false } },
      serverInfo: { name: "nodedc_module_foundry", version: "0.6.0" },
      instructions: "NDC Module Foundry edits application instances, provider-neutral map presentation profiles and approved server-owned data-product consumers. Page Library is read-only. Application deletion is unavailable. Provider endpoints and credentials are never MCP inputs or outputs.",
    }));
  }
  if (message.method === "notifications/initialized") return sendJson(response, 202, {});
  if (message.method === "ping") return sendJson(response, 200, mcpResult(id, {}));
  if (message.method === "tools/list") return sendJson(response, 200, mcpResult(id, { tools }));
  if (message.method !== "tools/call") return sendJson(response, 200, mcpError(id, -32601, "Method not found"));

  const params = message.params && typeof message.params === "object" ? message.params : {};
  const name = String(params.name || "");
  const handler = toolMap(options.operations)[name];
  if (!handler) return sendJson(response, 200, mcpError(id, -32602, "Unknown tool"));
  try {
    const result = await handler(params.arguments && typeof params.arguments === "object" ? params.arguments : {}, actor);
    return sendJson(response, 200, mcpResult(id, toolText(result)));
  } catch (error) {
    const normalized = normalizeMcpError(error);
    return sendJson(response, 200, mcpResult(id, toolText({ error: normalized.code }, true)));
  }
}

function ownerIdentity(owner) {
  const source = owner && typeof owner === "object" ? owner : {};
  const actorId = String(source.userId || source.user_id || source.id || "").trim();
  const ownerKey = String(source.key || source.ownerKey || source.owner_key || actorId).trim();
  const groups = [source.groups, source.roles, source.roleKeys]
    .flatMap((value) => Array.isArray(value) ? value : [])
    .map((value) => String(value || "").trim().toLowerCase())
    .filter(Boolean);
  return { actorId, ownerKey, groups };
}

function hasFoundryAccess(identity, config) {
  if (!identity.actorId || !identity.ownerKey) return false;
  // A Launcher/Authentik block revokes every Foundry surface, including a
  // previously discoverable AI Workspace entitlement.
  if (identity.groups.includes("nodedc:module-foundry:blocked")) return false;
  if (config.allowAllAuthenticated) return true;
  if (config.allowedOwnerIds.includes(identity.actorId) || config.allowedOwnerIds.includes(identity.ownerKey)) return true;
  return identity.groups.some((group) => config.allowedOwnerGroups.includes(group));
}

export async function handleFoundryEntitlementRequest(request, response, options) {
  const config = options.getConfig();
  if (request.method !== "POST") {
    response.writeHead(405, { Allow: "POST" });
    response.end();
    return;
  }
  if (!config.internalAccessToken || !config.capabilitySecret) return sendJson(response, 503, { ok: false, error: "foundry_entitlement_not_configured" });
  if (!timingSafeTokenEqual(bearerToken(request), config.internalAccessToken)) return sendJson(response, 401, { ok: false, error: "entitlement_unauthorized" });
  let body;
  try {
    body = await readJsonBody(request);
  } catch (error) {
    return sendJson(response, error?.message === "payload_too_large" ? 413 : 400, { ok: false, error: "invalid_json" });
  }
  if (body?.schemaVersion !== "ai-workspace.entitlement-request.v1") return sendJson(response, 400, { ok: false, error: "unsupported_entitlement_schema" });
  if (body?.appId !== "module-foundry") return sendJson(response, 400, { ok: false, error: "unsupported_entitlement_app" });
  const identity = ownerIdentity(body.owner);
  const accessAllowed = hasFoundryAccess(identity, config);
  const grantBase = {
    appId: "module-foundry",
    appTitle: "NDC Module Foundry",
    surface: "module-foundry",
    scopes: [
      "foundry.application.read",
      "foundry.application.create",
      "foundry.application.update",
      "foundry.page-instance.create",
      "foundry.map-pin.upsert",
      "foundry.map-pin.remove",
      "foundry.map-presentation-profile.upsert",
      "foundry.map-page-settings.update",
      "foundry.map-page-view-state.save",
      "foundry.map-data-product.upsert",
      "foundry.map-data-product-consumer.read",
      "foundry.map-data-product-consumer.lifecycle",
    ],
  };
  if (!accessAllowed) {
    return sendJson(response, 200, {
      appGrants: {
        "module-foundry": {
          ...grantBase,
          enabled: false,
          denied: true,
          reason: "foundry_access_not_granted",
          deniedText: "NDC Module Foundry недоступен в текущем контексте.",
          mcpServers: [],
        },
      },
    });
  }
  if (!config.mcpUrl) return sendJson(response, 503, { ok: false, error: "foundry_mcp_not_configured" });
  const capability = mintMcpCapability(identity, config);
  return sendJson(response, 200, {
    appGrants: {
      "module-foundry": {
        ...grantBase,
        enabled: true,
        context: { actorId: identity.actorId },
        mcpServers: [{
          serverName: "nodedc_module_foundry",
          url: config.mcpUrl,
          enabled: true,
          required: false,
          startupTimeoutSec: 20,
          toolTimeoutSec: 60,
          httpHeaders: {
            Authorization: `Bearer ${capability}`,
            "MCP-Protocol-Version": MCP_PROTOCOL_VERSION,
          },
        }],
      },
    },
  });
}
