import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { basename, extname, join, relative, resolve } from "node:path";
import { parse } from "@wrnexus/syntax";
import type { Range, TextDocument } from "./index.ts";

const SKIPPED_DIRECTORIES = new Set([
  ".git",
  ".wrnexus",
  ".wirefw",
  "build",
  "coverage",
  "dist",
  "node_modules",
  "out",
]);
const MAX_INDEX_FILES = 5_000;
const MAX_SOURCE_BYTES = 1_048_576;
// Completion can be requested repeatedly while the list is visible. Avoid
// rescanning thousands of files on that hot path; document saves invalidate it.
const INDEX_TTL_MS = 5 * 60_000;
const MAX_CACHED_ROOTS = 8;
const workspaceIndexCache = new Map<
  string,
  { expiresAt: number; items: WorkspaceCompletionItem[] }
>();

function walk(root: string, test: (file: string) => boolean): string[] {
  if (!existsSync(root)) return [];
  const files: string[] = [];
  const pending = [root];
  while (pending.length > 0 && files.length < MAX_INDEX_FILES) {
    const directory = pending.pop()!;
    let entries;
    try {
      entries = readdirSync(directory, { withFileTypes: true });
    } catch {
      continue;
    }
    for (const entry of entries) {
      if (files.length >= MAX_INDEX_FILES) break;
      const file = join(directory, entry.name);
      if (entry.isDirectory()) {
        if (!SKIPPED_DIRECTORIES.has(entry.name)) pending.push(file);
      } else if (entry.isFile() && test(file)) {
        try {
          if (statSync(file).size <= MAX_SOURCE_BYTES) files.push(file);
        } catch {
          // Files can disappear while the editor indexes a changing workspace.
        }
      }
    }
  }
  return files;
}
function flatten(value: unknown, prefix = ""): string[] {
  if (!value || typeof value !== "object" || Array.isArray(value)) return prefix ? [prefix] : [];
  return Object.entries(value).flatMap(([key, child]) =>
    flatten(child, prefix ? `${prefix}.${key}` : key),
  );
}
function routeFor(root: string, file: string): string {
  let value = relative(join(root, "app", "pages"), file)
    .replace(/\\/g, "/")
    .replace(/\.wrn$/, "");
  value = value
    .replace(/(?:^|\/)index$/, "")
    .replace(/\[\.\.\.([^\]]+)\]/g, "*$1")
    .replace(/\[([^\]]+)\]/g, ":$1");
  return `/${value}`.replace(/\/$/, "") || "/";
}

export interface WorkspaceCompletionItem {
  label: string;
  kind: number;
  detail: string;
  insertText?: string;
  data?: Record<string, unknown>;
}

function buildWorkspaceCompletionItems(root: string): WorkspaceCompletionItem[] {
  const app = join(root, "app");
  const items: WorkspaceCompletionItem[] = [];
  for (const file of walk(join(app, "components"), (path) => extname(path) === ".wrn")) {
    try {
      const source = readFileSync(file, "utf8");
      const ast = parse(source);
      const slots = [...source.matchAll(/<slot(?:\s+name=["']([^"']+)["'])?/g)].map(
        (match) => match[1] ?? "default",
      );
      items.push({
        label: ast.name,
        kind: 7,
        detail: `Component · ${relative(root, file)}`,
        insertText: `<${ast.name} />`,
        data: { file, props: ast.props, outputs: ast.outputs, slots },
      });
      for (const prop of ast.props)
        items.push({ label: prop.name, kind: 10, detail: `${ast.name} prop · ${prop.valueType}` });
      for (const output of ast.outputs)
        items.push({ label: `@${output.name}`, kind: 10, detail: `${ast.name} event` });
      for (const slot of slots)
        items.push({ label: `slot:${slot}`, kind: 10, detail: `${ast.name} slot` });
    } catch {
      // Diagnostics handle invalid components; indexing remains best-effort.
    }
  }
  for (const file of walk(join(app, "pages"), (path) => extname(path) === ".wrn")) {
    const route = routeFor(root, file);
    items.push({
      label: route,
      kind: 12,
      detail: `Application route · ${relative(root, file)}`,
      data: { file },
    });
  }
  for (const file of walk(join(app, "locales"), (path) => extname(path) === ".json")) {
    try {
      for (const key of flatten(JSON.parse(readFileSync(file, "utf8"))))
        items.push({
          label: key,
          kind: 12,
          detail: `Translation key · ${basename(file, ".json")}`,
        });
    } catch {
      // Invalid locale JSON is reported by application diagnostics.
    }
  }
  const sourceFiles = walk(app, (path) => /\.(?:ts|js|wrn)$/.test(path));
  for (const file of sourceFiles) {
    const source = readFileSync(file, "utf8");
    for (const match of source.matchAll(
      /\b(?:schema|model)\s+([A-Za-z_$][\w$]*)|\bexport\s+const\s+([A-Za-z_$][\w$]*Schema)\b/g,
    )) {
      const name = match[1] ?? match[2];
      if (name)
        items.push({
          label: name,
          kind: 7,
          detail: `Database/validation schema · ${relative(root, file)}`,
          data: { file },
        });
    }
    for (const match of source.matchAll(/(?:class|className)\s*=\s*["']([^"']+)["']/g)) {
      for (const name of match[1]!.split(/\s+/))
        if (name) items.push({ label: name, kind: 12, detail: "Workspace CSS/Tailwind class" });
    }
  }
  const unique = new Map(items.map((item) => [`${item.label}:${item.detail}`, item]));
  return [...unique.values()].slice(0, 2_000);
}

