import { diagnose, formatWrn, type WrnDiagnostic } from "@wrnexus/syntax";
import { checkWrnSource, virtualTypeScriptModule } from "@wrnexus/typecheck";

export interface Position {
  line: number;
  character: number;
}
export interface Range {
  start: Position;
  end: Position;
}
export interface TextDocument {
  uri: string;
  text: string;
  version?: number;
}

export const WRN_COMPLETIONS = [
  "page",
  "component",
  "layout",
  "props",
  "outputs",
  "state",
  "computed",
  "effect",
  "watch",
  "lifecycle",
  "load",
  "action",
  "api",
  "realtime",
  "view",
  "style",
  "runtime",
  "hydrate",
] as const;

export function offsetAt(text: string, position: Position): number {
  const lines = text.split(/\r?\n/);
  let offset = 0;
  for (let line = 0; line < Math.min(position.line, lines.length); line++)
    offset += (lines[line]?.length ?? 0) + 1;
  return Math.min(text.length, offset + Math.max(0, position.character));
}

export function positionAt(text: string, requestedOffset: number): Position {
  const offset = Math.max(0, Math.min(text.length, requestedOffset));
  const prefix = text.slice(0, offset);
  const lines = prefix.split(/\r?\n/);
  return { line: lines.length - 1, character: lines.at(-1)?.length ?? 0 };
}

export function wordAt(text: string, position: Position): { word: string; range: Range } | null {
  const offset = offsetAt(text, position);
  const left = text.slice(0, offset).match(/[A-Za-z_$][\w$]*$/)?.[0] ?? "";
  const right = text.slice(offset).match(/^[\w$]*/)?.[0] ?? "";
  const word = left + right;
  if (!word) return null;
  const start = offset - left.length;
  return {
    word,
    range: { start: positionAt(text, start), end: positionAt(text, start + word.length) },
  };
}

function diagnosticRange(diagnostic: WrnDiagnostic): Range {
  const start = diagnostic.position ?? { line: 1, column: 1, offset: 0 };
  return {
    start: { line: Math.max(0, start.line - 1), character: Math.max(0, start.column - 1) },
    end: {
      line: Math.max(0, start.line - 1),
      character: Math.max(1, start.column - 1 + Math.max(1, diagnostic.received?.length ?? 1)),
    },
  };
}

export function documentDiagnostics(
  document: TextDocument,
  options: { includeTypes?: boolean } = {},
) {
  if (Buffer.byteLength(document.text, "utf8") > 1_048_576) {
    return [
      {
        range: { start: { line: 0, character: 0 }, end: { line: 0, character: 1 } },
        severity: 2,
        code: "WRN-LSP-FILE-SIZE",
        source: "wrnexus",
        message: "Type analysis is disabled because this WRN document exceeds 1 MiB.",
      },
    ];
  }
  const syntax = diagnose(document.text, { file: document.uri, accessibility: true }).map(
    (diagnostic) => ({
      range: diagnosticRange(diagnostic),
      severity: diagnostic.severity === "error" ? 1 : diagnostic.severity === "warning" ? 2 : 3,
      code: diagnostic.code,
      source: "wrnexus",
      message: diagnostic.message,
    }),
  );
  if (syntax.some((diagnostic) => diagnostic.severity === 1) || options.includeTypes === false)
    return syntax;
  const types = checkWrnSource(document.text, { filePath: documentPath(document.uri) }).map(
    (diagnostic) => ({
      range: {
        start: {
          line: Math.max(0, diagnostic.line - 1),
          character: Math.max(0, diagnostic.column - 1),
        },
        end: {
          line: Math.max(0, diagnostic.line - 1),
          character: Math.max(1, diagnostic.column - 1 + Math.max(1, diagnostic.length)),
        },
      },
      severity: diagnostic.category === "error" ? 1 : diagnostic.category === "warning" ? 2 : 3,
      code: diagnostic.code,
      source: "wrnexus-types",
      message: diagnostic.message,
    }),
  );
  const seen = new Set(
    syntax.map((item) => `${item.code}:${item.range.start.line}:${item.range.start.character}`),
  );
  return [
    ...syntax,
    ...types.filter(
      (item) => !seen.has(`${item.code}:${item.range.start.line}:${item.range.start.character}`),
    ),
  ];
}

function documentPath(uri: string): string {
  if (!uri.startsWith("file://")) return uri;
  const value = decodeURIComponent(uri.slice("file://".length));
  return /^\/[A-Za-z]:\//.test(value) ? value.slice(1) : value;
}

/** TypeScript representation consumed by editor TypeScript plugins and safe refactoring tools. */
export function virtualTypeScriptDocument(document: TextDocument): {
  uri: string;
  languageId: "typescript";
  text: string;
  mappings: Array<{
    virtualStartLine: number;
    virtualEndLine: number;
    sourceStartLine: number;
    sourceStartColumn: number;
  }>;
} {
  const virtual = virtualTypeScriptModule(document.text, documentPath(document.uri));
  return {
    uri: `${document.uri}.ts`,
    languageId: "typescript",
    text: virtual.code,
    mappings: virtual.mappings,
  };
}

