"use strict";

const COMPONENT_DECLARATION = /\b(component|layout)\s+([A-Za-z_$][\w$]*)\s*\{/;

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) {
      if (escaped) escaped = false;
      else if (character === "\\") escaped = true;
      else if (character === quote) quote = null;
      continue;
    }
    if (character === '"' || character === "'" || character === "`") quote = character;
    else if (character === "{") depth += 1;
    else if (character === "}" && --depth === 0) return index;
  }

  return -1;
}

function inferType(defaultValue) {
  const value = defaultValue.trim();
  if (value === "undefined") return "unknown";
  if (/^(?:true|false)$/.test(value)) return "boolean";
  if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(value)) return "number";
  if (/^["'`]/.test(value)) return "string";
  if (value.startsWith("[")) return "array";
  if (value.startsWith("{")) return "object";
  if (value === "null") return "null";
  return "unknown";
}

function runtimeType(type) {
  const value = String(type || "")
    .trim()
    .replace(/^readonly\s+/, "");
  const unionParts = value.split("|").map((part) => part.trim());
  const concreteParts = unionParts.filter((part) => !/^(?:null|undefined)$/.test(part));
  if (
    /^(?:string|String)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
    (concreteParts.length > 0 && concreteParts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part)))
  )
    return "string";
  if (
    /^(?:number|Number)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
    (concreteParts.length > 0 &&
      concreteParts.every((part) => /^-?(?:\d+\.?\d*|\.\d+)$/.test(part)))
  )
    return "number";
  if (
    /^(?:boolean|Boolean)(?:\s*\|\s*(?:null|undefined))*$/.test(value) ||
    (concreteParts.length > 0 && concreteParts.every((part) => /^(?:true|false)$/.test(part)))
  )
    return "boolean";
  if (/^bigint(?:\s*\|\s*(?:null|undefined))*$/.test(value)) return "bigint";
  if (/^(?:Array\s*<|ReadonlyArray\s*<|.+\[\])/.test(value) || /^\[/.test(value)) return "array";
  if (/^(?:Record\s*<|object\b|\{)/.test(value)) return "object";
  if (/=>|^Function$/.test(value)) return "function";
  return value || "unknown";
}

function stringLiteral(value) {
  const match = /^(?:"([\s\S]*)"|'([\s\S]*)'|`([\s\S]*)`)$/.exec(value.trim());
  return match ? (match[1] ?? match[2] ?? match[3]) : null;
}

function declaredOptions(type) {
  const value = String(type || "").trim();
  if (!value) return [];
  const parts = value.split("|").map((part) => part.trim());
  if (!parts.every((part) => /^(?:"[^"]*"|'[^']*')$/.test(part))) return [];
  return parts.map((part) => part.slice(1, -1));
}

function inferOptions(source, propName) {
  const escaped = propName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const options = new Set();
  const comparisons = new RegExp(`\\b${escaped}\\s*(?:===|!==|==|!=)\\s*(["'])(.*?)\\1`, "g");
  let match;
  while ((match = comparisons.exec(source)) !== null) options.add(match[2]);
  return [...options].sort();
}

function parseComponentMetadata(source, uri = null) {
  const declaration = COMPONENT_DECLARATION.exec(source);
  if (!declaration) return null;

  const propsKeyword = /\bprops\s*\{/.exec(source.slice(declaration.index));
  const props = [];
  const events = [];
  if (propsKeyword) {
    const start = declaration.index + propsKeyword.index;
    const openingBrace = source.indexOf("{", start);
    const closingBrace = findMatchingBrace(source, openingBrace);
    const bodyEnd = closingBrace === -1 ? source.length : closingBrace;
    const body = source.slice(openingBrace + 1, bodyEnd);
    const linePattern =
      /^(?:\s*\/\/\s*@required\s*\r?\n)?\s*([A-Za-z_$][\w$]*)(\?)?(?:\s*:\s*([^=\r\n]+?))?(?:\s*=\s*(.*?))?\s*$/gm;
    let propMatch;
    while ((propMatch = linePattern.exec(body)) !== null) {
      const optional = Boolean(propMatch[2]);
      const annotation = propMatch[3] && propMatch[3].trim();
      const hasDefault = propMatch[4] !== undefined;
      const defaultValue = hasDefault ? propMatch[4] : "undefined";
      const name = propMatch[1];
      props.push({
        name,
        defaultValue,
        required:
          !optional &&
          (!hasDefault ||
            defaultValue.trim() === "undefined" ||
            /^\s*\/\/\s*@required/m.test(propMatch[0])),
        type: annotation || inferType(defaultValue),
        options:
          declaredOptions(annotation).length > 0
            ? declaredOptions(annotation)
            : inferOptions(source, name),
      });
    }
    const eventPattern = /^\s*@event\s+([A-Za-z_$][\w$]*)\s*=\s*function\s*$/gm;
    let eventMatch;
    while ((eventMatch = eventPattern.exec(body)) !== null) events.push(eventMatch[1]);
  }

  const outputsKeyword = /\boutputs\s*\{/.exec(source.slice(declaration.index));
  if (outputsKeyword) {
    const start = declaration.index + outputsKeyword.index;
    const openingBrace = source.indexOf("{", start);
    const closingBrace = findMatchingBrace(source, openingBrace);
    const outputBody = source.slice(
      openingBrace + 1,
      closingBrace === -1 ? source.length : closingBrace,
    );
    const outputPattern = /(?:^|\s)([A-Za-z_$][\w$]*)\s*\(/g;
    let outputMatch;
    while ((outputMatch = outputPattern.exec(outputBody)) !== null) {
      if (!events.includes(outputMatch[1])) events.push(outputMatch[1]);
    }
  }

  return { kind: declaration[1], name: declaration[2], props, events, uri };
}

function unwrapAttributeValue(value) {
  const trimmed = value.trim();
  if (trimmed.startsWith("{") && trimmed.endsWith("}")) return trimmed.slice(1, -1).trim();
  return trimmed;
}

function isWrappedExpression(value) {
  const trimmed = value.trim();
  return trimmed.startsWith("{") && trimmed.endsWith("}");
}

function expressionLiteralType(value) {
  const expression = value.trim();
  if (/^(?:true|false)$/.test(expression)) return "boolean";
  if (/^-?(?:\d+\.?\d*|\.\d+)$/.test(expression)) return "number";
  if (/^(?:"[\s\S]*"|'[\s\S]*'|`[\s\S]*`)$/.test(expression)) return "string";
  if (expression.startsWith("[")) return "array";
  if (expression.startsWith("{")) return "object";
  if (expression === "null") return "null";
  return null;
}

function attributeValueType(value, symbols = new Map()) {
  const expression = isWrappedExpression(value);
  const unwrapped = unwrapAttributeValue(value);
  if (/^[A-Za-z_$][\w$]*$/.test(unwrapped) && symbols.has(unwrapped)) {
    return symbols.get(unwrapped);
  }

  const literal = expressionLiteralType(unwrapped);
  if (literal) return literal;

  if (expression) {
    if (
      /^!\s*/.test(unwrapped) ||
      /(?:===|!==|==|!=|<=|>=|<|>|\bin\b|\binstanceof\b)/.test(unwrapped)
    ) {
      return "boolean";
    }

    // Dynamic member access, calls, logical expressions and ternaries cannot be
    // proven from component metadata alone. Treat them as unknown instead of as
    // quoted strings so valid expressions such as `{item.external || false}` do
    // not produce false boolean/string diagnostics.
    return "unknown";
  }

  return "string";
}

function isTypeCompatible(prop, value, symbols = new Map()) {
  const expected = runtimeType(prop.type);
  if (expected === "unknown" || expected === "null") return true;
  const actual = runtimeType(attributeValueType(value, symbols));
  if (actual === "unknown") return true;
  if (expected === actual) return true;
  if (expected === "number" && actual === "string") return Number.isFinite(Number(value));
  if (expected === "boolean" && actual === "string")
    return /^(?:true|false|1|0|yes|no|on|off)?$/i.test(value);
  return false;
}

function parseComponentTags(source) {
  const tags = [];
  const pattern = /<([A-Z][A-Za-z0-9_$]*)(\s[\s\S]*?)?\s*\/?>/g;
  let match;
  while ((match = pattern.exec(source)) !== null) {
    const attributes = [];
    const attributeSource = match[2] || "";
    const attributeOffset = match.index + match[0].indexOf(attributeSource);
    const attributePattern = /([^\s=/>]+)\s*=\s*(["'])([\s\S]*?)\2/g;
    let attributeMatch;
    while ((attributeMatch = attributePattern.exec(attributeSource)) !== null) {
      const nameStart = attributeOffset + attributeMatch.index;
      attributes.push({
        name: attributeMatch[1],
        value: attributeMatch[3],
        nameStart,
        nameEnd: nameStart + attributeMatch[1].length,
      });
    }
    tags.push({
      name: match[1],
      start: match.index,
      end: match.index + match[0].length,
      nameStart: match.index + 1,
      nameEnd: match.index + 1 + match[1].length,
      attributes,
    });
  }
  return tags;
}

function validateComponentTags(source, components) {
  const diagnostics = [];
  const own = parseComponentMetadata(source);
  const symbols = new Map((own?.props || []).map((prop) => [prop.name, prop.type]));
  const statePattern = /^\s*state\s+([A-Za-z_$][\w$]*)(?:\s*:\s*([^=\r\n]+?))?\s*=\s*(.*?)\s*$/gm;
  let stateMatch;
  while ((stateMatch = statePattern.exec(source)) !== null) {
    symbols.set(stateMatch[1], stateMatch[2]?.trim() || inferType(stateMatch[3]));
  }
  for (const tag of parseComponentTags(source)) {
    const component = components.get(tag.name);
    if (!component) continue;
    const provided = new Map(tag.attributes.map((attribute) => [attribute.name, attribute]));
    const declared = new Map(component.props.map((prop) => [prop.name, prop]));
    const declaredEvents = new Set(component.events || []);

    for (const prop of component.props) {
      if (prop.required && !provided.has(prop.name)) {
        diagnostics.push({
          severity: "error",
          code: "wrn-missing-component-prop",
          message: `<${tag.name}> requires prop \`${prop.name}\` (${prop.type}).`,
          start: tag.nameStart,
          end: tag.nameEnd,
        });
      }
    }

    for (const attribute of tag.attributes) {
      if (attribute.name.startsWith("@")) {
        const eventName = attribute.name.slice(1);
        if (!declaredEvents.has(eventName)) {
          diagnostics.push({
            severity: "warning",
            code: "wrn-unknown-component-event",
            message: `Unknown event \`${eventName}\` on <${tag.name}>.`,
            start: attribute.nameStart,
            end: attribute.nameEnd,
          });
        }
        continue;
      }
      const prop = declared.get(attribute.name);
      if (!prop) {
        diagnostics.push({
          severity: "warning",
          code: "wrn-unknown-component-prop",
          message: `Unknown prop \`${attribute.name}\` on <${tag.name}>.`,
          start: attribute.nameStart,
          end: attribute.nameEnd,
        });
        continue;
      }
      const actualType = attributeValueType(attribute.value, symbols);
      if (!isTypeCompatible(prop, attribute.value, symbols)) {
        diagnostics.push({
          severity: "error",
          code: "wrn-component-prop-type",
          message: `Prop \`${attribute.name}\` on <${tag.name}> expects ${prop.type}, but received ${runtimeType(actualType)}.`,
          start: attribute.nameStart,
          end: attribute.nameEnd,
        });
      }
      const optionValue = isWrappedExpression(attribute.value)
        ? stringLiteral(unwrapAttributeValue(attribute.value))
        : (stringLiteral(attribute.value) ?? attribute.value);
      if (optionValue !== null && prop.options.length > 0 && !prop.options.includes(optionValue)) {
        diagnostics.push({
          severity: "warning",
          code: "wrn-component-prop-option",
          message: `Prop \`${attribute.name}\` should be one of: ${prop.options.join(", ")}.`,
          start: attribute.nameStart,
          end: attribute.nameEnd,
        });
      }
    }
  }
  return diagnostics;
}

module.exports = {
  attributeValueType,
  declaredOptions,
  inferType,
  runtimeType,
  isTypeCompatible,
  parseComponentMetadata,
  parseComponentTags,
  validateComponentTags,
};