export function workspaceCompletionItems(root: string): WorkspaceCompletionItem[] {
  const normalizedRoot = resolve(root);
  const now = Date.now();
  const cached = workspaceIndexCache.get(normalizedRoot);
  if (cached && cached.expiresAt > now) return cached.items;
  const items = buildWorkspaceCompletionItems(normalizedRoot);
  workspaceIndexCache.delete(normalizedRoot);
  workspaceIndexCache.set(normalizedRoot, { expiresAt: now + INDEX_TTL_MS, items });
  while (workspaceIndexCache.size > MAX_CACHED_ROOTS) {
    const oldest = workspaceIndexCache.keys().next().value;
    if (typeof oldest !== "string") break;
    workspaceIndexCache.delete(oldest);
  }
  return items;
}

export function clearWorkspaceIndexCache(root?: string): void {
  if (root) workspaceIndexCache.delete(resolve(root));
  else workspaceIndexCache.clear();
}

export function workspaceSymbolLocations(root: string, name: string, limit = 1_000) {
  if (!/^[A-Za-z_$][\w$]*$/.test(name)) return [];
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const pattern = new RegExp(`(?<![A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`, "g");
  const locations: Array<{ uri: string; range: Range }> = [];
  for (const file of walk(resolve(root), (path) => extname(path) === ".wrn").slice(0, limit)) {
    let source: string;
    try {
      source = readFileSync(file, "utf8");
    } catch {
      continue;
    }
    for (const match of source.matchAll(pattern)) {
      const before = source.slice(0, match.index!).split(/\r?\n/);
      const start = { line: before.length - 1, character: before.at(-1)?.length ?? 0 };
      locations.push({
        uri: `file:///${file.replace(/\\/g, "/")}`,
        range: { start, end: { line: start.line, character: start.character + name.length } },
      });
    }
  }
  return locations;
}

export function extractComponentRefactor(document: TextDocument, range: Range, name: string) {
  if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name)) throw new Error("Component name must use PascalCase");
  const lines = document.text.split(/\r?\n/);
  const start =
    lines.slice(0, range.start.line).reduce((n, line) => n + line.length + 1, 0) +
    range.start.character;
  const end =
    lines.slice(0, range.end.line).reduce((n, line) => n + line.length + 1, 0) +
    range.end.character;
  const selected = document.text.slice(start, end);
  if (!selected.trim().startsWith("<")) throw new Error("Select WRN markup to extract");
  const sourcePath = decodeURIComponent(document.uri.replace(/^file:\/\//, "")).replace(
    /^\/([A-Za-z]:)/,
    "$1",
  );
  const root = sourcePath.includes(`${join("app", "pages")}`)
    ? sourcePath.slice(0, sourcePath.indexOf(`${join("app", "pages")}`))
    : resolve(".");
  const target = join(root, "app", "components", `${name}.wrn`);
  return {
    documentChanges: [
      { kind: "create", uri: `file:///${target.replace(/\\/g, "/")}` },
      {
        textDocument: { uri: `file:///${target.replace(/\\/g, "/")}`, version: null },
        edits: [
          {
            range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
            newText: `component ${name} {\n  view {\n    ${selected.trim()}\n  }\n}\n`,
          },
        ],
      },
      {
        textDocument: { uri: document.uri, version: document.version ?? null },
        edits: [{ range, newText: `<${name} />` }],
      },
    ],
  };
}

export function htmlToWrn(html: string, name = "ImportedPage"): string {
  if (!/^[A-Z][A-Za-z0-9_$]*$/.test(name)) throw new Error("WRN name must use PascalCase");
  const converted = html
    .replace(/\sclass=/g, " class=")
    .replace(/\son([a-z]+)=/gi, (_all, event) => ` @${String(event).toLowerCase()}=`)
    .replace(/<!--([\s\S]*?)-->/g, "{/*$1*/}");
  return `page ${name} {\n  view {\n    ${converted.trim()}\n  }\n}\n`;
}
