"use strict";

const vscode = require("vscode");
const path = require("node:path");
const {
  parseComponentMetadata,
  parseComponentTags,
  validateComponentTags,
} = require("./component-metadata");

let catalogPromise = null;

async function readMetadata(uri) {
  try {
    const document = await vscode.workspace.openTextDocument(uri);
    return parseComponentMetadata(document.getText(), uri);
  } catch {
    return null;
  }
}

async function installedUiUris() {
  const uris = [];
  for (const folder of vscode.workspace.workspaceFolders || []) {
    const directory = vscode.Uri.joinPath(
      folder.uri,
      "node_modules",
      "@wrnexus",
      "ui",
      "components",
    );
    try {
      for (const [name, type] of await vscode.workspace.fs.readDirectory(directory)) {
        if (type === vscode.FileType.File && name.endsWith(".wrn")) {
          uris.push(vscode.Uri.joinPath(directory, name));
        }
      }
    } catch {
      // The UI package is optional; app-local components still work.
    }
  }
  return uris;
}

async function loadComponentCatalog() {
  const localUris = await vscode.workspace.findFiles(
    "**/*.wrn",
    "**/{node_modules,dist,.wrnexus,.git}/**",
  );
  const uiUris = await installedUiUris();
  const metadata = await Promise.all([...uiUris, ...localUris].map(readMetadata));
  const catalog = new Map();
  for (const component of metadata) {
    if (component && (component.kind === "component" || component.kind === "layout")) {
      catalog.set(component.name, component);
    }
  }
  return catalog;
}

function componentCatalog() {
  catalogPromise ||= loadComponentCatalog();
  return catalogPromise;
}

function invalidateCatalog() {
  catalogPromise = null;
}

function openingTagAt(source, offset) {
  const opening = source.lastIndexOf("<", offset);
  const closing = source.lastIndexOf(">", offset);
  if (opening <= closing) return null;
  const fragment = source.slice(opening, offset);
  const match = /^<([A-Z][A-Za-z0-9_$]*)\b/.exec(fragment);
  return match ? { name: match[1], fragment } : null;
}

function componentAlreadyImported(source, name) {
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return (
    new RegExp(`\\bimport[\\s\\S]*?\\b${escaped}\\b[\\s\\S]*?\\bfrom\\b`).test(source) ||
    new RegExp(`\\b(?:component|layout)\\s+${escaped}\\b`).test(source)
  );
}

function importEditForComponent(document, component) {
  if (!component?.uri || componentAlreadyImported(document.getText(), component.name)) return null;
  const target = component.uri.fsPath.replace(/\\/g, "/");
  const current = document.uri.fsPath.replace(/\\/g, "/");
  let statement;
  if (
    target.includes("/node_modules/@wrnexus/ui/components/") ||
    target.includes("/packages/ui/components/")
  ) {
    statement = `import { ${component.name} } from "@wrnexus/ui"\n`;
  } else {
    const appMarker = "/app/";
    const appIndex = target.lastIndexOf(appMarker);
    if (appIndex !== -1) {
      statement = `import ${component.name} from "@/${target.slice(appIndex + appMarker.length)}"\n`;
    } else {
      let relative = path.posix.relative(path.posix.dirname(current), target);
      if (!relative.startsWith(".")) relative = `./${relative}`;
      statement = `import ${component.name} from "${relative}"\n`;
    }
  }
  return vscode.TextEdit.insert(new vscode.Position(0, 0), statement);
}

function propSnippet(prop) {
  if (prop.type === "boolean") return `${prop.name}="{\${1:false}}"`;
  if (prop.type === "number") return `${prop.name}="{\${1:0}}"`;
  if (prop.type === "array") return `${prop.name}="{[\${1}]}"`;
  if (prop.type === "object") return `${prop.name}="{{ \${1} }}"`;
  if (prop.options.length > 0) return `${prop.name}="\${1|${prop.options.join(",")}|}"`;
  return `${prop.name}="\${1}"`;
}

