/**
 * n8n MCP Proxy Server — Production-grade
 *
 * A lightweight, zero-dependency MCP-compatible proxy that bridges
 * the Chrome Extension ↔ n8n REST API via SSE + JSON-RPC.
 *
 * Inspired by czlonkowski/n8n-mcp architecture.
 *
 * Run: node server/mcp-proxy.mjs
 * Default: http://localhost:3006
 */

import http from "node:http"
import { SYSTEM_PROMPT } from "./system-prompt.mjs"

const PORT = process.env.MCP_PORT || 3006
const N8N_API_URL = process.env.N8N_API_URL || ""
const N8N_API_KEY = process.env.N8N_API_KEY || ""

// ─── State ──────────────────────────────────────────────────────────

const sessions = new Map() // sessionId → SSE response
let sessionCounter = 0

// ─── Tools Registry ─────────────────────────────────────────────────

const TOOLS = [
  // ── Workflow Management ──
  {
    name: "n8n_list_workflows",
    description: "List workflows (minimal metadata). Returns id/name/active/dates/tags.",
    inputSchema: {
      type: "object",
      properties: {
        limit: { type: "number", description: "Max results (1-100, default 100)" },
        active: { type: "boolean", description: "Filter by active status" },
        tags: { type: "array", items: { type: "string" }, description: "Filter by tags" },
      },
    },
  },
  {
    name: "n8n_get_workflow",
    description:
      "Get workflow by ID. mode: full=complete JSON, structure=nodes+connections, minimal=id/name/active.",
    inputSchema: {
      type: "object",
      properties: {
        id: { type: "string", description: "Workflow ID" },
        mode: {
          type: "string",
          enum: ["full", "structure", "minimal"],
          default: "full",
          description: "Detail level",
        },
      },
      required: ["id"],
    },
  },
  {
    name: "n8n_create_workflow",
    description:
      "Create a new workflow. Requires: name, nodes[], connections{}. Created inactive. Returns workflow with ID.",
    inputSchema: {
      type: "object",
      properties: {
        name: { type: "string", description: "Workflow name" },
        nodes: {
          type: "array",
          description:
            "Array of nodes. Each: {id, name, type, typeVersion, position:[x,y], parameters:{}}",
          items: { type: "object" },
        },
        connections: { type: "object", description: "Connections object (source→target)" },
        settings: { type: "object", description: "Optional workflow settings" },
      },
      required: ["name", "nodes", "connections"],
    },
  },
  {
    name: "n8n_update_workflow",
    description:
      "Full workflow update. Send complete nodes[] and connections{} to replace the current workflow.",
    inputSchema: {
      type: "object",
      properties: {
        id: { type: "string", description: "Workflow ID" },
        name: { type: "string", description: "New name (optional)" },
        nodes: { type: "array", items: { type: "object" }, description: "Complete nodes array" },
        connections: { type: "object", description: "Complete connections" },
        settings: { type: "object", description: "Workflow settings" },
      },
      required: ["id"],
    },
  },
  {
    name: "n8n_delete_workflow",
    description: "Permanently delete a workflow. Cannot be undone.",
    inputSchema: {
      type: "object",
      properties: { id: { type: "string", description: "Workflow ID" } },
      required: ["id"],
    },
  },
  {
    name: "n8n_activate_workflow",
    description: "Activate a workflow so its triggers start listening.",
    inputSchema: {
      type: "object",
      properties: { id: { type: "string", description: "Workflow ID" } },
      required: ["id"],
    },
  },
  {
    name: "n8n_deactivate_workflow",
    description: "Deactivate a workflow (stops all triggers).",
    inputSchema: {
      type: "object",
      properties: { id: { type: "string", description: "Workflow ID" } },
      required: ["id"],
    },
  },

  // ── Execution Management ──
  {
    name: "n8n_execute_workflow",
    description: "Trigger a workflow execution with optional input data.",
    inputSchema: {
      type: "object",
      properties: {
        id: { type: "string", description: "Workflow ID" },
        data: { type: "object", description: "Input data for the workflow" },
      },
      required: ["id"],
    },
  },
  {
    name: "n8n_list_executions",
    description: "List recent executions. Filter by workflow_id or status.",
    inputSchema: {
      type: "object",
      properties: {
        workflow_id: { type: "string", description: "Filter by workflow ID" },
        status: { type: "string", enum: ["success", "error", "waiting"], description: "Filter" },
        limit: { type: "number", description: "Max results (default 20)" },
      },
    },
  },
  {
    name: "n8n_get_execution",
    description: "Get execution details by ID. Includes node-level I/O data.",
    inputSchema: {
      type: "object",
      properties: { id: { type: "string", description: "Execution ID" } },
      required: ["id"],
    },
  },

  // ── Credential Management ──
  {
    name: "n8n_list_credentials",
    description: "List all credentials (names and types only — values are never exposed).",
    inputSchema: { type: "object", properties: {} },
  },

  // ── System ──
  {
    name: "n8n_health_check",
    description: "Check n8n instance health and API connectivity.",
    inputSchema: { type: "object", properties: {} },
  },
]

