import {
  eraseFunctionTypes,
  type PageAst,
  type RuntimeFunctionDecl,
  type StructuredImportDecl,
  type ViewNode,
} from "@wrnexus/syntax";

const RESERVED_BINDINGS = new Set([
  "await",
  "break",
  "case",
  "catch",
  "class",
  "const",
  "continue",
  "debugger",
  "default",
  "delete",
  "do",
  "else",
  "enum",
  "export",
  "extends",
  "false",
  "finally",
  "for",
  "function",
  "if",
  "implements",
  "import",
  "in",
  "instanceof",
  "interface",
  "let",
  "new",
  "null",
  "package",
  "private",
  "protected",
  "public",
  "return",
  "static",
  "super",
  "switch",
  "this",
  "throw",
  "true",
  "try",
  "typeof",
  "var",
  "void",
  "while",
  "with",
  "yield",
]);
const RUNTIME_BINDINGS = new Set([
  "context",
  "state",
  "output",
  "server",
  "props",
  "refs",
  "event",
  "payload",
]);

function safeIdentifier(name: string): boolean {
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) && !RESERVED_BINDINGS.has(name);
}

function identifierReferenced(source: string, name: string): boolean {
  const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return new RegExp(`(?:^|[^A-Za-z0-9_$])${escaped}(?![A-Za-z0-9_$])`).test(source);
}

function viewReferenceSource(nodes: ViewNode[]): string[] {
  return nodes.flatMap((node): string[] => {
    if (node.type === "text") return [node.value];
    if (node.type === "each") {
      return [
        node.list,
        node.key ?? "",
        ...viewReferenceSource(node.body),
        ...viewReferenceSource(node.empty),
      ];
    }
    if (node.type === "if") {
      return node.branches.flatMap((branch) => [
        branch.cond ?? "",
        ...viewReferenceSource(branch.body),
      ]);
    }
    return [...node.attrs.map((attr) => attr.value), ...viewReferenceSource(node.children)];
  });
}

function browserReferenceSource(ast: PageAst, functions: RuntimeFunctionDecl[]): string {
  return [
    ...functions.flatMap((fn) => [fn.source, fn.body]),
    ...ast.computed.map((entry) => entry.expr),
    ...ast.effects,
    ast.lifecycle.mount ?? "",
    ast.lifecycle.update ?? "",
    ast.lifecycle.unmount ?? "",
    ...viewReferenceSource(ast.view),
  ].join("\n");
}

function renderSelectedImport(
  entry: StructuredImportDecl,
  source: string,
): { code: string; bindings: string[] } | null {
  if (entry.typeOnly) return null;
  const isStore = entry.source.endsWith(".wrn") && /(?:^|\/)stores?\//.test(entry.source);
  if (entry.source.endsWith(".wrn") && !isStore) return null;

  const defaultImport =
    entry.defaultImport && (isStore || identifierReferenced(source, entry.defaultImport))
      ? entry.defaultImport
      : undefined;
  const namespaceImport =
    entry.namespaceImport && (isStore || identifierReferenced(source, entry.namespaceImport))
      ? entry.namespaceImport
      : undefined;
  const namedImports = entry.namedImports.filter(
    (item) => !item.typeOnly && (isStore || identifierReferenced(source, item.local)),
  );

  const bindings = [
    ...(defaultImport ? [defaultImport] : []),
    ...(namespaceImport ? [namespaceImport] : []),
    ...namedImports.map((item) => item.local),
  ].filter(safeIdentifier);

  const hasBindings = Boolean(defaultImport || namespaceImport || namedImports.length);
  const sideEffectOnly =
    !entry.defaultImport && !entry.namespaceImport && entry.namedImports.length === 0;
  if (!hasBindings && !sideEffectOnly) return null;
  if (sideEffectOnly) return { code: entry.raw, bindings: [] };

  const clauses: string[] = [];
  if (defaultImport) clauses.push(defaultImport);
  if (namespaceImport) clauses.push(`* as ${namespaceImport}`);
  if (namedImports.length) {
    clauses.push(
      `{ ${namedImports
        .map((item) =>
          item.imported === item.local ? item.imported : `${item.imported} as ${item.local}`,
        )
        .join(", ")} }`,
    );
  }
  return {
    code: `import ${clauses.join(", ")} from ${JSON.stringify(entry.source)};`,
    bindings,
  };
}

function selectedBrowserImports(
  ast: PageAst,
  functions: RuntimeFunctionDecl[],
): { code: string; bindings: string[] }[] {
  const referenceSource = browserReferenceSource(ast, functions);
  return ast.structuredImports
    .map((entry) => renderSelectedImport(entry, referenceSource))
    .filter((entry): entry is { code: string; bindings: string[] } => entry !== null);
}

