"use strict";

const vscode = require("vscode");

const COLLECTION_NAME = "wrnexus";
const WRN_LANGUAGE_ID = "wrn";

const TOP_LEVEL_PATTERN =
  /^\s*((?:global|page)\s+store|page|component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/;

const VALID_TOP_LEVEL_KINDS = new Set([
  "page",
  "component",
  "layout",
  "global store",
  "page store",
]);

const VALID_LIFECYCLE_HOOKS = new Set(["mount", "update", "unmount"]);

const ROOT_MEMBER_NAMES = [
  "layout",
  "runtime",
  "hydrate",
  "client",
  "types",
  "props",
  "outputs",
  "state",
  "persist",
  "computed",
  "effect",
  "watch",
  "lifecycle",
  "view",
  "seo",
  "security",
  "load",
  "action",
  "api",
  "ssr",
  "realtime",
  "style",
  "functions",
];

const VALID_MEMBERS = {
  page: new Set(ROOT_MEMBER_NAMES),
  component: new Set(ROOT_MEMBER_NAMES),
  layout: new Set(ROOT_MEMBER_NAMES),
  "global store": new Set(ROOT_MEMBER_NAMES),
  "page store": new Set(ROOT_MEMBER_NAMES),
};

function createDiagnostic(
  document,
  startOffset,
  endOffset,
  message,
  severity = vscode.DiagnosticSeverity.Error,
  code,
) {
  const diagnostic = new vscode.Diagnostic(
    new vscode.Range(document.positionAt(startOffset), document.positionAt(endOffset)),
    message,
    severity,
  );

  diagnostic.source = "WRNexus";

  if (code) {
    diagnostic.code = code;
  }

  return diagnostic;
}

function lineDiagnostic(
  document,
  lineNumber,
  message,
  severity = vscode.DiagnosticSeverity.Error,
  code,
) {
  const line = document.lineAt(lineNumber);
  const diagnostic = new vscode.Diagnostic(line.range, message, severity);

  diagnostic.source = "WRNexus";

  if (code) {
    diagnostic.code = code;
  }

  return diagnostic;
}

function maskLeadingTrivia(source) {
  const masked = [...source];
  let offset = 0;
  const importPattern = /import\s+(?:type\s+)?(?:[\s\S]*?\s+from\s+)?["'][^"'\r\n]+["']\s*;?/y;

  while (offset < source.length) {
    if (/\s/u.test(source[offset])) {
      offset += 1;
      continue;
    }

    importPattern.lastIndex = offset;
    const importStatement = importPattern.exec(source);
    if (importStatement) {
      const end = importPattern.lastIndex;
      while (offset < end) {
        if (source[offset] !== "\n" && source[offset] !== "\r") masked[offset] = " ";
        offset += 1;
      }
      continue;
    }

    if (!source.startsWith("//", offset)) break;

    while (offset < source.length && source[offset] !== "\n") {
      masked[offset] = " ";
      offset += 1;
    }
  }

  return masked.join("");
}

function findTopLevelDeclaration(document, source) {
  const sourceWithoutLeadingTrivia = maskLeadingTrivia(source);
  const match = TOP_LEVEL_PATTERN.exec(sourceWithoutLeadingTrivia);

  if (!match) {
    const firstMeaningfulLine = sourceWithoutLeadingTrivia
      .split(/\r?\n/)
      .findIndex((line) => line.trim().length > 0);

    return {
      diagnostic: lineDiagnostic(
        document,
        Math.max(0, firstMeaningfulLine),
        "A .wrn file must start with `page`, `component`, or `layout`.",
        vscode.DiagnosticSeverity.Error,
        "wrn-invalid-root",
      ),
    };
  }

  return {
    kind: match[1],
    name: match[2],
    match,
  };
}

function validateBalancedCharacters(document, source) {
  const diagnostics = [];
  const stack = [];

  let quote = null;
  let escaped = false;

  const pairs = {
    "}": "{",
    "]": "[",
    ")": "(",
  };

  for (let index = 0; index < source.length; index += 1) {
    const character = source[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
        continue;
      }

      if (character === "\\") {
        escaped = true;
        continue;
      }

      if (character === quote) {
        quote = null;
      }

      continue;
    }

    if (source.startsWith("//", index)) {
      const lineEnd = source.indexOf("\n", index + 2);

      index = lineEnd === -1 ? source.length : lineEnd;
      continue;
    }

    // Apostrophes in rendered copy (for example, "person's name") are text,
    // not the beginning of a WRN/HTML quoted value.
    if (
      character === "'" &&
      /[\p{L}\p{N}]/u.test(source[index - 1] ?? "") &&
      /[\p{L}\p{N}]/u.test(source[index + 1] ?? "")
    ) {
      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      continue;
    }

    if (source.startsWith("<!--", index)) {
      const commentEnd = source.indexOf("-->", index + 4);

      if (commentEnd === -1) {
        diagnostics.push(
          createDiagnostic(
            document,
            index,
            Math.min(source.length, index + 4),
            "Unclosed HTML comment.",
            vscode.DiagnosticSeverity.Error,
            "wrn-unclosed-comment",
          ),
        );

        break;
      }

      index = commentEnd + 2;
      continue;
    }

    if (character === "{" || character === "[" || character === "(") {
      stack.push({
        character,
        offset: index,
      });

      continue;
    }

    if (character === "}" || character === "]" || character === ")") {
      const expectedOpening = pairs[character];
      const opening = stack.pop();

      if (!opening || opening.character !== expectedOpening) {
        diagnostics.push(
          createDiagnostic(
            document,
            index,
            index + 1,
            `Unexpected \`${character}\`.`,
            vscode.DiagnosticSeverity.Error,
            "wrn-unexpected-closing",
          ),
        );
      }
    }
  }

  for (const opening of stack) {
    const expectedClosing = opening.character === "{" ? "}" : opening.character === "[" ? "]" : ")";

    diagnostics.push(
      createDiagnostic(
        document,
        opening.offset,
        opening.offset + 1,
        `Missing closing \`${expectedClosing}\`.`,
        vscode.DiagnosticSeverity.Error,
        "wrn-missing-closing",
      ),
    );
  }

  if (quote !== null) {
    diagnostics.push(
      createDiagnostic(
        document,
        Math.max(0, source.length - 1),
        source.length,
        `Unclosed ${quote === '"' ? "double" : "single"} quote.`,
        vscode.DiagnosticSeverity.Error,
        "wrn-unclosed-string",
      ),
    );
  }

  return diagnostics;
}