// ─── n8n API Helper ─────────────────────────────────────────────────

async function n8nFetch(apiUrl, apiKey, path, method = "GET", body = null) {
  const url = `${apiUrl}/api/v1${path}`
  const headers = {
    "X-N8N-API-KEY": apiKey,
    "Content-Type": "application/json",
  }

  const opts = { method, headers }
  if (body) opts.body = JSON.stringify(body)

  console.log(`  [n8n] ${method} ${path}`)
  const res = await fetch(url, opts)

  if (!res.ok) {
    const text = await res.text()
    throw new Error(`n8n API ${res.status}: ${text}`)
  }

  return res.json()
}

// ─── Tool Execution ─────────────────────────────────────────────────

async function executeTool(toolName, args, apiUrl, apiKey) {
  switch (toolName) {
    case "n8n_list_workflows": {
      let path = "/workflows"
      const params = []
      if (args.limit) params.push(`limit=${args.limit}`)
      if (args.active !== undefined) params.push(`active=${args.active}`)
      if (params.length) path += "?" + params.join("&")
      return await n8nFetch(apiUrl, apiKey, path)
    }

    case "n8n_get_workflow": {
      const wf = await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}`)
      if (args.mode === "minimal") return { id: wf.id, name: wf.name, active: wf.active, tags: wf.tags }
      if (args.mode === "structure")
        return { id: wf.id, name: wf.name, nodes: wf.nodes, connections: wf.connections }
      return wf
    }

    case "n8n_create_workflow":
      return await n8nFetch(apiUrl, apiKey, "/workflows", "POST", {
        name: args.name,
        nodes: args.nodes,
        connections: args.connections,
        settings: args.settings || { executionOrder: "v1" },
      })

    case "n8n_update_workflow": {
      const payload = {}
      if (args.name) payload.name = args.name
      if (args.nodes) payload.nodes = args.nodes
      if (args.connections) payload.connections = args.connections
      if (args.settings) payload.settings = args.settings
      return await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}`, "PUT", payload)
    }

    case "n8n_delete_workflow":
      return await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}`, "DELETE")

    case "n8n_activate_workflow":
      return await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}/activate`, "POST")

    case "n8n_deactivate_workflow":
      return await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}/deactivate`, "POST")

    case "n8n_execute_workflow":
      return await n8nFetch(apiUrl, apiKey, `/workflows/${args.id}/execute`, "POST", {
        data: args.data || {},
      })

    case "n8n_list_executions": {
      let path = "/executions?limit=" + (args.limit || 20)
      if (args.workflow_id) path += `&workflowId=${args.workflow_id}`
      if (args.status) path += `&status=${args.status}`
      return await n8nFetch(apiUrl, apiKey, path)
    }

    case "n8n_get_execution":
      return await n8nFetch(apiUrl, apiKey, `/executions/${args.id}`)

    case "n8n_list_credentials":
      return await n8nFetch(apiUrl, apiKey, "/credentials")

    case "n8n_health_check":
      try {
        await n8nFetch(apiUrl, apiKey, "/workflows?limit=1")
        return { status: "healthy", apiUrl, timestamp: new Date().toISOString() }
      } catch (err) {
        return { status: "unhealthy", error: err.message, apiUrl }
      }

    default:
      throw new Error(`Unknown tool: ${toolName}`)
  }
}

// ─── JSON-RPC Handler ───────────────────────────────────────────────

async function handleJsonRpc(body, apiUrl, apiKey) {
  const { id, method, params } = body

  switch (method) {
    case "initialize":
      return {
        jsonrpc: "2.0",
        id,
        result: {
          protocolVersion: "2024-11-05",
          serverInfo: { name: "n8n-agent-mcp-proxy", version: "1.0.0" },
          capabilities: { tools: {} },
        },
      }

    case "tools/list":
      return { jsonrpc: "2.0", id, result: { tools: TOOLS } }

    case "tools/call": {
      const { name, arguments: args } = params
      try {
        const result = await executeTool(name, args || {}, apiUrl, apiKey)
        return {
          jsonrpc: "2.0",
          id,
          result: { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] },
        }
      } catch (err) {
        return {
          jsonrpc: "2.0",
          id,
          result: { content: [{ type: "text", text: `Error: ${err.message}` }], isError: true },
        }
      }
    }

    // ── Custom: get system prompt ──
    case "system_prompt":
      return {
        jsonrpc: "2.0",
        id,
        result: { content: [{ type: "text", text: SYSTEM_PROMPT }] },
      }

    default:
      return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown method: ${method}` } }
  }
}

