#!/usr/bin/env node
import fs from 'node:fs/promises'
import path from 'node:path'

const ROOT = process.env.NDC_AGENT_MCP_ROOT || process.cwd()
const DEFAULT_API_BASE = 'http://127.0.0.1:3001/api/ndc-agent-mcp'
const DEFAULT_SCHEMA_VERSION = 'v2.3.2'
const ENGINE_FETCH_TIMEOUT_MS = Number(process.env.NDC_AGENT_MCP_FETCH_TIMEOUT_MS || 8000)
const DEFAULT_ASSISTANT_ACTION_GATEWAY_PATH = '/api/ai-workspace/assistant/v1/actions'
const ASSISTANT_ACTION_FETCH_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_FETCH_TIMEOUT_MS || ENGINE_FETCH_TIMEOUT_MS)
const context = parseContext()
const nodeCatalogCache = new Map()
let transportMode = null
let inputBuffer = Buffer.alloc(0)

const FALLBACK_NODE_CATALOG = [
  {
    name: 'webhook',
    displayName: 'Webhook',
    description: 'Starts a workflow from an HTTP webhook request.',
    group: ['trigger'],
    version: [2.1],
    defaults: { name: 'Webhook' },
    inputs: [],
    outputs: ['main'],
    properties: [
      { name: 'httpMethod', displayName: 'HTTP Method', type: 'options', default: 'POST' },
      { name: 'path', displayName: 'Path', type: 'string', default: '' },
      { name: 'responseMode', displayName: 'Response Mode', type: 'options', default: 'onReceived' },
    ],
  },
  {
    name: 'set',
    displayName: 'Set',
    description: 'Sets or edits item fields.',
    group: ['transform'],
    version: [3.4],
    defaults: { name: 'Set' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      { name: 'assignments', displayName: 'Assignments', type: 'fixedCollection', default: {} },
      { name: 'options', displayName: 'Options', type: 'collection', default: {} },
    ],
  },
  {
    name: 'code',
    displayName: 'Code',
    description: 'Runs custom JavaScript code.',
    group: ['transform'],
    version: [2],
    defaults: { name: 'Code' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      { name: 'mode', displayName: 'Mode', type: 'options', default: 'runOnceForAllItems' },
      { name: 'jsCode', displayName: 'JavaScript Code', type: 'string', default: 'return items;' },
    ],
  },
  {
    name: 'httpRequest',
    displayName: 'HTTP Request',
    description: 'Makes an HTTP request and returns the response.',
    group: ['input'],
    version: [4.2],
    defaults: { name: 'HTTP Request' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      { name: 'method', displayName: 'Method', type: 'options', default: 'GET' },
      { name: 'url', displayName: 'URL', type: 'string', default: '' },
      { name: 'sendHeaders', displayName: 'Send Headers', type: 'boolean', default: false },
      { name: 'sendBody', displayName: 'Send Body', type: 'boolean', default: false },
    ],
  },
  {
    name: 'respondToWebhook',
    displayName: 'Respond to Webhook',
    description: 'Returns a response to a Webhook trigger.',
    group: ['output'],
    version: [1.1],
    defaults: { name: 'Respond to Webhook' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [
      { name: 'respondWith', displayName: 'Respond With', type: 'options', default: 'json' },
      { name: 'responseBody', displayName: 'Response Body', type: 'json', default: '{}' },
    ],
  },
  {
    name: 'noOp',
    displayName: 'No Operation, do nothing',
    description: 'Passes data through without changing it.',
    group: ['transform'],
    version: [1],
    defaults: { name: 'No Operation, do nothing' },
    inputs: ['main'],
    outputs: ['main'],
    properties: [],
  },
]

function parseContext() {
  const raw = String(process.env.NDC_AGENT_MCP_CONTEXT || '').trim()
  let parsed = {}
  if (raw) {
    try { parsed = JSON.parse(raw) || {} } catch {}
  }
  const assistantActions = parseObject(
    parsed.assistantActions ||
    process.env.AI_WORKSPACE_ASSISTANT_ACTIONS ||
    {},
  )
  return {
    modeId: cleanString(parsed.modeId || 'ndc-agent-core'),
    workflowId: cleanString(parsed.workflowId || process.env.NDC_AGENT_MCP_WORKFLOW_ID || ''),
    workflowTitle: cleanString(parsed.workflowTitle || process.env.NDC_AGENT_MCP_WORKFLOW_TITLE || ''),
    agentNodeId: cleanString(parsed.agentNodeId || process.env.NDC_AGENT_MCP_NODE_ID || ''),
    agentNodeTitle: cleanString(parsed.agentNodeTitle || process.env.NDC_AGENT_MCP_NODE_TITLE || ''),
    roleId: cleanString(parsed.roleId || process.env.NDC_AGENT_MCP_ROLE_ID || ''),
    roleTitle: cleanString(parsed.roleTitle || process.env.NDC_AGENT_MCP_ROLE_TITLE || ''),
    apiBaseUrl: cleanApiBase(parsed.ndcAgentMcpApiBaseUrl || process.env.NDC_AGENT_MCP_API_BASE_URL || DEFAULT_API_BASE),
    schemaVersion: cleanString(parsed.schemaVersion || process.env.NDC_AGENT_MCP_SCHEMA_VERSION || DEFAULT_SCHEMA_VERSION),
    n8nMcpRefPath: cleanString(parsed.n8nMcpRefPath || process.env.NDC_AGENT_MCP_REF_PATH || path.resolve(ROOT, '..', 'tools', 'NDCMCP')),
    assistantActions,
    assistantActionOwner: parseObject(parsed.assistantActionOwner || {}),
    assistantActionGatewayUrl: cleanHttpUrl(
      parsed.assistantActionGatewayUrl ||
      process.env.AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL ||
      deriveAssistantActionGatewayUrlFromHub() ||
      '',
    ),
    assistantActionGatewayToken: cleanString(
      parsed.assistantActionGatewayToken ||
      process.env.AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_TOKEN ||
      '',
      4000,
    ),
  }
}

function cleanString(value, max = 1000) {
  return String(value || '').trim().slice(0, max)
}

function parseObject(value, fallback = {}) {
  if (value && typeof value === 'object' && !Array.isArray(value)) return value
  const text = String(value || '').trim()
  if (!text) return fallback
  try {
    const parsed = JSON.parse(text)
    return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback
  } catch {
    return fallback
  }
}

function cleanHttpUrl(value) {
  const text = cleanString(value, 2000).replace(/\/+$/, '')
  if (!text) return ''
  try {
    const url = new URL(text)
    if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
    url.username = ''
    url.password = ''
    return url.toString().replace(/\/+$/, '')
  } catch {
    return ''
  }
}

function cleanApiBase(value) {
  const raw = cleanString(value || DEFAULT_API_BASE)
  return raw.replace(/\/+$/, '') || DEFAULT_API_BASE
}

function uniqueStrings(values) {
  const result = []
  const seen = new Set()
  for (const value of values || []) {
    const clean = cleanString(value, 1000).replace(/\/+$/, '')
    if (!clean || seen.has(clean)) continue
    seen.add(clean)
    result.push(clean)
  }
  return result
}

function isLoopbackUrl(value) {
  try {
    const host = new URL(value).hostname.toLowerCase()
    return host === 'localhost' || host === '0.0.0.0' || host === '::1' || host.startsWith('127.')
  } catch {
    return false
  }
}

function isPrivateNetworkUrl(value) {
  try {
    const host = new URL(value).hostname.toLowerCase()
    if (host === 'localhost' || host === '0.0.0.0' || host === '::1' || host.startsWith('127.')) return true
    if (host.startsWith('192.168.')) return true
    if (host.startsWith('10.')) return true
    if (host.startsWith('169.254.')) return true
    const match = host.match(/^172\.(\d+)\./)
    return Boolean(match && Number(match[1]) >= 16 && Number(match[1]) <= 31)
  } catch {
    return false
  }
}

function hubOriginToApiBase(value) {
  try {
    const url = new URL(cleanString(value, 1000))
    const host = url.hostname.toLowerCase()
    if (!host) return ''
    if (url.protocol === 'wss:') url.protocol = 'https:'
    else if (url.protocol === 'ws:') url.protocol = 'http:'
    else if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
    url.pathname = ''
    url.search = ''
    url.hash = ''
    const origin = url.toString().replace(/\/+$/, '')
    if (host === 'ai-hub.nodedc.ru') {
      const pairingCode = cleanPairingCode(process.env.AI_BRIDGE_PAIRING_CODE)
      return pairingCode ? `${origin}/api/ai-workspace/hub/v1/ndc-agent-mcp/${pairingCode}` : ''
    }
    return `${origin}/api/ndc-agent-mcp`
  } catch {
    return ''
  }
}

function hubOriginToAssistantActionGateway(value) {
  try {
    const url = new URL(cleanString(value, 1000))
    const host = url.hostname.toLowerCase()
    if (!host) return ''
    if (url.protocol === 'wss:') url.protocol = 'https:'
    else if (url.protocol === 'ws:') url.protocol = 'http:'
    else if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
    url.pathname = ''
    url.search = ''
    url.hash = ''
    const origin = url.toString().replace(/\/+$/, '')
    return `${origin}${DEFAULT_ASSISTANT_ACTION_GATEWAY_PATH}`
  } catch {
    return ''
  }
}

function deriveAssistantActionGatewayUrlFromHub() {
  const candidates = [
    hubOriginToAssistantActionGateway(process.env.AI_BRIDGE_HUB_URL),
    ...splitList(process.env.AI_BRIDGE_HUB_URLS).map(hubOriginToAssistantActionGateway),
  ].filter(Boolean)
  return uniqueStrings(candidates)[0] || ''
}

function cleanPairingCode(value) {
  return cleanString(value, 100).toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 32)
}

