// Simple JSON-RPC 2.0 LSP-like fake server over stdio

let nextId = 1
let readBuffer = Buffer.alloc(0)
let lastChange = null
let initializeParams = null
let diagnosticRequestCount = 0
let registeredCapability = false
const pendingClientRequests = new Map()
let pullConfig = {
  delayMs: 0,
  registerOn: undefined,
  registrations: [],
  documentDiagnostics: [],
  documentDiagnosticsByIdentifier: {},
  documentDelayMsByIdentifier: {},
  workspaceDiagnostics: [],
  workspaceDiagnosticsByIdentifier: {},
  workspaceDelayMsByIdentifier: {},
}

function encode(message) {
  const json = JSON.stringify(message)
  const header = `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n`
  return Buffer.concat([Buffer.from(header, "utf8"), Buffer.from(json, "utf8")])
}

function decodeFrames(buffer) {
  const results = []
  let idx
  while ((idx = buffer.indexOf("\r\n\r\n")) !== -1) {
    const header = buffer.slice(0, idx).toString("utf8")
    const match = /Content-Length:\s*(\d+)/i.exec(header)
    const length = match ? parseInt(match[1], 10) : 0
    const bodyStart = idx + 4
    const bodyEnd = bodyStart + length
    if (buffer.length < bodyEnd) break
    results.push(buffer.slice(bodyStart, bodyEnd).toString("utf8"))
    buffer = buffer.slice(bodyEnd)
  }
  return { messages: results, rest: buffer }
}

function send(message) {
  process.stdout.write(encode(message))
}

function sendRequest(method, params) {
  const id = nextId++
  send({ jsonrpc: "2.0", id, method, params })
  return id
}

function sendResponse(id, result) {
  send({ jsonrpc: "2.0", id, result })
}

function sendNotification(method, params) {
  send({ jsonrpc: "2.0", method, params })
}

function maybeRegister(method) {
  if (pullConfig.registerOn !== method || registeredCapability) return
  registeredCapability = true
  sendRequest("client/registerCapability", {
    registrations: pullConfig.registrations.map((registration, index) => ({
      id: registration.id ?? `pull-${index}`,
      method: registration.method ?? "textDocument/diagnostic",
      registerOptions: registration.registerOptions ?? registration,
    })),
  })
}

function delayed(id, result, delayMs = pullConfig.delayMs) {
  if (!delayMs) {
    sendResponse(id, result)
    return
  }
  setTimeout(() => sendResponse(id, result), delayMs)
}

function diagnosticsForIdentifier(identifier) {
  return pullConfig.documentDiagnosticsByIdentifier[identifier] ?? pullConfig.documentDiagnostics
}

function workspaceDiagnosticsForIdentifier(identifier) {
  return pullConfig.workspaceDiagnosticsByIdentifier[identifier] ?? pullConfig.workspaceDiagnostics
}

function documentDelayForIdentifier(identifier) {
  return pullConfig.documentDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
}

function workspaceDelayForIdentifier(identifier) {
  return pullConfig.workspaceDelayMsByIdentifier[identifier] ?? pullConfig.delayMs
}

function handle(raw) {
  let data
  try {
    data = JSON.parse(raw)
  } catch {
    return
  }

  if (typeof data.method === "undefined" && typeof data.id !== "undefined") {
    const pending = pendingClientRequests.get(data.id)
    if (!pending) return
    pendingClientRequests.delete(data.id)
    sendResponse(pending, data.result ?? null)
    return
  }

  if (data.method === "initialize") {
    initializeParams = data.params
    sendResponse(data.id, {
      capabilities: {
        textDocumentSync: {
          change: 2,
        },
      },
    })
    return
  }

  if (data.method === "test/get-initialize-params") {
    sendResponse(data.id, initializeParams)
    return
  }

  if (data.method === "test/request-configuration") {
    const id = sendRequest("workspace/configuration", data.params)
    pendingClientRequests.set(id, data.id)
    return
  }

  if (data.method === "initialized" || data.method === "workspace/didChangeConfiguration") {
    return
  }

  if (data.method === "textDocument/didOpen") {
    maybeRegister("didOpen")
    return
  }

  if (data.method === "textDocument/didChange") {
    lastChange = data.params
    maybeRegister("didChange")
    return
  }

  if (data.method === "test/trigger") {
    const method = data.params && data.params.method
    if (method === "client/registerCapability") {
      sendRequest(method, {
        registrations: [
          {
            id: "test-diagnostic-registration",
            method: "textDocument/diagnostic",
            registerOptions: { identifier: "syntax" },
          },
        ],
      })
      return
    }
    if (method === "client/unregisterCapability") {
      sendRequest(method, {
        unregisterations: [{ id: "test-diagnostic-registration", method: "textDocument/diagnostic" }],
      })
      return
    }
    if (method) sendRequest(method, {})
    return
  }

  if (data.method === "test/configure-pull-diagnostics") {
    pullConfig = {
      delayMs: data.params?.delayMs ?? 0,
      registerOn: data.params?.registerOn,
      registrations: data.params?.registrations ?? [],
      documentDiagnostics: data.params?.documentDiagnostics ?? [],
      documentDiagnosticsByIdentifier: data.params?.documentDiagnosticsByIdentifier ?? {},
      documentDelayMsByIdentifier: data.params?.documentDelayMsByIdentifier ?? {},
      workspaceDiagnostics: data.params?.workspaceDiagnostics ?? [],
      workspaceDiagnosticsByIdentifier: data.params?.workspaceDiagnosticsByIdentifier ?? {},
      workspaceDelayMsByIdentifier: data.params?.workspaceDelayMsByIdentifier ?? {},
    }
    registeredCapability = false
    sendResponse(data.id, null)
    return
  }

  if (data.method === "test/register-configured-pull-diagnostics") {
    maybeRegister(undefined)
    sendResponse(data.id, null)
    return
  }

  if (data.method === "test/publish-diagnostics") {
    sendNotification("textDocument/publishDiagnostics", data.params)
    return
  }

  if (data.method === "test/get-last-change") {
    sendResponse(data.id, lastChange)
    return
  }

  if (data.method === "test/get-diagnostic-request-count") {
    sendResponse(data.id, diagnosticRequestCount)
    return
  }

  if (data.method === "textDocument/diagnostic") {
    diagnosticRequestCount += 1
    delayed(
      data.id,
      {
        kind: "full",
        items: diagnosticsForIdentifier(data.params?.identifier ?? ""),
      },
      documentDelayForIdentifier(data.params?.identifier ?? ""),
    )
    return
  }

  if (data.method === "workspace/diagnostic") {
    diagnosticRequestCount += 1
    delayed(
      data.id,
      {
        items: workspaceDiagnosticsForIdentifier(data.params?.identifier ?? ""),
      },
      workspaceDelayForIdentifier(data.params?.identifier ?? ""),
    )
    return
  }

  if (typeof data.id !== "undefined") {
    sendResponse(data.id, null)
  }
}

process.stdin.on("data", (chunk) => {
  readBuffer = Buffer.concat([readBuffer, chunk])
  const { messages, rest } = decodeFrames(readBuffer)
  readBuffer = rest
  for (const message of messages) handle(message)
})