function maskHtmlComments(source) {
  const masked = [...source];
  let index = 0;

  while (index < source.length) {
    if (!source.startsWith("<!--", index)) {
      index += 1;
      continue;
    }

    const commentEnd = source.indexOf("-->", index + 4);
    const end = commentEnd === -1 ? source.length : commentEnd + 3;
    for (let cursor = index; cursor < end; cursor += 1) {
      if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
    }
    index = end;
  }

  return masked.join("");
}

function maskTemplateExpressions(source) {
  const masked = [...source];
  let index = 0;

  while (index < source.length) {
    if (source[index] !== "{") {
      index += 1;
      continue;
    }

    const start = index;
    let depth = 0;
    let quote = null;
    let escaped = false;

    while (index < source.length) {
      const character = source[index];

      if (quote !== null) {
        if (escaped) escaped = false;
        else if (character === "\\") escaped = true;
        else if (character === quote) quote = null;
        index += 1;
        continue;
      }

      if (character === '"' || character === "'" || character === "`") {
        quote = character;
        index += 1;
        continue;
      }

      if (character === "{") depth += 1;
      else if (character === "}") {
        depth -= 1;
        index += 1;
        if (depth === 0) break;
        continue;
      }

      index += 1;
    }

    for (let cursor = start; cursor < index; cursor += 1) {
      if (source[cursor] !== "\n" && source[cursor] !== "\r") masked[cursor] = " ";
    }
  }

  return masked.join("");
}