// ─── HTTP Server ────────────────────────────────────────────────────

const server = http.createServer(async (req, res) => {
  // CORS
  res.setHeader("Access-Control-Allow-Origin", "*")
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, x-n8n-url, x-n8n-key, Authorization")

  if (req.method === "OPTIONS") {
    res.writeHead(204)
    res.end()
    return
  }

  const url = new URL(req.url, `http://localhost:${PORT}`)

  // ── Health ──
  if (url.pathname === "/health" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ status: "ok", sessions: sessions.size, tools: TOOLS.length }))
    return
  }

  // ── System Prompt (for extension popup to read) ──
  if (url.pathname === "/system-prompt" && req.method === "GET") {
    res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" })
    res.end(SYSTEM_PROMPT)
    return
  }

  // ── SSE ──
  if (url.pathname === "/sse" && req.method === "GET") {
    const sessionId = `session-${++sessionCounter}`
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    })

    sessions.set(sessionId, res)
    console.log(`  [SSE] Session ${sessionId} connected (total: ${sessions.size})`)

    res.write(`event: endpoint\ndata: /message?sessionId=${sessionId}\n\n`)
    res.write(
      `event: message\ndata: ${JSON.stringify({ jsonrpc: "2.0", method: "notifications/tools/list_changed" })}\n\n`
    )

    const heartbeat = setInterval(() => {
      try {
        res.write(": heartbeat\n\n")
      } catch {
        clearInterval(heartbeat)
      }
    }, 30000)

    req.on("close", () => {
      sessions.delete(sessionId)
      clearInterval(heartbeat)
      console.log(`  [SSE] Session ${sessionId} disconnected (total: ${sessions.size})`)
    })
    return
  }

  // ── JSON-RPC ──
  if (url.pathname === "/message" && req.method === "POST") {
    const apiUrl = req.headers["x-n8n-url"] || N8N_API_URL
    const apiKey = req.headers["x-n8n-key"] || N8N_API_KEY

    if (!apiUrl) {
      res.writeHead(400, { "Content-Type": "application/json" })
      res.end(JSON.stringify({ error: "Missing n8n API URL. Set x-n8n-url header or N8N_API_URL env." }))
      return
    }

    let body = ""
    for await (const chunk of req) body += chunk

    try {
      const parsed = JSON.parse(body)
      const response = await handleJsonRpc(parsed, apiUrl, apiKey)

      const sessionId = url.searchParams.get("sessionId")
      if (sessionId && sessions.has(sessionId)) {
        sessions.get(sessionId).write(`event: message\ndata: ${JSON.stringify(response)}\n\n`)
      }

      res.writeHead(200, { "Content-Type": "application/json" })
      res.end(JSON.stringify(response))
    } catch (err) {
      res.writeHead(500, { "Content-Type": "application/json" })
      res.end(JSON.stringify({ error: err.message }))
    }
    return
  }

  res.writeHead(404, { "Content-Type": "application/json" })
  res.end(JSON.stringify({ error: "Not found" }))
})

server.listen(PORT, () => {
  console.log(``)
  console.log(`  ⚡ n8n MCP Proxy Server`)
  console.log(`  ├── HTTP:    http://localhost:${PORT}`)
  console.log(`  ├── SSE:     http://localhost:${PORT}/sse`)
  console.log(`  ├── RPC:     http://localhost:${PORT}/message`)
  console.log(`  ├── Tools:   ${TOOLS.length} registered`)
  console.log(`  ├── n8n:     ${N8N_API_URL || "⚠️ Not configured (use headers)"}`)
  console.log(`  └── Prompt:  http://localhost:${PORT}/system-prompt`)
  console.log(``)
})
