import type { Diagnostic as VSCodeDiagnostic } from "vscode-languageserver-types"
import { BusEvent } from "@/bus/bus-event"
import { Schema } from "effect"
import { Filesystem } from "@/util/filesystem"
import { fileURLToPath } from "url"
import { NamedError } from "@saeeol/core/util/error"
import z from "zod"

export type Diagnostic = VSCodeDiagnostic

export const DIAGNOSTICS_DEBOUNCE_MS = 150
export const DIAGNOSTICS_DOCUMENT_WAIT_TIMEOUT_MS = 5_000
export const DIAGNOSTICS_FULL_WAIT_TIMEOUT_MS = 10_000
export const DIAGNOSTICS_REQUEST_TIMEOUT_MS = 3_000
export const INITIALIZE_TIMEOUT_MS = 45_000
export const FILE_CHANGE_CREATED = 1
export const FILE_CHANGE_CHANGED = 2
export const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2

export const InitializeError = NamedError.create(
  "LSPInitializeError",
  z.object({ serverID: z.string() }),
)

export const Event = {
  Diagnostics: BusEvent.define(
    "lsp.client.diagnostics",
    Schema.Struct({ serverID: Schema.String, path: Schema.String }),
  ),
}

export type DocumentDiagnosticReport = {
  items?: Diagnostic[]
  relatedDocuments?: Record<string, DocumentDiagnosticReport>
}

export type WorkspaceDiagnosticReport = {
  items?: { uri?: string; items?: Diagnostic[] }[]
}

export type DiagnosticRequestResult = {
  handled: boolean
  matched: boolean
  byFile: Map<string, Diagnostic[]>
}

export type CapabilityRegistration = {
  id: string
  method: string
  registerOptions?: { identifier?: string; workspaceDiagnostics?: boolean }
}

export type ServerCapabilities = {
  textDocumentSync?: number | { change?: number }
  diagnosticProvider?: unknown
  [key: string]: unknown
}

export function getFilePath(uri: string) {
  if (!uri.startsWith("file://")) return
  return Filesystem.normalizePath(fileURLToPath(uri))
}

export function getSyncKind(capabilities?: ServerCapabilities) {
  if (!capabilities) return
  const sync = capabilities.textDocumentSync
  if (typeof sync === "number") return sync
  return sync?.change
}

export function endPosition(text: string) {
  const lines = text.split(/\r\n|\r|\n/)
  return { line: lines.length - 1, character: lines.at(-1)?.length ?? 0 }
}

export function dedupeDiagnostics(items: Diagnostic[]) {
  const seen = new Set<string>()
  return items.filter((item) => {
    const key = JSON.stringify({ code: item.code, severity: item.severity, message: item.message, source: item.source, range: item.range })
    if (seen.has(key)) return false
    seen.add(key)
    return true
  })
}

export function configurationValue(settings: unknown, section?: string) {
  if (!section) return settings ?? null
  const result = section.split(".").reduce<unknown>((acc, key) => {
    if (!acc || typeof acc !== "object" || !(key in acc)) return undefined
    return (acc as Record<string, unknown>)[key]
  }, settings)
  return result ?? null
}

export function shouldSeedDiagnosticsOnFirstPush(serverID: string) {
  return serverID === "typescript"
}