function findViewRanges(source) {
  const ranges = [];
  const pattern = /\bview\s*\{/g;
  let match;

  while ((match = pattern.exec(source)) !== null) {
    const openingBrace = source.indexOf("{", match.index);
    const closingBrace = findMatchingBrace(source, openingBrace);
    if (closingBrace === -1) break;
    ranges.push({ start: openingBrace + 1, end: closingBrace });
    pattern.lastIndex = closingBrace + 1;
  }

  return ranges;
}

function validateHtmlTags(document, source) {
  const diagnostics = [];
  const voidElements = new Set([
    "area",
    "base",
    "br",
    "col",
    "embed",
    "hr",
    "img",
    "input",
    "link",
    "meta",
    "param",
    "source",
    "track",
    "wbr",
  ]);

  for (const range of findViewRanges(source)) {
    const fragment = source.slice(range.start, range.end);
    // JavaScript-style `//` is valid rendered text inside <pre>/<code>. Only
    // HTML comments and WRN expressions are masked before tag validation.
    const cleaned = maskTemplateExpressions(maskHtmlComments(fragment));
    const stack = [];
    const tagPattern = /<\/?([A-Za-z][A-Za-z0-9_$:.-]*)(?:\s[\s\S]*?)?\/?>/g;
    let match;

    while ((match = tagPattern.exec(cleaned)) !== null) {
      const completeTag = match[0];
      const tagName = match[1];
      const normalizedName = /^[a-z]/.test(tagName) ? tagName.toLowerCase() : tagName;
      const lowerTag = tagName.toLowerCase();
      const absoluteStart = range.start + match.index;
      const isClosing = completeTag.startsWith("</");
      const isSelfClosing = /\/\s*>$/.test(completeTag);
      const isVoid = voidElements.has(lowerTag);

      if (isClosing) {
        const matchingIndex = stack.findLastIndex((item) => item.normalizedName === normalizedName);
        if (matchingIndex === -1) {
          diagnostics.push(
            createDiagnostic(
              document,
              absoluteStart,
              absoluteStart + completeTag.length,
              `Unexpected closing tag </${tagName}>.`,
              vscode.DiagnosticSeverity.Error,
              "wrn-unexpected-html-close",
            ),
          );
          continue;
        }

        const last = stack.at(-1);
        if (last.normalizedName !== normalizedName) {
          diagnostics.push(
            createDiagnostic(
              document,
              absoluteStart,
              absoluteStart + completeTag.length,
              `Mismatched closing tag </${tagName}>. Expected </${last.tagName}>.`,
              vscode.DiagnosticSeverity.Error,
              "wrn-mismatched-html-tag",
            ),
          );
        }
        stack.splice(matchingIndex);
        continue;
      }

      if (!isSelfClosing && !isVoid) {
        stack.push({
          tagName,
          normalizedName,
          offset: absoluteStart,
          length: completeTag.length,
        });
      }
    }

    for (const tag of stack) {
      diagnostics.push(
        createDiagnostic(
          document,
          tag.offset,
          tag.offset + tag.length,
          `Missing closing tag </${tag.tagName}>.`,
          vscode.DiagnosticSeverity.Error,
          "wrn-missing-html-close",
        ),
      );
    }
  }

  return diagnostics;
}

function getRootBodyRange(source, rootMatch) {
  const openingBrace = rootMatch.index + rootMatch[0].lastIndexOf("{");

  const closingBrace = findMatchingBrace(source, openingBrace);

  return {
    start: openingBrace + 1,
    end: closingBrace === -1 ? source.length : closingBrace,
  };
}

function findMatchingBrace(source, openingBrace) {
  let depth = 0;
  let quote = null;
  let escaped = false;

  for (let index = openingBrace; index < source.length; index += 1) {
    const character = source[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
        continue;
      }

      if (character === "\\") {
        escaped = true;
        continue;
      }

      if (character === quote) {
        quote = null;
      }

      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      continue;
    }

    if (source.startsWith("<!--", index)) {
      const commentEnd = source.indexOf("-->", index + 4);

      if (commentEnd === -1) {
        return -1;
      }

      index = commentEnd + 2;
      continue;
    }

    if (character === "{") {
      depth += 1;
      continue;
    }

    if (character === "}") {
      depth -= 1;

      if (depth === 0) {
        return index;
      }
    }
  }

  return -1;
}