export function browserModuleRequired(ast: PageAst): boolean {
  const functions = ast.runtimeFunctions.filter((fn) =>
    ["legacy", "client", "shared"].includes(fn.runtime),
  );
  return functions.length > 0 || selectedBrowserImports(ast, functions).length > 0;
}

function _functionEntry(
  ast: PageAst,
  fn: RuntimeFunctionDecl,
  availableFunctions: string[],
): string {
  const parameterNames = new Set(fn.parameters.map((parameter) => parameter.name));

  /*
   * Names the function body declares for itself.
   *
   * Props and state are destructured into the SAME scope as the body, so a
   * body that declares `var size` when `size` is also a prop produced
   * "Identifier 'size' has already been declared" and the entire module
   * failed to parse -- taking every function in the component down with it,
   * with nothing to point at the one line responsible. Skipping the alias for
   * a shadowed name is also what plain JavaScript does: inside that function
   * the local wins.
   */
  const declaredLocals = new Set<string>();
  for (const match of fn.body.matchAll(
    /\b(?:var|let|const)\s+([A-Za-z_$][\w$]*)|\bfunction\s+([A-Za-z_$][\w$]*)/g,
  )) {
    const name = match[1] ?? match[2];
    if (name) declaredLocals.add(name);
  }

  const stateNames = ast.states
    .filter(
      (state) =>
        state.runtime !== "server" &&
        safeIdentifier(state.name) &&
        !RUNTIME_BINDINGS.has(state.name) &&
        !parameterNames.has(state.name) &&
        !declaredLocals.has(state.name),
    )
    .map((state) => state.name);
  const stateSet = new Set(stateNames);
  const propNames = ast.props
    .filter(
      (prop) =>
        safeIdentifier(prop.name) &&
        !RUNTIME_BINDINGS.has(prop.name) &&
        !parameterNames.has(prop.name) &&
        !stateSet.has(prop.name) &&
        !declaredLocals.has(prop.name),
    )
    .map((prop) => prop.name);
  const functionAliases = availableFunctions.filter(
    (name) =>
      safeIdentifier(name) &&
      !RUNTIME_BINDINGS.has(name) &&
      !parameterNames.has(name) &&
      !stateSet.has(name) &&
      !propNames.includes(name) &&
      !declaredLocals.has(name),
  );
  const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
  const initialStateSnapshot = stateNames.length
    ? `const __wrnexusInitialState = { ${stateNames.map((name) => `${JSON.stringify(name)}: context.state.${name}`).join(", ")} };`
    : "";
  const stateAliases = stateNames.length ? `let { ${stateNames.join(", ")} } = context.state;` : "";
  const propAliases = propNames.length ? `const { ${propNames.join(", ")} } = context.props;` : "";
  const syncStateToContext = stateNames.map((name) => `context.state.${name} = ${name};`).join(" ");
  const syncStateFromContext = stateNames
    .map((name) => `${name} = context.state.${name};`)
    .join(" ");
  const peerAliases = !stateNames.length
    ? functionAliases
        .map(
          (name) =>
            `const ${name} = (...__wrnexusPeerArgs) => context.functions[${JSON.stringify(name)}](...__wrnexusPeerArgs);`,
        )
        .join("\n")
    : `const __wrnexusFlush = () => { ${syncStateToContext} };
    const __wrnexusRestore = () => { ${syncStateFromContext} };
    const __wrnexusPeer = (name, args) => {
      __wrnexusFlush();
      let result;
      try {
        result = context.functions[name](...args);
      } catch (error) {
        __wrnexusRestore();
        throw error;
      }
      if (result && typeof result.then === "function") {
        return Promise.resolve(result).finally(__wrnexusRestore);
      }
      __wrnexusRestore();
      return result;
    };
    ${functionAliases.map((name) => `const ${name} = (...__wrnexusPeerArgs) => __wrnexusPeer(${JSON.stringify(name)}, __wrnexusPeerArgs);`).join("\n")}`;
  const copyBack = stateNames
    .map(
      (name) =>
        `if (!Object.is(${name}, __wrnexusInitialState[${JSON.stringify(name)}])) context.state.${name} = ${name};`,
    )
    .join("\n");
  const commitBinding = stateNames.length
    ? `const __wrnexusCommit = () => { ${copyBack} };
    ${!parameterNames.has("commit") && !declaredLocals.has("commit") && !functionAliases.includes("commit") ? "const commit = __wrnexusCommit;" : ""}
    ${
      !parameterNames.has("setTimeout") &&
      !declaredLocals.has("setTimeout") &&
      !functionAliases.includes("setTimeout")
        ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
      try { return callback(...args); } finally { __wrnexusCommit(); }
    }, delay);`
        : ""
    }`
    : "";
  const body = eraseFunctionTypes(fn.body);
  const runtimeBindings = [
    !parameterNames.has("output") ? "const output = context.output;" : "",
    !parameterNames.has("server") ? "const server = context.server;" : "",
    !parameterNames.has("props") ? "const props = context.props;" : "",
    !parameterNames.has("refs") ? "const refs = context.refs;" : "",
  ]
    .filter(Boolean)
    .join("\n    ");
  return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(context${parameters ? `, ${parameters}` : ""}) {
    ${initialStateSnapshot}
    ${stateAliases}
    ${propAliases}
    ${commitBinding}
    ${peerAliases}
    ${runtimeBindings}
    try {
      ${body}
    } finally {
      ${stateNames.length ? "__wrnexusCommit();" : ""}
    }
  }`;
}