function propDocumentation(prop) {
  const lines = [
    `**${prop.name}** — \`${prop.type}\` ${prop.required ? "**required**" : "optional"}`,
  ];
  if (!prop.required) lines.push(`Default: \`${prop.defaultValue}\``);
  if (prop.options.length > 0)
    lines.push(`Allowed: ${prop.options.map((v) => `\`${v}\``).join(", ")}`);
  return lines.join("\n\n");
}

async function provideComponentCompletions(document, position) {
  const source = document.getText();
  const offset = document.offsetAt(position);
  const tag = openingTagAt(source, offset);
  const catalog = await componentCatalog();
  const items = [];

  if (tag) {
    const component = catalog.get(tag.name);
    if (!component) return items;
    const used = new Set([...tag.fragment.matchAll(/\s([^\s=/>]+)\s*=/g)].map((m) => m[1]));
    for (const prop of component.props) {
      if (used.has(prop.name)) continue;
      const item = new vscode.CompletionItem(prop.name, vscode.CompletionItemKind.Property);
      item.detail = `${prop.required ? "required" : "optional"} · ${prop.type}`;
      item.documentation = new vscode.MarkdownString(propDocumentation(prop));
      item.insertText = new vscode.SnippetString(propSnippet(prop));
      item.sortText = `${prop.required ? "0" : "1"}-${prop.name}`;
      items.push(item);
    }
    return items;
  }

  const prefix = source.slice(Math.max(0, offset - 80), offset);
  if (!/<[A-Za-z0-9_$]*$/.test(prefix)) return items;
  for (const component of catalog.values()) {
    const item = new vscode.CompletionItem(component.name, vscode.CompletionItemKind.Class);
    const required = component.props.filter((prop) => prop.required);
    item.detail = `WRN ${component.kind} · ${component.props.length} props`;
    item.documentation = new vscode.MarkdownString(componentMarkdown(component));
    item.insertText = new vscode.SnippetString(
      `${component.name}${required.map((prop, index) => ` ${prop.name}="\${${index + 1}}"`).join("")} />`,
    );
    const importEdit = importEditForComponent(document, component);
    if (importEdit) item.additionalTextEdits = [importEdit];
    items.push(item);
  }
  return items;
}

function componentMarkdown(component) {
  const lines = [
    `### <${component.name}>`,
    "",
    "| Prop | Type | Required | Default / options |",
    "|---|---|---:|---|",
  ];
  for (const prop of component.props) {
    const detail =
      prop.options.length > 0
        ? prop.options.join(" \\| ")
        : prop.required
          ? "—"
          : prop.defaultValue;
    lines.push(
      `| \`${prop.name}\` | \`${prop.type}\` | ${prop.required ? "yes" : "no"} | ${detail} |`,
    );
  }
  return lines.join("\n");
}

async function provideComponentHover(document, position) {
  const source = document.getText();
  const offset = document.offsetAt(position);
  const catalog = await componentCatalog();
  const tag = parseComponentTags(source).find(
    (candidate) => offset >= candidate.start && offset <= candidate.end,
  );
  if (!tag) return null;
  const component = catalog.get(tag.name);
  if (!component) return null;
  const attribute = tag.attributes.find(
    (candidate) => offset >= candidate.nameStart && offset <= candidate.nameEnd,
  );
  const markdown = attribute
    ? propDocumentation(
        component.props.find((prop) => prop.name === attribute.name) || {
          name: attribute.name,
          type: "unknown",
          required: false,
          defaultValue: "unknown",
          options: [],
        },
      )
    : componentMarkdown(component);
  return new vscode.Hover(new vscode.MarkdownString(markdown));
}

function registerComponentIntelligence(context) {
  const diagnostics = vscode.languages.createDiagnosticCollection("wrnexus-components");

  const update = async (document) => {
    if (document.languageId !== "wrn") return;
    const catalog = await componentCatalog();
    const results = validateComponentTags(document.getText(), catalog).map((result) => {
      const diagnostic = new vscode.Diagnostic(
        new vscode.Range(document.positionAt(result.start), document.positionAt(result.end)),
        result.message,
        result.severity === "error"
          ? vscode.DiagnosticSeverity.Error
          : vscode.DiagnosticSeverity.Warning,
      );
      diagnostic.source = "WRNexus Components";
      diagnostic.code = result.code;
      return diagnostic;
    });
    diagnostics.set(document.uri, results);
  };

  // Watch only application sources. A workspace-wide **/*.wrn watcher can
  // recursively subscribe to dependency and generated trees in large repos.
  const watchers = (vscode.workspace.workspaceFolders || []).map((folder) =>
    vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, "app/**/*.wrn")),
  );
  const refreshOpenDocuments = () => {
    invalidateCatalog();
    for (const document of vscode.workspace.textDocuments) void update(document);
  };

  context.subscriptions.push(
    diagnostics,
    ...watchers,
    ...watchers.flatMap((watcher) => [
      watcher.onDidCreate(refreshOpenDocuments),
      watcher.onDidChange(refreshOpenDocuments),
      watcher.onDidDelete(refreshOpenDocuments),
    ]),
    vscode.workspace.onDidOpenTextDocument(update),
    vscode.workspace.onDidChangeTextDocument((event) => void update(event.document)),
    vscode.workspace.onDidSaveTextDocument((document) => {
      invalidateCatalog();
      void update(document);
    }),
    vscode.workspace.onDidCloseTextDocument((document) => diagnostics.delete(document.uri)),
    vscode.languages.registerCompletionItemProvider(
      { language: "wrn" },
      { provideCompletionItems: provideComponentCompletions },
      "<",
      " ",
    ),
    vscode.languages.registerHoverProvider(
      { language: "wrn" },
      { provideHover: provideComponentHover },
    ),
  );

  for (const document of vscode.workspace.textDocuments) void update(document);
}

module.exports = {
  componentAlreadyImported,
  componentMarkdown,
  importEditForComponent,
  openingTagAt,
  propDocumentation,
  propSnippet,
  registerComponentIntelligence,
};