function skipWhitespace(source, index, end) {
  while (index < end && /\s/.test(source[index])) {
    index += 1;
  }

  return index;
}

function readIdentifier(source, index, end) {
  if (index >= end || !/[A-Za-z_$]/.test(source[index])) {
    return null;
  }

  const start = index;
  index += 1;

  while (index < end && /[A-Za-z0-9_$-]/.test(source[index])) {
    index += 1;
  }

  return {
    name: source.slice(start, index),
    start,
    end: index,
  };
}

function findRootMembers(source, bodyStart, bodyEnd) {
  const members = [];

  let index = bodyStart;
  let depth = 0;
  let quote = null;
  let escaped = false;

  const skipLine = () => {
    while (index < bodyEnd && source[index] !== "\n" && source[index] !== "\r") {
      index += 1;
    }
  };

  while (index < bodyEnd) {
    const character = source[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
        index += 1;
        continue;
      }

      if (character === "\\") {
        escaped = true;
        index += 1;
        continue;
      }

      if (character === quote) {
        quote = null;
      }

      index += 1;
      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      index += 1;
      continue;
    }

    if (source.startsWith("//", index)) {
      skipLine();
      continue;
    }

    if (source.startsWith("/*", index)) {
      const end = source.indexOf("*/", index + 2);
      index = end === -1 ? bodyEnd : end + 2;
      continue;
    }

    if (source.startsWith("<!--", index)) {
      const end = source.indexOf("-->", index + 4);

      index = end === -1 ? bodyEnd : end + 3;

      continue;
    }

    if (character === "{") {
      depth += 1;
      index += 1;
      continue;
    }

    if (character === "}") {
      depth = Math.max(0, depth - 1);
      index += 1;
      continue;
    }

    if (depth === 0 && /[A-Za-z_$]/.test(character)) {
      const identifier = readIdentifier(source, index, bodyEnd);

      if (!identifier) {
        index += 1;
        continue;
      }

      index = identifier.end;

      members.push({
        name: identifier.name,
        start: identifier.start,
        end: identifier.end,
      });

      const assignmentMembers = new Set(["layout", "runtime", "hydrate", "state"]);

      if (assignmentMembers.has(identifier.name)) {
        skipLine();
        continue;
      }

      if (identifier.name === "client") {
        const next = skipWhitespace(source, index, bodyEnd);

        if (source[next] === "=") {
          skipLine();
          continue;
        }
      }

      const blockMembers = new Set([
        "types",
        "props",
        "computed",
        "effect",
        "watch",
        "lifecycle",
        "view",
        "seo",
        "security",
        "load",
        "action",
        "api",
        "ssr",
        "client",
        "realtime",
        "style",
        "functions",
      ]);

      if (blockMembers.has(identifier.name)) {
        let cursor = index;
        let parenthesisDepth = 0;
        let bracketDepth = 0;
        let memberQuote = null;
        let memberEscaped = false;

        while (cursor < bodyEnd) {
          const current = source[cursor];

          if (memberQuote !== null) {
            if (memberEscaped) {
              memberEscaped = false;
            } else if (current === "\\") {
              memberEscaped = true;
            } else if (current === memberQuote) {
              memberQuote = null;
            }

            cursor += 1;
            continue;
          }

          if (current === '"' || current === "'") {
            memberQuote = current;
            cursor += 1;
            continue;
          }

          if (current === "(") parenthesisDepth += 1;
          if (current === ")") parenthesisDepth = Math.max(0, parenthesisDepth - 1);
          if (current === "[") bracketDepth += 1;
          if (current === "]") bracketDepth = Math.max(0, bracketDepth - 1);

          if (current === "{" && parenthesisDepth === 0 && bracketDepth === 0) {
            index = cursor;
            break;
          }

          cursor += 1;
        }

        if (cursor >= bodyEnd) index = bodyEnd;
      }

      continue;
    }

    index += 1;
  }

  return members;
}