export function formatDocument(document: TextDocument, tabSize = 4, insertSpaces = true) {
  const formatted = formatWrn(document.text, { tabSize, insertSpaces });
  if (formatted === document.text) return [];
  return [
    {
      range: {
        start: { line: 0, character: 0 },
        end: positionAt(document.text, document.text.length),
      },
      newText: formatted,
    },
  ];
}

export function documentSymbols(document: TextDocument) {
  const pattern =
    /\b(page|component|layout|state|computed|watch|effect|load|action|api)\s+([A-Za-z_$][\w$]*)/g;
  return [...document.text.matchAll(pattern)].map((match) => {
    const name = match[2]!;
    const start = match.index! + match[0].lastIndexOf(name);
    const range = {
      start: positionAt(document.text, start),
      end: positionAt(document.text, start + name.length),
    };
    return {
      name,
      kind: ["page", "component", "layout"].includes(match[1]!) ? 5 : 13,
      range,
      selectionRange: range,
    };
  });
}

export function symbolLocations(document: TextDocument, position: Position) {
  const selected = wordAt(document.text, position);
  if (!selected) return [];
  const pattern = new RegExp(
    `(?<![A-Za-z0-9_$])${selected.word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9_$])`,
    "g",
  );
  return [...document.text.matchAll(pattern)].map((match) => ({
    uri: document.uri,
    range: {
      start: positionAt(document.text, match.index!),
      end: positionAt(document.text, match.index! + selected.word.length),
    },
  }));
}

export function definitionLocation(document: TextDocument, position: Position) {
  const selected = wordAt(document.text, position);
  if (!selected) return null;
  const declaration = new RegExp(
    `\\b(?:state|computed|page|component|layout)\\s+${selected.word}\\b|\\b${selected.word}\\s*(?=[:?])`,
  ).exec(document.text);
  if (!declaration) return null;
  const start = declaration.index + declaration[0].lastIndexOf(selected.word);
  return {
    uri: document.uri,
    range: {
      start: positionAt(document.text, start),
      end: positionAt(document.text, start + selected.word.length),
    },
  };
}

export function hover(document: TextDocument, position: Position) {
  const selected = wordAt(document.text, position);
  if (!selected) return null;
  const declaration = new RegExp(
    `\\b(state|computed|prop|page|component|layout)\\s+${selected.word}\\b`,
  ).exec(document.text);
  if (!declaration) return null;
  return {
    contents: { kind: "markdown", value: `\`\`\`wrn\n${declaration[0]}\n\`\`\`` },
    range: selected.range,
  };
}

export function completionItems() {
  return WRN_COMPLETIONS.map((label) => ({ label, kind: 14, detail: "WRNexus language keyword" }));
}

const semanticTokenTypes = ["variable"] as const;
const semanticTokenModifiers = ["declaration", "modification"] as const;
export const semanticTokensLegend = {
  tokenTypes: [...semanticTokenTypes],
  tokenModifiers: [...semanticTokenModifiers],
};

/** Encode state declarations, reads, and writes using the LSP relative token format. */
export function semanticTokens(document: TextDocument): { data: number[] } {
  const declarations = new Map<string, Set<number>>();
  for (const match of document.text.matchAll(
    /\bstate\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*(?==|;|\r?$)/gm,
  )) {
    const name = match[1]!;
    const offset = match.index! + match[0].lastIndexOf(name);
    const offsets = declarations.get(name) ?? new Set<number>();
    offsets.add(offset);
    declarations.set(name, offsets);
  }
  const comments = [...document.text.matchAll(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g)].map((match) => ({
    start: match.index!,
    end: match.index! + match[0].length,
  }));
  const tokens: Array<{ line: number; character: number; length: number; modifiers: number }> = [];
  for (const [name, offsets] of declarations) {
    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
    for (const match of document.text.matchAll(
      new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g"),
    )) {
      const offset = match.index!;
      if (comments.some((range) => offset >= range.start && offset < range.end)) continue;
      const position = positionAt(document.text, offset);
      const after = document.text
        .slice(offset + name.length)
        .match(/^\s*(?:=|\+=|-=|\*=|\/=|%=|\+\+|--)/);
      const before = document.text.slice(Math.max(0, offset - 8), offset).match(/(?:\+\+|--)\s*$/);
      tokens.push({
        ...position,
        length: name.length,
        modifiers: offsets.has(offset) ? 1 : after || before ? 2 : 0,
      });
    }
  }
  tokens.sort((a, b) => a.line - b.line || a.character - b.character);
  const data: number[] = [];
  let previousLine = 0;
  let previousCharacter = 0;
  for (const token of tokens) {
    const deltaLine = token.line - previousLine;
    const deltaCharacter = deltaLine === 0 ? token.character - previousCharacter : token.character;
    data.push(deltaLine, deltaCharacter, token.length, 0, token.modifiers);
    previousLine = token.line;
    previousCharacter = token.character;
  }
  return { data };
}
export * from "./workspace.ts";
