import path from "path"
import { pathToFileURL, fileURLToPath } from "url"
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node"
import * as Log from "@saeeol/core/util/log"
import { Process } from "@/util/process"
import { LANGUAGE_EXTENSIONS } from "./language"
import { Filesystem } from "@/util/filesystem"
import { withTimeout } from "../util/timeout"
import type * as LSPServer from "./server"
import {
  type Diagnostic,
  type ServerCapabilities,
  type CapabilityRegistration,
  INITIALIZE_TIMEOUT_MS,
  FILE_CHANGE_CREATED,
  FILE_CHANGE_CHANGED,
  TEXT_DOCUMENT_SYNC_INCREMENTAL,
  InitializeError,
  Event,
  getSyncKind,
  endPosition,
  configurationValue,
  shouldSeedDiagnosticsOnFirstPush,
} from "./client-types"
import { createDiagnosticsManager } from "./client-diagnostics"

const log = Log.create({ service: "lsp.client" })

export type Info = NonNullable<Awaited<ReturnType<typeof create>>>
export type { Diagnostic }
export { InitializeError, Event as LSPEvent }

export async function create(input: { serverID: string; server: LSPServer.Handle; root: string; directory: string }) {
  const logger = log.clone().tag("serverID", input.serverID)
  logger.info("starting client")

  const connection = createMessageConnection(
    new StreamMessageReader(input.server.process.stdout as any),
    new StreamMessageWriter(input.server.process.stdin as any),
  )
  input.server.process.stderr?.on("data", (data: Buffer) => {
    const text = data.toString().trim()
    if (text) logger.debug("server stderr", { text: text.slice(0, 1000) })
  })

  const pushDiagnostics = new Map<string, Diagnostic[]>()
  const pullDiagnostics = new Map<string, Diagnostic[]>()
  const published = new Map<string, { at: number; version?: number }>()
  const diagnosticRegistrations = new Map<string, CapabilityRegistration>()
  const registrationListeners = new Set<() => void>()

  const diag = createDiagnosticsManager({
    connection,
    serverID: input.serverID,
    hasStaticPullDiagnostics: false, // set properly after init
    pushDiagnostics,
    pullDiagnostics,
    published,
    diagnosticRegistrations,
    registrationListeners,
  })

  connection.onNotification("textDocument/publishDiagnostics", (params) => {
    const filePath = (() => {
      if (!params.uri.startsWith("file://")) return
      return Filesystem.normalizePath(fileURLToPath(params.uri))
    })()
    if (!filePath) return
    logger.info("textDocument/publishDiagnostics", { path: filePath, count: params.diagnostics.length, version: params.version })
    published.set(filePath, { at: Date.now(), version: typeof params.version === "number" ? params.version : undefined })
    if (shouldSeedDiagnosticsOnFirstPush(input.serverID) && !pushDiagnostics.has(filePath)) {
      pushDiagnostics.set(filePath, params.diagnostics)
      return
    }
    diag.updatePushDiagnostics(filePath, params.diagnostics)
  })
  connection.onRequest("window/workDoneProgress/create", () => null)
  connection.onRequest("workspace/configuration", async (params) => {
    const items = (params as { items?: { section?: string }[] }).items ?? []
    return items.map((item) => configurationValue(input.server.initialization, item.section))
  })
  connection.onRequest("client/registerCapability", async (params) => {
    const regs = (params as { registrations?: CapabilityRegistration[] }).registrations ?? []
    let changed = false
    for (const reg of regs) {
      if (reg.method !== "textDocument/diagnostic") continue
      diagnosticRegistrations.set(reg.id, reg); changed = true
    }
    if (changed) diag.emitRegistrationChange()
  })
  connection.onRequest("client/unregisterCapability", async (params) => {
    const regs = (params as { unregisterations?: { id: string; method: string }[] }).unregisterations ?? []
    let changed = false
    for (const reg of regs) {
      if (reg.method !== "textDocument/diagnostic") continue
      diagnosticRegistrations.delete(reg.id); changed = true
    }
    if (changed) diag.emitRegistrationChange()
  })
  connection.onRequest("workspace/workspaceFolders", async () => [{ name: "workspace", uri: pathToFileURL(input.root).href }])
  connection.onRequest("workspace/diagnostic/refresh", async () => null)
  connection.listen()

  logger.info("sending initialize")
  const initialized = await withTimeout(
    connection.sendRequest<{ capabilities?: ServerCapabilities }>("initialize", {
      rootUri: pathToFileURL(input.root).href,
      processId: input.server.process.pid,
      workspaceFolders: [{ name: "workspace", uri: pathToFileURL(input.root).href }],
      initializationOptions: { ...input.server.initialization },
      capabilities: {
        window: { workDoneProgress: true },
        workspace: { configuration: true, didChangeWatchedFiles: { dynamicRegistration: true }, diagnostics: { refreshSupport: false } },
        textDocument: {
          synchronization: { didOpen: true, didChange: true },
          diagnostic: { dynamicRegistration: true, relatedDocumentSupport: true },
          publishDiagnostics: { versionSupport: false },
        },
      },
    }),
    INITIALIZE_TIMEOUT_MS,
  ).catch((err) => {
    logger.error("initialize error", { error: err })
    throw new InitializeError({ serverID: input.serverID }, { cause: err })
  })

  const syncKind = getSyncKind(initialized.capabilities)
  await connection.sendNotification("initialized", {})
  if (input.server.initialization) {
    await connection.sendNotification("workspace/didChangeConfiguration", { settings: input.server.initialization })
  }

  const files: Record<string, { version: number; text: string }> = {}

  const result = {
    root: input.root,
    get serverID() { return input.serverID },
    get connection() { return connection },
    notify: {
      async open(request: { path: string }) {
        request.path = Filesystem.normalizePath(path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path))
        const text = await Filesystem.readText(request.path)
        const extension = path.extname(request.path)
        const languageId = LANGUAGE_EXTENSIONS[extension] ?? "plaintext"
        const document = files[request.path]
        if (document !== undefined) {
          logger.info("workspace/didChangeWatchedFiles", request)
          await connection.sendNotification("workspace/didChangeWatchedFiles", {
            changes: [{ uri: pathToFileURL(request.path).href, type: FILE_CHANGE_CHANGED }],
          })
          const next = document.version + 1
          files[request.path] = { version: next, text }
          logger.info("textDocument/didChange", { path: request.path, version: next })
          await connection.sendNotification("textDocument/didChange", {
            textDocument: { uri: pathToFileURL(request.path).href, version: next },
            contentChanges: syncKind === TEXT_DOCUMENT_SYNC_INCREMENTAL
              ? [{ range: { start: { line: 0, character: 0 }, end: endPosition(document.text) }, text }]
              : [{ text }],
          })
          return next
        }
        logger.info("workspace/didChangeWatchedFiles", request)
        await connection.sendNotification("workspace/didChangeWatchedFiles", {
          changes: [{ uri: pathToFileURL(request.path).href, type: FILE_CHANGE_CREATED }],
        })
        logger.info("textDocument/didOpen", request)
        pushDiagnostics.delete(request.path)
        pullDiagnostics.delete(request.path)
        await connection.sendNotification("textDocument/didOpen", {
          textDocument: { uri: pathToFileURL(request.path).href, languageId, version: 0, text },
        })
        files[request.path] = { version: 0, text }
        return 0
      },
    },
    get diagnostics() {
      const result = new Map<string, Diagnostic[]>()
      for (const key of new Set([...pushDiagnostics.keys(), ...pullDiagnostics.keys()])) {
        result.set(key, diag.mergedDiagnostics(key))
      }
      return result
    },
    async waitForDiagnostics(request: { path: string; version: number; mode?: "document" | "full"; after?: number }) {
      const normalizedPath = Filesystem.normalizePath(path.isAbsolute(request.path) ? request.path : path.resolve(input.directory, request.path))
      logger.info("waiting for diagnostics", { path: normalizedPath, mode: request.mode ?? "full", version: request.version })
      if (request.mode === "document") {
        await diag.waitForDocumentDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
        return
      }
      await diag.waitForFullDiagnostics({ path: normalizedPath, version: request.version, after: request.after })
    },
    async shutdown() {
      logger.info("shutting down")
      connection.end()
      connection.dispose()
      await Process.stop(input.server.process)
      logger.info("shutdown")
    },
  }

  logger.info("initialized")
  return result
}

export * as LSPClient from "./client"