function findNamedBlocks(source, bodyStart, bodyEnd, blockName) {
  const blocks = [];
  let index = bodyStart;

  while (index < bodyEnd) {
    index = skipWhitespace(source, index, bodyEnd);

    const identifier = readIdentifier(source, index, bodyEnd);

    if (!identifier) {
      index += 1;
      continue;
    }

    index = identifier.end;

    if (identifier.name !== blockName) {
      const possibleBrace = skipWhitespace(source, index, bodyEnd);

      if (source[possibleBrace] === "{") {
        const end = findMatchingBrace(source, possibleBrace);

        index = end === -1 ? bodyEnd : end + 1;
      }

      continue;
    }

    const openingBrace = skipWhitespace(source, index, bodyEnd);

    if (source[openingBrace] !== "{") {
      blocks.push({
        name: blockName,
        nameStart: identifier.start,
        nameEnd: identifier.end,
        openingBrace: -1,
        closingBrace: -1,
      });

      continue;
    }

    const closingBrace = findMatchingBrace(source, openingBrace);

    blocks.push({
      name: blockName,
      nameStart: identifier.start,
      nameEnd: identifier.end,
      openingBrace,
      closingBrace,
    });

    index = closingBrace === -1 ? bodyEnd : closingBrace + 1;
  }

  return blocks;
}

function findStateDeclarations(source, bodyStart, bodyEnd) {
  const states = new Map();
  let index = bodyStart;
  let depth = 0;
  let quote = null;
  let escaped = false;

  while (index < bodyEnd) {
    const character = source[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
      } else if (character === "\\") {
        escaped = true;
      } else if (character === quote) {
        quote = null;
      }

      index += 1;
      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      index += 1;
      continue;
    }

    if (source.startsWith("<!--", index)) {
      const end = source.indexOf("-->", index + 4);

      index = end === -1 ? bodyEnd : end + 3;

      continue;
    }

    if (character === "{") {
      depth += 1;
      index += 1;
      continue;
    }

    if (character === "}") {
      depth = Math.max(0, depth - 1);
      index += 1;
      continue;
    }

    if (
      depth === 0 &&
      source.startsWith("state", index) &&
      !/[A-Za-z0-9_$]/.test(source[index - 1] || "") &&
      !/[A-Za-z0-9_$]/.test(source[index + 5] || "")
    ) {
      let cursor = skipWhitespace(source, index + 5, bodyEnd);

      const state = readIdentifier(source, cursor, bodyEnd);

      if (state) {
        states.set(state.name, state);
        index = state.end;
        continue;
      }
    }

    index += 1;
  }

  return states;
}

function findWatchDeclarations(source, bodyStart, bodyEnd) {
  const watches = [];
  let index = bodyStart;
  let depth = 0;
  let quote = null;
  let escaped = false;

  while (index < bodyEnd) {
    const character = source[index];

    if (quote !== null) {
      if (escaped) {
        escaped = false;
      } else if (character === "\\") {
        escaped = true;
      } else if (character === quote) {
        quote = null;
      }

      index += 1;
      continue;
    }

    if (character === '"' || character === "'") {
      quote = character;
      index += 1;
      continue;
    }

    if (source.startsWith("<!--", index)) {
      const end = source.indexOf("-->", index + 4);

      index = end === -1 ? bodyEnd : end + 3;

      continue;
    }

    if (character === "{") {
      depth += 1;
      index += 1;
      continue;
    }

    if (character === "}") {
      depth = Math.max(0, depth - 1);
      index += 1;
      continue;
    }

    if (
      depth === 0 &&
      source.startsWith("watch", index) &&
      !/[A-Za-z0-9_$]/.test(source[index - 1] || "") &&
      !/[A-Za-z0-9_$]/.test(source[index + 5] || "")
    ) {
      const watchStart = index;
      let cursor = skipWhitespace(source, index + 5, bodyEnd);

      const watchedState = readIdentifier(source, cursor, bodyEnd);

      if (!watchedState) {
        watches.push({
          watchStart,
          watchEnd: index + 5,
          state: null,
          openingBrace: -1,
          closingBrace: -1,
        });

        index += 5;
        continue;
      }

      cursor = skipWhitespace(source, watchedState.end, bodyEnd);

      const openingBrace = source[cursor] === "{" ? cursor : -1;

      const closingBrace = openingBrace === -1 ? -1 : findMatchingBrace(source, openingBrace);

      watches.push({
        watchStart,
        watchEnd: index + 5,
        state: watchedState,
        openingBrace,
        closingBrace,
      });

      index = closingBrace === -1 ? watchedState.end : closingBrace + 1;

      continue;
    }

    index += 1;
  }

  return watches;
}

