#!/usr/bin/env bun
import {
  completionItems,
  definitionLocation,
  documentDiagnostics,
  documentSymbols,
  formatDocument,
  hover,
  offsetAt,
  positionAt,
  semanticTokens,
  semanticTokensLegend,
  wordAt,
  virtualTypeScriptDocument,
  workspaceCompletionItems,
  workspaceSymbolLocations,
  clearWorkspaceIndexCache,
  extractComponentRefactor,
  htmlToWrn,
  type TextDocument,
} from "./index.ts";

type JsonRpc = { jsonrpc?: string; id?: number | string; method?: string; params?: any };
const documents = new Map<string, TextDocument>();
const diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();
const MAX_OPEN_DOCUMENTS = 256;
const DIAGNOSTIC_DEBOUNCE_MS = 300;
let buffer = Buffer.alloc(0);
let workspaceRoot = process.cwd();

function rootFromUri(uri?: string): string {
  if (!uri?.startsWith("file://")) return workspaceRoot;
  return decodeURIComponent(uri.slice(7)).replace(/^\/([A-Za-z]:)/, "$1");
}

function send(value: unknown): void {
  const body = Buffer.from(JSON.stringify(value));
  process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
  process.stdout.write(body);
}
function result(id: JsonRpc["id"], value: unknown): void {
  send({ jsonrpc: "2.0", id, result: value });
}
function internalDiagnostic(document: TextDocument, error: unknown) {
  const message = error instanceof Error ? error.message : String(error);
  return {
    range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
    severity: 1,
    code: "WRN-LSP-INTERNAL",
    source: "wrnexus",
    message: `WRNexus language analysis failed safely: ${message}`,
  };
}