function splitList(value) {
  return cleanString(value, 4000).split(/[,;\r\n]+/).map((item) => item.trim()).filter(Boolean)
}

function engineApiBaseCandidates() {
  const explicit = cleanApiBase(context.apiBaseUrl)
  const hubBases = [
    hubOriginToApiBase(process.env.AI_BRIDGE_HUB_URL),
    ...splitList(process.env.AI_BRIDGE_HUB_URLS).map(hubOriginToApiBase),
  ]
  const envBase = cleanString(process.env.NDC_AGENT_MCP_API_BASE_URL, 1000).replace(/\/+$/, '')
  if (explicit && !isPrivateNetworkUrl(explicit)) {
    return uniqueStrings([explicit, envBase])
  }
  if (envBase && !isPrivateNetworkUrl(envBase)) {
    return uniqueStrings([envBase, explicit])
  }
  if (hubBases.some(Boolean)) {
    return uniqueStrings([...hubBases, envBase, explicit])
  }
  return uniqueStrings([explicit, envBase, DEFAULT_API_BASE])
}

function engineApiUrls(pathname) {
  const pathValue = String(pathname || '')
  return engineApiBaseCandidates().map((baseValue) => {
    try {
      const base = new URL(baseValue)
      if (pathValue.startsWith('/api/')) return `${base.origin}${pathValue}`
      if (pathValue.startsWith('/')) return `${baseValue}${pathValue}`
      return new URL(pathValue, `${baseValue}/`).toString()
    } catch {
      return ''
    }
  }).filter(Boolean)
}