function validateRootMembers(document, source, rootKind, rootMatch) {
  const diagnostics = [];
  const allowed = VALID_MEMBERS[rootKind];

  if (!allowed) {
    return diagnostics;
  }

  const bodyRange = getRootBodyRange(source, rootMatch);

  const members = findRootMembers(source, bodyRange.start, bodyRange.end);

  for (const member of members) {
    if (allowed.has(member.name)) {
      continue;
    }

    diagnostics.push(
      createDiagnostic(
        document,
        member.start,
        member.end,
        `Unknown ${rootKind} member \`${member.name}\`.`,
        vscode.DiagnosticSeverity.Error,
        "wrn-unknown-member",
      ),
    );
  }

  return diagnostics;
}

function validateLifecycleBlocks(document, source, rootKind, rootMatch) {
  if (rootKind !== "component" && rootKind !== "layout") {
    return [];
  }

  const diagnostics = [];
  const bodyRange = getRootBodyRange(source, rootMatch);

  const blocks = findNamedBlocks(source, bodyRange.start, bodyRange.end, "lifecycle");

  if (blocks.length > 1) {
    for (const block of blocks.slice(1)) {
      diagnostics.push(
        createDiagnostic(
          document,
          block.nameStart,
          block.nameEnd,
          "Only one `lifecycle { ... }` block is allowed.",
          vscode.DiagnosticSeverity.Error,
          "wrn-duplicate-lifecycle",
        ),
      );
    }
  }

  for (const block of blocks) {
    if (block.openingBrace === -1) {
      diagnostics.push(
        createDiagnostic(
          document,
          block.nameStart,
          block.nameEnd,
          "`lifecycle` must be followed by a block.",
          vscode.DiagnosticSeverity.Error,
          "wrn-invalid-lifecycle",
        ),
      );

      continue;
    }

    const lifecycleEnd = block.closingBrace === -1 ? bodyRange.end : block.closingBrace;

    let index = block.openingBrace + 1;
    const seenHooks = new Set();

    while (index < lifecycleEnd) {
      index = skipWhitespace(source, index, lifecycleEnd);

      if (index >= lifecycleEnd) {
        break;
      }

      const hook = readIdentifier(source, index, lifecycleEnd);

      if (!hook) {
        index += 1;
        continue;
      }

      index = hook.end;

      if (!VALID_LIFECYCLE_HOOKS.has(hook.name)) {
        diagnostics.push(
          createDiagnostic(
            document,
            hook.start,
            hook.end,
            `Unknown lifecycle hook \`${hook.name}\`. Use \`mount\`, \`update\`, or \`unmount\`.`,
            vscode.DiagnosticSeverity.Error,
            "wrn-invalid-lifecycle-hook",
          ),
        );
      } else if (seenHooks.has(hook.name)) {
        diagnostics.push(
          createDiagnostic(
            document,
            hook.start,
            hook.end,
            `Duplicate lifecycle hook \`${hook.name}\`.`,
            vscode.DiagnosticSeverity.Error,
            "wrn-duplicate-lifecycle-hook",
          ),
        );
      } else {
        seenHooks.add(hook.name);
      }

      const openingBrace = skipWhitespace(source, index, lifecycleEnd);

      if (source[openingBrace] !== "{") {
        diagnostics.push(
          createDiagnostic(
            document,
            hook.start,
            hook.end,
            `Lifecycle hook \`${hook.name}\` must be followed by a block.`,
            vscode.DiagnosticSeverity.Error,
            "wrn-invalid-lifecycle-hook",
          ),
        );

        index = hook.end;
        continue;
      }

      const closingBrace = findMatchingBrace(source, openingBrace);

      index = closingBrace === -1 ? lifecycleEnd : closingBrace + 1;
    }
  }

  return diagnostics;
}