function safeDocumentDiagnostics(document: TextDocument, includeTypes = true) {
  try {
    return documentDiagnostics(document, { includeTypes });
  } catch (error) {
    process.stderr.write(
      `[wrnexus-lsp] diagnostics failed for ${document.uri}: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
    );
    return [internalDiagnostic(document, error)];
  }
}

function publish(document: TextDocument, includeTypes = true): void {
  send({
    jsonrpc: "2.0",
    method: "textDocument/publishDiagnostics",
    params: { uri: document.uri, diagnostics: safeDocumentDiagnostics(document, includeTypes) },
  });
}

function clearDiagnosticTimer(uri: string): void {
  const timer = diagnosticTimers.get(uri);
  if (timer) clearTimeout(timer);
  diagnosticTimers.delete(uri);
}

function schedulePublish(document: TextDocument): void {
  clearDiagnosticTimer(document.uri);
  const expectedVersion = document.version;
  diagnosticTimers.set(
    document.uri,
    setTimeout(() => {
      diagnosticTimers.delete(document.uri);
      const current = documents.get(document.uri);
      if (current && current.version === expectedVersion) publish(current, false);
    }, DIAGNOSTIC_DEBOUNCE_MS),
  );
}

function rememberDocument(document: TextDocument): void {
  documents.delete(document.uri);
  documents.set(document.uri, document);
  while (documents.size > MAX_OPEN_DOCUMENTS) {
    const oldest = documents.keys().next().value;
    if (typeof oldest !== "string") break;
    documents.delete(oldest);
    clearDiagnosticTimer(oldest);
  }
}

async function handle(message: JsonRpc): Promise<void> {
  const params = message.params ?? {};
  switch (message.method) {
    case "initialize":
      workspaceRoot = params.rootPath ?? rootFromUri(params.rootUri);
      result(message.id, {
        serverInfo: { name: "WRNexus Language Server", version: "0.8.3" },
        capabilities: {
          textDocumentSync: { openClose: true, change: 1, save: true },
          documentFormattingProvider: true,
          completionProvider: { triggerCharacters: ["<", "@", ":", "."] },
          hoverProvider: true,
          definitionProvider: true,
          referencesProvider: true,
          renameProvider: { prepareProvider: true },
          documentSymbolProvider: true,
          semanticTokensProvider: { legend: semanticTokensLegend, full: true },
          codeActionProvider: {
            codeActionKinds: [
              "quickfix",
              "refactor.extract",
              "refactor.rewrite",
              "source.organizeImports",
            ],
          },
          experimental: { wrnexusVirtualTypeScript: true },
        },
      });
      break;
    case "initialized":
      break;
    case "shutdown":
      for (const timer of diagnosticTimers.values()) clearTimeout(timer);
      diagnosticTimers.clear();
      result(message.id, null);
      break;
    case "exit":
      for (const timer of diagnosticTimers.values()) clearTimeout(timer);
      diagnosticTimers.clear();
      documents.clear();
      process.exit(0);
      break;
    case "textDocument/didOpen": {
      const item = params.textDocument;
      const document = { uri: item.uri, text: item.text, version: item.version };
      rememberDocument(document);
      publish(document);
      break;
    }
    case "textDocument/didChange": {
      const existing = documents.get(params.textDocument.uri);
      const text = params.contentChanges?.at(-1)?.text;
      if (existing && typeof text === "string") {
        existing.text = text;
        existing.version = params.textDocument.version;
        schedulePublish(existing);
      }
      break;
    }
    case "textDocument/didSave": {
      const document = documents.get(params.textDocument.uri);
      if (document) {
        clearDiagnosticTimer(document.uri);
        clearWorkspaceIndexCache(workspaceRoot);
        publish(document, true);
      }
      break;
    }
    case "textDocument/didClose":
      clearDiagnosticTimer(params.textDocument.uri);
      documents.delete(params.textDocument.uri);
      send({
        jsonrpc: "2.0",
        method: "textDocument/publishDiagnostics",
        params: { uri: params.textDocument.uri, diagnostics: [] },
      });
      break;
    case "wrnexus/serverStatus":
      result(message.id, {
        openDocuments: documents.size,
        pendingDiagnostics: diagnosticTimers.size,
        memory: process.memoryUsage(),
      });
      break;
    case "textDocument/formatting": {
      const document = documents.get(params.textDocument.uri);
      result(
        message.id,
        document
          ? formatDocument(document, params.options?.tabSize, params.options?.insertSpaces)
          : [],
      );
      break;
    }
    case "textDocument/completion":
      result(message.id, [...completionItems(), ...workspaceCompletionItems(workspaceRoot)]);
      break;
    case "textDocument/documentSymbol": {
      const document = documents.get(params.textDocument.uri);
      result(message.id, document ? documentSymbols(document) : []);
      break;
    }
    case "textDocument/semanticTokens/full": {
      const document = documents.get(params.textDocument.uri);
      result(message.id, document ? semanticTokens(document) : { data: [] });
      break;
    }
    case "textDocument/hover": {
      const document = documents.get(params.textDocument.uri);
      result(message.id, document ? hover(document, params.position) : null);
      break;
    }
    case "textDocument/definition": {
      const document = documents.get(params.textDocument.uri);
      result(message.id, document ? definitionLocation(document, params.position) : null);
      break;
    }
    case "textDocument/references": {
      const document = documents.get(params.textDocument.uri);
      const selected = document ? wordAt(document.text, params.position) : null;
      result(message.id, selected ? workspaceSymbolLocations(workspaceRoot, selected.word) : []);
      break;
    }
    case "textDocument/prepareRename": {
      const document = documents.get(params.textDocument.uri);
      result(message.id, document ? (wordAt(document.text, params.position)?.range ?? null) : null);
      break;
    }
    case "textDocument/rename": {
      const document = documents.get(params.textDocument.uri);
      const selected = document ? wordAt(document.text, params.position) : null;
      if (!selected || !/^[A-Za-z_$][\w$]*$/.test(params.newName)) {
        result(message.id, null);
        break;
      }
      const changes: Record<string, Array<{ range: any; newText: string }>> = {};
      for (const location of workspaceSymbolLocations(workspaceRoot, selected.word)) {
        (changes[location.uri] ??= []).push({ range: location.range, newText: params.newName });
      }
      result(message.id, { changes });
      break;
    }
    case "textDocument/codeAction": {
      const document = documents.get(params.textDocument.uri);
      if (!document) {
        result(message.id, []);
        break;
      }
      const actions: any[] = safeDocumentDiagnostics(document)
        .filter((item) => item.code === "WRNA11Y001")
        .map((item) => ({
          title: "Add empty alt attribute",
          kind: "quickfix",
          diagnostics: [item],
          edit: {
            changes: {
              [document.uri]: [
                { range: { start: item.range.end, end: item.range.end }, newText: ' alt=""' },
              ],
            },
          },
        }));
      const imports = [
        ...document.text.matchAll(
          /^import(?:\s+type)?[\s\S]*?(?:;\s*|\n(?=import|\s*(?:page|component|layout|global\s+store|page\s+store)\b))/gm,
        ),
      ].map((match) => ({
        start: match.index!,
        end: match.index! + match[0].length,
        text: match[0].trim(),
      }));
      if (imports.length > 1) {
        const ordered = [...imports].sort((a, b) => {
          const aType = /^import\s+type\b/.test(a.text) ? 0 : 1;
          const bType = /^import\s+type\b/.test(b.text) ? 0 : 1;
          return aType - bType || a.text.localeCompare(b.text);
        });
        const replacement = ordered.map((entry) => entry.text.replace(/;$/, "")).join("\n") + "\n";
        const start = imports[0]!.start;
        const end = imports.at(-1)!.end;
        if (document.text.slice(start, end) !== replacement) {
          actions.push({
            title: "Organize WRN imports",
            kind: "source.organizeImports",
            edit: {
              changes: {
                [document.uri]: [
                  {
                    range: {
                      start: positionAt(document.text, start),
                      end: positionAt(document.text, end),
                    },
                    newText: replacement,
                  },
                ],
              },
            },
          });
        }
      }
      for (const diagnostic of params.context?.diagnostics ?? []) {
        if (diagnostic.code !== "WRN-OUTPUT-LEGACY-EMIT") continue;
        const start = offsetAt(document.text, diagnostic.range.start);
        const end = offsetAt(document.text, diagnostic.range.end);
        const source = document.text.slice(start, end);
        const match = /\$emit\(\s*["']([A-Za-z_$][\w$]*)["']\s*,?/.exec(source);
        if (!match) continue;
        actions.push({
          title: `Convert $emit to output.${match[1]}`,
          kind: "quickfix",
          diagnostics: [diagnostic],
          isPreferred: true,
          edit: {
            changes: {
              [document.uri]: [
                {
                  range: diagnostic.range,
                  newText: source.replace(
                    /\$emit\(\s*["'][A-Za-z_$][\w$]*["']\s*,?\s*/,
                    `output.${match[1]}(`,
                  ),
                },
              ],
            },
          },
        });
      }
      const selected = document.text.slice(
        document.text
          .split(/\r?\n/)
          .slice(0, params.range.start.line)
          .reduce((n, line) => n + line.length + 1, 0) + params.range.start.character,
        document.text
          .split(/\r?\n/)
          .slice(0, params.range.end.line)
          .reduce((n, line) => n + line.length + 1, 0) + params.range.end.character,
      );
      if (selected.trim().startsWith("<")) {
        try {
          actions.push({
            title: "Extract selection to WRN component",
            kind: "refactor.extract",
            edit: extractComponentRefactor(document, params.range, "ExtractedComponent"),
          });
          actions.push({
            title: "Convert selected HTML to WRN page",
            kind: "refactor.rewrite",
            edit: {
              changes: {
                [document.uri]: [
                  { range: params.range, newText: htmlToWrn(selected, "ImportedPage") },
                ],
              },
            },
          });
        } catch {
          /* selection is not safely convertible */
        }
      }
      result(message.id, actions);
      break;
    }
    case "wrnexus/virtualDocument": {
      const document = documents.get(params.textDocument?.uri ?? params.uri);
      result(message.id, document ? virtualTypeScriptDocument(document) : null);
      break;
    }
    default:
      if (message.id !== undefined) result(message.id, null);
  }
}

function consume(): void {
  while (true) {
    const end = buffer.indexOf("\r\n\r\n");
    if (end < 0) return;
    const header = buffer.subarray(0, end).toString();
    const length = Number(/Content-Length:\s*(\d+)/i.exec(header)?.[1]);
    if (!Number.isFinite(length)) {
      buffer = Buffer.alloc(0);
      return;
    }
    const bodyStart = end + 4;
    if (buffer.length < bodyStart + length) return;
    const body = buffer.subarray(bodyStart, bodyStart + length).toString();
    buffer = buffer.subarray(bodyStart + length);
    let message: JsonRpc;
    try {
      message = JSON.parse(body) as JsonRpc;
    } catch (error) {
      process.stderr.write(
        `[wrnexus-lsp] invalid JSON-RPC payload: ${error instanceof Error ? error.message : String(error)}\n`,
      );
      continue;
    }
    void handle(message).catch((error) => {
      const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
      process.stderr.write(`[wrnexus-lsp] request failed: ${detail}\n`);
      if (message.id !== undefined) {
        send({
          jsonrpc: "2.0",
          id: message.id,
          error: { code: -32603, message: "WRNexus language server request failed", data: detail },
        });
      }
    });
  }
}
process.stdin.on("data", (chunk) => {
  try {
    buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
    consume();
  } catch (error) {
    process.stderr.write(
      `[wrnexus-lsp] input processing failed: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`,
    );
  }
});
process.stdin.resume();