function fetchFailureText(error) {
  const parts = [
    error?.name,
    error?.message,
    error?.cause?.code,
    error?.cause?.message,
  ].map((item) => cleanString(item, 300)).filter(Boolean)
  return parts.join(':') || 'fetch_failed'
}

function toolText(value) {
  const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
  return { content: [{ type: 'text', text }], structuredContent: typeof value === 'string' ? undefined : value }
}

function jsonRpcResult(id, result) {
  return { jsonrpc: '2.0', id, result }
}

function jsonRpcError(id, code, message, data) {
  return { jsonrpc: '2.0', id, error: { code, message: String(message || 'error'), ...(data ? { data } : {}) } }
}

function writeMessage(message) {
  const json = JSON.stringify(message)
  if (transportMode === 'content-length') {
    const body = Buffer.from(json, 'utf8')
    process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`)
    process.stdout.write(body)
    return
  }
  process.stdout.write(`${json}\n`)
}

async function readJson(file, fallback = null) {
  try { return JSON.parse(await fs.readFile(file, 'utf8')) } catch { return fallback }
}

async function loadNodeCatalog(version = context.schemaVersion) {
  const key = cleanString(version || DEFAULT_SCHEMA_VERSION)
  if (nodeCatalogCache.has(key)) return nodeCatalogCache.get(key)
  const remoteCatalog = await loadRemoteNodeCatalog(key)
  if (remoteCatalog.length) {
    nodeCatalogCache.set(key, remoteCatalog)
    return remoteCatalog
  }
  const direct = path.resolve(ROOT, 'server', 'assets', 'n8n', 'schema', key, 'nodes.catalog.json')
  const fallback = path.resolve(ROOT, 'server', 'assets', 'n8n', 'schema', DEFAULT_SCHEMA_VERSION, 'nodes.catalog.json')
  const raw = await readJson(direct, null) || await readJson(fallback, [])
  const catalog = Array.isArray(raw) && raw.length ? raw : FALLBACK_NODE_CATALOG
  nodeCatalogCache.set(key, catalog)
  return catalog
}

async function loadN8nMcpVersion() {
  const pkg = await readJson(path.join(context.n8nMcpRefPath, 'package.json'), null)
  return pkg?.version ? String(pkg.version) : '2.33.2'
}

function assistantActionGatewayEndpoint() {
  const base = cleanHttpUrl(context.assistantActionGatewayUrl)
  if (!base) return ''
  if (base.endsWith(DEFAULT_ASSISTANT_ACTION_GATEWAY_PATH) || base.endsWith('/actions')) return base
  return `${base}${DEFAULT_ASSISTANT_ACTION_GATEWAY_PATH}`
}

function assistantActionIds() {
  return Array.isArray(context.assistantActions?.actionIds)
    ? context.assistantActions.actionIds.map((item) => cleanString(item, 200)).filter(Boolean)
    : []
}

function assistantActionOwnerHeaders() {
  const owner = parseObject(context.assistantActionOwner, {})
  const headers = {}
  const put = (name, value) => {
    const text = cleanString(value, 1000)
    if (text) headers[name] = text
  }
  put('x-nodedc-user-id', owner.userId || owner.user_id)
  put('x-nodedc-user-email', owner.email)
  put('x-nodedc-user-role', owner.role)
  const groups = Array.isArray(owner.groups)
    ? owner.groups.map((item) => cleanString(item, 120)).filter(Boolean).join(',')
    : owner.groups
  put('x-nodedc-user-groups', groups)
  return headers
}

async function assistantActionFetch(payload = {}) {
  const endpoint = assistantActionGatewayEndpoint()
  const token = context.assistantActionGatewayToken
  if (!endpoint || !token) {
    return {
      ok: false,
      decision: 'assistant_action_gateway_unavailable',
      reason: 'Assistant action gateway URL/token is not configured for this run.',
      actionIds: assistantActionIds(),
    }
  }

  let res = null
  let text = ''
  let json = null
  try {
    res = await fetch(endpoint, {
      method: 'POST',
      headers: {
        Accept: 'application/json',
        'Content-Type': 'application/json',
        Authorization: `Bearer ${token}`,
        ...assistantActionOwnerHeaders(),
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(ASSISTANT_ACTION_FETCH_TIMEOUT_MS),
    })
    text = await res.text().catch(() => '')
    try { json = text ? JSON.parse(text) : null } catch {}
  } catch (error) {
    const out = new Error(`assistant_action_gateway_fetch_failed:${fetchFailureText(error)}`)
    out.payload = { decision: 'assistant_action_gateway_fetch_failed' }
    throw out
  }

  if (!res.ok || json?.ok === false) {
    const out = new Error(json?.message || json?.error || `assistant_action_http_${res.status}`)
    out.status = res.status
    out.payload = json && typeof json === 'object'
      ? json
      : { status: res.status, preview: cleanString(text, 400) }
    throw out
  }
  if (!json || typeof json !== 'object') {
    const out = new Error(`assistant_action_non_json_response:${res.status}`)
    out.status = 502
    out.payload = { status: res.status, preview: cleanString(text, 400) }
    throw out
  }
  return json
}

function engineApiUrl(pathname) {
  return engineApiUrls(pathname)[0] || ''
}

async function loadRemoteNodeCatalog(version) {
  const query = new URLSearchParams({ kind: 'nodes', version: version || DEFAULT_SCHEMA_VERSION })
  const urls = engineApiUrls(`/api/n8n/schema?${query.toString()}`)
  for (const url of urls) {
    try {
      const res = await fetch(url, { headers: { Accept: 'application/json' }, signal: AbortSignal.timeout(ENGINE_FETCH_TIMEOUT_MS) })
      if (!res.ok) continue
      const json = await res.json()
      if (Array.isArray(json?.nodes)) return json.nodes
    } catch {}
  }
  return []
}

function normalizeTypeName(value) {
  const raw = cleanString(value, 240)
  if (!raw) return ''
  if (raw.startsWith('n8n-nodes-base.')) return raw.slice('n8n-nodes-base.'.length)
  return raw
}

function fullNodeType(value) {
  const raw = cleanString(value, 240)
  if (!raw) return ''
  if (isPackageQualifiedNodeType(raw)) return raw
  return raw.startsWith('n8n-nodes-base.') ? raw : `n8n-nodes-base.${raw}`
}

function isPackageQualifiedNodeType(value) {
  return /^(?:@[a-z0-9._-]+\/)?n8n-nodes-[a-z0-9._-]+\.[a-z0-9._-]+$/i.test(cleanString(value, 240))
}

function catalogNodeRuntimeType(node) {
  const explicit = [node?.runtimeType, node?.fullType, node?.publicType, node?.type]
    .map((value) => cleanString(value, 240))
    .find(isPackageQualifiedNodeType)
  if (explicit) return explicit
  const packageName = cleanString(node?.packageName || node?.package, 160)
  const name = cleanString(node?.name, 160)
  if (name && /^(?:@[a-z0-9._-]+\/)?n8n-nodes-[a-z0-9._-]+$/i.test(packageName)) {
    return `${packageName}.${name}`
  }
  return fullNodeType(node?.publicType || name)
}

function cleanWebhookSegment(value, fallback) {
  const out = cleanString(value, 240)
    .toLowerCase()
    .replace(/[^a-z0-9_-]+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-+|-+$/g, '')
  return out || fallback
}

function makeNdcWebhookPath(workflowId, nodeId) {
  return `ndc/${cleanWebhookSegment(workflowId, 'workflow')}/${cleanWebhookSegment(nodeId, 'agent')}/run`
}

function defaultSubworkflowGraph(workflowId = context.workflowId, nodeId = context.agentNodeId) {
  return {
    version: 1,
    nodes: [{
      id: 'webhook-trigger',
      type: 'n8nOp',
      position: { x: 0, y: 0 },
      data: {
        title: 'Webhook Trigger',
        n8n: {
          parameters: {
            httpMethod: 'POST',
            path: makeNdcWebhookPath(workflowId, nodeId),
            responseMode: 'onReceived',
            options: {},
          },
          type: 'n8n-nodes-base.webhook',
          typeVersion: 2.1,
          position: [0, 0],
          id: 'webhook-trigger',
          name: 'Webhook Trigger',
        },
        w: 220,
        h: 120,
        n8nTypeLabel: 'Webhook',
        n8nShowId: false,
        n8nOutputLabels: [],
      },
    }],
    edges: [],
    source: {
      createdBy: 'ndc-agent-mcp',
      initialized: 'default-webhook',
    },
  }
}

function normalizeGraphResult(value, workflowId = context.workflowId, nodeId = context.agentNodeId) {
  const graph = value && typeof value === 'object' ? { ...value } : defaultSubworkflowGraph(workflowId, nodeId)
  graph.version = Number(graph.version || 1) || 1
  graph.nodes = Array.isArray(graph.nodes) && graph.nodes.length ? graph.nodes : defaultSubworkflowGraph(workflowId, nodeId).nodes
  graph.edges = Array.isArray(graph.edges) ? graph.edges : []
  return graph
}

function versionSummary(version) {
  if (Array.isArray(version)) return version
  if (version === undefined || version === null) return []
  return [version]
}

function compactNodeDefinition(node, maxProperties = 80) {
  const properties = Array.isArray(node?.properties) ? node.properties.slice(0, maxProperties) : []
  return {
    name: node?.name || '',
    publicType: node?.publicType || node?.name || '',
    fullType: catalogNodeRuntimeType(node),
    displayName: node?.displayName || node?.name || '',
    description: node?.description || '',
    group: node?.group || [],
    versions: versionSummary(node?.version),
    defaults: node?.defaults || {},
    inputs: node?.inputs || [],
    outputs: node?.outputs || [],
    credentials: node?.credentials || [],
    properties,
    propertiesTruncated: Array.isArray(node?.properties) && node.properties.length > properties.length,
  }
}

async function engineFetch(pathname, options = {}) {
  const urls = engineApiUrls(pathname)
  const failures = []
  for (const url of urls) {
    let res = null
    try {
      res = await fetch(url, {
        ...options,
        redirect: 'manual',
        signal: options.signal || AbortSignal.timeout(ENGINE_FETCH_TIMEOUT_MS),
        headers: {
          Accept: 'application/json',
          ...(options.body ? { 'Content-Type': 'application/json' } : {}),
          ...(options.headers || {}),
        },
      })
    } catch (error) {
      failures.push(`${url} -> ${fetchFailureText(error)}`)
      continue
    }
    const text = await res.text().catch(() => '')
    let json = null
    try { json = text ? JSON.parse(text) : null } catch {}
    if (!res.ok || json?.ok === false) {
      const error = new Error(json?.message || json?.error || `engine_http_${res.status}`)
      error.status = res.status
      error.payload = { ...(json && typeof json === 'object' ? json : {}), url }
      throw error
    }
    if (!json || typeof json !== 'object') {
      const error = new Error(`engine_non_json_response:${res.status}`)
      error.status = 502
      error.payload = {
        url,
        status: res.status,
        contentType: res.headers.get('content-type') || '',
        preview: cleanString(text, 200),
      }
      throw error
    }
    return json
  }
  const error = new Error(`engine_fetch_failed:${failures.join(' | ') || 'no_engine_api_urls'}`)
  error.payload = { attempts: failures, apiBaseCandidates: engineApiBaseCandidates(), pathname }
  throw error
}

function targetFromArgs(args = {}) {
  const workflowId = cleanString(args.workflowId || context.workflowId)
  const nodeId = cleanString(args.nodeId || args.agentNodeId || context.agentNodeId)
  if (!workflowId || !nodeId) throw new Error('workflowId_and_nodeId_required')
  return { workflowId, nodeId }
}

async function handleGetContext() {
  const n8nMcpVersion = await loadN8nMcpVersion()
  return {
    ok: true,
    ...context,
    apiBaseCandidates: engineApiBaseCandidates(),
    n8nMcpVersion,
    assistantActionGatewayConfigured: Boolean(context.assistantActionGatewayUrl && context.assistantActionGatewayToken),
    contract: {
      sourceOfTruth: 'Engine second-level dc.subworkflow.json',
      writeApi: `${context.apiBaseUrl}/subworkflow/patch`,
      schemaApi: engineApiUrl(`/api/n8n/schema?kind=nodes&version=${encodeURIComponent(context.schemaVersion)}`),
      directCoreWritesAllowed: false,
      directFileWritesAllowed: false,
    },
  }
}

async function handleAssistantActionCall(args = {}) {
  const phase = cleanString(args.phase || args.mode || 'preview', 40)
  if (!['preview', 'dry-run', 'execute'].includes(phase)) throw new Error('assistant_action_phase_invalid')

  const input = parseObject(args.input || {}, {})
  const actionId = cleanString(args.actionId || input.actionId || '', 240)
  const intent = cleanString(args.intent || input.intent || '', 2000)
  if (actionId) input.actionId = actionId
  if (intent) input.intent = intent

  const availableIds = assistantActionIds()
  if (actionId && availableIds.length && !availableIds.includes(actionId)) {
    throw new Error(`assistant_action_not_advertised:${actionId}`)
  }
  if (!actionId && !intent) {
    throw new Error('assistant_action_input_required')
  }

  const confirmationToken = cleanString(
    args.confirmationToken ||
    args.confirmation?.token ||
    input.confirmationToken ||
    '',
    2000,
  )
  if (confirmationToken) input.confirmationToken = confirmationToken

  return assistantActionFetch({
    phase,
    input,
    ...(confirmationToken ? { confirmationToken } : {}),
  })
}

async function handleGetSubworkflow(args) {
  const target = targetFromArgs(args)
  const q = new URLSearchParams(target)
  const result = await engineFetch(`/subworkflow?${q.toString()}`)
  return {
    ok: true,
    ...(result && typeof result === 'object' ? result : {}),
    graph: normalizeGraphResult(result?.graph, target.workflowId, target.nodeId),
  }
}

async function handleApplyPatch(args) {
  const target = targetFromArgs(args)
  const operations = Array.isArray(args.operations) ? args.operations : []
  if (!operations.length) throw new Error('operations_required')
  const result = await engineFetch('/subworkflow/patch', {
    method: 'POST',
    body: JSON.stringify({
      workflowId: target.workflowId,
      nodeId: target.nodeId,
      intent: cleanString(args.intent || 'NDC Agent MCP patch', 500),
      expectedUpdatedAt: cleanString(args.expectedUpdatedAt || ''),
      operations,
    }),
  })
  if (result?.graph) {
    return { ...result, graph: normalizeGraphResult(result.graph, target.workflowId, target.nodeId) }
  }
  const loaded = await handleGetSubworkflow(target)
  return {
    ok: true,
    ...(result && typeof result === 'object' ? result : {}),
    graph: loaded.graph,
    recoveredAfterEmptyPatchResponse: !result?.graph,
  }
}

async function handleSearchNodes(args) {
  const query = cleanString(args.query || args.q || '', 160).toLowerCase()
  const limit = Math.min(Math.max(Number(args.limit || 12) || 12, 1), 50)
  const catalog = await loadNodeCatalog(args.schemaVersion)
  const terms = query.split(/\s+/).filter(Boolean)
  const matches = catalog
    .map((node) => {
      const name = String(node?.name || '').toLowerCase()
      const displayName = String(node?.displayName || '').toLowerCase()
      const description = String(node?.description || '').toLowerCase()
      const groups = (Array.isArray(node?.group) ? node.group : []).map((item) => String(item || '').toLowerCase())
      const score = !terms.length ? 1 : terms.reduce((acc, term) => {
        if (name === term) return acc + 100
        if (displayName === term) return acc + 90
        if (name.includes(term)) return acc + 25
        if (displayName.includes(term)) return acc + 20
        if (groups.some((group) => group.includes(term))) return acc + 6
        if (description.includes(term)) return acc + 2
        return acc
      }, 0)
      return { node, score }
    })
    .filter((item) => item.score > 0)
    .sort((a, b) => b.score - a.score || String(a.node?.displayName || '').localeCompare(String(b.node?.displayName || '')))
    .slice(0, limit)
    .map(({ node }) => ({
      name: node?.name || '',
      publicType: node?.publicType || node?.name || '',
      fullType: catalogNodeRuntimeType(node),
      displayName: node?.displayName || node?.name || '',
      description: node?.description || '',
      group: node?.group || [],
      versions: versionSummary(node?.version),
    }))
  return { ok: true, query, count: matches.length, nodes: matches }
}

async function handleGetNodeDefinition(args) {
  const nodeType = normalizeTypeName(args.nodeType || args.type || args.name)
  if (!nodeType) throw new Error('nodeType_required')
  const catalog = await loadNodeCatalog(args.schemaVersion)
  const node = catalog.find((item) => (
    String(item?.name || '') === nodeType ||
    String(item?.publicType || '') === nodeType ||
    catalogNodeRuntimeType(item) === cleanString(args.nodeType || args.type || args.name)
  ))
  if (!node) throw new Error(`node_not_found:${nodeType}`)
  return { ok: true, node: compactNodeDefinition(node, Number(args.maxProperties || 80) || 80) }
}

async function handleValidateSubworkflow(args) {
  const target = (() => {
    try { return targetFromArgs(args) } catch { return { workflowId: context.workflowId, nodeId: context.agentNodeId } }
  })()
  const graph = args.graph && typeof args.graph === 'object'
    ? args.graph
    : normalizeGraphResult((await handleGetSubworkflow(args)).graph, target.workflowId, target.nodeId)
  const nodes = Array.isArray(graph?.nodes) ? graph.nodes : []
  const edges = Array.isArray(graph?.edges) ? graph.edges : []
  const nodeIds = new Set()
  const issues = []
  nodes.forEach((node, index) => {
    const id = cleanString(node?.id)
    if (!id) issues.push({ level: 'error', code: 'node_id_missing', index })
    if (id && nodeIds.has(id)) issues.push({ level: 'error', code: 'node_id_duplicate', nodeId: id })
    if (id) nodeIds.add(id)
    const typeName = cleanString(node?.data?.n8n?.type)
    if (!typeName) issues.push({ level: 'warn', code: 'node_type_missing', nodeId: id })
  })
  edges.forEach((edge, index) => {
    const source = cleanString(edge?.source)
    const target = cleanString(edge?.target)
    if (!source || !target) issues.push({ level: 'error', code: 'edge_endpoint_missing', index })
    if (source && !nodeIds.has(source)) issues.push({ level: 'error', code: 'edge_source_missing', edgeId: edge?.id, source })
    if (target && !nodeIds.has(target)) issues.push({ level: 'error', code: 'edge_target_missing', edgeId: edge?.id, target })
  })
  return {
    ok: !issues.some((issue) => issue.level === 'error'),
    nodesCount: nodes.length,
    edgesCount: edges.length,
    issues,
  }
}

const tools = [
  {
    name: 'ndc_get_context',
    description: 'Return selected NDC Agent Core target context and safety contract.',
    inputSchema: { type: 'object', properties: {}, additionalProperties: false },
  },
  {
    name: 'ndc_get_subworkflow',
    description: 'Read the selected Engine second-level workflow graph from dc.subworkflow.json.',
    inputSchema: {
      type: 'object',
      properties: {
        workflowId: { type: 'string' },
        nodeId: { type: 'string' },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'ndc_apply_subworkflow_patch',
    description: 'Apply safe graph patch operations to the selected Engine second-level workflow.',
    inputSchema: {
      type: 'object',
      properties: {
        workflowId: { type: 'string' },
        nodeId: { type: 'string' },
        intent: { type: 'string' },
        expectedUpdatedAt: { type: 'string' },
        operations: {
          type: 'array',
          items: { type: 'object', additionalProperties: true },
        },
      },
      required: ['operations'],
      additionalProperties: false,
    },
  },
  {
    name: 'ndc_search_nodes',
    description: 'Search NDC node definitions from the pinned v2.3.2 schema catalog.',
    inputSchema: {
      type: 'object',
      properties: {
        query: { type: 'string' },
        limit: { type: 'number' },
        schemaVersion: { type: 'string' },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'ndc_get_node_definition',
    description: 'Return a compact NDC node definition, including properties, credentials, inputs, and outputs.',
    inputSchema: {
      type: 'object',
      properties: {
        nodeType: { type: 'string' },
        schemaVersion: { type: 'string' },
        maxProperties: { type: 'number' },
      },
      required: ['nodeType'],
      additionalProperties: false,
    },
  },
  {
    name: 'ndc_validate_subworkflow',
    description: 'Validate a second-level workflow graph or the selected saved graph for basic structural issues.',
    inputSchema: {
      type: 'object',
      properties: {
        workflowId: { type: 'string' },
        nodeId: { type: 'string' },
        graph: { type: 'object', additionalProperties: true },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'assistant_action_call',
    description: 'Call the NODE.DC assistant action layer after interpreting user intent. Use execute for read actions after structured action selection; use preview before any privileged/write action and execute only after explicit confirmation.',
    inputSchema: {
      type: 'object',
      properties: {
        phase: { type: 'string', enum: ['preview', 'dry-run', 'execute'] },
        actionId: { type: 'string' },
        intent: { type: 'string' },
        input: { type: 'object', additionalProperties: true },
        confirmationToken: { type: 'string' },
        confirmation: { type: 'object', additionalProperties: true },
      },
      additionalProperties: false,
    },
  },
]

async function callTool(name, args) {
  if (name === 'ndc_get_context') return handleGetContext()
  if (name === 'ndc_get_subworkflow') return handleGetSubworkflow(args)
  if (name === 'ndc_apply_subworkflow_patch') return handleApplyPatch(args)
  if (name === 'ndc_search_nodes') return handleSearchNodes(args)
  if (name === 'ndc_get_node_definition') return handleGetNodeDefinition(args)
  if (name === 'ndc_validate_subworkflow') return handleValidateSubworkflow(args)
  if (name === 'assistant_action_call') return handleAssistantActionCall(args)
  throw new Error(`unknown_tool:${name}`)
}

async function handleRequest(message) {
  const id = message?.id
  const method = String(message?.method || '')
  if (!method) return
  if (method.startsWith('notifications/')) return

  try {
    if (method === 'initialize') {
      return writeMessage(jsonRpcResult(id, {
        protocolVersion: message?.params?.protocolVersion || '2025-06-18',
        capabilities: { tools: {} },
        serverInfo: { name: 'ndc-agent-mcp', version: '0.1.0' },
      }))
    }
    if (method === 'ping') return writeMessage(jsonRpcResult(id, {}))
    if (method === 'tools/list') return writeMessage(jsonRpcResult(id, { tools }))
    if (method === 'tools/call') {
      const name = cleanString(message?.params?.name)
      const args = message?.params?.arguments && typeof message.params.arguments === 'object'
        ? message.params.arguments
        : {}
      const result = await callTool(name, args)
      return writeMessage(jsonRpcResult(id, toolText(result)))
    }
    if (method === 'resources/list') return writeMessage(jsonRpcResult(id, { resources: [] }))
    if (method === 'prompts/list') return writeMessage(jsonRpcResult(id, { prompts: [] }))
    return writeMessage(jsonRpcError(id, -32601, `method_not_found:${method}`))
  } catch (error) {
    return writeMessage(jsonRpcError(id, -32000, error?.message || error, error?.payload))
  }
}

function parseJsonMessage(raw) {
  try {
    return JSON.parse(raw)
  } catch {
    writeMessage(jsonRpcError(null, -32700, 'parse_error'))
    return null
  }
}

function consumeJsonLineMessages() {
  for (;;) {
    const newlineIndex = inputBuffer.indexOf(0x0a)
    if (newlineIndex < 0) return
    const line = inputBuffer.subarray(0, newlineIndex).toString('utf8')
    inputBuffer = inputBuffer.subarray(newlineIndex + 1)
    const trimmed = line.trim()
    if (!trimmed) continue
    const message = parseJsonMessage(trimmed)
    if (message) void handleRequest(message)
  }
}

function consumeContentLengthMessages() {
  for (;;) {
    const headerEnd = inputBuffer.indexOf('\r\n\r\n')
    const altHeaderEnd = headerEnd < 0 ? inputBuffer.indexOf('\n\n') : -1
    const boundary = headerEnd >= 0 ? headerEnd : altHeaderEnd
    if (boundary < 0) return

    const separatorLength = headerEnd >= 0 ? 4 : 2
    const header = inputBuffer.subarray(0, boundary).toString('utf8')
    const match = /content-length:\s*(\d+)/i.exec(header)
    if (!match) {
      inputBuffer = inputBuffer.subarray(boundary + separatorLength)
      writeMessage(jsonRpcError(null, -32600, 'content_length_missing'))
      continue
    }

    const contentLength = Number(match[1])
    const bodyStart = boundary + separatorLength
    const bodyEnd = bodyStart + contentLength
    if (inputBuffer.length < bodyEnd) return

    const body = inputBuffer.subarray(bodyStart, bodyEnd).toString('utf8')
    inputBuffer = inputBuffer.subarray(bodyEnd)
    const message = parseJsonMessage(body)
    if (message) void handleRequest(message)
  }
}

function consumeInputBuffer() {
  if (!inputBuffer.length) return

  if (!transportMode) {
    const trimmedStart = inputBuffer.toString('utf8', 0, Math.min(inputBuffer.length, 32)).trimStart()
    if (trimmedStart.startsWith('Content-Length:') || trimmedStart.toLowerCase().startsWith('content-length:')) {
      transportMode = 'content-length'
    } else if (trimmedStart.startsWith('{')) {
      transportMode = 'json-lines'
    } else if (inputBuffer.length < 32) {
      return
    } else {
      transportMode = 'json-lines'
    }
  }

  if (transportMode === 'content-length') consumeContentLengthMessages()
  else consumeJsonLineMessages()
}

process.stdin.on('data', (chunk) => {
  inputBuffer = Buffer.concat([inputBuffer, Buffer.from(chunk)])
  consumeInputBuffer()
})

process.stdin.on('end', () => {
  if (transportMode === 'json-lines') {
    const line = inputBuffer.toString('utf8').trim()
    inputBuffer = Buffer.alloc(0)
    if (!line) return
    const message = parseJsonMessage(line)
    if (message) void handleRequest(message)
  }
})

if (process.env.NDC_AGENT_MCP_DEBUG_JSONL === '1') {
  process.stdin.setEncoding('utf8')
}

process.on('uncaughtException', (error) => {
  writeMessage(jsonRpcError(null, -32000, error?.message || error))
})

process.on('unhandledRejection', (reason) => {
  writeMessage(jsonRpcError(null, -32000, reason?.message || reason))
})

if (process.stdin.isTTY) {
  process.stderr.write('ndc-agent-mcp expects JSON-RPC messages on stdin.\n')
}