function validateWatchBlocks(document, source, rootKind, rootMatch) {
  if (rootKind !== "component" && rootKind !== "layout") {
    return [];
  }

  const diagnostics = [];
  const bodyRange = getRootBodyRange(source, rootMatch);

  const states = findStateDeclarations(source, bodyRange.start, bodyRange.end);

  const watches = findWatchDeclarations(source, bodyRange.start, bodyRange.end);

  for (const watch of watches) {
    if (!watch.state) {
      diagnostics.push(
        createDiagnostic(
          document,
          watch.watchStart,
          watch.watchEnd,
          "`watch` must be followed by a declared state name.",
          vscode.DiagnosticSeverity.Error,
          "wrn-invalid-watch",
        ),
      );

      continue;
    }

    if (!states.has(watch.state.name)) {
      diagnostics.push(
        createDiagnostic(
          document,
          watch.state.start,
          watch.state.end,
          `Cannot watch undeclared state \`${watch.state.name}\`.`,
          vscode.DiagnosticSeverity.Error,
          "wrn-unknown-watch-state",
        ),
      );
    }

    if (watch.openingBrace === -1) {
      diagnostics.push(
        createDiagnostic(
          document,
          watch.state.start,
          watch.state.end,
          `Watcher for \`${watch.state.name}\` must be followed by a block.`,
          vscode.DiagnosticSeverity.Error,
          "wrn-invalid-watch",
        ),
      );
    }
  }

  return diagnostics;
}