export function generateBrowserModule(ast: PageAst): string {
  const functions = ast.runtimeFunctions.filter((fn) =>
    ["legacy", "client", "shared"].includes(fn.runtime),
  );
  const functionNames = functions.map((fn) => fn.name);
  const state = ast.states.filter((entry) => entry.runtime !== "server").map((entry) => entry.name);
  const selectedImports = selectedBrowserImports(ast, functions);
  const imports = selectedImports.map((entry) => entry.code).join("\n");
  const importedBindings = [...new Set(selectedImports.flatMap((entry) => entry.bindings))];
  const sharedState = state.filter((name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name));
  const sharedProps = ast.props
    .map((entry) => entry.name)
    .filter(
      (name) => safeIdentifier(name) && !RUNTIME_BINDINGS.has(name) && !sharedState.includes(name),
    );
  const callableAliases = functionNames.filter(
    (name) =>
      safeIdentifier(name) &&
      !RUNTIME_BINDINGS.has(name) &&
      !sharedState.includes(name) &&
      !sharedProps.includes(name),
  );
  const sharedCommit = sharedState.map((name) => `context.state.${name} = ${name};`).join(" ");
  const sharedRestore = [
    ...sharedState.map((name) => `${name} = context.state.${name};`),
    ...sharedProps.map((name) => `${name} = context.props.${name};`),
  ].join(" ");
  const hasAuthoredCommit = callableAliases.includes("commit");
  const hasAuthoredSetTimeout = callableAliases.includes("setTimeout");
  const implementations = functions
    .map((fn) => {
      const parameters = fn.parameters.map((parameter) => parameter.name).join(", ");
      return `${JSON.stringify(fn.name)}: ${fn.async ? "async " : ""}function(${parameters}) {
        try { ${eraseFunctionTypes(fn.body)} } finally { __wrnexusCommit(); }
      }`;
    })
    .join(",\n");
  return `// generated WRNexusJS browser module for ${ast.name}
${imports}
export const __wrnexusClientFunctions = {
${functions
  .map(
    (fn) =>
      `  ${JSON.stringify(fn.name)}: (context, ...args) => __wrnexusBindings(context)[${JSON.stringify(fn.name)}](...args)`,
  )
  .join(",\n")}
};
export const __wrnexusClientState = ${JSON.stringify(state)};
export const __wrnexusOutputs = ${JSON.stringify(ast.outputs)};
export const __wrnexusImportedBindings = { ${importedBindings.join(", ")} };
function __wrnexusCreateClientFunctions(context) {
  ${sharedState.length ? `let { ${sharedState.join(", ")} } = context.state;` : ""}
  ${sharedProps.length ? `let { ${sharedProps.join(", ")} } = context.props;` : ""}
  const output = context.output;
  const server = context.server;
  const props = context.props;
  const refs = context.refs;
  const __wrnexusCommit = () => { ${sharedCommit} };
  const __wrnexusRestore = () => { ${sharedRestore} };
  ${!hasAuthoredCommit ? "const commit = __wrnexusCommit;" : ""}
  ${
    !hasAuthoredSetTimeout
      ? `const setTimeout = (callback, delay, ...args) => globalThis.setTimeout(() => {
    try { return callback(...args); } finally { __wrnexusCommit(); }
  }, delay);`
      : ""
  }
  const implementations = {
    ${implementations}
  };
  ${callableAliases.map((name) => `const ${name} = (...args) => implementations[${JSON.stringify(name)}](...args);`).join("\n  ")}
  const functions = {};
  for (const name of Object.keys(implementations)) {
    functions[name] = (...args) => {
      __wrnexusRestore();
      return implementations[name](...args);
    };
  }
  return functions;
}
function __wrnexusBindings(context) {
  return context.__wrnexusBoundFunctions ||
    (context.__wrnexusBoundFunctions = __wrnexusCreateClientFunctions(context));
}
export function bindClientScope(context) {
  return __wrnexusBindings(context);
}
`;
}
