"use strict";

const vscode = require("vscode");

const BLOCK_COMPLETIONS = [
  {
    label: "import",
    detail: "WRN server module import",
    documentation:
      "Import a package or module before the page, component, or layout declaration for server rendering.",
    snippet: 'import { ${1:appUrl} } from "${2:@wrnexus/helpers}";\n\n$0',
  },
  {
    label: "page",
    detail: "WRN page",
    documentation: "Create a WRN page declaration.",
    snippet: [
      "page ${1:PageName} {",
      '    layout = "${2:default}"',
      "",
      "    view {",
      "        $0",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "component",
    detail: "WRN component",
    documentation:
      "Create a reusable WRN component with state, functions, lifecycle hooks, watchers, and a view.",
    snippet: [
      "component ${1:ComponentName} {",
      "    state ${2:ready} = false",
      "",
      "    functions {",
      "        function ${3:initialize}() {",
      "            $0",
      "        }",
      "    }",
      "",
      "    lifecycle {",
      "        mount {",
      "            ${3:initialize}()",
      "        }",
      "    }",
      "",
      "    view {",
      "        <div></div>",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "layout",
    detail: "WRN layout",
    documentation: "Create a reusable WRN layout.",
    snippet: [
      "layout ${1:LayoutName} {",
      "    view {",
      "        <main>",
      "            {content}",
      "        </main>",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "types",
    detail: "TypeScript type declarations block",
    documentation: "Declare interfaces and type aliases used by props, state, and functions.",
    snippet: [
      "types {",
      "    interface ${1:Item} {",
      "        ${2:id}: ${3:string}",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "props",
    detail: "Component or layout props block",
    documentation: "Declare values accepted by a component or layout.",
    snippet: ["props {", '    ${1:title}: ${2:string} = "${3:Title}"', "}"].join("\n"),
  },
  {
    label: "seo",
    detail: "SEO metadata block",
    documentation: "Declare title, description, and other page metadata.",
    snippet: [
      "seo {",
      '    title = "${1:Page title}"',
      '    description = "${2:Page description}"',
      "}",
    ].join("\n"),
  },
  {
    label: "view",
    detail: "WRN view block",
    documentation: "Declare the HTML view rendered by the page, component, or layout.",
    snippet: ["view {", "    $0", "}"].join("\n"),
  },
  {
    label: "state",
    detail: "Reactive state declaration",
    documentation: "Declare reactive state owned by the current component, layout, or page.",
    snippet: 'state ${1:name}: ${2:string} = ${3:"value"}',
  },
  {
    label: "runtime",
    detail: "WRN execution target",
    documentation: "Choose whether this declaration executes on the server, client, or both.",
    snippet: 'runtime = "${1|universal,server,client|}"',
  },
  {
    label: "hydrate",
    detail: "WRN hydration strategy",
    documentation: "Choose when client interactivity is initialized for this declaration.",
    snippet: 'hydrate = "${1|load,idle,visible,interaction,none|}"',
  },
  {
    label: "computed",
    detail: "Derived reactive values block",
    documentation:
      "Declare values that are recomputed only when their reactive dependencies change.",
    snippet: ["computed {", "    ${1:displayName} = ${2:firstName + ' ' + lastName}", "}"].join(
      "\n",
    ),
  },
  {
    label: "effect",
    detail: "Reactive side-effect block",
    documentation: "Run browser-side code whenever its referenced reactive values change.",
    snippet: ["effect {", "    ${1:console.log(value)}", "}"].join("\n"),
  },
  {
    label: "security",
    detail: "Route security metadata block",
    documentation: "Declare authentication, authorization, CSRF, and rate-limit policy metadata.",
    snippet: [
      "security {",
      '    auth = "${1|required,optional,public|}"',
      '    csrf = "${2:true}"',
      "}",
    ].join("\n"),
  },
  {
    label: "load",
    detail: "Typed data-loading block",
    documentation: "Load data on the server or client with an explicit execution boundary.",
    snippet: ["load ${1|server,client|} {", "    ${2:return {}}", "}"].join("\n"),
  },
  {
    label: "action",
    detail: "Named server action",
    documentation: "Declare a callable mutation with an explicit name and arguments.",
    snippet: ["action ${1:save}(${2:input}) {", "    $0", "}"].join("\n"),
  },
  {
    label: "functions",
    detail: "Browser component functions block",
    documentation:
      "Declare functions that can be called from events, lifecycle hooks, and watchers.",
    snippet: [
      "functions {",
      "    function ${1:handler}(${2:value}: ${3:unknown}): ${4:void} {",
      "        $0",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "function",
    detail: "WRN component function",
    documentation: "Declare a browser-side function inside a functions block.",
    snippet: ["function ${1:name}(${2:value}: ${3:unknown}): ${4:void} {", "    $0", "}"].join(
      "\n",
    ),
  },
  {
    label: "lifecycle",
    detail: "Component lifecycle block",
    documentation: "Declare mount, update, and unmount hooks for a component or layout.",
    snippet: [
      "lifecycle {",
      "    mount {",
      "        $1",
      "    }",
      "",
      "    update {",
      "        $2",
      "    }",
      "",
      "    unmount {",
      "        $0",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "mount",
    detail: "Lifecycle mount hook",
    documentation: "Runs once after the component is connected and hydrated.",
    snippet: ["mount {", "    $0", "}"].join("\n"),
  },
  {
    label: "update",
    detail: "Lifecycle update hook",
    documentation: "Runs once after a batch of reactive state changes.",
    snippet: ["update {", "    $0", "}"].join("\n"),
  },
  {
    label: "unmount",
    detail: "Lifecycle unmount hook",
    documentation:
      "Runs before the component is removed. Use it to remove global listeners and release resources.",
    snippet: ["unmount {", "    $0", "}"].join("\n"),
  },
  {
    label: "watch",
    detail: "Reactive state watcher",
    documentation:
      "Run code when one declared state value changes. The watcher receives `value` and `previous`.",
    snippet: ["watch ${1:stateName} {", "    console.log(value, previous)", "    $0", "}"].join(
      "\n",
    ),
  },
  {
    label: "style",
    detail: "Scoped style block",
    documentation: "Declare styles for the current WRN declaration.",
    snippet: ["style {", "    $0", "}"].join("\n"),
  },
  {
    label: "import type",
    detail: "WRN v0.6 TypeScript type import",
    snippet: 'import type { ${1:PublicUser} } from "${2:@/types/user.ts}"\n\n$0',
  },
  {
    label: "outputs",
    detail: "Typed callable component outputs",
    snippet: [
      "outputs {",
      "    ${1:confirm}(payload: ${2:ConfirmPayload})",
      "    ${3:cancel}()",
      "}",
    ].join("\n"),
  },
  {
    label: "client function",
    detail: "Browser-only typed function",
    snippet: ["client ${1|,async |}function ${2:name}(${3}): ${4:void} {", "    $0", "}"].join(
      "\n",
    ),
  },
  {
    label: "server function",
    detail: "Server-only typed function",
    snippet: ["server ${1|,async |}function ${2:name}(${3}): ${4:void} {", "    $0", "}"].join(
      "\n",
    ),
  },
  {
    label: "shared function",
    detail: "Server-and-browser typed function",
    snippet: ["shared function ${1:name}(${2}): ${3:void} {", "    $0", "}"].join("\n"),
  },
  {
    label: "state block",
    detail: "Grouped typed reactive state",
    snippet: ["state {", '    ${1:name}: ${2:string} = ${3:""}', "}"].join("\n"),
  },
  {
    label: "client state",
    detail: "Browser-only state block",
    snippet: ["client state {", "    ${1:menuOpen}: boolean = false", "}"].join("\n"),
  },
  {
    label: "server state",
    detail: "Server-only state block",
    snippet: ["server state {", "    ${1:sessionId}: string | null = null", "}"].join("\n"),
  },
  {
    label: "global store",
    detail: "Request-scoped global store",
    snippet: [
      "global store ${1:UserStore} {",
      "    state {",
      "        ${2:user}: ${3:PublicUser | null} = null",
      "    }",
      "",
      "    computed {",
      "        ${4:authenticated}: boolean = ${2:user} !== null",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "page store",
    detail: "Route-scoped page store",
    snippet: [
      "page store ${1:PageStore} {",
      "    state {",
      "        ${2:loading}: boolean = false",
      "    }",
      "}",
    ].join("\n"),
  },
  {
    label: "persist",
    detail: "Include-only store persistence",
    snippet: [
      "persist {",
      '    storage = "${1|memory,session,local|}"',
      '    include = ["${2:field}"]',
      "    version = ${3:1}",
      "}",
    ].join("\n"),
  },
];

const ATTRIBUTE_COMPLETIONS = [
  ["@click", "Click event handler", '@click="${1:handler()}"'],
  ["@change", "Change event handler", '@change="${1:handler()}"'],
  ["@input", "Input event handler", '@input="${1:handler()}"'],
  ["@submit", "Submit event handler", '@submit="${1:handler()}"'],
  ["@focus", "Focus event handler", '@focus="${1:handler()}"'],
  ["@blur", "Blur event handler", '@blur="${1:handler()}"'],
  ["@keydown", "Keyboard key-down event handler", '@keydown="${1:handler()}"'],
  ["@keyup", "Keyboard key-up event handler", '@keyup="${1:handler()}"'],
  ["@mouseenter", "Pointer enter event handler", '@mouseenter="${1:handler()}"'],
  ["@mouseleave", "Pointer leave event handler", '@mouseleave="${1:handler()}"'],
  ["@window:scroll", "Window scroll event handler", '@window:scroll="${1:handler()}"'],
  ["@window:resize", "Window resize event handler", '@window:resize="${1:handler()}"'],
  ["@window:keydown", "Window key-down event handler", '@window:keydown="${1:handler()}"'],
  ["@document:click", "Document click event handler", '@document:click="${1:handler()}"'],
  [
    "@document:visibilitychange",
    "Document visibility-change event handler",
    '@document:visibilitychange="${1:handler()}"',
  ],
  ["data-show", "Conditional visibility", 'data-show="${1:condition}"'],
  ["data-for", "Reactive loop", 'data-for="${1:item} in ${2:items}"'],
  ["class:", "Conditional CSS class", 'class:${1:border-indigo-500}="${2:condition}"'],
];

const CONTEXT_COMPLETIONS = [
  ["ctx.params", "Dynamic route parameters"],
  ["ctx.query", "URL query parameters"],
  ["ctx.request", "Current Request object"],
  ["ctx.user", "Authenticated user"],
  ["ctx.session", "Current session"],
  ["ctx.locals", "Request-local data"],
  ["ctx.tenant", "Resolved tenant context"],
  ["ctx.tracer", "Request tracing interface"],
];

const WATCH_VALUE_COMPLETIONS = [
  ["value", "Current watcher state value"],
  ["previous", "Previous watcher state value"],
];

function completionKindFor(label) {
  if (label.startsWith("@")) {
    return vscode.CompletionItemKind.Event;
  }

  if (label.startsWith("class:") || label.startsWith("data-")) {
    return vscode.CompletionItemKind.Property;
  }

  if (label === "function" || label === "functions") {
    return vscode.CompletionItemKind.Function;
  }

  return vscode.CompletionItemKind.Keyword;
}

function createCompletion(label, detail, snippet, documentation) {
  const item = new vscode.CompletionItem(label, completionKindFor(label));

  item.detail = detail;
  item.insertText = new vscode.SnippetString(snippet || label);

  item.documentation = new vscode.MarkdownString(documentation || detail);

  return item;
}

function createVariableCompletion(label, detail, insertion = label) {
  const item = new vscode.CompletionItem(label, vscode.CompletionItemKind.Variable);

  item.detail = detail;
  item.insertText = insertion;

  return item;
}

function createFunctionCompletion(name, parameters = []) {
  const item = new vscode.CompletionItem(name, vscode.CompletionItemKind.Function);

  item.detail = `WRN component function${
    parameters.length > 0 ? ` (${parameters.join(", ")})` : ""
  }`;

  item.documentation = new vscode.MarkdownString(
    `Call the browser function \`${name}\` declared in the current \`functions { ... }\` block.`,
  );

  const placeholders = parameters.map((parameter, index) => `\${${index + 1}:${parameter}}`);

  item.insertText = new vscode.SnippetString(`${name}(${placeholders.join(", ")})`);

  return item;
}

function getCurrentOpeningTag(document, position) {
  const textBeforeCursor = document.getText(
    new vscode.Range(new vscode.Position(position.line, 0), position),
  );

  const lastOpen = textBeforeCursor.lastIndexOf("<");

  const lastClose = textBeforeCursor.lastIndexOf(">");

  if (lastOpen > lastClose) {
    return textBeforeCursor.slice(lastOpen);
  }

  return null;
}

function extractRouteParams(document) {
  const fileName = document.fileName.replace(/\\/g, "/");

  const matches = [...fileName.matchAll(/\[([A-Za-z_$][\w$]*)\]/g)];

  return matches.map((match) => match[1]);
}

function extractStates(document) {
  const source = document.getText();

  const matches = [...source.matchAll(/^\s*state\s+([A-Za-z_$][\w$]*)(?:\s*:\s*[^=\r\n]+)?\s*=/gm)];

  return [...new Set(matches.map((match) => match[1]))];
}

function extractProps(document) {
  const source = document.getText();
  const propsBlockPattern = /\bprops\s*\{([\s\S]*?)\}/g;

  const props = new Set();
  let blockMatch;

  while ((blockMatch = propsBlockPattern.exec(source)) !== null) {
    const body = blockMatch[1];

    for (const match of body.matchAll(
      /^\s*([A-Za-z_$][\w$]*)(?:\s*:\s*[^=\r\n]+)?(?:\s*=|\s*$)/gm,
    )) {
      props.add(match[1]);
    }
  }

  return [...props];
}

function extractFunctions(document) {
  const source = document.getText();
  const functions = [];
  const seen = new Set();

  const pattern = /(?:^|\s)(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)/g;

  let match;

  while ((match = pattern.exec(source)) !== null) {
    const name = match[1];

    if (seen.has(name)) {
      continue;
    }

    seen.add(name);

    const parameters = match[2]
      .split(",")
      .map((parameter) => parameter.trim())
      .filter(Boolean)
      .map((parameter) =>
        parameter
          .replace(/=.*$/, "")
          .replace(/\??\s*:\s*[\s\S]+$/, "")
          .trim(),
      );

    functions.push({
      name,
      parameters,
    });
  }

  return functions;
}

function sourceBeforePosition(document, position) {
  return document.getText(new vscode.Range(new vscode.Position(0, 0), position));
}

function isInsideNamedBlock(document, position, blockName) {
  const source = sourceBeforePosition(document, position);

  const tokenPattern = new RegExp(`\\b${blockName}\\s*\\{|\\{|\\}`, "g");

  const stack = [];
  let match;

  while ((match = tokenPattern.exec(source)) !== null) {
    const token = match[0];

    if (token.startsWith(blockName)) {
      stack.push(blockName);
      continue;
    }

    if (token === "{") {
      stack.push(null);
      continue;
    }

    if (token === "}") {
      stack.pop();
    }
  }

  return stack.includes(blockName);
}

function isInsideLifecycle(document, position) {
  return isInsideNamedBlock(document, position, "lifecycle");
}

function isInsideFunctions(document, position) {
  return isInsideNamedBlock(document, position, "functions");
}

function isInsideWatch(document, position) {
  const source = sourceBeforePosition(document, position);

  const watchMatches = [...source.matchAll(/\bwatch\s+[A-Za-z_$][\w$]*\s*\{/g)];

  if (watchMatches.length === 0) {
    return false;
  }

  const latest = watchMatches[watchMatches.length - 1];

  const tail = source.slice(latest.index);
  let depth = 0;

  for (const character of tail) {
    if (character === "{") {
      depth += 1;
    } else if (character === "}") {
      depth -= 1;
    }
  }

  return depth > 0;
}

function isAfterWatchKeyword(document, position) {
  const linePrefix = document.lineAt(position.line).text.slice(0, position.character);

  return /^\s*watch\s+[A-Za-z0-9_$]*$/.test(linePrefix);
}

function addBlockCompletions(items, document, position) {
  const insideLifecycle = isInsideLifecycle(document, position);

  const insideFunctions = isInsideFunctions(document, position);

  for (const completion of BLOCK_COMPLETIONS) {
    if (insideLifecycle && !["mount", "update", "unmount"].includes(completion.label)) {
      continue;
    }

    if (insideFunctions && completion.label !== "function") {
      continue;
    }

    if (!insideLifecycle && ["mount", "update", "unmount"].includes(completion.label)) {
      continue;
    }

    if (!insideFunctions && completion.label === "function") {
      continue;
    }

    items.push(
      createCompletion(
        completion.label,
        completion.detail,
        completion.snippet,
        completion.documentation,
      ),
    );
  }
}

function addStateCompletions(items, document, position) {
  const states = extractStates(document);
  const afterWatch = isAfterWatchKeyword(document, position);

  for (const state of states) {
    const item = createVariableCompletion(
      state,
      afterWatch ? "Declared WRN state available for watching" : "WRN reactive state",
    );

    if (afterWatch) {
      item.sortText = `0-${state}`;
    }

    items.push(item);
  }
}

function addFunctionCompletions(items, document) {
  for (const fn of extractFunctions(document)) {
    items.push(createFunctionCompletion(fn.name, fn.parameters));
  }
}

function provideCompletionItems(document, position) {
  const items = [];

  const linePrefix = document.lineAt(position.line).text.slice(0, position.character);

  const openingTag = getCurrentOpeningTag(document, position);

  if (openingTag !== null) {
    for (const [label, detail, snippet] of ATTRIBUTE_COMPLETIONS) {
      items.push(createCompletion(label, detail, snippet));
    }
  } else if (isAfterWatchKeyword(document, position)) {
    addStateCompletions(items, document, position);
  } else {
    addBlockCompletions(items, document, position);
  }

  if (linePrefix.includes("ctx.") || linePrefix.includes("ctx.params.")) {
    for (const [label, detail] of CONTEXT_COMPLETIONS) {
      items.push(createCompletion(label, detail, label));
    }
  }

  for (const param of extractRouteParams(document)) {
    items.push(createVariableCompletion(param, `Route parameter from [${param}].wrn`));

    items.push(
      createVariableCompletion(`ctx.params.${param}`, `Route parameter from [${param}].wrn`),
    );
  }

  if (!isAfterWatchKeyword(document, position)) {
    addStateCompletions(items, document, position);
  }

  for (const prop of extractProps(document)) {
    items.push(createVariableCompletion(prop, "WRN component or layout prop"));
  }

  addFunctionCompletions(items, document);

  if (isInsideWatch(document, position)) {
    for (const [label, detail] of WATCH_VALUE_COMPLETIONS) {
      items.push(createVariableCompletion(label, detail));
    }
  }

  return items;
}

function registerCompletionProvider(context) {
  const provider = vscode.languages.registerCompletionItemProvider(
    {
      language: "wrn",
    },
    {
      provideCompletionItems,
    },
    "@",
    ":",
    ".",
    "<",
    " ",
    "(",
  );

  context.subscriptions.push(provider);
}

module.exports = {
  extractFunctions,
  extractProps,
  extractRouteParams,
  extractStates,
  provideCompletionItems,
  registerCompletionProvider,
};