function validateRequiredView(document, source, rootKind, rootMatch) {
  const bodyRange = getRootBodyRange(source, rootMatch);

  const body = source.slice(bodyRange.start, bodyRange.end);

  if (/\bview\s*\{/.test(body)) {
    return [];
  }

  return [
    createDiagnostic(
      document,
      rootMatch.index,
      rootMatch.index + rootMatch[0].length,
      `The ${rootKind} \`${rootMatch[2]}\` does not contain a \`view { ... }\` block.`,
      vscode.DiagnosticSeverity.Warning,
      "wrn-missing-view",
    ),
  ];
}

function validateLayoutUsage(document, source, rootKind, rootMatch) {
  const diagnostics = [];

  if (rootKind !== "page") {
    const bodyRange = getRootBodyRange(source, rootMatch);
    const layoutMember = findRootMembers(source, bodyRange.start, bodyRange.end).find(
      (member) => member.name === "layout",
    );

    if (layoutMember) {
      diagnostics.push(
        createDiagnostic(
          document,
          layoutMember.start,
          layoutMember.end,
          '`layout = "..."` is only valid inside a page.',
          vscode.DiagnosticSeverity.Error,
          "wrn-invalid-layout-member",
        ),
      );
    }
  }

  return diagnostics;
}

function validateV060Features(document, source) {
  const diagnostics = [];
  const duplicateRuntimeFunctions = new Map();
  for (const match of source.matchAll(
    /\b(client|server|shared)\s+(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
  )) {
    const key = `${match[1]}:${match[2]}`;
    if (duplicateRuntimeFunctions.has(key)) {
      diagnostics.push(
        createDiagnostic(
          document,
          match.index,
          match.index + match[0].length,
          `Duplicate ${match[1]} function '${match[2]}'.`,
          vscode.DiagnosticSeverity.Error,
          "WRN-FUNCTION-DUPLICATE",
        ),
      );
    } else duplicateRuntimeFunctions.set(key, match.index);
  }
  for (const match of source.matchAll(/\$emit\s*\(/g)) {
    diagnostics.push(
      createDiagnostic(
        document,
        match.index,
        match.index + match[0].length,
        "Use typed output.name(payload) instead of deprecated $emit().",
        vscode.DiagnosticSeverity.Warning,
        "WRN-OUTPUT-LEGACY-EMIT",
      ),
    );
  }
  for (const match of source.matchAll(/\bevent\.detail\b/g)) {
    diagnostics.push(
      createDiagnostic(
        document,
        match.index,
        match.index + match[0].length,
        "Component output handlers receive payload directly.",
        vscode.DiagnosticSeverity.Warning,
        "WRN-OUTPUT-LEGACY-DETAIL",
      ),
    );
  }
  for (const match of source.matchAll(
    /\bserver\s+(?:async\s+)?function\b[\s\S]*?\b(window|document|localStorage|navigator)\b/g,
  )) {
    const offset = match.index + match[0].lastIndexOf(match[1]);
    diagnostics.push(
      createDiagnostic(
        document,
        offset,
        offset + match[1].length,
        `Browser API '${match[1]}' is unavailable in a server function.`,
        vscode.DiagnosticSeverity.Error,
        "WRN-SERVER-BROWSER-API",
      ),
    );
  }
  return diagnostics;
}

function validateDocument(document) {
  if (document.languageId !== WRN_LANGUAGE_ID) {
    return [];
  }

  const source = document.getText();

  if (!source.trim()) {
    return [];
  }

  const diagnostics = [];
  const declaration = findTopLevelDeclaration(document, source);

  if (declaration.diagnostic) {
    diagnostics.push(declaration.diagnostic);

    diagnostics.push(...validateBalancedCharacters(document, source));

    return diagnostics;
  }

  if (!VALID_TOP_LEVEL_KINDS.has(declaration.kind)) {
    diagnostics.push(
      createDiagnostic(
        document,
        declaration.match.index,
        declaration.match.index + declaration.match[0].length,
        `Unsupported WRN declaration \`${declaration.kind}\`.`,
        vscode.DiagnosticSeverity.Error,
        "wrn-invalid-kind",
      ),
    );

    return diagnostics;
  }

  diagnostics.push(...validateBalancedCharacters(document, source));

  diagnostics.push(...validateHtmlTags(document, source));

  diagnostics.push(...validateRootMembers(document, source, declaration.kind, declaration.match));

  diagnostics.push(
    ...validateLifecycleBlocks(document, source, declaration.kind, declaration.match),
  );

  diagnostics.push(...validateWatchBlocks(document, source, declaration.kind, declaration.match));

  diagnostics.push(...validateRequiredView(document, source, declaration.kind, declaration.match));

  diagnostics.push(...validateLayoutUsage(document, source, declaration.kind, declaration.match));

  diagnostics.push(...validateV060Features(document, source));

  return diagnostics;
}

function registerDiagnostics(context) {
  const collection = vscode.languages.createDiagnosticCollection(COLLECTION_NAME);

  const timers = new Map();

  const update = (document) => {
    if (document.languageId !== WRN_LANGUAGE_ID) {
      return;
    }

    const key = document.uri.toString();
    const previousTimer = timers.get(key);

    if (previousTimer) {
      clearTimeout(previousTimer);
      timers.delete(key);
    }

    const configuration = vscode.workspace.getConfiguration("wrnexus", document.uri);

    if (!configuration.get("diagnostics.enable", true)) {
      collection.delete(document.uri);
      return;
    }

    const timer = setTimeout(() => {
      timers.delete(key);

      collection.set(document.uri, validateDocument(document));
    }, 150);

    timers.set(key, timer);
  };

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

  context.subscriptions.push(
    collection,

    vscode.workspace.onDidOpenTextDocument(update),

    vscode.workspace.onDidChangeTextDocument((event) => {
      update(event.document);
    }),

    vscode.workspace.onDidSaveTextDocument(update),

    vscode.workspace.onDidChangeConfiguration((event) => {
      if (!event.affectsConfiguration("wrnexus.diagnostics.enable")) {
        return;
      }

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

    vscode.workspace.onDidCloseTextDocument((document) => {
      const key = document.uri.toString();
      const timer = timers.get(key);

      if (timer) {
        clearTimeout(timer);
        timers.delete(key);
      }

      collection.delete(document.uri);
    }),

    {
      dispose() {
        for (const timer of timers.values()) {
          clearTimeout(timer);
        }

        timers.clear();
      },
    },
  );
}

module.exports = {
  findTopLevelDeclaration,
  maskLeadingTrivia,
  registerDiagnostics,
  validateBalancedCharacters,
  validateDocument,
  validateHtmlTags,
  validateLifecycleBlocks,
  validateLayoutUsage,
  validateRootMembers,
  validateWatchBlocks,
};
